Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e4dd4b86f | |||
| 4559c8af74 | |||
| 4a67ae1195 | |||
| 1efa25eed5 | |||
| 54b65a89fd | |||
| b4008b0b30 | |||
| 16c95991d3 | |||
| f3bd81073d | |||
| 6d9ed32929 | |||
| ad2ffc33d8 | |||
| e183b0e5ff | |||
| 4b6b33b08b | |||
| c00a6fa02a | |||
| 8e62c36148 | |||
| f76a569527 | |||
| 9a6c2fafe3 | |||
| 1c355929fc | |||
| ea074fa9bf | |||
| 60ee08a8ac | |||
| 9f7ef209fa | |||
| fc1a66ea76 | |||
| 5d0b77e64a | |||
| a4810a5e42 | |||
| 39c466d542 | |||
| c8418da8c1 | |||
| c0c875eae2 | |||
| 3eba963d03 | |||
| 78a145f181 | |||
| 9928114ca8 | |||
| e5f4f64102 | |||
| 7a98c1750f | |||
| ee279b7976 | |||
| dbfb6d5993 | |||
| 1275371e10 | |||
| f56da1a39f | |||
| fe9685867e | |||
| dfa2cf9e6d | |||
| 262501304c | |||
| a0dbc970a9 | |||
| 2abb6788fe | |||
| 91cec11500 | |||
| 68aca76805 | |||
| 2f501aee14 | |||
| 3c599e24e5 | |||
| a6ee7415d8 | |||
| d3a3d27f70 | |||
| 16f9dcf17a | |||
| cb988e58ba | |||
| bec00e7e64 | |||
| 7ba98ad498 | |||
| aba2ca39d4 | |||
| 8db439a0d1 | |||
| aec8e47fbb | |||
| d03a00758f | |||
| 717bbe62a6 | |||
| b9da72560a | |||
| 460bc43846 | |||
| 26505079b8 | |||
| 3de4c6189b | |||
| 2e4f060ebb | |||
| 53858c9b0e | |||
| 454a386612 | |||
| 346caaa1db | |||
| 42ba43fc81 | |||
| 3f5e36271f | |||
| 18ae5860bc | |||
| 7f70de99d3 | |||
| 385fcfe8d0 | |||
| 09a6206d28 | |||
| b0d9aa38d2 | |||
| 26575c5086 | |||
| 91d53530e6 | |||
| c07e7221e5 | |||
| 5844d0525a | |||
| 7ea572fdca | |||
| 162643a4b1 | |||
| 4727e5cbb1 | |||
| b8136951e4 | |||
| 5300d2c531 | |||
| d6fd2a8228 | |||
| 493e3068d8 | |||
| e130ad74d1 | |||
| a382e82dec | |||
| bdd2ac0015 | |||
| c0a06f7db4 | |||
| e82027310d | |||
| c6c0bc19d8 | |||
| 33bb787b15 | |||
| c794d59fd5 | |||
| 1fefafb254 | |||
| 90dadf0bec | |||
| 95ff355737 | |||
| c2d41b0376 | |||
| 8f3c9b391b | |||
| 5a9d883a93 | |||
| c2e8af5ed1 | |||
| 70a85d6111 | |||
| 1163745a03 | |||
| 529fcaa5c9 | |||
| 681fd4ff3b | |||
| 305ec59d76 | |||
| 0917fa6f4a | |||
| c7d3969a53 | |||
| c41b33c9c0 | |||
| 7131ac24a7 | |||
| 501cd2071c | |||
| 85a1233733 | |||
| 670fa83e2c | |||
| d03a53e755 | |||
| 2ec416bd6b | |||
| b1a391ca5d | |||
| c0ca447041 | |||
| 5d3db15eca | |||
| eead69068c | |||
| edae1ca4c7 | |||
| 0c9cc2baa3 | |||
| 326ac41422 | |||
| dd2941bb43 | |||
| a691ee08d8 | |||
| e8fc522eba | |||
| 482a41e49b | |||
| ae3d8ce0c0 | |||
| a2e30ace02 | |||
| cde9672a09 | |||
| db6ca856de | |||
| bb5e615841 | |||
| 48ac4d9f2a | |||
| eb7bba81fe | |||
| a43cb1eb25 | |||
| e2d8872060 | |||
| ede584320f | |||
| bc458d3ee0 | |||
| 0d1f7265c4 | |||
| 539201a032 | |||
| 50ad2aa215 | |||
| 5cd28c04b8 | |||
| c3ce4d7f6a | |||
| 9bd054490f | |||
| 2be14d57bf | |||
| 61e0a85aed | |||
| e483c6b598 | |||
| db3b075796 | |||
| 0bb26ae504 | |||
| 9d16dd997a | |||
| e4fe1fff97 | |||
| 37ed31b0e2 | |||
| dc82e681dd | |||
| 4944824544 | |||
| 67cd9b76ae | |||
| 8a86f32700 | |||
| f62b15d8da | |||
| d709038b5b | |||
| a2631ed38e | |||
| 8ffd8c3505 | |||
| f74df0433b | |||
| 93fa98245b | |||
| e7727aea9e | |||
| 884eb9f7ee | |||
| 912b75f8ac | |||
| 3bec60b80c | |||
| 07b08ef67e | |||
| 83f9475584 | |||
| 1ad5456fe0 | |||
| bf1936de34 | |||
| 827dd1b044 | |||
| b2028d187d | |||
| 1e1b372d07 | |||
| bd5a8567ef | |||
| fc46532955 | |||
| 5f29a9edf4 | |||
| 91bc65c7da | |||
| a91db7f7a6 | |||
| e6fe3ada57 | |||
| 354683296b | |||
| 5237439e29 | |||
| 049b3136e8 | |||
| 3d6f5f418d | |||
| a27175d672 | |||
| ff1cd306d8 | |||
| aa7d25600f | |||
| 0bae7ca615 | |||
| 3578b5e337 | |||
| 8464b30485 | |||
| c961964647 | |||
| dd7ac4c53a | |||
| 4601a0246f | |||
| 25c0eca414 | |||
| ca8be1ee4a | |||
| 1611a3aa70 | |||
| b434ebf3ad | |||
| d2ed99e201 | |||
| 06976c4551 | |||
| 626643104e | |||
| c5683dd24c | |||
| 6589464ddf | |||
| e27c264081 | |||
| 3a96e1f109 | |||
| c4629c8c2d | |||
| adea3af8d7 | |||
| ec516e95fa | |||
| 481fd9f88a | |||
| 2bbaf1234c | |||
| 690c25215d | |||
| b72243c166 | |||
| 8f2e799615 | |||
| 8ecf262457 | |||
| 7fb785901e | |||
| 5dc9b93916 | |||
| 1241bc3e89 | |||
| c5ed3452d2 | |||
| c2e6e44714 | |||
| 6e7e575a18 | |||
| f43b545bdd | |||
| 12a04b118f | |||
| 8b1e2ce279 | |||
| fb0f106afe | |||
| dd423f43de | |||
| f2bd3fdf19 | |||
| 8ad52f0fcc | |||
| 5a3736f1ee | |||
| 54bbfa5e89 | |||
| 3933db2c91 | |||
| dc8f0987e1 | |||
| 644f0fe6c3 | |||
| 87b04b0fc3 | |||
| 455403a4a4 | |||
| 547b990041 | |||
| fcf8f2a704 | |||
| 496e798cc7 | |||
| e01b3569d5 | |||
| a75b68c19c | |||
| 34f04c53fc | |||
| 2b87ab32ff | |||
| d20f6cb45b | |||
| 48c9fb0ddf | |||
| c609cc23eb | |||
| 3566002b0b | |||
| 140d7ff721 | |||
| 7349e1d8c8 | |||
| bf5a62298c | |||
| 2972774d87 | |||
| 75d455ac8f | |||
| 64b6a8c5db | |||
| 8947da57af | |||
| 16f09fff63 | |||
| 14eb667fc8 | |||
| 79bf8d6dd3 | |||
| 14d33f0fcc | |||
| 4dcec4855e | |||
| efb4a710c8 | |||
| 8e9e429a91 |
@@ -11,8 +11,6 @@ on:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-main-image:
|
||||
@@ -28,6 +26,15 @@ jobs:
|
||||
- linux/arm64
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
@@ -116,6 +123,15 @@ jobs:
|
||||
- linux/arm64
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
@@ -207,6 +223,15 @@ jobs:
|
||||
- linux/arm64
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
@@ -289,6 +314,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-main-image ]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -335,6 +369,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-cuda-image ]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -382,6 +425,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-ollama-image ]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -5,6 +5,72 @@ 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.5] - 2024-06-16
|
||||
|
||||
### Added
|
||||
|
||||
- **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion.
|
||||
- **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input.
|
||||
- **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction.
|
||||
- **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items.
|
||||
- **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc.
|
||||
- **🧠 Editable Memories**: Adds the capability to modify memories.
|
||||
- **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel.
|
||||
- **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents).
|
||||
- **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization.
|
||||
- **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface.
|
||||
- **🌐 Tavily Web Search**: Includes Tavily as a web search provider option.
|
||||
- **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication.
|
||||
- **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed.
|
||||
- **🌐 Enhanced Translations**: Improved Chinese and Swedish translations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout.
|
||||
- **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze.
|
||||
|
||||
## [0.3.4] - 2024-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔒 Mixed Content with HTTPS Issue**: Resolved a problem where mixed content (HTTP and HTTPS) was causing security warnings and blocking resources on HTTPS sites.
|
||||
- **🔍 Web Search Issue**: Addressed the problem where web search functionality was not working correctly. The 'ENABLE_RAG_LOCAL_WEB_FETCH' option has been reintroduced to restore proper web searching capabilities.
|
||||
- **💾 RAG Template Not Being Saved**: Fixed an issue where the RAG template was not being saved correctly, ensuring your custom templates are now preserved as expected.
|
||||
|
||||
## [0.3.3] - 2024-06-12
|
||||
|
||||
### Added
|
||||
|
||||
- **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. We’ve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLM’s capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages.
|
||||
- **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options.
|
||||
- **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading.
|
||||
- **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability.
|
||||
- **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors.
|
||||
|
||||
## [0.3.2] - 2024-06-10
|
||||
|
||||
### Added
|
||||
|
||||
- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
|
||||
- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
|
||||
- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
|
||||
- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
|
||||
- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
|
||||
|
||||
## [0.3.1] - 2024-06-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
|
||||
|
||||
## [0.3.0] - 2024-06-09
|
||||
|
||||
### Added
|
||||
|
||||
@@ -29,11 +29,15 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-
|
||||
|
||||
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
||||
|
||||
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
|
||||
|
||||
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
|
||||
|
||||
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
|
||||
|
||||
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
||||
|
||||
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
|
||||
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo` and `TavilySearch` and inject the results directly into your chat experience.
|
||||
|
||||
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
||||
|
||||
@@ -156,7 +160,7 @@ Check our Migration Guide available in our [Open WebUI Documentation](https://do
|
||||
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:dev
|
||||
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
|
||||
```
|
||||
|
||||
## What's Next? 🌟
|
||||
|
||||
@@ -18,6 +18,10 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
### Error on Slow Reponses for Ollama
|
||||
|
||||
Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
|
||||
|
||||
### General Connection Errors
|
||||
|
||||
**Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
|
||||
|
||||
@@ -37,6 +37,10 @@ from config import (
|
||||
ENABLE_IMAGE_GENERATION,
|
||||
AUTOMATIC1111_BASE_URL,
|
||||
COMFYUI_BASE_URL,
|
||||
COMFYUI_CFG_SCALE,
|
||||
COMFYUI_SAMPLER,
|
||||
COMFYUI_SCHEDULER,
|
||||
COMFYUI_SD3,
|
||||
IMAGES_OPENAI_API_BASE_URL,
|
||||
IMAGES_OPENAI_API_KEY,
|
||||
IMAGE_GENERATION_MODEL,
|
||||
@@ -78,6 +82,10 @@ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
|
||||
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
||||
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
||||
app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
|
||||
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
|
||||
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
|
||||
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
@@ -457,6 +465,18 @@ def generate_image(
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
if app.state.config.COMFYUI_CFG_SCALE:
|
||||
data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
|
||||
|
||||
if app.state.config.COMFYUI_SAMPLER is not None:
|
||||
data["sampler"] = app.state.config.COMFYUI_SAMPLER
|
||||
|
||||
if app.state.config.COMFYUI_SCHEDULER is not None:
|
||||
data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
|
||||
|
||||
if app.state.config.COMFYUI_SD3 is not None:
|
||||
data["sd3"] = app.state.config.COMFYUI_SD3
|
||||
|
||||
data = ImageGenerationPayload(**data)
|
||||
|
||||
res = comfyui_generate_image(
|
||||
|
||||
@@ -190,6 +190,10 @@ class ImageGenerationPayload(BaseModel):
|
||||
width: int
|
||||
height: int
|
||||
n: int = 1
|
||||
cfg_scale: Optional[float] = None
|
||||
sampler: Optional[str] = None
|
||||
scheduler: Optional[str] = None
|
||||
sd3: Optional[bool] = None
|
||||
|
||||
|
||||
def comfyui_generate_image(
|
||||
@@ -199,6 +203,18 @@ def comfyui_generate_image(
|
||||
|
||||
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
||||
|
||||
if payload.cfg_scale:
|
||||
comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale
|
||||
|
||||
if payload.sampler:
|
||||
comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler
|
||||
|
||||
if payload.scheduler:
|
||||
comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler
|
||||
|
||||
if payload.sd3:
|
||||
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
|
||||
|
||||
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
||||
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
||||
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
||||
|
||||
@@ -46,6 +46,7 @@ from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
OLLAMA_BASE_URLS,
|
||||
ENABLE_OLLAMA_API,
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
ENABLE_MODEL_FILTER,
|
||||
MODEL_FILTER_LIST,
|
||||
UPLOAD_DIR,
|
||||
@@ -154,7 +155,9 @@ async def cleanup_response(
|
||||
async def post_streaming_url(url: str, payload: str):
|
||||
r = None
|
||||
try:
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
session = aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
)
|
||||
r = await session.post(url, data=payload)
|
||||
r.raise_for_status()
|
||||
|
||||
@@ -751,6 +754,14 @@ async def generate_chat_completion(
|
||||
if model_info.params.get("num_ctx", None):
|
||||
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
||||
|
||||
if model_info.params.get("num_batch", None):
|
||||
payload["options"]["num_batch"] = model_info.params.get(
|
||||
"num_batch", None
|
||||
)
|
||||
|
||||
if model_info.params.get("num_keep", None):
|
||||
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
|
||||
|
||||
if model_info.params.get("repeat_last_n", None):
|
||||
payload["options"]["repeat_last_n"] = model_info.params.get(
|
||||
"repeat_last_n", None
|
||||
@@ -839,8 +850,7 @@ async def generate_chat_completion(
|
||||
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
print(payload)
|
||||
log.debug(payload)
|
||||
|
||||
return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
|
||||
|
||||
|
||||
@@ -430,13 +430,11 @@ async def generate_chat_completion(
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
print(payload)
|
||||
log.debug(payload)
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
print(payload)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
@@ -8,13 +8,15 @@ from fastapi import (
|
||||
Form,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import requests
|
||||
import os, shutil, logging, re
|
||||
from datetime import datetime
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Union, Sequence
|
||||
from typing import List, Union, Sequence, Iterator, Any
|
||||
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langchain_community.document_loaders import (
|
||||
WebBaseLoader,
|
||||
@@ -69,7 +71,9 @@ from apps.rag.search.main import SearchResult
|
||||
from apps.rag.search.searxng import search_searxng
|
||||
from apps.rag.search.serper import search_serper
|
||||
from apps.rag.search.serpstack import search_serpstack
|
||||
|
||||
from apps.rag.search.serply import search_serply
|
||||
from apps.rag.search.duckduckgo import search_duckduckgo
|
||||
from apps.rag.search.tavily import search_tavily
|
||||
|
||||
from utils.misc import (
|
||||
calculate_sha256,
|
||||
@@ -115,6 +119,8 @@ from config import (
|
||||
SERPSTACK_API_KEY,
|
||||
SERPSTACK_HTTPS,
|
||||
SERPER_API_KEY,
|
||||
SERPLY_API_KEY,
|
||||
TAVILY_API_KEY,
|
||||
RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
@@ -167,6 +173,8 @@ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
|
||||
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
||||
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
||||
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
||||
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
||||
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
||||
|
||||
@@ -394,6 +402,8 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
||||
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
||||
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
||||
"serper_api_key": app.state.config.SERPER_API_KEY,
|
||||
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
||||
"tavily_api_key": app.state.config.TAVILY_API_KEY,
|
||||
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
},
|
||||
@@ -421,6 +431,8 @@ class WebSearchConfig(BaseModel):
|
||||
serpstack_api_key: Optional[str] = None
|
||||
serpstack_https: Optional[bool] = None
|
||||
serper_api_key: Optional[str] = None
|
||||
serply_api_key: Optional[str] = None
|
||||
tavily_api_key: Optional[str] = None
|
||||
result_count: Optional[int] = None
|
||||
concurrent_requests: Optional[int] = None
|
||||
|
||||
@@ -471,6 +483,8 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
||||
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
|
||||
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
|
||||
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
|
||||
app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
|
||||
app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
|
||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
||||
form_data.web.search.concurrent_requests
|
||||
@@ -499,6 +513,8 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
||||
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
||||
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
||||
"serper_api_key": app.state.config.SERPER_API_KEY,
|
||||
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
||||
"tavily_api_key": app.state.config.TAVILY_API_KEY,
|
||||
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
},
|
||||
@@ -695,7 +711,7 @@ def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
|
||||
# Check if the URL is valid
|
||||
if not validate_url(url):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
return WebBaseLoader(
|
||||
return SafeWebBaseLoader(
|
||||
url,
|
||||
verify_ssl=verify_ssl,
|
||||
requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
@@ -746,7 +762,8 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
|
||||
- BRAVE_SEARCH_API_KEY
|
||||
- SERPSTACK_API_KEY
|
||||
- SERPER_API_KEY
|
||||
|
||||
- SERPLY_API_KEY
|
||||
- TAVILY_API_KEY
|
||||
Args:
|
||||
query (str): The query to search for
|
||||
"""
|
||||
@@ -804,6 +821,26 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
|
||||
)
|
||||
else:
|
||||
raise Exception("No SERPER_API_KEY found in environment variables")
|
||||
elif engine == "serply":
|
||||
if app.state.config.SERPLY_API_KEY:
|
||||
return search_serply(
|
||||
app.state.config.SERPLY_API_KEY,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception("No SERPLY_API_KEY found in environment variables")
|
||||
elif engine == "duckduckgo":
|
||||
return search_duckduckgo(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
|
||||
elif engine == "tavily":
|
||||
if app.state.config.TAVILY_API_KEY:
|
||||
return search_tavily(
|
||||
app.state.config.TAVILY_API_KEY,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception("No TAVILY_API_KEY found in environment variables")
|
||||
else:
|
||||
raise Exception("No search engine API key found in environment variables")
|
||||
|
||||
@@ -811,6 +848,9 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
|
||||
@app.post("/web/search")
|
||||
def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
|
||||
try:
|
||||
logging.info(
|
||||
f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
|
||||
)
|
||||
web_results = search_web(
|
||||
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
|
||||
)
|
||||
@@ -1221,6 +1261,33 @@ def reset(user=Depends(get_admin_user)) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class SafeWebBaseLoader(WebBaseLoader):
|
||||
"""WebBaseLoader with enhanced error handling for URLs."""
|
||||
|
||||
def lazy_load(self) -> Iterator[Document]:
|
||||
"""Lazy load text from the url(s) in web_path with error handling."""
|
||||
for path in self.web_paths:
|
||||
try:
|
||||
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
|
||||
text = soup.get_text(**self.bs_get_text_kwargs)
|
||||
|
||||
# Build metadata
|
||||
metadata = {"source": path}
|
||||
if title := soup.find("title"):
|
||||
metadata["title"] = title.get_text()
|
||||
if description := soup.find("meta", attrs={"name": "description"}):
|
||||
metadata["description"] = description.get(
|
||||
"content", "No description found."
|
||||
)
|
||||
if html := soup.find("html"):
|
||||
metadata["language"] = html.get("lang", "No language found.")
|
||||
|
||||
yield Document(page_content=text, metadata=metadata)
|
||||
except Exception as e:
|
||||
# Log the error and continue with the next URL
|
||||
log.error(f"Error loading {path}: {e}")
|
||||
|
||||
|
||||
if ENV == "dev":
|
||||
|
||||
@app.get("/ef")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from duckduckgo_search import DDGS
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_duckduckgo(query: str, count: int) -> list[SearchResult]:
|
||||
"""
|
||||
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
||||
Args:
|
||||
query (str): The query to search for
|
||||
count (int): The number of results to return
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of search results
|
||||
"""
|
||||
# Use the DDGS context manager to create a DDGS object
|
||||
with DDGS() as ddgs:
|
||||
# Use the ddgs.text() method to perform the search
|
||||
ddgs_gen = ddgs.text(
|
||||
query, safesearch="moderate", max_results=count, backend="api"
|
||||
)
|
||||
# Check if there are search results
|
||||
if ddgs_gen:
|
||||
# Convert the search results into a list
|
||||
search_results = [r for r in ddgs_gen]
|
||||
|
||||
# Create an empty list to store the SearchResult objects
|
||||
results = []
|
||||
# Iterate over each search result
|
||||
for result in search_results:
|
||||
# Create a SearchResult object and append it to the results list
|
||||
results.append(
|
||||
SearchResult(
|
||||
link=result["href"],
|
||||
title=result.get("title"),
|
||||
snippet=result.get("body"),
|
||||
)
|
||||
)
|
||||
print(results)
|
||||
# Return the list of search results
|
||||
return results
|
||||
@@ -25,6 +25,7 @@ def search_searxng(
|
||||
|
||||
Keyword Args:
|
||||
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
||||
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
|
||||
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
||||
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||
|
||||
@@ -37,6 +38,7 @@ def search_searxng(
|
||||
|
||||
# Default values for optional parameters are provided as empty strings or None when not specified.
|
||||
language = kwargs.get("language", "en-US")
|
||||
safesearch = kwargs.get("safesearch", "1")
|
||||
time_range = kwargs.get("time_range", "")
|
||||
categories = "".join(kwargs.get("categories", []))
|
||||
|
||||
@@ -44,6 +46,7 @@ def search_searxng(
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"pageno": 1,
|
||||
"safesearch": safesearch,
|
||||
"language": language,
|
||||
"time_range": time_range,
|
||||
"categories": categories,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_serply(
|
||||
api_key: str,
|
||||
query: str,
|
||||
count: int,
|
||||
hl: str = "us",
|
||||
limit: int = 10,
|
||||
device_type: str = "desktop",
|
||||
proxy_location: str = "US",
|
||||
) -> list[SearchResult]:
|
||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A serply.io API key
|
||||
query (str): The query to search for
|
||||
hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
|
||||
limit (int): The maximum number of results to return [10-100, defaults to 10]
|
||||
"""
|
||||
log.info("Searching with Serply")
|
||||
|
||||
url = "https://api.serply.io/v1/search/"
|
||||
|
||||
query_payload = {
|
||||
"q": query,
|
||||
"language": "en",
|
||||
"num": limit,
|
||||
"gl": proxy_location.upper(),
|
||||
"hl": hl.lower(),
|
||||
}
|
||||
|
||||
url = f"{url}{urlencode(query_payload)}"
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"X-User-Agent": device_type,
|
||||
"User-Agent": "open-webui",
|
||||
"X-Proxy-Location": proxy_location,
|
||||
}
|
||||
|
||||
response = requests.request("GET", url, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
log.info(f"results from serply search: {json_response}")
|
||||
|
||||
results = sorted(
|
||||
json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
|
||||
)
|
||||
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["link"],
|
||||
title=result.get("title"),
|
||||
snippet=result.get("description"),
|
||||
)
|
||||
for result in results[:count]
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
|
||||
"""Search using Tavily's Search API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A Tavily Search API key
|
||||
query (str): The query to search for
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of search results
|
||||
"""
|
||||
url = "https://api.tavily.com/search"
|
||||
data = {"query": query, "api_key": api_key}
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
|
||||
raw_search_results = json_response.get("results", [])
|
||||
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["url"],
|
||||
title=result.get("title", ""),
|
||||
snippet=result.get("content"),
|
||||
)
|
||||
for result in raw_search_results[:count]
|
||||
]
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"ads": [],
|
||||
"ads_count": 0,
|
||||
"answers": [],
|
||||
"results": [
|
||||
{
|
||||
"title": "Apple",
|
||||
"link": "https://www.apple.com/",
|
||||
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "AppleApplehttps://www.apple.com",
|
||||
"href": "https://www.apple.com/"
|
||||
}
|
||||
],
|
||||
"cite": {},
|
||||
"subdomains": [
|
||||
{
|
||||
"title": "Support",
|
||||
"link": "https://support.apple.com/",
|
||||
"description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair"
|
||||
},
|
||||
{
|
||||
"title": "Store",
|
||||
"link": "https://www.apple.com/store",
|
||||
"description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..."
|
||||
},
|
||||
{
|
||||
"title": "Mac",
|
||||
"link": "https://www.apple.com/mac/",
|
||||
"description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini"
|
||||
},
|
||||
{
|
||||
"title": "iPad",
|
||||
"link": "https://www.apple.com/ipad/",
|
||||
"description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..."
|
||||
},
|
||||
{
|
||||
"title": "Watch",
|
||||
"link": "https://www.apple.com/watch/",
|
||||
"description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..."
|
||||
}
|
||||
],
|
||||
"realPosition": 1
|
||||
},
|
||||
{
|
||||
"title": "Apple",
|
||||
"link": "https://www.apple.com/",
|
||||
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "AppleApplehttps://www.apple.com",
|
||||
"href": "https://www.apple.com/"
|
||||
}
|
||||
],
|
||||
"cite": {},
|
||||
"realPosition": 2
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc.",
|
||||
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
|
||||
"description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc",
|
||||
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "History",
|
||||
"href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "List of Apple products",
|
||||
"href": "https://en.wikipedia.org/wiki/List_of_Apple_products"
|
||||
},
|
||||
{
|
||||
"text": "Litigation involving Apple Inc.",
|
||||
"href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "Apple Park",
|
||||
"href": "https://en.wikipedia.org/wiki/Apple_Park"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://en.wikipedia.org › wiki › Apple_Inc",
|
||||
"span": " › wiki › Apple_Inc"
|
||||
},
|
||||
"realPosition": 3
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. (AAPL) Company Profile & Facts",
|
||||
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
|
||||
"description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile",
|
||||
"href": "https://finance.yahoo.com/quote/AAPL/profile/"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://finance.yahoo.com › quote › AAPL › profile",
|
||||
"span": " › quote › AAPL › profile"
|
||||
},
|
||||
"realPosition": 4
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc - Company Profile and News",
|
||||
"link": "https://www.bloomberg.com/profile/company/AAPL:US",
|
||||
"description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US",
|
||||
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://www.bloomberg.com › company › AAPL:US",
|
||||
"span": " › company › AAPL:US"
|
||||
},
|
||||
"realPosition": 5
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. | History, Products, Headquarters, & Facts",
|
||||
"link": "https://www.britannica.com/money/Apple-Inc",
|
||||
"description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc",
|
||||
"href": "https://www.britannica.com/money/Apple-Inc"
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"href": "https://www.britannica.com/money/Apple-Inc"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://www.britannica.com › money › Apple-Inc",
|
||||
"span": " › money › Apple-Inc"
|
||||
},
|
||||
"realPosition": 6
|
||||
}
|
||||
],
|
||||
"shopping_ads": [],
|
||||
"places": [
|
||||
{
|
||||
"title": "Apple Inc."
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc"
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc"
|
||||
}
|
||||
],
|
||||
"related_searches": {
|
||||
"images": [],
|
||||
"text": [
|
||||
{
|
||||
"title": "apple inc full form",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple company history",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple store",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple id",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple inc industry",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE"
|
||||
},
|
||||
{
|
||||
"title": "apple login",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"image_results": [],
|
||||
"carousel": [],
|
||||
"total": 2450000000,
|
||||
"knowledge_graph": "",
|
||||
"related_questions": [
|
||||
"What does the Apple Inc do?",
|
||||
"Why did Apple change to Apple Inc?",
|
||||
"Who owns Apple Inc.?",
|
||||
"What is Apple Inc best known for?"
|
||||
],
|
||||
"carousel_count": 0,
|
||||
"ts": 2.491065263748169,
|
||||
"device_type": null
|
||||
}
|
||||
@@ -236,10 +236,9 @@ def get_embedding_function(
|
||||
return lambda query: generate_multiple(query, func)
|
||||
|
||||
|
||||
def rag_messages(
|
||||
def get_rag_context(
|
||||
docs,
|
||||
messages,
|
||||
template,
|
||||
embedding_function,
|
||||
k,
|
||||
reranking_function,
|
||||
@@ -318,16 +317,7 @@ def rag_messages(
|
||||
|
||||
context_string = context_string.strip()
|
||||
|
||||
ra_content = rag_template(
|
||||
template=template,
|
||||
context=context_string,
|
||||
query=query,
|
||||
)
|
||||
|
||||
log.debug(f"ra_content: {ra_content}")
|
||||
messages = add_or_update_system_message(ra_content, messages)
|
||||
|
||||
return messages, citations
|
||||
return context_string, citations
|
||||
|
||||
|
||||
def get_model_path(model: str, update_model: bool = False):
|
||||
|
||||
@@ -19,8 +19,6 @@ TIMEOUT_DURATION = 3
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ, auth):
|
||||
print("connect ", sid)
|
||||
|
||||
user = None
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
@@ -37,7 +35,6 @@ async def connect(sid, environ, auth):
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
print(len(set(USER_POOL)))
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
@@ -64,13 +61,11 @@ async def user_join(sid, data):
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
print(len(set(USER_POOL)))
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
async def user_count(sid):
|
||||
print("user-count", sid)
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
@@ -79,14 +74,12 @@ def get_models_in_use():
|
||||
models_in_use = []
|
||||
for model_id, data in USAGE_POOL.items():
|
||||
models_in_use.append(model_id)
|
||||
print(f"Models in use: {models_in_use}")
|
||||
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
print(f'Received "usage" event from {sid}: {data}')
|
||||
|
||||
model_id = data["model"]
|
||||
|
||||
@@ -114,7 +107,6 @@ async def usage(sid, data):
|
||||
|
||||
async def remove_after_timeout(sid, model_id):
|
||||
try:
|
||||
print("remove_after_timeout", sid, model_id)
|
||||
await asyncio.sleep(TIMEOUT_DURATION)
|
||||
if model_id in USAGE_POOL:
|
||||
print(USAGE_POOL[model_id]["sids"])
|
||||
@@ -124,7 +116,6 @@ async def remove_after_timeout(sid, model_id):
|
||||
if len(USAGE_POOL[model_id]["sids"]) == 0:
|
||||
del USAGE_POOL[model_id]
|
||||
|
||||
print(f"Removed usage data for {model_id} due to timeout")
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
except asyncio.CancelledError:
|
||||
@@ -143,9 +134,6 @@ async def disconnect(sid):
|
||||
if len(USER_POOL[user_id]) == 0:
|
||||
del USER_POOL[user_id]
|
||||
|
||||
print(f"user {user_id} disconnected with session ID {sid}")
|
||||
print(USER_POOL)
|
||||
|
||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||
else:
|
||||
print(f"Unknown session ID {sid} disconnected")
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class Tool(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
|
||||
name = pw.TextField()
|
||||
content = pw.TextField()
|
||||
specs = pw.TextField()
|
||||
|
||||
meta = pw.TextField()
|
||||
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = "tool"
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model("tool")
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Adding fields info to the 'user' table
|
||||
migrator.add_fields("user", info=pw.TextField(null=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
# Remove the settings field
|
||||
migrator.remove_fields("user", "info")
|
||||
@@ -6,6 +6,7 @@ from apps.webui.routers import (
|
||||
users,
|
||||
chats,
|
||||
documents,
|
||||
tools,
|
||||
models,
|
||||
prompts,
|
||||
configs,
|
||||
@@ -24,10 +25,11 @@ from config import (
|
||||
USER_PERMISSIONS,
|
||||
WEBHOOK_URL,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||
JWT_EXPIRES_IN,
|
||||
WEBUI_BANNERS,
|
||||
AppConfig,
|
||||
ENABLE_COMMUNITY_SHARING,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
@@ -38,6 +40,8 @@ app.state.config = AppConfig()
|
||||
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
||||
|
||||
|
||||
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||
@@ -54,7 +58,7 @@ app.state.config.BANNERS = WEBUI_BANNERS
|
||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||
|
||||
app.state.MODELS = {}
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
app.state.TOOLS = {}
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
@@ -70,6 +74,7 @@ app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||
|
||||
app.include_router(documents.router, prefix="/documents", tags=["documents"])
|
||||
app.include_router(tools.router, prefix="/tools", tags=["tools"])
|
||||
app.include_router(models.router, prefix="/models", tags=["models"])
|
||||
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||
|
||||
@@ -65,6 +65,20 @@ class MemoriesTable:
|
||||
else:
|
||||
return None
|
||||
|
||||
def update_memory_by_id(
|
||||
self,
|
||||
id: str,
|
||||
content: str,
|
||||
) -> Optional[MemoryModel]:
|
||||
try:
|
||||
memory = Memory.get(Memory.id == id)
|
||||
memory.content = content
|
||||
memory.updated_at = int(time.time())
|
||||
memory.save()
|
||||
return MemoryModel(**model_to_dict(memory))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_memories(self) -> List[MemoryModel]:
|
||||
try:
|
||||
memories = Memory.select()
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
from pydantic import BaseModel
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
import time
|
||||
import logging
|
||||
from apps.webui.internal.db import DB, JSONField
|
||||
|
||||
import json
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
####################
|
||||
# Tools DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Tool(Model):
|
||||
id = CharField(unique=True)
|
||||
user_id = CharField()
|
||||
name = TextField()
|
||||
content = TextField()
|
||||
specs = JSONField()
|
||||
meta = JSONField()
|
||||
updated_at = BigIntegerField()
|
||||
created_at = BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class ToolMeta(BaseModel):
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ToolModel(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
content: str
|
||||
specs: List[dict]
|
||||
meta: ToolMeta
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
meta: ToolMeta
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
class ToolForm(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
content: str
|
||||
meta: ToolMeta
|
||||
|
||||
|
||||
class ToolsTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.db.create_tables([Tool])
|
||||
|
||||
def insert_new_tool(
|
||||
self, user_id: str, form_data: ToolForm, specs: List[dict]
|
||||
) -> Optional[ToolModel]:
|
||||
tool = ToolModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
"specs": specs,
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = Tool.create(**tool.model_dump())
|
||||
if result:
|
||||
return tool
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error creating tool: {e}")
|
||||
return None
|
||||
|
||||
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
||||
try:
|
||||
tool = Tool.get(Tool.id == id)
|
||||
return ToolModel(**model_to_dict(tool))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_tools(self) -> List[ToolModel]:
|
||||
return [ToolModel(**model_to_dict(tool)) for tool in Tool.select()]
|
||||
|
||||
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
||||
try:
|
||||
query = Tool.update(
|
||||
**updated,
|
||||
updated_at=int(time.time()),
|
||||
).where(Tool.id == id)
|
||||
query.execute()
|
||||
|
||||
tool = Tool.get(Tool.id == id)
|
||||
return ToolModel(**model_to_dict(tool))
|
||||
except:
|
||||
return None
|
||||
|
||||
def delete_tool_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
query = Tool.delete().where((Tool.id == id))
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Tools = ToolsTable(DB)
|
||||
@@ -26,6 +26,7 @@ class User(Model):
|
||||
|
||||
api_key = CharField(null=True, unique=True)
|
||||
settings = JSONField(null=True)
|
||||
info = JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
@@ -50,6 +51,7 @@ class UserModel(BaseModel):
|
||||
|
||||
api_key: Optional[str] = None
|
||||
settings: Optional[UserSettings] = None
|
||||
info: Optional[dict] = None
|
||||
|
||||
|
||||
####################
|
||||
|
||||
@@ -33,7 +33,11 @@ from utils.utils import (
|
||||
from utils.misc import parse_duration, validate_email_format
|
||||
from utils.webhook import post_webhook
|
||||
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
from config import (
|
||||
WEBUI_AUTH,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -110,11 +114,16 @@ async def signin(request: Request, form_data: SigninForm):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
|
||||
|
||||
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
|
||||
trusted_name = trusted_email
|
||||
if WEBUI_AUTH_TRUSTED_NAME_HEADER:
|
||||
trusted_name = request.headers.get(
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email
|
||||
)
|
||||
if not Users.get_user_by_email(trusted_email.lower()):
|
||||
await signup(
|
||||
request,
|
||||
SignupForm(
|
||||
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
|
||||
email=trusted_email, password=str(uuid.uuid4()), name=trusted_name
|
||||
),
|
||||
)
|
||||
user = Auths.authenticate_user_by_trusted_header(trusted_email)
|
||||
|
||||
@@ -161,7 +161,7 @@ async def get_archived_session_user_chat_list(
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/archive/all", response_model=List[ChatTitleIdResponse])
|
||||
@router.post("/archive/all", response_model=bool)
|
||||
async def archive_all_chats(user=Depends(get_current_user)):
|
||||
return Chats.archive_all_chats_by_user_id(user.id)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/name/{name}", response_model=Optional[DocumentResponse])
|
||||
@router.get("/doc", response_model=Optional[DocumentResponse])
|
||||
async def get_doc_by_name(name: str, user=Depends(get_current_user)):
|
||||
doc = Documents.get_doc_by_name(name)
|
||||
|
||||
@@ -105,7 +105,7 @@ class TagDocumentForm(BaseModel):
|
||||
tags: List[dict]
|
||||
|
||||
|
||||
@router.post("/name/{name}/tags", response_model=Optional[DocumentResponse])
|
||||
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
|
||||
async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_current_user)):
|
||||
doc = Documents.update_doc_content_by_name(form_data.name, {"tags": form_data.tags})
|
||||
|
||||
@@ -128,7 +128,7 @@ async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_current_u
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/name/{name}/update", response_model=Optional[DocumentResponse])
|
||||
@router.post("/doc/update", response_model=Optional[DocumentResponse])
|
||||
async def update_doc_by_name(
|
||||
name: str, form_data: DocumentUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
@@ -152,7 +152,7 @@ async def update_doc_by_name(
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/name/{name}/delete", response_model=bool)
|
||||
@router.delete("/doc/delete", response_model=bool)
|
||||
async def delete_doc_by_name(name: str, user=Depends(get_admin_user)):
|
||||
result = Documents.delete_doc_by_name(name)
|
||||
return result
|
||||
|
||||
@@ -44,6 +44,10 @@ class AddMemoryForm(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class MemoryUpdateModel(BaseModel):
|
||||
content: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/add", response_model=Optional[MemoryModel])
|
||||
async def add_memory(
|
||||
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
|
||||
@@ -62,6 +66,34 @@ async def add_memory(
|
||||
return memory
|
||||
|
||||
|
||||
@router.post("/{memory_id}/update", response_model=Optional[MemoryModel])
|
||||
async def update_memory_by_id(
|
||||
memory_id: str,
|
||||
request: Request,
|
||||
form_data: MemoryUpdateModel,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
memory = Memories.update_memory_by_id(memory_id, form_data.content)
|
||||
if memory is None:
|
||||
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}
|
||||
],
|
||||
)
|
||||
|
||||
return memory
|
||||
|
||||
|
||||
############################
|
||||
# QueryMemory
|
||||
############################
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
||||
from apps.webui.utils import load_toolkit_module_by_id
|
||||
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
from utils.tools import get_tools_specs
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from importlib import util
|
||||
import os
|
||||
|
||||
from config import DATA_DIR
|
||||
|
||||
|
||||
TOOLS_DIR = f"{DATA_DIR}/tools"
|
||||
os.makedirs(TOOLS_DIR, exist_ok=True)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetToolkits
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ToolResponse])
|
||||
async def get_toolkits(user=Depends(get_current_user)):
|
||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||
return toolkits
|
||||
|
||||
|
||||
############################
|
||||
# ExportToolKits
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/export", response_model=List[ToolModel])
|
||||
async def get_toolkits(user=Depends(get_admin_user)):
|
||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||
return toolkits
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewToolKit
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/create", response_model=Optional[ToolResponse])
|
||||
async def create_new_toolkit(
|
||||
request: Request, form_data: ToolForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if not form_data.id.isidentifier():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Only alphanumeric characters and underscores are allowed in the id",
|
||||
)
|
||||
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||
if toolkit == None:
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
tool_file.write(form_data.content)
|
||||
|
||||
toolkit_module = load_toolkit_module_by_id(form_data.id)
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
TOOLS[form_data.id] = toolkit_module
|
||||
|
||||
specs = get_tools_specs(TOOLS[form_data.id])
|
||||
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ID_TAKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetToolkitById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/id/{id}", response_model=Optional[ToolModel])
|
||||
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
|
||||
toolkit = Tools.get_tool_by_id(id)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateToolkitById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
|
||||
async def update_toolkit_by_id(
|
||||
request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user)
|
||||
):
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
tool_file.write(form_data.content)
|
||||
|
||||
toolkit_module = load_toolkit_module_by_id(id)
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
TOOLS[id] = toolkit_module
|
||||
|
||||
specs = get_tools_specs(TOOLS[id])
|
||||
|
||||
updated = {
|
||||
**form_data.model_dump(exclude={"id"}),
|
||||
"specs": specs,
|
||||
}
|
||||
|
||||
print(updated)
|
||||
toolkit = Tools.update_tool_by_id(id, updated)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# DeleteToolkitById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/id/{id}/delete", response_model=bool)
|
||||
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
|
||||
result = Tools.delete_tool_by_id(id)
|
||||
|
||||
if result:
|
||||
TOOLS = request.app.state.TOOLS
|
||||
if id in TOOLS:
|
||||
del TOOLS[id]
|
||||
|
||||
# delete the toolkit file
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||
os.remove(toolkit_path)
|
||||
|
||||
return result
|
||||
@@ -115,6 +115,52 @@ async def update_user_settings_by_session_user(
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserInfoBySessionUser
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/user/info", response_model=Optional[dict])
|
||||
async def get_user_info_by_session_user(user=Depends(get_verified_user)):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
if user:
|
||||
return user.info
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateUserInfoBySessionUser
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/user/info/update", response_model=Optional[dict])
|
||||
async def update_user_settings_by_session_user(
|
||||
form_data: dict, user=Depends(get_verified_user)
|
||||
):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
if user:
|
||||
if user.info is None:
|
||||
user.info = {}
|
||||
|
||||
user = Users.update_user_by_id(user.id, {"info": {**user.info, **form_data}})
|
||||
if user:
|
||||
return user.info
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserById
|
||||
############################
|
||||
|
||||
@@ -7,6 +7,8 @@ from pydantic import BaseModel
|
||||
|
||||
from fpdf import FPDF
|
||||
import markdown
|
||||
import black
|
||||
|
||||
|
||||
from apps.webui.internal.db import DB
|
||||
from utils.utils import get_admin_user
|
||||
@@ -26,6 +28,21 @@ async def get_gravatar(
|
||||
return get_gravatar_url(email)
|
||||
|
||||
|
||||
class CodeFormatRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
@router.post("/code/format")
|
||||
async def format_code(request: CodeFormatRequest):
|
||||
try:
|
||||
formatted_code = black.format_str(request.code, mode=black.Mode())
|
||||
return {"code": formatted_code}
|
||||
except black.NothingChanged:
|
||||
return {"code": request.code}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
class MarkdownForm(BaseModel):
|
||||
md: str
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from importlib import util
|
||||
import os
|
||||
|
||||
from config import TOOLS_DIR
|
||||
|
||||
|
||||
def load_toolkit_module_by_id(toolkit_id):
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py")
|
||||
spec = util.spec_from_file_location(toolkit_id, toolkit_path)
|
||||
module = util.module_from_spec(spec)
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
print(f"Loaded module: {module.__name__}")
|
||||
if hasattr(module, "Tools"):
|
||||
return module.Tools()
|
||||
else:
|
||||
raise Exception("No Tools class found")
|
||||
except Exception as e:
|
||||
print(f"Error loading module: {toolkit_id}")
|
||||
# Move the file to the error folder
|
||||
os.rename(toolkit_path, f"{toolkit_path}.error")
|
||||
raise e
|
||||
+71
-6
@@ -294,6 +294,7 @@ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||
)
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
|
||||
JWT_EXPIRES_IN = PersistentConfig(
|
||||
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
|
||||
)
|
||||
@@ -308,8 +309,9 @@ frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
|
||||
if frontend_favicon.exists():
|
||||
try:
|
||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||
except PermissionError:
|
||||
logging.error(f"No write permission to {STATIC_DIR / 'favicon.png'}")
|
||||
except Exception as e:
|
||||
logging.error(f"An error occurred: {e}")
|
||||
|
||||
else:
|
||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||
|
||||
@@ -367,6 +369,14 @@ DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
|
||||
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Tools DIR
|
||||
####################################
|
||||
|
||||
TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
|
||||
Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# LITELLM_CONFIG
|
||||
####################################
|
||||
@@ -416,6 +426,14 @@ OLLAMA_API_BASE_URL = os.environ.get(
|
||||
)
|
||||
|
||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "300")
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT == "":
|
||||
AIOHTTP_CLIENT_TIMEOUT = None
|
||||
else:
|
||||
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
||||
|
||||
|
||||
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
||||
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
|
||||
|
||||
@@ -668,16 +686,28 @@ Question:
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
"task.search.prompt_length_threshold",
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
100,
|
||||
int(
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
100,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"task.tools.prompt_template",
|
||||
os.environ.get(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"""Tools: {{TOOLS}}
|
||||
If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_SECRET_KEY
|
||||
####################################
|
||||
@@ -915,6 +945,17 @@ SERPER_API_KEY = PersistentConfig(
|
||||
os.getenv("SERPER_API_KEY", ""),
|
||||
)
|
||||
|
||||
SERPLY_API_KEY = PersistentConfig(
|
||||
"SERPLY_API_KEY",
|
||||
"rag.web.search.serply_api_key",
|
||||
os.getenv("SERPLY_API_KEY", ""),
|
||||
)
|
||||
|
||||
TAVILY_API_KEY = PersistentConfig(
|
||||
"TAVILY_API_KEY",
|
||||
"rag.web.search.tavily_api_key",
|
||||
os.getenv("TAVILY_API_KEY", ""),
|
||||
)
|
||||
|
||||
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
||||
"RAG_WEB_SEARCH_RESULT_COUNT",
|
||||
@@ -967,6 +1008,30 @@ COMFYUI_BASE_URL = PersistentConfig(
|
||||
os.getenv("COMFYUI_BASE_URL", ""),
|
||||
)
|
||||
|
||||
COMFYUI_CFG_SCALE = PersistentConfig(
|
||||
"COMFYUI_CFG_SCALE",
|
||||
"image_generation.comfyui.cfg_scale",
|
||||
os.getenv("COMFYUI_CFG_SCALE", ""),
|
||||
)
|
||||
|
||||
COMFYUI_SAMPLER = PersistentConfig(
|
||||
"COMFYUI_SAMPLER",
|
||||
"image_generation.comfyui.sampler",
|
||||
os.getenv("COMFYUI_SAMPLER", ""),
|
||||
)
|
||||
|
||||
COMFYUI_SCHEDULER = PersistentConfig(
|
||||
"COMFYUI_SCHEDULER",
|
||||
"image_generation.comfyui.scheduler",
|
||||
os.getenv("COMFYUI_SCHEDULER", ""),
|
||||
)
|
||||
|
||||
COMFYUI_SD3 = PersistentConfig(
|
||||
"COMFYUI_SD3",
|
||||
"image_generation.comfyui.sd3",
|
||||
os.environ.get("COMFYUI_SD3", "").lower() == "true",
|
||||
)
|
||||
|
||||
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"IMAGES_OPENAI_API_BASE_URL",
|
||||
"image_generation.openai.api_base_url",
|
||||
|
||||
@@ -32,6 +32,7 @@ class ERROR_MESSAGES(str, Enum):
|
||||
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
|
||||
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
|
||||
|
||||
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
|
||||
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
|
||||
|
||||
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
|
||||
|
||||
+345
-32
@@ -11,6 +11,7 @@ import requests
|
||||
import mimetypes
|
||||
import shutil
|
||||
import os
|
||||
import inspect
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form
|
||||
@@ -47,15 +48,24 @@ from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
from apps.webui.models.models import Models, ModelModel
|
||||
from apps.webui.models.tools import Tools
|
||||
from apps.webui.utils import load_toolkit_module_by_id
|
||||
|
||||
|
||||
from utils.utils import (
|
||||
get_admin_user,
|
||||
get_verified_user,
|
||||
get_current_user,
|
||||
get_http_authorization_cred,
|
||||
)
|
||||
from utils.task import title_generation_template, search_query_generation_template
|
||||
from utils.task import (
|
||||
title_generation_template,
|
||||
search_query_generation_template,
|
||||
tools_function_calling_generation_template,
|
||||
)
|
||||
from utils.misc import get_last_user_message, add_or_update_system_message
|
||||
|
||||
from apps.rag.utils import rag_messages
|
||||
from apps.rag.utils import get_rag_context, rag_template
|
||||
|
||||
from config import (
|
||||
CONFIG_DATA,
|
||||
@@ -82,6 +92,7 @@ from config import (
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
AppConfig,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -148,24 +159,121 @@ app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||
)
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
)
|
||||
|
||||
app.state.MODELS = {}
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
# Custom middleware to add security headers
|
||||
# class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
# async def dispatch(self, request: Request, call_next):
|
||||
# response: Response = await call_next(request)
|
||||
# response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
||||
# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
|
||||
# return response
|
||||
|
||||
async def get_function_call_response(messages, tool_id, template, task_model_id, user):
|
||||
tool = Tools.get_tool_by_id(tool_id)
|
||||
tools_specs = json.dumps(tool.specs, indent=2)
|
||||
content = tools_function_calling_generation_template(template, tools_specs)
|
||||
|
||||
user_message = get_last_user_message(messages)
|
||||
prompt = (
|
||||
"History:\n"
|
||||
+ "\n".join(
|
||||
[
|
||||
f"{message['role'].upper()}: \"\"\"{message['content']}\"\"\""
|
||||
for message in messages[::-1][:4]
|
||||
]
|
||||
)
|
||||
+ f"\nQuery: {user_message}"
|
||||
)
|
||||
|
||||
print(prompt)
|
||||
|
||||
payload = {
|
||||
"model": task_model_id,
|
||||
"messages": [
|
||||
{"role": "system", "content": content},
|
||||
{"role": "user", "content": f"Query: {prompt}"},
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
response = None
|
||||
try:
|
||||
if model["owned_by"] == "ollama":
|
||||
response = await generate_ollama_chat_completion(
|
||||
OpenAIChatCompletionForm(**payload), user=user
|
||||
)
|
||||
else:
|
||||
response = await generate_openai_chat_completion(payload, user=user)
|
||||
|
||||
content = None
|
||||
|
||||
if hasattr(response, "body_iterator"):
|
||||
async for chunk in response.body_iterator:
|
||||
data = json.loads(chunk.decode("utf-8"))
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
|
||||
# Cleanup any remaining background tasks if necessary
|
||||
if response.background is not None:
|
||||
await response.background()
|
||||
else:
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
|
||||
# Parse the function response
|
||||
if content is not None:
|
||||
print(f"content: {content}")
|
||||
result = json.loads(content)
|
||||
print(result)
|
||||
|
||||
# Call the function
|
||||
if "name" in result:
|
||||
if tool_id in webui_app.state.TOOLS:
|
||||
toolkit_module = webui_app.state.TOOLS[tool_id]
|
||||
else:
|
||||
toolkit_module = load_toolkit_module_by_id(tool_id)
|
||||
webui_app.state.TOOLS[tool_id] = toolkit_module
|
||||
|
||||
function = getattr(toolkit_module, result["name"])
|
||||
function_result = None
|
||||
try:
|
||||
# Get the signature of the function
|
||||
sig = inspect.signature(function)
|
||||
# Check if '__user__' is a parameter of the function
|
||||
if "__user__" in sig.parameters:
|
||||
# Call the function with the '__user__' parameter included
|
||||
function_result = function(
|
||||
**{
|
||||
**result["parameters"],
|
||||
"__user__": {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Call the function without modifying the parameters
|
||||
function_result = function(**result["parameters"])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Add the function result to the system prompt
|
||||
if function_result:
|
||||
return function_result
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
|
||||
class RAGMiddleware(BaseHTTPMiddleware):
|
||||
class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
return_citations = False
|
||||
|
||||
@@ -182,35 +290,98 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||
# Parse string to JSON
|
||||
data = json.loads(body_str) if body_str else {}
|
||||
|
||||
user = get_current_user(
|
||||
get_http_authorization_cred(request.headers.get("Authorization"))
|
||||
)
|
||||
|
||||
# Remove the citations from the body
|
||||
return_citations = data.get("citations", False)
|
||||
if "citations" in data:
|
||||
del data["citations"]
|
||||
|
||||
# Example: Add a new key-value pair or modify existing ones
|
||||
# data["modified"] = True # Example modification
|
||||
# Set the task model
|
||||
task_model_id = data["model"]
|
||||
if task_model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama":
|
||||
if (
|
||||
app.state.config.TASK_MODEL
|
||||
and app.state.config.TASK_MODEL in app.state.MODELS
|
||||
):
|
||||
task_model_id = app.state.config.TASK_MODEL
|
||||
else:
|
||||
if (
|
||||
app.state.config.TASK_MODEL_EXTERNAL
|
||||
and app.state.config.TASK_MODEL_EXTERNAL in app.state.MODELS
|
||||
):
|
||||
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||
|
||||
prompt = get_last_user_message(data["messages"])
|
||||
context = ""
|
||||
|
||||
# If tool_ids field is present, call the functions
|
||||
if "tool_ids" in data:
|
||||
print(data["tool_ids"])
|
||||
for tool_id in data["tool_ids"]:
|
||||
print(tool_id)
|
||||
try:
|
||||
response = await get_function_call_response(
|
||||
messages=data["messages"],
|
||||
tool_id=tool_id,
|
||||
template=app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
task_model_id=task_model_id,
|
||||
user=user,
|
||||
)
|
||||
|
||||
if response:
|
||||
context += ("\n" if context != "" else "") + response
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
del data["tool_ids"]
|
||||
|
||||
print(f"tool_context: {context}")
|
||||
|
||||
# If docs field is present, generate RAG completions
|
||||
if "docs" in data:
|
||||
data = {**data}
|
||||
data["messages"], citations = rag_messages(
|
||||
rag_context, citations = get_rag_context(
|
||||
docs=data["docs"],
|
||||
messages=data["messages"],
|
||||
template=rag_app.state.config.RAG_TEMPLATE,
|
||||
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
|
||||
k=rag_app.state.config.TOP_K,
|
||||
reranking_function=rag_app.state.sentence_transformer_rf,
|
||||
r=rag_app.state.config.RELEVANCE_THRESHOLD,
|
||||
hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
)
|
||||
|
||||
if rag_context:
|
||||
context += ("\n" if context != "" else "") + rag_context
|
||||
|
||||
del data["docs"]
|
||||
|
||||
log.debug(
|
||||
f"data['messages']: {data['messages']}, citations: {citations}"
|
||||
log.debug(f"rag_context: {rag_context}, citations: {citations}")
|
||||
|
||||
if context != "":
|
||||
system_prompt = rag_template(
|
||||
rag_app.state.config.RAG_TEMPLATE, context, prompt
|
||||
)
|
||||
|
||||
print(system_prompt)
|
||||
|
||||
data["messages"] = add_or_update_system_message(
|
||||
f"\n{system_prompt}", data["messages"]
|
||||
)
|
||||
|
||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||
|
||||
# Replace the request body with the modified one
|
||||
request._body = modified_body_bytes
|
||||
|
||||
# Set custom header to ensure content-length matches new body length
|
||||
request.headers.__dict__["_list"] = [
|
||||
(b"content-length", str(len(modified_body_bytes)).encode("utf-8")),
|
||||
@@ -253,7 +424,7 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||
yield data
|
||||
|
||||
|
||||
app.add_middleware(RAGMiddleware)
|
||||
app.add_middleware(ChatCompletionMiddleware)
|
||||
|
||||
|
||||
def filter_pipeline(payload, user):
|
||||
@@ -308,13 +479,10 @@ def filter_pipeline(payload, user):
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
return JSONResponse(
|
||||
status_code=r.status_code,
|
||||
content=res,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
if "detail" in res:
|
||||
raise Exception(r.status_code, res["detail"])
|
||||
|
||||
else:
|
||||
pass
|
||||
@@ -325,6 +493,10 @@ def filter_pipeline(payload, user):
|
||||
|
||||
if "title" in payload:
|
||||
del payload["title"]
|
||||
|
||||
if "task" in payload:
|
||||
del payload["task"]
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@@ -346,7 +518,14 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
||||
user = get_current_user(
|
||||
get_http_authorization_cred(request.headers.get("Authorization"))
|
||||
)
|
||||
data = filter_pipeline(data, user)
|
||||
|
||||
try:
|
||||
data = filter_pipeline(data, user)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
|
||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||
# Replace the request body with the modified one
|
||||
@@ -515,6 +694,7 @@ async def get_task_config(user=Depends(get_verified_user)):
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
}
|
||||
|
||||
|
||||
@@ -524,6 +704,7 @@ class TaskConfigForm(BaseModel):
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
|
||||
|
||||
|
||||
@app.post("/api/task/config/update")
|
||||
@@ -539,6 +720,9 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||
)
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||
form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
)
|
||||
|
||||
return {
|
||||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||
@@ -546,6 +730,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
}
|
||||
|
||||
|
||||
@@ -579,7 +764,12 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
|
||||
content = title_generation_template(
|
||||
template, form_data["prompt"], user.model_dump()
|
||||
template,
|
||||
form_data["prompt"],
|
||||
{
|
||||
"name": user.name,
|
||||
"location": user.info.get("location") if user.info else None,
|
||||
},
|
||||
)
|
||||
|
||||
payload = {
|
||||
@@ -591,8 +781,15 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
"title": True,
|
||||
}
|
||||
|
||||
print(payload)
|
||||
payload = filter_pipeline(payload, user)
|
||||
log.debug(payload)
|
||||
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(
|
||||
@@ -638,7 +835,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
|
||||
content = search_query_generation_template(
|
||||
template, form_data["prompt"], user.model_dump()
|
||||
template, form_data["prompt"], {"name": user.name}
|
||||
)
|
||||
|
||||
payload = {
|
||||
@@ -646,10 +843,18 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 30,
|
||||
"task": True,
|
||||
}
|
||||
|
||||
print(payload)
|
||||
payload = filter_pipeline(payload, user)
|
||||
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(
|
||||
@@ -659,6 +864,114 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
return await generate_openai_chat_completion(payload, user=user)
|
||||
|
||||
|
||||
@app.post("/api/task/emoji/completions")
|
||||
async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_emoji")
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||
if app.state.config.TASK_MODEL:
|
||||
task_model_id = app.state.config.TASK_MODEL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
else:
|
||||
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
template = '''
|
||||
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"],
|
||||
{
|
||||
"name": user.name,
|
||||
"location": user.info.get("location") if user.info else None,
|
||||
},
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 4,
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"task": True,
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(
|
||||
OpenAIChatCompletionForm(**payload), user=user
|
||||
)
|
||||
else:
|
||||
return await generate_openai_chat_completion(payload, user=user)
|
||||
|
||||
|
||||
@app.post("/api/task/tools/completions")
|
||||
async def get_tools_function_calling(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("get_tools_function_calling")
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||
if app.state.config.TASK_MODEL:
|
||||
task_model_id = app.state.config.TASK_MODEL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
else:
|
||||
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
|
||||
print(model_id)
|
||||
template = app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
|
||||
try:
|
||||
context = await get_function_call_response(
|
||||
form_data["messages"], form_data["tool_id"], template, model_id, user
|
||||
)
|
||||
return context
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/chat/completions")
|
||||
async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
|
||||
model_id = form_data["model"]
|
||||
|
||||
@@ -59,4 +59,5 @@ youtube-transcript-api==0.6.2
|
||||
pytube==15.0.0
|
||||
|
||||
extract_msg
|
||||
pydub
|
||||
pydub
|
||||
duckduckgo-search~=6.1.5
|
||||
@@ -8,6 +8,7 @@ cd /d "%SCRIPT_DIR%" || exit /b
|
||||
|
||||
SET "KEY_FILE=.webui_secret_key"
|
||||
IF "%PORT%"=="" SET PORT=8080
|
||||
IF "%HOST%"=="" SET HOST=0.0.0.0
|
||||
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
||||
|
||||
@@ -29,4 +30,4 @@ IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
|
||||
|
||||
:: Execute uvicorn
|
||||
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||
uvicorn main:app --host 0.0.0.0 --port "%PORT%" --forwarded-allow-ips '*'
|
||||
uvicorn main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*'
|
||||
|
||||
+16
-7
@@ -6,24 +6,28 @@ from typing import Optional
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: str = None, current_location: str = None
|
||||
template: str, user_name: str = None, user_location: str = None
|
||||
) -> str:
|
||||
# Get the current date
|
||||
current_date = datetime.now()
|
||||
|
||||
# Format the date to YYYY-MM-DD
|
||||
formatted_date = current_date.strftime("%Y-%m-%d")
|
||||
formatted_time = current_date.strftime("%I:%M:%S %p")
|
||||
|
||||
# Replace {{CURRENT_DATE}} in the template with the formatted date
|
||||
template = template.replace("{{CURRENT_DATE}}", formatted_date)
|
||||
template = template.replace("{{CURRENT_TIME}}", formatted_time)
|
||||
template = template.replace(
|
||||
"{{CURRENT_DATETIME}}", f"{formatted_date} {formatted_time}"
|
||||
)
|
||||
|
||||
if user_name:
|
||||
# Replace {{USER_NAME}} in the template with the user's name
|
||||
template = template.replace("{{USER_NAME}}", user_name)
|
||||
|
||||
if current_location:
|
||||
# Replace {{CURRENT_LOCATION}} in the template with the current location
|
||||
template = template.replace("{{CURRENT_LOCATION}}", current_location)
|
||||
if user_location:
|
||||
# Replace {{USER_LOCATION}} in the template with the current location
|
||||
template = template.replace("{{USER_LOCATION}}", user_location)
|
||||
|
||||
return template
|
||||
|
||||
@@ -61,7 +65,7 @@ def title_generation_template(
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||
{"user_name": user.get("name"), "user_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
@@ -104,9 +108,14 @@ def search_query_generation_template(
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||
{"user_name": user.get("name"), "user_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
return template
|
||||
|
||||
|
||||
def tools_function_calling_generation_template(template: str, tools_specs: str) -> str:
|
||||
template = template.replace("{{TOOLS}}", tools_specs)
|
||||
return template
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import inspect
|
||||
from typing import get_type_hints, List, Dict, Any
|
||||
|
||||
|
||||
def doc_to_dict(docstring):
|
||||
lines = docstring.split("\n")
|
||||
description = lines[1].strip()
|
||||
param_dict = {}
|
||||
|
||||
for line in lines:
|
||||
if ":param" in line:
|
||||
line = line.replace(":param", "").strip()
|
||||
param, desc = line.split(":", 1)
|
||||
param_dict[param.strip()] = desc.strip()
|
||||
ret_dict = {"description": description, "params": param_dict}
|
||||
return ret_dict
|
||||
|
||||
|
||||
def get_tools_specs(tools) -> List[dict]:
|
||||
function_list = [
|
||||
{"name": func, "function": getattr(tools, func)}
|
||||
for func in dir(tools)
|
||||
if callable(getattr(tools, func)) and not func.startswith("__")
|
||||
]
|
||||
|
||||
specs = []
|
||||
for function_item in function_list:
|
||||
function_name = function_item["name"]
|
||||
function = function_item["function"]
|
||||
|
||||
function_doc = doc_to_dict(function.__doc__ or function_name)
|
||||
specs.append(
|
||||
{
|
||||
"name": function_name,
|
||||
# TODO: multi-line desc?
|
||||
"description": function_doc.get("description", function_name),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
param_name: {
|
||||
"type": param_annotation.__name__.lower(),
|
||||
**(
|
||||
{
|
||||
"enum": (
|
||||
str(param_annotation.__args__)
|
||||
if hasattr(param_annotation, "__args__")
|
||||
else None
|
||||
)
|
||||
}
|
||||
if hasattr(param_annotation, "__args__")
|
||||
else {}
|
||||
),
|
||||
"description": function_doc.get("params", {}).get(
|
||||
param_name, param_name
|
||||
),
|
||||
}
|
||||
for param_name, param_annotation in get_type_hints(
|
||||
function
|
||||
).items()
|
||||
if param_name != "return" and param_name != "__user__"
|
||||
},
|
||||
"required": [
|
||||
name
|
||||
for name, param in inspect.signature(
|
||||
function
|
||||
).parameters.items()
|
||||
if param.default is param.empty
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return specs
|
||||
@@ -28,19 +28,6 @@ describe('Settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Connections', () => {
|
||||
it('user can open the Connections modal and hit save', () => {
|
||||
cy.get('button').contains('Connections').click();
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
});
|
||||
|
||||
context('Models', () => {
|
||||
it('user can open the Models modal', () => {
|
||||
cy.get('button').contains('Models').click();
|
||||
});
|
||||
});
|
||||
|
||||
context('Interface', () => {
|
||||
it('user can open the Interface modal and hit save', () => {
|
||||
cy.get('button').contains('Interface').click();
|
||||
@@ -55,14 +42,6 @@ describe('Settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Images', () => {
|
||||
it('user can open the Images modal and hit save', () => {
|
||||
cy.get('button').contains('Images').click();
|
||||
// Currently fails because the backend requires a valid URL
|
||||
// cy.get('button').contains('Save').click();
|
||||
});
|
||||
});
|
||||
|
||||
context('Chats', () => {
|
||||
it('user can open the Chats modal', () => {
|
||||
cy.get('button').contains('Chats').click();
|
||||
|
||||
Generated
+196
-9
@@ -1,17 +1,21 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.5",
|
||||
"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": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -108,6 +112,119 @@
|
||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
|
||||
"integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz",
|
||||
"integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
|
||||
"integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
|
||||
"integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-python": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz",
|
||||
"integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.3.2",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/python": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
|
||||
"integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.0.tgz",
|
||||
"integrity": "sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
|
||||
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
|
||||
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
|
||||
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.0.tgz",
|
||||
"integrity": "sha512-fo7CelaUDKWIyemw4b+J57cWuRkOu4SWCCPfNDkPvfWkGjM9D5racHQXr4EQeYCD6zEBIBxGCeaKkQo+ysl0gA==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
@@ -825,6 +942,47 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
|
||||
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
|
||||
"integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.16.tgz",
|
||||
"integrity": "sha512-84UXR3N7s11MPQHWgMnjb9571fr19MmXnr5zTv2XX0gHXXUvW3uPJ8GCjKrfTXmSdfktjRK0ayKklw+A13rk4g==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz",
|
||||
"integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/python": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz",
|
||||
"integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@melt-ui/svelte": {
|
||||
"version": "0.76.0",
|
||||
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.0.tgz",
|
||||
@@ -2224,12 +2382,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -2769,6 +2927,20 @@
|
||||
"plain-tag": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
|
||||
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/coincident": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz",
|
||||
@@ -2891,6 +3063,11 @@
|
||||
"layout-base": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -4429,9 +4606,9 @@
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -8278,6 +8455,11 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
|
||||
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
|
||||
@@ -10022,6 +10204,11 @@
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
||||
},
|
||||
"node_modules/walk-sync": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -16,7 +16,7 @@
|
||||
"format:backend": "black . --exclude \".venv/|/venv/\"",
|
||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"",
|
||||
"cy:open": "cypress open",
|
||||
"test:frontend": "vitest",
|
||||
"test:frontend": "vitest --passWithNoTests",
|
||||
"pyodide:fetch": "node scripts/prepare-pyodide.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,10 +48,14 @@
|
||||
},
|
||||
"type": "module",
|
||||
"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": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
|
||||
+4
-2
@@ -26,8 +26,6 @@ dependencies = [
|
||||
"PyMySQL==1.1.0",
|
||||
"bcrypt==4.1.3",
|
||||
|
||||
"litellm[proxy]==1.37.20",
|
||||
|
||||
"boto3==1.34.110",
|
||||
|
||||
"argon2-cffi==23.1.0",
|
||||
@@ -66,6 +64,10 @@ dependencies = [
|
||||
"langfuse==2.33.0",
|
||||
"youtube-transcript-api==0.6.2",
|
||||
"pytube==15.0.0",
|
||||
"extract_msg",
|
||||
"pydub",
|
||||
"duckduckgo-search~=6.1.5"
|
||||
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11, < 3.12.0a1"
|
||||
|
||||
+40
-48
@@ -12,7 +12,6 @@
|
||||
aiohttp==3.9.5
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via litellm
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
@@ -20,11 +19,9 @@ annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
# via httpx
|
||||
# via openai
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
# via litellm
|
||||
# via open-webui
|
||||
argon2-cffi==23.1.0
|
||||
# via open-webui
|
||||
@@ -38,7 +35,6 @@ av==11.0.0
|
||||
# via faster-whisper
|
||||
backoff==2.2.1
|
||||
# via langfuse
|
||||
# via litellm
|
||||
# via posthog
|
||||
# via unstructured
|
||||
bcrypt==4.1.3
|
||||
@@ -46,6 +42,7 @@ bcrypt==4.1.3
|
||||
# via open-webui
|
||||
# via passlib
|
||||
beautifulsoup4==4.12.3
|
||||
# via extract-msg
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
@@ -83,17 +80,20 @@ chromadb==0.5.0
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
# via black
|
||||
# via duckduckgo-search
|
||||
# via flask
|
||||
# via litellm
|
||||
# via nltk
|
||||
# via peewee-migrate
|
||||
# via rq
|
||||
# via typer
|
||||
# via uvicorn
|
||||
colorclass==2.2.2
|
||||
# via oletools
|
||||
coloredlogs==15.0.1
|
||||
# via onnxruntime
|
||||
compressed-rtf==1.0.6
|
||||
# via extract-msg
|
||||
cryptography==42.0.7
|
||||
# via litellm
|
||||
# via msoffcrypto-tool
|
||||
# via pyjwt
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
@@ -109,33 +109,34 @@ defusedxml==0.7.1
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
distro==1.9.0
|
||||
# via openai
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
ebcdic==1.1.1
|
||||
# via extract-msg
|
||||
ecdsa==0.19.0
|
||||
# via python-jose
|
||||
email-validator==2.1.1
|
||||
# via fastapi
|
||||
# via pydantic
|
||||
emoji==2.11.1
|
||||
# via unstructured
|
||||
et-xmlfile==1.1.0
|
||||
# via openpyxl
|
||||
extract-msg==0.48.5
|
||||
# via open-webui
|
||||
fake-useragent==1.5.1
|
||||
# via open-webui
|
||||
fastapi==0.111.0
|
||||
# via chromadb
|
||||
# via fastapi-sso
|
||||
# via langchain-chroma
|
||||
# via litellm
|
||||
# via open-webui
|
||||
fastapi-cli==0.0.4
|
||||
# via fastapi
|
||||
fastapi-sso==0.10.0
|
||||
# via litellm
|
||||
faster-whisper==1.0.2
|
||||
# via open-webui
|
||||
filelock==3.14.0
|
||||
@@ -191,8 +192,6 @@ grpcio==1.63.0
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
grpcio-status==1.62.2
|
||||
# via google-api-core
|
||||
gunicorn==22.0.0
|
||||
# via litellm
|
||||
h11==0.14.0
|
||||
# via httpcore
|
||||
# via uvicorn
|
||||
@@ -206,9 +205,7 @@ httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via fastapi
|
||||
# via fastapi-sso
|
||||
# via langfuse
|
||||
# via openai
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
@@ -225,7 +222,6 @@ idna==3.7
|
||||
# via unstructured-client
|
||||
# via yarl
|
||||
importlib-metadata==7.0.0
|
||||
# via litellm
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
@@ -234,7 +230,6 @@ itsdangerous==2.2.0
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via litellm
|
||||
# via torch
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
@@ -272,8 +267,8 @@ langsmith==0.1.57
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
litellm==1.37.20
|
||||
# via open-webui
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via unstructured
|
||||
markdown==3.6
|
||||
@@ -294,6 +289,8 @@ monotonic==1.6
|
||||
# via posthog
|
||||
mpmath==1.3.0
|
||||
# via sympy
|
||||
msoffcrypto-tool==5.4.1
|
||||
# via oletools
|
||||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
@@ -325,15 +322,19 @@ numpy==1.26.4
|
||||
# via transformers
|
||||
# via unstructured
|
||||
oauthlib==3.2.2
|
||||
# via fastapi-sso
|
||||
# via kubernetes
|
||||
# via requests-oauthlib
|
||||
olefile==0.47
|
||||
# via extract-msg
|
||||
# via msoffcrypto-tool
|
||||
# via oletools
|
||||
oletools==0.60.1
|
||||
# via pcodedmp
|
||||
# via rtfde
|
||||
onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
openai==1.28.1
|
||||
# via litellm
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
@@ -375,15 +376,14 @@ ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
# via litellm
|
||||
overrides==7.7.0
|
||||
# via chromadb
|
||||
packaging==23.2
|
||||
# via black
|
||||
# via build
|
||||
# via gunicorn
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
@@ -397,6 +397,8 @@ passlib==1.7.4
|
||||
# via open-webui
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
@@ -437,27 +439,27 @@ pycparser==2.22
|
||||
pydantic==2.7.1
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via fastapi-sso
|
||||
# via google-generativeai
|
||||
# via langchain
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
# via openai
|
||||
pydantic-core==2.18.2
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
# via litellm
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==3.1.2
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
@@ -465,6 +467,8 @@ pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
@@ -472,7 +476,6 @@ python-dateutil==2.9.0.post0
|
||||
# via posthog
|
||||
# via unstructured-client
|
||||
python-dotenv==1.0.1
|
||||
# via litellm
|
||||
# via uvicorn
|
||||
python-engineio==4.9.0
|
||||
# via python-socketio
|
||||
@@ -484,7 +487,6 @@ python-magic==0.4.27
|
||||
# via unstructured
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via litellm
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
# via open-webui
|
||||
@@ -503,7 +505,6 @@ pyyaml==6.0.1
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
# via litellm
|
||||
# via rapidocr-onnxruntime
|
||||
# via transformers
|
||||
# via uvicorn
|
||||
@@ -513,11 +514,10 @@ rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
# via open-webui
|
||||
redis==5.0.4
|
||||
# via rq
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
@@ -527,11 +527,9 @@ requests==2.32.2
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langsmith
|
||||
# via litellm
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -540,11 +538,11 @@ requests-oauthlib==2.0.0
|
||||
# via kubernetes
|
||||
rich==13.7.1
|
||||
# via typer
|
||||
rq==1.16.2
|
||||
# via litellm
|
||||
rsa==4.9
|
||||
# via google-auth
|
||||
# via python-jose
|
||||
rtfde==0.1.1
|
||||
# via extract-msg
|
||||
s3transfer==0.10.1
|
||||
# via boto3
|
||||
safetensors==0.4.3
|
||||
@@ -577,7 +575,6 @@ six==1.16.0
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
# via httpx
|
||||
# via openai
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
@@ -597,12 +594,9 @@ tenacity==8.3.0
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tiktoken==0.6.0
|
||||
# via litellm
|
||||
tokenizers==0.15.2
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via litellm
|
||||
# via transformers
|
||||
torch==2.3.0
|
||||
# via sentence-transformers
|
||||
@@ -611,7 +605,6 @@ tqdm==4.66.4
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via openai
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
transformers==4.39.3
|
||||
@@ -624,7 +617,6 @@ typing-extensions==4.11.0
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via openai
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
@@ -641,6 +633,7 @@ tzdata==2024.1
|
||||
# via pandas
|
||||
tzlocal==5.2
|
||||
# via apscheduler
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
@@ -657,7 +650,6 @@ urllib3==2.2.1
|
||||
uvicorn==0.22.0
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via litellm
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
|
||||
+40
-48
@@ -12,7 +12,6 @@
|
||||
aiohttp==3.9.5
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via litellm
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
@@ -20,11 +19,9 @@ annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
# via httpx
|
||||
# via openai
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
# via litellm
|
||||
# via open-webui
|
||||
argon2-cffi==23.1.0
|
||||
# via open-webui
|
||||
@@ -38,7 +35,6 @@ av==11.0.0
|
||||
# via faster-whisper
|
||||
backoff==2.2.1
|
||||
# via langfuse
|
||||
# via litellm
|
||||
# via posthog
|
||||
# via unstructured
|
||||
bcrypt==4.1.3
|
||||
@@ -46,6 +42,7 @@ bcrypt==4.1.3
|
||||
# via open-webui
|
||||
# via passlib
|
||||
beautifulsoup4==4.12.3
|
||||
# via extract-msg
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
@@ -83,17 +80,20 @@ chromadb==0.5.0
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
# via black
|
||||
# via duckduckgo-search
|
||||
# via flask
|
||||
# via litellm
|
||||
# via nltk
|
||||
# via peewee-migrate
|
||||
# via rq
|
||||
# via typer
|
||||
# via uvicorn
|
||||
colorclass==2.2.2
|
||||
# via oletools
|
||||
coloredlogs==15.0.1
|
||||
# via onnxruntime
|
||||
compressed-rtf==1.0.6
|
||||
# via extract-msg
|
||||
cryptography==42.0.7
|
||||
# via litellm
|
||||
# via msoffcrypto-tool
|
||||
# via pyjwt
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
@@ -109,33 +109,34 @@ defusedxml==0.7.1
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
distro==1.9.0
|
||||
# via openai
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
ebcdic==1.1.1
|
||||
# via extract-msg
|
||||
ecdsa==0.19.0
|
||||
# via python-jose
|
||||
email-validator==2.1.1
|
||||
# via fastapi
|
||||
# via pydantic
|
||||
emoji==2.11.1
|
||||
# via unstructured
|
||||
et-xmlfile==1.1.0
|
||||
# via openpyxl
|
||||
extract-msg==0.48.5
|
||||
# via open-webui
|
||||
fake-useragent==1.5.1
|
||||
# via open-webui
|
||||
fastapi==0.111.0
|
||||
# via chromadb
|
||||
# via fastapi-sso
|
||||
# via langchain-chroma
|
||||
# via litellm
|
||||
# via open-webui
|
||||
fastapi-cli==0.0.4
|
||||
# via fastapi
|
||||
fastapi-sso==0.10.0
|
||||
# via litellm
|
||||
faster-whisper==1.0.2
|
||||
# via open-webui
|
||||
filelock==3.14.0
|
||||
@@ -191,8 +192,6 @@ grpcio==1.63.0
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
grpcio-status==1.62.2
|
||||
# via google-api-core
|
||||
gunicorn==22.0.0
|
||||
# via litellm
|
||||
h11==0.14.0
|
||||
# via httpcore
|
||||
# via uvicorn
|
||||
@@ -206,9 +205,7 @@ httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via fastapi
|
||||
# via fastapi-sso
|
||||
# via langfuse
|
||||
# via openai
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
@@ -225,7 +222,6 @@ idna==3.7
|
||||
# via unstructured-client
|
||||
# via yarl
|
||||
importlib-metadata==7.0.0
|
||||
# via litellm
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
@@ -234,7 +230,6 @@ itsdangerous==2.2.0
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via litellm
|
||||
# via torch
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
@@ -272,8 +267,8 @@ langsmith==0.1.57
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
litellm==1.37.20
|
||||
# via open-webui
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via unstructured
|
||||
markdown==3.6
|
||||
@@ -294,6 +289,8 @@ monotonic==1.6
|
||||
# via posthog
|
||||
mpmath==1.3.0
|
||||
# via sympy
|
||||
msoffcrypto-tool==5.4.1
|
||||
# via oletools
|
||||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
@@ -325,15 +322,19 @@ numpy==1.26.4
|
||||
# via transformers
|
||||
# via unstructured
|
||||
oauthlib==3.2.2
|
||||
# via fastapi-sso
|
||||
# via kubernetes
|
||||
# via requests-oauthlib
|
||||
olefile==0.47
|
||||
# via extract-msg
|
||||
# via msoffcrypto-tool
|
||||
# via oletools
|
||||
oletools==0.60.1
|
||||
# via pcodedmp
|
||||
# via rtfde
|
||||
onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
openai==1.28.1
|
||||
# via litellm
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
@@ -375,15 +376,14 @@ ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
# via litellm
|
||||
overrides==7.7.0
|
||||
# via chromadb
|
||||
packaging==23.2
|
||||
# via black
|
||||
# via build
|
||||
# via gunicorn
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
@@ -397,6 +397,8 @@ passlib==1.7.4
|
||||
# via open-webui
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
@@ -437,27 +439,27 @@ pycparser==2.22
|
||||
pydantic==2.7.1
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via fastapi-sso
|
||||
# via google-generativeai
|
||||
# via langchain
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
# via openai
|
||||
pydantic-core==2.18.2
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
# via litellm
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==3.1.2
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
@@ -465,6 +467,8 @@ pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
@@ -472,7 +476,6 @@ python-dateutil==2.9.0.post0
|
||||
# via posthog
|
||||
# via unstructured-client
|
||||
python-dotenv==1.0.1
|
||||
# via litellm
|
||||
# via uvicorn
|
||||
python-engineio==4.9.0
|
||||
# via python-socketio
|
||||
@@ -484,7 +487,6 @@ python-magic==0.4.27
|
||||
# via unstructured
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via litellm
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
# via open-webui
|
||||
@@ -503,7 +505,6 @@ pyyaml==6.0.1
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
# via litellm
|
||||
# via rapidocr-onnxruntime
|
||||
# via transformers
|
||||
# via uvicorn
|
||||
@@ -513,11 +514,10 @@ rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
# via open-webui
|
||||
redis==5.0.4
|
||||
# via rq
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
@@ -527,11 +527,9 @@ requests==2.32.2
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langsmith
|
||||
# via litellm
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -540,11 +538,11 @@ requests-oauthlib==2.0.0
|
||||
# via kubernetes
|
||||
rich==13.7.1
|
||||
# via typer
|
||||
rq==1.16.2
|
||||
# via litellm
|
||||
rsa==4.9
|
||||
# via google-auth
|
||||
# via python-jose
|
||||
rtfde==0.1.1
|
||||
# via extract-msg
|
||||
s3transfer==0.10.1
|
||||
# via boto3
|
||||
safetensors==0.4.3
|
||||
@@ -577,7 +575,6 @@ six==1.16.0
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
# via httpx
|
||||
# via openai
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
@@ -597,12 +594,9 @@ tenacity==8.3.0
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tiktoken==0.6.0
|
||||
# via litellm
|
||||
tokenizers==0.15.2
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via litellm
|
||||
# via transformers
|
||||
torch==2.3.0
|
||||
# via sentence-transformers
|
||||
@@ -611,7 +605,6 @@ tqdm==4.66.4
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via openai
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
transformers==4.39.3
|
||||
@@ -624,7 +617,6 @@ typing-extensions==4.11.0
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via openai
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
@@ -641,6 +633,7 @@ tzdata==2024.1
|
||||
# via pandas
|
||||
tzlocal==5.2
|
||||
# via apscheduler
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
@@ -657,7 +650,6 @@ urllib3==2.2.1
|
||||
uvicorn==0.22.0
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via litellm
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
|
||||
+25
@@ -28,6 +28,10 @@ math {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
counter-increment: list-number;
|
||||
display: block;
|
||||
@@ -92,10 +96,18 @@ select {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
@@ -111,3 +123,16 @@ input::-webkit-inner-spin-button {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply scrollbar-hidden;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
+106
-2
@@ -32,6 +32,9 @@
|
||||
} 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') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add('her');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
@@ -68,17 +71,66 @@
|
||||
</style>
|
||||
|
||||
<img
|
||||
id="logo"
|
||||
style="
|
||||
position: absolute;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
top: 46%;
|
||||
top: 41%;
|
||||
left: 50%;
|
||||
margin: -40px 0 0 -40px;
|
||||
margin-left: -3rem;
|
||||
"
|
||||
src="/logo.svg"
|
||||
/>
|
||||
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
top: 33%;
|
||||
left: 50%;
|
||||
|
||||
width: 24rem;
|
||||
margin-left: -12rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<img
|
||||
id="logo-her"
|
||||
style="width: 13rem; height: 13rem"
|
||||
src="/logo.svg"
|
||||
class="animate-pulse-fast"
|
||||
/>
|
||||
|
||||
<div style="position: relative; width: 24rem; margin-top: 0.5rem">
|
||||
<div
|
||||
id="progress-background"
|
||||
style="
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0.75rem;
|
||||
|
||||
border-radius: 9999px;
|
||||
background-color: #fafafa9a;
|
||||
"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="progress-bar"
|
||||
style="
|
||||
position: absolute;
|
||||
width: 0%;
|
||||
height: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
background-color: #fff;
|
||||
"
|
||||
class="bg-white"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <span style="position: absolute; bottom: 32px; left: 50%; margin: -36px 0 0 -36px">
|
||||
Footer content
|
||||
</span> -->
|
||||
@@ -101,4 +153,56 @@
|
||||
html.dark #splash-screen img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
html.her #splash-screen {
|
||||
background: #983724;
|
||||
}
|
||||
|
||||
#logo-her {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-background {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.her #logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.her #logo-her {
|
||||
display: block;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
html.her #progress-background {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html.her #progress-bar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 24rem) {
|
||||
html.her #progress-background {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.her #progress-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
.animate-pulse-fast {
|
||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,7 +76,10 @@ export const getDocs = async (token: string = '') => {
|
||||
export const getDocByName = async (token: string, name: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}`, {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('name', name);
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/docs?${searchParams.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -113,7 +116,10 @@ type DocUpdateForm = {
|
||||
export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/update`, {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('name', name);
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/update?${searchParams.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -154,7 +160,10 @@ type TagDocForm = {
|
||||
export const tagDocByName = async (token: string, name: string, form: TagDocForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/tags`, {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('name', name);
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/tags?${searchParams.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -190,7 +199,10 @@ export const tagDocByName = async (token: string, name: string, form: TagDocForm
|
||||
export const deleteDocByName = async (token: string, name: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/delete`, {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('name', name);
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/delete?${searchParams.toString()}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -205,6 +205,54 @@ export const generateTitle = async (
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generateEmoji = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
prompt: string,
|
||||
chat_id?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/emoji/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
...(chat_id && { chat_id: chat_id })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const response = res?.choices[0]?.message?.content.replace(/["']/g, '') ?? null;
|
||||
|
||||
if (response) {
|
||||
if (/\p{Extended_Pictographic}/u.test(response)) {
|
||||
return response.match(/\p{Extended_Pictographic}/gu)[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const generateSearchQuery = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
export const getMemories = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -59,6 +59,37 @@ export const addNewMemory = async (token: string, content: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateMemoryById = async (token: string, id: string, content: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const queryMemory = async (token: string, content: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const createNewTool = async (token: string, tool: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...tool
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getTools = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const exportTools = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/export`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getToolById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateToolById = async (token: string, id: string, tool: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...tool
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteToolById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { getUserPosition } from '$lib/utils';
|
||||
|
||||
export const getUserPermissions = async (token: string) => {
|
||||
let error = null;
|
||||
@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getUserInfo = async (token: string) => {
|
||||
let error = null;
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateUserInfo = async (token: string, info: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...info
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAndUpdateUserLocation = async (token: string) => {
|
||||
const location = await getUserPosition().catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (location) {
|
||||
await updateUserInfo(token, { location: location });
|
||||
return location;
|
||||
} else {
|
||||
throw new Error('Failed to get user location');
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserById = async (token: string, userId: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -22,6 +22,39 @@ export const getGravatarUrl = async (email: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const formatPythonCode = async (code: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: code
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
error = err;
|
||||
if (err.detail) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadChatAsPDF = async (chat: object) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
let ENABLE_OLLAMA_API = null;
|
||||
|
||||
const verifyOpenAIHandler = async (idx) => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
|
||||
|
||||
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
|
||||
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
|
||||
|
||||
@@ -63,6 +65,10 @@
|
||||
};
|
||||
|
||||
const verifyOllamaHandler = async (idx) => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
|
||||
url.replace(/\/$/, '')
|
||||
);
|
||||
|
||||
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
|
||||
|
||||
const res = await getOllamaVersion(localStorage.token, idx).catch((error) => {
|
||||
@@ -78,6 +84,8 @@
|
||||
};
|
||||
|
||||
const updateOpenAIHandler = async () => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
|
||||
|
||||
// Check if API KEYS length is same than API URLS length
|
||||
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
|
||||
// if there are more keys than urls, remove the extra keys
|
||||
@@ -100,7 +108,10 @@
|
||||
};
|
||||
|
||||
const updateOllamaUrlsHandler = async () => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '');
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
|
||||
url.replace(/\/$/, '')
|
||||
);
|
||||
|
||||
console.log(OLLAMA_BASE_URLS);
|
||||
|
||||
if (OLLAMA_BASE_URLS.length === 0) {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
getRAGConfig,
|
||||
updateRAGConfig
|
||||
} from '$lib/apis/rag';
|
||||
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import { documents, models } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
@@ -166,6 +168,8 @@
|
||||
chunk_size: chunkSize
|
||||
}
|
||||
});
|
||||
|
||||
await updateQuerySettings(localStorage.token, querySettings);
|
||||
};
|
||||
|
||||
const setEmbeddingConfig = async () => {
|
||||
@@ -211,6 +215,34 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<ResetUploadDirConfirmDialog
|
||||
bind:show={showResetUploadDirConfirm}
|
||||
on:confirm={() => {
|
||||
const res = resetUploadDir(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ResetVectorDBConfirmDialog
|
||||
bind:show={showResetConfirm}
|
||||
on:confirm={() => {
|
||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
@@ -638,199 +670,56 @@
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
{#if showResetUploadDirConfirm}
|
||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{$i18n.t('Are you sure?')}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
const res = resetUploadDir(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
|
||||
showResetUploadDirConfirm = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||
</button>
|
||||
|
||||
{#if showResetConfirm}
|
||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{$i18n.t('Are you sure?')}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
|
||||
showResetConfirm = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
|
||||
import {
|
||||
createModel,
|
||||
@@ -11,15 +16,11 @@
|
||||
uploadModel,
|
||||
getOllamaConfig
|
||||
} from '$lib/apis/ollama';
|
||||
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -29,6 +30,8 @@
|
||||
|
||||
let modelUploadInputElement: HTMLInputElement;
|
||||
|
||||
let showModelDeleteConfirm = false;
|
||||
|
||||
// Models
|
||||
|
||||
let ollamaEnabled = null;
|
||||
@@ -549,6 +552,13 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<ModelDeleteConfirmDialog
|
||||
bind:show={showModelDeleteConfirm}
|
||||
on:confirm={() => {
|
||||
deleteModelHandler();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if ollamaEnabled}
|
||||
@@ -763,7 +773,7 @@
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
deleteModelHandler();
|
||||
showModelDeleteConfirm = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
permissions.chat.deletion = !permissions.chat.deletion;
|
||||
permissions.chat.deletion = !(permissions?.chat?.deletion ?? true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if permissions.chat.deletion}
|
||||
{#if permissions?.chat?.deletion ?? true}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
|
||||
@@ -11,7 +11,16 @@
|
||||
export let saveHandler: Function;
|
||||
|
||||
let webConfig = null;
|
||||
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper'];
|
||||
let webSearchEngines = [
|
||||
'searxng',
|
||||
'google_pse',
|
||||
'brave',
|
||||
'serpstack',
|
||||
'serper',
|
||||
'serply',
|
||||
'duckduckgo',
|
||||
'tavily'
|
||||
];
|
||||
|
||||
let youtubeLanguage = 'en';
|
||||
let youtubeTranslation = null;
|
||||
@@ -188,6 +197,42 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serply'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serply API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Serply API Key')}
|
||||
bind:value={webConfig.search.serply_api_key}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'tavily'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Tavily API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Tavily API Key')}
|
||||
bind:value={webConfig.search.tavily_api_key}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
let sortKey = 'updated_at'; // default sort key
|
||||
let sortOrder = 'desc'; // default sort order
|
||||
function setSortKey(key) {
|
||||
if (sortKey === key) {
|
||||
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortOrder = 'asc';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal size="lg" bind:show>
|
||||
@@ -69,18 +80,56 @@
|
||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
|
||||
>
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
||||
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-2 cursor-pointer select-none"
|
||||
on:click={() => setSortKey('title')}
|
||||
>
|
||||
{$i18n.t('Title')}
|
||||
{#if sortKey === 'title'}
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
{:else}
|
||||
<span class="invisible">▲</span>
|
||||
{/if}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-2 cursor-pointer select-none"
|
||||
on:click={() => setSortKey('created_at')}
|
||||
>
|
||||
{$i18n.t('Created at')}
|
||||
{#if sortKey === 'created_at'}
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
{:else}
|
||||
<span class="invisible">▲</span>
|
||||
{/if}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-2 hidden md:flex cursor-pointer select-none"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
{$i18n.t('Updated at')}
|
||||
{#if sortKey === 'updated_at'}
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
{:else}
|
||||
<span class="invisible">▲</span>
|
||||
{/if}
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 text-right" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each chats as chat, idx}
|
||||
{#each chats.sort((a, b) => {
|
||||
if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
|
||||
if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
}) as chat, idx}
|
||||
<tr
|
||||
class="bg-transparent {idx !== chats.length - 1 &&
|
||||
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
|
||||
>
|
||||
<td class="px-3 py-1 w-2/3">
|
||||
<td class="px-3 py-1">
|
||||
<a href="/s/{chat.id}" target="_blank">
|
||||
<div class=" underline line-clamp-1">
|
||||
{chat.title}
|
||||
@@ -88,11 +137,16 @@
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
|
||||
<td class=" px-3 py-1 h-[2.5rem]">
|
||||
<div class="my-auto">
|
||||
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
|
||||
</div>
|
||||
</td>
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
|
||||
<div class="my-auto">
|
||||
{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-1 text-right">
|
||||
<div class="flex justify-end w-full">
|
||||
|
||||
+309
-168
@@ -24,11 +24,14 @@
|
||||
banners,
|
||||
user,
|
||||
socket,
|
||||
showCallOverlay
|
||||
showCallOverlay,
|
||||
tools
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
copyToClipboard,
|
||||
extractSentencesForAudio,
|
||||
getUserPosition,
|
||||
promptTemplate,
|
||||
splitStream
|
||||
} from '$lib/utils';
|
||||
@@ -48,7 +51,7 @@
|
||||
import { runWebSearch } from '$lib/apis/rag';
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
import { queryMemory } from '$lib/apis/memories';
|
||||
import { getUserSettings } from '$lib/apis/users';
|
||||
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
||||
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
|
||||
|
||||
import Banner from '../common/Banner.svelte';
|
||||
@@ -63,6 +66,8 @@
|
||||
export let chatIdProp = '';
|
||||
let loaded = false;
|
||||
|
||||
const eventTarget = new EventTarget();
|
||||
|
||||
let stopResponseFlag = false;
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
@@ -73,6 +78,10 @@
|
||||
let selectedModels = [''];
|
||||
let atSelectedModel: Model | undefined;
|
||||
|
||||
let selectedModelIds = [];
|
||||
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
|
||||
|
||||
let selectedToolIds = [];
|
||||
let webSearchEnabled = false;
|
||||
|
||||
let chat = null;
|
||||
@@ -103,7 +112,8 @@
|
||||
|
||||
$: if (chatIdProp) {
|
||||
(async () => {
|
||||
if (await loadChat()) {
|
||||
console.log(chatIdProp);
|
||||
if (chatIdProp && (await loadChat())) {
|
||||
await tick();
|
||||
loaded = true;
|
||||
|
||||
@@ -118,7 +128,11 @@
|
||||
|
||||
onMount(async () => {
|
||||
if (!$chatId) {
|
||||
await initNewChat();
|
||||
chatId.subscribe(async (value) => {
|
||||
if (!value) {
|
||||
await initNewChat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!($settings.saveChatHistory ?? true)) {
|
||||
await goto('/');
|
||||
@@ -295,7 +309,7 @@
|
||||
// Chat functions
|
||||
//////////////////////////
|
||||
|
||||
const submitPrompt = async (userPrompt, _user = null) => {
|
||||
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
|
||||
let _responses = [];
|
||||
console.log('submitPrompt', $chatId);
|
||||
|
||||
@@ -327,6 +341,9 @@
|
||||
chatTextAreaElement.style.height = '';
|
||||
}
|
||||
|
||||
const _files = JSON.parse(JSON.stringify(files));
|
||||
files = [];
|
||||
|
||||
prompt = '';
|
||||
|
||||
// Create user message
|
||||
@@ -336,9 +353,8 @@
|
||||
parentId: messages.length !== 0 ? messages.at(-1).id : null,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
user: _user ?? undefined,
|
||||
content: userPrompt,
|
||||
files: files.length > 0 ? files : undefined,
|
||||
files: _files.length > 0 ? _files : undefined,
|
||||
timestamp: Math.floor(Date.now() / 1000), // Unix epoch
|
||||
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
||||
};
|
||||
@@ -354,51 +370,86 @@
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// Create new chat if only one message in messages
|
||||
if (messages.length == 1) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
id: $chatId,
|
||||
title: $i18n.t('New Chat'),
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
...($settings.params ?? {})
|
||||
},
|
||||
messages: messages,
|
||||
history: history,
|
||||
tags: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await chatId.set(chat.id);
|
||||
} else {
|
||||
await chatId.set('local');
|
||||
}
|
||||
await tick();
|
||||
}
|
||||
|
||||
files = [];
|
||||
|
||||
// Send prompt
|
||||
_responses = await sendPrompt(userPrompt, userMessageId);
|
||||
_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
|
||||
}
|
||||
|
||||
return _responses;
|
||||
};
|
||||
|
||||
const sendPrompt = async (prompt, parentId, modelId = null) => {
|
||||
const sendPrompt = async (prompt, parentId, { modelId = null, newChat = false } = {}) => {
|
||||
let _responses = [];
|
||||
|
||||
// If modelId is provided, use it, else use selected model
|
||||
let selectedModelIds = modelId
|
||||
? [modelId]
|
||||
: atSelectedModel !== undefined
|
||||
? [atSelectedModel.id]
|
||||
: selectedModels;
|
||||
|
||||
// Create response messages for each selected model
|
||||
const responseMessageIds = {};
|
||||
for (const modelId of selectedModelIds) {
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
if (model) {
|
||||
let responseMessageId = uuidv4();
|
||||
let responseMessage = {
|
||||
parentId: parentId,
|
||||
id: responseMessageId,
|
||||
childrenIds: [],
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
model: model.id,
|
||||
modelName: model.name ?? model.id,
|
||||
userContext: null,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
responseMessageIds[modelId] = responseMessageId;
|
||||
}
|
||||
}
|
||||
await tick();
|
||||
|
||||
// Create new chat if only one message in messages
|
||||
if (newChat && messages.length == 2) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
id: $chatId,
|
||||
title: $i18n.t('New Chat'),
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
...($settings.params ?? {})
|
||||
},
|
||||
messages: messages,
|
||||
history: history,
|
||||
tags: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await chatId.set(chat.id);
|
||||
} else {
|
||||
await chatId.set('local');
|
||||
}
|
||||
await tick();
|
||||
}
|
||||
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
|
||||
await Promise.all(
|
||||
(modelId
|
||||
? [modelId]
|
||||
: atSelectedModel !== undefined
|
||||
? [atSelectedModel.id]
|
||||
: selectedModels
|
||||
).map(async (modelId) => {
|
||||
selectedModelIds.map(async (modelId) => {
|
||||
console.log('modelId', modelId);
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
@@ -416,33 +467,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
// Create response message
|
||||
let responseMessageId = uuidv4();
|
||||
let responseMessage = {
|
||||
parentId: parentId,
|
||||
id: responseMessageId,
|
||||
childrenIds: [],
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
model: model.id,
|
||||
modelName: model.name ?? model.id,
|
||||
userContext: null,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
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 tick();
|
||||
let responseMessageId = responseMessageIds[modelId];
|
||||
let responseMessage = history.messages[responseMessageId];
|
||||
|
||||
let userContext = null;
|
||||
if ($settings?.memory ?? false) {
|
||||
@@ -451,7 +477,6 @@
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
if (res.documents[0].length > 0) {
|
||||
userContext = res.documents.reduce((acc, doc, index) => {
|
||||
@@ -471,13 +496,11 @@
|
||||
responseMessage.userContext = userContext;
|
||||
|
||||
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
|
||||
|
||||
if (webSearchEnabled) {
|
||||
await getWebSearchResults(model.id, parentId, responseMessageId);
|
||||
}
|
||||
|
||||
let _response = null;
|
||||
|
||||
if (model?.owned_by === 'openai') {
|
||||
_response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||
} else if (model) {
|
||||
@@ -485,8 +508,6 @@
|
||||
}
|
||||
_responses.push(_response);
|
||||
|
||||
console.log('chatEventEmitter', chatEventEmitter);
|
||||
|
||||
if (chatEventEmitter) clearInterval(chatEventEmitter);
|
||||
} else {
|
||||
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
||||
@@ -495,83 +516,9 @@
|
||||
);
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
return _responses;
|
||||
};
|
||||
|
||||
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
|
||||
const responseMessage = history.messages[responseId];
|
||||
|
||||
responseMessage.status = {
|
||||
done: false,
|
||||
action: 'web_search',
|
||||
description: $i18n.t('Generating search query')
|
||||
};
|
||||
messages = messages;
|
||||
|
||||
const prompt = history.messages[parentId].content;
|
||||
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return prompt;
|
||||
}
|
||||
);
|
||||
|
||||
if (!searchQuery) {
|
||||
toast.warning($i18n.t('No search query generated'));
|
||||
responseMessage.status = {
|
||||
...responseMessage.status,
|
||||
done: true,
|
||||
error: true,
|
||||
description: 'No search query generated'
|
||||
};
|
||||
messages = messages;
|
||||
}
|
||||
|
||||
responseMessage.status = {
|
||||
...responseMessage.status,
|
||||
description: $i18n.t("Searching the web for '{{searchQuery}}'", { searchQuery })
|
||||
};
|
||||
messages = messages;
|
||||
|
||||
const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (results) {
|
||||
responseMessage.status = {
|
||||
...responseMessage.status,
|
||||
done: true,
|
||||
description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
|
||||
urls: results.filenames
|
||||
};
|
||||
|
||||
if (responseMessage?.files ?? undefined === undefined) {
|
||||
responseMessage.files = [];
|
||||
}
|
||||
|
||||
responseMessage.files.push({
|
||||
collection_name: results.collection_name,
|
||||
name: searchQuery,
|
||||
type: 'web_search_results',
|
||||
urls: results.filenames
|
||||
});
|
||||
|
||||
messages = messages;
|
||||
} else {
|
||||
responseMessage.status = {
|
||||
...responseMessage.status,
|
||||
done: true,
|
||||
error: true,
|
||||
description: 'No search results found'
|
||||
};
|
||||
messages = messages;
|
||||
}
|
||||
};
|
||||
|
||||
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||
let _response = null;
|
||||
|
||||
@@ -587,7 +534,13 @@
|
||||
$settings.system || (responseMessage?.userContext ?? null)
|
||||
? {
|
||||
role: 'system',
|
||||
content: `${promptTemplate($settings?.system ?? '', $user.name)}${
|
||||
content: `${promptTemplate(
|
||||
$settings?.system ?? '',
|
||||
$user.name,
|
||||
$settings?.userLocation
|
||||
? await getAndUpdateUserLocation(localStorage.token)
|
||||
: undefined
|
||||
)}${
|
||||
responseMessage?.userContext ?? null
|
||||
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
|
||||
: ''
|
||||
@@ -634,7 +587,7 @@
|
||||
|
||||
let docs = [];
|
||||
|
||||
if (model.info.meta.knowledge) {
|
||||
if (model?.info?.meta?.knowledge ?? false) {
|
||||
docs = model.info.meta.knowledge;
|
||||
}
|
||||
|
||||
@@ -653,6 +606,16 @@
|
||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||
);
|
||||
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat:start', {
|
||||
detail: {
|
||||
id: responseMessageId
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await tick();
|
||||
|
||||
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
||||
model: model.id,
|
||||
messages: messagesBody,
|
||||
@@ -669,6 +632,7 @@
|
||||
},
|
||||
format: $settings.requestFormat ?? undefined,
|
||||
keep_alive: $settings.keepAlive ?? undefined,
|
||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||
docs: docs.length > 0 ? docs : undefined,
|
||||
citations: docs.length > 0,
|
||||
chat_id: $chatId
|
||||
@@ -721,6 +685,23 @@
|
||||
continue;
|
||||
} else {
|
||||
responseMessage.content += data.message.content;
|
||||
|
||||
const sentences = extractSentencesForAudio(responseMessage.content);
|
||||
sentences.pop();
|
||||
|
||||
// dispatch only last sentence and make sure it hasn't been dispatched before
|
||||
if (
|
||||
sentences.length > 0 &&
|
||||
sentences[sentences.length - 1] !== responseMessage.lastSentence
|
||||
) {
|
||||
responseMessage.lastSentence = sentences[sentences.length - 1];
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat', {
|
||||
detail: { id: responseMessageId, content: sentences[sentences.length - 1] }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
messages = messages;
|
||||
}
|
||||
} else {
|
||||
@@ -747,21 +728,13 @@
|
||||
messages = messages;
|
||||
|
||||
if ($settings.notificationEnabled && !document.hasFocus()) {
|
||||
const notification = new Notification(
|
||||
selectedModelfile
|
||||
? `${
|
||||
selectedModelfile.title.charAt(0).toUpperCase() +
|
||||
selectedModelfile.title.slice(1)
|
||||
}`
|
||||
: `${model.id}`,
|
||||
{
|
||||
body: responseMessage.content,
|
||||
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
}
|
||||
);
|
||||
const notification = new Notification(`${model.id}`, {
|
||||
body: responseMessage.content,
|
||||
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
});
|
||||
}
|
||||
|
||||
if ($settings.responseAutoCopy) {
|
||||
if ($settings?.responseAutoCopy ?? false) {
|
||||
copyToClipboard(responseMessage.content);
|
||||
}
|
||||
|
||||
@@ -823,6 +796,23 @@
|
||||
stopResponseFlag = false;
|
||||
await tick();
|
||||
|
||||
let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? '';
|
||||
if (lastSentence) {
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat', {
|
||||
detail: { id: responseMessageId, content: lastSentence }
|
||||
})
|
||||
);
|
||||
}
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat:finish', {
|
||||
detail: {
|
||||
id: responseMessageId,
|
||||
content: responseMessage.content
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (autoScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
@@ -842,7 +832,7 @@
|
||||
|
||||
let docs = [];
|
||||
|
||||
if (model.info.meta.knowledge) {
|
||||
if (model?.info?.meta?.knowledge ?? false) {
|
||||
docs = model.info.meta.knowledge;
|
||||
}
|
||||
|
||||
@@ -863,6 +853,15 @@
|
||||
|
||||
scrollToBottom();
|
||||
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat:start', {
|
||||
detail: {
|
||||
id: responseMessageId
|
||||
}
|
||||
})
|
||||
);
|
||||
await tick();
|
||||
|
||||
try {
|
||||
const [res, controller] = await generateOpenAIChatCompletion(
|
||||
localStorage.token,
|
||||
@@ -879,7 +878,13 @@
|
||||
$settings.system || (responseMessage?.userContext ?? null)
|
||||
? {
|
||||
role: 'system',
|
||||
content: `${promptTemplate($settings?.system ?? '', $user.name)}${
|
||||
content: `${promptTemplate(
|
||||
$settings?.system ?? '',
|
||||
$user.name,
|
||||
$settings?.userLocation
|
||||
? await getAndUpdateUserLocation(localStorage.token)
|
||||
: undefined
|
||||
)}${
|
||||
responseMessage?.userContext ?? null
|
||||
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
|
||||
: ''
|
||||
@@ -930,6 +935,7 @@
|
||||
top_p: $settings?.params?.top_p ?? undefined,
|
||||
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
|
||||
max_tokens: $settings?.params?.max_tokens ?? undefined,
|
||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||
docs: docs.length > 0 ? docs : undefined,
|
||||
citations: docs.length > 0,
|
||||
chat_id: $chatId
|
||||
@@ -982,6 +988,23 @@
|
||||
continue;
|
||||
} else {
|
||||
responseMessage.content += value;
|
||||
|
||||
const sentences = extractSentencesForAudio(responseMessage.content);
|
||||
sentences.pop();
|
||||
|
||||
// dispatch only last sentence and make sure it hasn't been dispatched before
|
||||
if (
|
||||
sentences.length > 0 &&
|
||||
sentences[sentences.length - 1] !== responseMessage.lastSentence
|
||||
) {
|
||||
responseMessage.lastSentence = sentences[sentences.length - 1];
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat', {
|
||||
detail: { id: responseMessageId, content: sentences[sentences.length - 1] }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
messages = messages;
|
||||
}
|
||||
|
||||
@@ -1032,6 +1055,24 @@
|
||||
stopResponseFlag = false;
|
||||
await tick();
|
||||
|
||||
let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? '';
|
||||
if (lastSentence) {
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat', {
|
||||
detail: { id: responseMessageId, content: lastSentence }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('chat:finish', {
|
||||
detail: {
|
||||
id: responseMessageId,
|
||||
content: responseMessage.content
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (autoScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
@@ -1098,9 +1139,12 @@
|
||||
let userPrompt = userMessage.content;
|
||||
|
||||
if ((userMessage?.models ?? [...selectedModels]).length == 1) {
|
||||
// If user message has only one model selected, sendPrompt automatically selects it for regeneration
|
||||
await sendPrompt(userPrompt, userMessage.id);
|
||||
} else {
|
||||
await sendPrompt(userPrompt, userMessage.id, message.model);
|
||||
// If there are multiple models selected, use the model of the response message for regeneration
|
||||
// e.g. many model chat
|
||||
await sendPrompt(userPrompt, userMessage.id, { modelId: message.model });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1166,6 +1210,84 @@
|
||||
}
|
||||
};
|
||||
|
||||
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
|
||||
const responseMessage = history.messages[responseId];
|
||||
|
||||
responseMessage.statusHistory = [
|
||||
{
|
||||
done: false,
|
||||
action: 'web_search',
|
||||
description: $i18n.t('Generating search query')
|
||||
}
|
||||
];
|
||||
messages = messages;
|
||||
|
||||
const prompt = history.messages[parentId].content;
|
||||
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return prompt;
|
||||
}
|
||||
);
|
||||
|
||||
if (!searchQuery) {
|
||||
toast.warning($i18n.t('No search query generated'));
|
||||
responseMessage.statusHistory.push({
|
||||
done: true,
|
||||
error: true,
|
||||
action: 'web_search',
|
||||
description: 'No search query generated'
|
||||
});
|
||||
|
||||
messages = messages;
|
||||
}
|
||||
|
||||
responseMessage.statusHistory.push({
|
||||
done: false,
|
||||
action: 'web_search',
|
||||
description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery })
|
||||
});
|
||||
messages = messages;
|
||||
|
||||
const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (results) {
|
||||
responseMessage.statusHistory.push({
|
||||
done: true,
|
||||
action: 'web_search',
|
||||
description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
|
||||
query: searchQuery,
|
||||
urls: results.filenames
|
||||
});
|
||||
|
||||
if (responseMessage?.files ?? undefined === undefined) {
|
||||
responseMessage.files = [];
|
||||
}
|
||||
|
||||
responseMessage.files.push({
|
||||
collection_name: results.collection_name,
|
||||
name: searchQuery,
|
||||
type: 'web_search_results',
|
||||
urls: results.filenames
|
||||
});
|
||||
|
||||
messages = messages;
|
||||
} else {
|
||||
responseMessage.statusHistory.push({
|
||||
done: true,
|
||||
error: true,
|
||||
action: 'web_search',
|
||||
description: 'No search results found'
|
||||
});
|
||||
messages = messages;
|
||||
}
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
return await getTagsById(localStorage.token, $chatId).catch(async (error) => {
|
||||
return [];
|
||||
@@ -1181,7 +1303,18 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<CallOverlay {submitPrompt} bind:files />
|
||||
<audio id="audioElement" src="" style="display: none;" />
|
||||
|
||||
{#if $showCallOverlay}
|
||||
<CallOverlay
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
bind:files
|
||||
modelId={selectedModelIds?.at(0) ?? null}
|
||||
chatId={$chatId}
|
||||
{eventTarget}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !chatIdProp || (loaded && chatIdProp)}
|
||||
<div
|
||||
@@ -1256,8 +1389,16 @@
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
||||
const model = $models.find((m) => m.id === e);
|
||||
if (model?.info?.meta?.toolIds ?? false) {
|
||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
||||
}
|
||||
return a;
|
||||
}, [])}
|
||||
{selectedModels}
|
||||
{messages}
|
||||
{submitPrompt}
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
showSidebar,
|
||||
models,
|
||||
config,
|
||||
showCallOverlay
|
||||
showCallOverlay,
|
||||
tools,
|
||||
user as _user
|
||||
} from '$lib/stores';
|
||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||
|
||||
@@ -29,6 +31,7 @@
|
||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||
import Headphone from '../icons/Headphone.svelte';
|
||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -57,6 +60,8 @@
|
||||
|
||||
export let files = [];
|
||||
|
||||
export let availableToolIds = [];
|
||||
export let selectedToolIds = [];
|
||||
export let webSearchEnabled = false;
|
||||
|
||||
export let prompt = '';
|
||||
@@ -343,7 +348,6 @@
|
||||
<Models
|
||||
bind:this={modelsElement}
|
||||
bind:prompt
|
||||
bind:user
|
||||
bind:chatInputPlaceholder
|
||||
{messages}
|
||||
on:select={(e) => {
|
||||
@@ -462,7 +466,7 @@
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
|
||||
if ($settings?.speechAutoSend ?? false) {
|
||||
submitPrompt(prompt, user);
|
||||
submitPrompt(prompt);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -471,7 +475,7 @@
|
||||
class="w-full flex gap-1.5"
|
||||
on:submit|preventDefault={() => {
|
||||
// check if selectedModels support image input
|
||||
submitPrompt(prompt, user);
|
||||
submitPrompt(prompt);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -652,6 +656,17 @@
|
||||
<div class=" ml-0.5 self-end mb-1.5 flex space-x-1">
|
||||
<InputMenu
|
||||
bind:webSearchEnabled
|
||||
bind:selectedToolIds
|
||||
tools={$tools.reduce((a, e, i, arr) => {
|
||||
if (availableToolIds.includes(e.id) || ($_user?.role ?? 'user') === 'admin') {
|
||||
a[e.id] = {
|
||||
name: e.name,
|
||||
description: e.meta.description,
|
||||
enabled: false
|
||||
};
|
||||
}
|
||||
return a;
|
||||
}, {})}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
@@ -702,7 +717,7 @@
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||
submitPrompt(prompt, user);
|
||||
submitPrompt(prompt);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -919,7 +934,7 @@
|
||||
}
|
||||
// check if user has access to getUserMedia
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
// If the user grants the permission, proceed to show the call overlay
|
||||
|
||||
showCallOverlay.set(true);
|
||||
|
||||
@@ -2,342 +2,49 @@
|
||||
import { config, settings, showCallOverlay } from '$lib/stores';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
import { blobToFile, calculateSHA256, extractSentences, findWordIndices } from '$lib/utils';
|
||||
import {
|
||||
blobToFile,
|
||||
calculateSHA256,
|
||||
extractSentencesForAudio,
|
||||
findWordIndices
|
||||
} from '$lib/utils';
|
||||
import { generateEmoji } from '$lib/apis';
|
||||
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let eventTarget: EventTarget;
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let stopResponse: Function;
|
||||
|
||||
export let files;
|
||||
|
||||
export let chatId;
|
||||
export let modelId;
|
||||
|
||||
let loading = false;
|
||||
let confirmed = false;
|
||||
let interrupted = false;
|
||||
let assistantSpeaking = false;
|
||||
|
||||
let emoji = null;
|
||||
|
||||
let camera = false;
|
||||
let cameraStream = null;
|
||||
|
||||
let assistantSpeaking = false;
|
||||
let assistantAudio = {};
|
||||
let assistantAudioIdx = null;
|
||||
let chatStreaming = false;
|
||||
|
||||
let rmsLevel = 0;
|
||||
let hasStartedSpeaking = false;
|
||||
|
||||
let currentUtterance = null;
|
||||
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
|
||||
const MIN_DECIBELS = -45;
|
||||
const VISUALIZER_BUFFER_LENGTH = 300;
|
||||
|
||||
// Function to calculate the RMS level from time domain data
|
||||
const calculateRMS = (data: Uint8Array) => {
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
|
||||
sumSquares += normalizedValue * normalizedValue;
|
||||
}
|
||||
return Math.sqrt(sumSquares / data.length);
|
||||
};
|
||||
|
||||
const normalizeRMS = (rms) => {
|
||||
rms = rms * 10;
|
||||
const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more
|
||||
const scaledRMS = Math.pow(rms, exp);
|
||||
|
||||
// Scale between 0.01 (1%) and 1.0 (100%)
|
||||
return Math.min(1.0, Math.max(0.01, scaledRMS));
|
||||
};
|
||||
|
||||
const analyseAudio = (stream) => {
|
||||
const audioContext = new AudioContext();
|
||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.minDecibels = MIN_DECIBELS;
|
||||
audioStreamSource.connect(analyser);
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
|
||||
const domainData = new Uint8Array(bufferLength);
|
||||
const timeDomainData = new Uint8Array(analyser.fftSize);
|
||||
|
||||
let lastSoundTime = Date.now();
|
||||
hasStartedSpeaking = false;
|
||||
|
||||
const detectSound = () => {
|
||||
const processFrame = () => {
|
||||
if (!mediaRecorder || !$showCallOverlay) {
|
||||
if (mediaRecorder) {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
analyser.getByteTimeDomainData(timeDomainData);
|
||||
analyser.getByteFrequencyData(domainData);
|
||||
|
||||
// Calculate RMS level from time domain data
|
||||
rmsLevel = calculateRMS(timeDomainData);
|
||||
|
||||
// Check if initial speech/noise has started
|
||||
const hasSound = domainData.some((value) => value > 0);
|
||||
if (hasSound) {
|
||||
stopAllAudio();
|
||||
hasStartedSpeaking = true;
|
||||
lastSoundTime = Date.now();
|
||||
}
|
||||
|
||||
// Start silence detection only after initial speech/noise has been detected
|
||||
if (hasStartedSpeaking) {
|
||||
if (Date.now() - lastSoundTime > 2000) {
|
||||
confirmed = true;
|
||||
|
||||
if (mediaRecorder) {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
detectSound();
|
||||
};
|
||||
|
||||
const stopAllAudio = () => {
|
||||
if (currentUtterance) {
|
||||
speechSynthesis.cancel();
|
||||
currentUtterance = null;
|
||||
}
|
||||
if (assistantAudio[assistantAudioIdx]) {
|
||||
assistantAudio[assistantAudioIdx].pause();
|
||||
assistantAudio[assistantAudioIdx].currentTime = 0;
|
||||
}
|
||||
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
audioElement.pause();
|
||||
audioElement.currentTime = 0;
|
||||
|
||||
assistantSpeaking = false;
|
||||
};
|
||||
|
||||
const playAudio = (idx) => {
|
||||
if ($showCallOverlay) {
|
||||
return new Promise((res) => {
|
||||
assistantAudioIdx = idx;
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
const audio = assistantAudio[idx];
|
||||
|
||||
audioElement.src = audio.src; // Assume `assistantAudio` has objects with a `src` property
|
||||
|
||||
audioElement.muted = true;
|
||||
|
||||
audioElement
|
||||
.play()
|
||||
.then(() => {
|
||||
audioElement.muted = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
|
||||
audioElement.onended = async (e) => {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
if (Object.keys(assistantAudio).length - 1 === idx) {
|
||||
assistantSpeaking = false;
|
||||
}
|
||||
|
||||
res(e);
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const getOpenAISpeech = async (text) => {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
text
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
assistantSpeaking = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
assistantAudio = audio;
|
||||
}
|
||||
};
|
||||
|
||||
const transcribeHandler = async (audioBlob) => {
|
||||
// Create a blob from the audio chunks
|
||||
|
||||
await tick();
|
||||
const file = blobToFile(audioBlob, 'recording.wav');
|
||||
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res.text);
|
||||
|
||||
if (res.text !== '') {
|
||||
const _responses = await submitPrompt(res.text);
|
||||
console.log(_responses);
|
||||
|
||||
if (_responses.at(0)) {
|
||||
const content = _responses[0];
|
||||
if (content) {
|
||||
assistantSpeakingHandler(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assistantSpeakingHandler = async (content) => {
|
||||
assistantSpeaking = true;
|
||||
|
||||
if (($config.audio.tts.engine ?? '') == '') {
|
||||
let voices = [];
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
|
||||
const voice =
|
||||
voices
|
||||
?.filter(
|
||||
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
)
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
currentUtterance = new SpeechSynthesisUtterance(content);
|
||||
|
||||
if (voice) {
|
||||
currentUtterance.voice = voice;
|
||||
}
|
||||
|
||||
speechSynthesis.speak(currentUtterance);
|
||||
}
|
||||
}, 100);
|
||||
} else if ($config.audio.tts.engine === 'openai') {
|
||||
console.log('openai');
|
||||
|
||||
const sentences = extractSentences(content).reduce((mergedTexts, currentText) => {
|
||||
const lastIndex = mergedTexts.length - 1;
|
||||
if (lastIndex >= 0) {
|
||||
const previousText = mergedTexts[lastIndex];
|
||||
const wordCount = previousText.split(/\s+/).length;
|
||||
if (wordCount < 2) {
|
||||
mergedTexts[lastIndex] = previousText + ' ' + currentText;
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
}
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
}
|
||||
return mergedTexts;
|
||||
}, []);
|
||||
|
||||
console.log(sentences);
|
||||
|
||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||
|
||||
for (const [idx, sentence] of sentences.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
assistantSpeaking = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
assistantAudio[idx] = audio;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecordingCallback = async () => {
|
||||
if ($showCallOverlay) {
|
||||
if (confirmed) {
|
||||
loading = true;
|
||||
|
||||
if (cameraStream) {
|
||||
const imageUrl = takeScreenshot();
|
||||
|
||||
files = [
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||
await transcribeHandler(audioBlob);
|
||||
|
||||
confirmed = false;
|
||||
loading = false;
|
||||
}
|
||||
audioChunks = [];
|
||||
mediaRecorder = false;
|
||||
|
||||
startRecording();
|
||||
} else {
|
||||
audioChunks = [];
|
||||
mediaRecorder = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.onstart = () => {
|
||||
console.log('Recording started');
|
||||
audioChunks = [];
|
||||
analyseAudio(stream);
|
||||
};
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (hasStartedSpeaking) {
|
||||
audioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
mediaRecorder.onstop = async () => {
|
||||
console.log('Recording stopped');
|
||||
|
||||
await stopRecordingCallback();
|
||||
};
|
||||
mediaRecorder.start();
|
||||
};
|
||||
|
||||
let videoInputDevices = [];
|
||||
let selectedVideoInputDeviceId = null;
|
||||
|
||||
@@ -439,23 +146,473 @@
|
||||
camera = false;
|
||||
};
|
||||
|
||||
$: if ($showCallOverlay) {
|
||||
const MIN_DECIBELS = -55;
|
||||
const VISUALIZER_BUFFER_LENGTH = 300;
|
||||
|
||||
const transcribeHandler = async (audioBlob) => {
|
||||
// Create a blob from the audio chunks
|
||||
|
||||
await tick();
|
||||
const file = blobToFile(audioBlob, 'recording.wav');
|
||||
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res.text);
|
||||
|
||||
if (res.text !== '') {
|
||||
const _responses = await submitPrompt(res.text, { _raw: true });
|
||||
console.log(_responses);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecordingCallback = async (_continue = true) => {
|
||||
if ($showCallOverlay) {
|
||||
console.log('%c%s', 'color: red; font-size: 20px;', '🚨 stopRecordingCallback 🚨');
|
||||
|
||||
// deep copy the audioChunks array
|
||||
const _audioChunks = audioChunks.slice(0);
|
||||
|
||||
audioChunks = [];
|
||||
mediaRecorder = false;
|
||||
|
||||
if (_continue) {
|
||||
startRecording();
|
||||
}
|
||||
|
||||
if (confirmed) {
|
||||
loading = true;
|
||||
emoji = null;
|
||||
|
||||
if (cameraStream) {
|
||||
const imageUrl = takeScreenshot();
|
||||
|
||||
files = [
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const audioBlob = new Blob(_audioChunks, { type: 'audio/wav' });
|
||||
await transcribeHandler(audioBlob);
|
||||
|
||||
confirmed = false;
|
||||
loading = false;
|
||||
}
|
||||
} else {
|
||||
audioChunks = [];
|
||||
mediaRecorder = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
|
||||
mediaRecorder.onstart = () => {
|
||||
console.log('Recording started');
|
||||
audioChunks = [];
|
||||
analyseAudio(stream);
|
||||
};
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (hasStartedSpeaking) {
|
||||
audioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = (e) => {
|
||||
console.log('Recording stopped', e);
|
||||
stopRecordingCallback();
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
};
|
||||
|
||||
// Function to calculate the RMS level from time domain data
|
||||
const calculateRMS = (data: Uint8Array) => {
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
|
||||
sumSquares += normalizedValue * normalizedValue;
|
||||
}
|
||||
return Math.sqrt(sumSquares / data.length);
|
||||
};
|
||||
|
||||
const analyseAudio = (stream) => {
|
||||
const audioContext = new AudioContext();
|
||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.minDecibels = MIN_DECIBELS;
|
||||
audioStreamSource.connect(analyser);
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
|
||||
const domainData = new Uint8Array(bufferLength);
|
||||
const timeDomainData = new Uint8Array(analyser.fftSize);
|
||||
|
||||
let lastSoundTime = Date.now();
|
||||
hasStartedSpeaking = false;
|
||||
|
||||
console.log('🔊 Sound detection started', lastSoundTime, hasStartedSpeaking);
|
||||
|
||||
const detectSound = () => {
|
||||
const processFrame = () => {
|
||||
if (!mediaRecorder || !$showCallOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (assistantSpeaking) {
|
||||
// Mute the audio if the assistant is speaking
|
||||
analyser.maxDecibels = 0;
|
||||
analyser.minDecibels = -1;
|
||||
} else {
|
||||
analyser.minDecibels = MIN_DECIBELS;
|
||||
analyser.maxDecibels = -30;
|
||||
}
|
||||
|
||||
analyser.getByteTimeDomainData(timeDomainData);
|
||||
analyser.getByteFrequencyData(domainData);
|
||||
|
||||
// Calculate RMS level from time domain data
|
||||
rmsLevel = calculateRMS(timeDomainData);
|
||||
|
||||
// Check if initial speech/noise has started
|
||||
const hasSound = domainData.some((value) => value > 0);
|
||||
if (hasSound) {
|
||||
// BIG RED TEXT
|
||||
console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected');
|
||||
|
||||
if (!hasStartedSpeaking) {
|
||||
hasStartedSpeaking = true;
|
||||
stopAllAudio();
|
||||
}
|
||||
|
||||
lastSoundTime = Date.now();
|
||||
}
|
||||
|
||||
// Start silence detection only after initial speech/noise has been detected
|
||||
if (hasStartedSpeaking) {
|
||||
if (Date.now() - lastSoundTime > 2000) {
|
||||
confirmed = true;
|
||||
|
||||
if (mediaRecorder) {
|
||||
console.log('%c%s', 'color: red; font-size: 20px;', '🔇 Silence detected');
|
||||
mediaRecorder.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
detectSound();
|
||||
};
|
||||
|
||||
let finishedMessages = {};
|
||||
let currentMessageId = null;
|
||||
let currentUtterance = null;
|
||||
|
||||
const speakSpeechSynthesisHandler = (content) => {
|
||||
if ($showCallOverlay) {
|
||||
return new Promise((resolve) => {
|
||||
let voices = [];
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
|
||||
const voice =
|
||||
voices
|
||||
?.filter(
|
||||
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
)
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
currentUtterance = new SpeechSynthesisUtterance(content);
|
||||
|
||||
if (voice) {
|
||||
currentUtterance.voice = voice;
|
||||
}
|
||||
|
||||
speechSynthesis.speak(currentUtterance);
|
||||
currentUtterance.onend = async (e) => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
resolve(e);
|
||||
};
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const playAudio = (audio) => {
|
||||
if ($showCallOverlay) {
|
||||
return new Promise((resolve) => {
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
|
||||
if (audioElement) {
|
||||
audioElement.src = audio.src;
|
||||
audioElement.muted = true;
|
||||
|
||||
audioElement
|
||||
.play()
|
||||
.then(() => {
|
||||
audioElement.muted = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
audioElement.onended = async (e) => {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
resolve(e);
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const stopAllAudio = async () => {
|
||||
assistantSpeaking = false;
|
||||
interrupted = true;
|
||||
|
||||
if (chatStreaming) {
|
||||
stopResponse();
|
||||
}
|
||||
|
||||
if (currentUtterance) {
|
||||
speechSynthesis.cancel();
|
||||
currentUtterance = null;
|
||||
}
|
||||
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
if (audioElement) {
|
||||
audioElement.muted = true;
|
||||
audioElement.pause();
|
||||
audioElement.currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
let audioAbortController = new AbortController();
|
||||
|
||||
// Audio cache map where key is the content and value is the Audio object.
|
||||
const audioCache = new Map();
|
||||
const emojiCache = new Map();
|
||||
|
||||
const fetchAudio = async (content) => {
|
||||
if (!audioCache.has(content)) {
|
||||
try {
|
||||
// Set the emoji for the content if needed
|
||||
if ($settings?.showEmojiInCall ?? false) {
|
||||
const emoji = await generateEmoji(localStorage.token, modelId, content, chatId);
|
||||
if (emoji) {
|
||||
emojiCache.set(content, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config.audio.tts.engine !== '') {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
content
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
audioCache.set(content, new Audio(blobUrl));
|
||||
}
|
||||
} else {
|
||||
audioCache.set(content, true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error synthesizing speech:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return audioCache.get(content);
|
||||
};
|
||||
|
||||
let messages = {};
|
||||
|
||||
const monitorAndPlayAudio = async (id, signal) => {
|
||||
while (!signal.aborted) {
|
||||
if (messages[id] && messages[id].length > 0) {
|
||||
// Retrieve the next content string from the queue
|
||||
const content = messages[id].shift(); // Dequeues the content for playing
|
||||
|
||||
if (audioCache.has(content)) {
|
||||
// If content is available in the cache, play it
|
||||
|
||||
// Set the emoji for the content if available
|
||||
if (($settings?.showEmojiInCall ?? false) && emojiCache.has(content)) {
|
||||
emoji = emojiCache.get(content);
|
||||
} else {
|
||||
emoji = null;
|
||||
}
|
||||
|
||||
if ($config.audio.tts.engine !== '') {
|
||||
try {
|
||||
console.log(
|
||||
'%c%s',
|
||||
'color: red; font-size: 20px;',
|
||||
`Playing audio for content: ${content}`
|
||||
);
|
||||
|
||||
const audio = audioCache.get(content);
|
||||
await playAudio(audio); // Here ensure that playAudio is indeed correct method to execute
|
||||
console.log(`Played audio for content: ${content}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 200)); // Wait before retrying to reduce tight loop
|
||||
} catch (error) {
|
||||
console.error('Error playing audio:', error);
|
||||
}
|
||||
} else {
|
||||
await speakSpeechSynthesisHandler(content);
|
||||
}
|
||||
} else {
|
||||
// If not available in the cache, push it back to the queue and delay
|
||||
messages[id].unshift(content); // Re-queue the content at the start
|
||||
console.log(`Audio for "${content}" not yet available in the cache, re-queued...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 200)); // Wait before retrying to reduce tight loop
|
||||
}
|
||||
} else if (finishedMessages[id] && messages[id] && messages[id].length === 0) {
|
||||
// If the message is finished and there are no more messages to process, break the loop
|
||||
assistantSpeaking = false;
|
||||
break;
|
||||
} else {
|
||||
// No messages to process, sleep for a bit
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
console.log(`Audio monitoring and playing stopped for message ID ${id}`);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
startRecording();
|
||||
} else {
|
||||
stopCamera();
|
||||
}
|
||||
|
||||
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 () => {
|
||||
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();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $showCallOverlay}
|
||||
<audio id="audioElement" src="" style="display: none;" />
|
||||
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
|
||||
<div
|
||||
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
||||
>
|
||||
<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
|
||||
{#if camera}
|
||||
<div class="flex justify-center items-center w-full min-h-20">
|
||||
{#if loading}
|
||||
<button
|
||||
type="button"
|
||||
class="flex justify-center items-center w-full h-20 min-h-20"
|
||||
on:click={() => {
|
||||
if (assistantSpeaking) {
|
||||
stopAllAudio();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if emoji}
|
||||
<div
|
||||
class=" transition-all rounded-full"
|
||||
style="font-size:{rmsLevel * 100 > 4
|
||||
? '4.5'
|
||||
: rmsLevel * 100 > 2
|
||||
? '4.25'
|
||||
: rmsLevel * 100 > 1
|
||||
? '3.75'
|
||||
: '3.5'}rem;width: 100%; text-align:center;"
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
{:else if loading || assistantSpeaking}
|
||||
<svg
|
||||
class="size-12 text-gray-900 dark:text-gray-400"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -504,59 +661,81 @@
|
||||
/>
|
||||
{/if}
|
||||
<!-- navbar -->
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
|
||||
{#if !camera}
|
||||
{#if loading}
|
||||
<svg
|
||||
class="size-44 text-gray-900 dark:text-gray-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (assistantSpeaking) {
|
||||
stopAllAudio();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if emoji}
|
||||
<div
|
||||
class=" transition-all rounded-full"
|
||||
style="font-size:{rmsLevel * 100 > 4
|
||||
? '13'
|
||||
: rmsLevel * 100 > 2
|
||||
? '12'
|
||||
: rmsLevel * 100 > 1
|
||||
? '11.5'
|
||||
: '11'}rem;width:100%;text-align:center;"
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
{:else if loading || assistantSpeaking}
|
||||
<svg
|
||||
class="size-44 text-gray-900 dark:text-gray-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
|
||||
>
|
||||
{:else}
|
||||
<div
|
||||
class=" {rmsLevel * 100 > 4
|
||||
? ' size-52'
|
||||
: rmsLevel * 100 > 2
|
||||
? 'size-48'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-[11.5rem]'
|
||||
: 'size-44'} transition-all bg-black dark:bg-white rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
|
||||
>
|
||||
{:else}
|
||||
<div
|
||||
class=" {rmsLevel * 100 > 4
|
||||
? ' size-52'
|
||||
: rmsLevel * 100 > 2
|
||||
? 'size-48'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-[11.5rem]'
|
||||
: 'size-44'} transition-all bg-black dark:bg-white rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
|
||||
@@ -626,7 +805,8 @@
|
||||
<button
|
||||
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
on:click={async () => {
|
||||
await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
startCamera();
|
||||
}}
|
||||
>
|
||||
@@ -655,10 +835,19 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (assistantSpeaking) {
|
||||
stopAllAudio();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" line-clamp-1 text-sm font-medium">
|
||||
{#if loading}
|
||||
{$i18n.t('Thinking...')}
|
||||
{:else if assistantSpeaking}
|
||||
{$i18n.t('Tap to interrupt')}
|
||||
{:else}
|
||||
{$i18n.t('Listening...')}
|
||||
{/if}
|
||||
|
||||
@@ -102,17 +102,19 @@
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">#</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredItems as doc, docIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -126,17 +128,19 @@
|
||||
on:focus={() => {}}
|
||||
>
|
||||
{#if doc.type === 'collection'}
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
{doc?.title ?? `#${doc.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div>
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
|
||||
{$i18n.t('Collection')}
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
#{doc.name} ({doc.filename})
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
|
||||
{doc.title}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,24 +4,33 @@
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Tags from '$lib/components/chat/Tags.svelte';
|
||||
import Share from '$lib/components/icons/Share.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let uploadFilesHandler: Function;
|
||||
|
||||
export let selectedToolIds: string[] = [];
|
||||
export let webSearchEnabled: boolean;
|
||||
|
||||
export let tools = {};
|
||||
export let onClose: Function;
|
||||
|
||||
$: tools = Object.fromEntries(
|
||||
Object.keys(tools).map((toolId) => [
|
||||
toolId,
|
||||
{
|
||||
...tools[toolId],
|
||||
enabled: selectedToolIds.includes(toolId)
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
@@ -39,20 +48,48 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[190px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
sideOffset={15}
|
||||
alignOffset={-8}
|
||||
side="top"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
{#if Object.keys(tools).length > 0}
|
||||
<div class=" max-h-28 overflow-y-auto scrollbar-hidden">
|
||||
{#each Object.keys(tools) as toolId}
|
||||
<div
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<WrenchSolid />
|
||||
<Tooltip content={tools[toolId]?.description ?? ''} className="flex-1">
|
||||
<div class=" line-clamp-1">{tools[toolId].name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
bind:state={tools[toolId].enabled}
|
||||
on:change={(e) => {
|
||||
selectedToolIds = e.detail
|
||||
? [...selectedToolIds, toolId]
|
||||
: selectedToolIds.filter((id) => id !== toolId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_web_search}
|
||||
<div
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<GlobeAltSolid />
|
||||
<div class="flex items-center">{$i18n.t('Web Search')}</div>
|
||||
<div class=" line-clamp-1">{$i18n.t('Web Search')}</div>
|
||||
</div>
|
||||
|
||||
<Switch bind:state={webSearchEnabled} />
|
||||
@@ -68,7 +105,7 @@
|
||||
}}
|
||||
>
|
||||
<DocumentArrowUpSolid />
|
||||
<div class="flex items-center">{$i18n.t('Upload Files')}</div>
|
||||
<div class=" line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
|
||||
@@ -134,17 +134,19 @@
|
||||
{#if prompt.charAt(0) === '@'}
|
||||
{#if filteredModels.length > 0}
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -155,7 +157,7 @@
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
{model.name}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -89,17 +89,19 @@
|
||||
|
||||
{#if filteredPromptCommands.length > 0}
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">/</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredPromptCommands as command, commandIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -110,11 +112,11 @@
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black">
|
||||
<div class=" font-medium text-black dark:text-gray-100">
|
||||
{command.command}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600">
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
||||
{command.title}
|
||||
</div>
|
||||
</button>
|
||||
@@ -122,7 +124,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-xl flex items-center space-x-1"
|
||||
class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
|
||||
@@ -202,38 +202,51 @@
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const messageDeleteHandler = async (messageId) => {
|
||||
const deleteMessageHandler = async (messageId) => {
|
||||
const messageToDelete = history.messages[messageId];
|
||||
const messageParentId = messageToDelete.parentId;
|
||||
const messageChildrenIds = messageToDelete.childrenIds ?? [];
|
||||
const hasSibling = messageChildrenIds.some(
|
||||
|
||||
const parentMessageId = messageToDelete.parentId;
|
||||
const childMessageIds = messageToDelete.childrenIds ?? [];
|
||||
|
||||
const hasDescendantMessages = childMessageIds.some(
|
||||
(childId) => history.messages[childId]?.childrenIds?.length > 0
|
||||
);
|
||||
messageChildrenIds.forEach((childId) => {
|
||||
const child = history.messages[childId];
|
||||
if (child && child.childrenIds) {
|
||||
if (child.childrenIds.length === 0 && !hasSibling) {
|
||||
// if last prompt/response pair
|
||||
history.messages[messageParentId].childrenIds = [];
|
||||
history.currentId = messageParentId;
|
||||
|
||||
history.currentId = parentMessageId;
|
||||
await tick();
|
||||
|
||||
// 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 {
|
||||
child.childrenIds.forEach((grandChildId) => {
|
||||
childMessage.childrenIds.forEach((grandChildId) => {
|
||||
if (history.messages[grandChildId]) {
|
||||
history.messages[grandChildId].parentId = messageParentId;
|
||||
history.messages[messageParentId].childrenIds.push(grandChildId);
|
||||
history.messages[grandChildId].parentId = parentMessageId;
|
||||
history.messages[parentMessageId].childrenIds.push(grandChildId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// remove response
|
||||
history.messages[messageParentId].childrenIds = history.messages[
|
||||
messageParentId
|
||||
|
||||
// Remove child message id from the parent message's children array
|
||||
history.messages[parentMessageId].childrenIds = history.messages[
|
||||
parentMessageId
|
||||
].childrenIds.filter((id) => id !== childId);
|
||||
});
|
||||
// remove prompt
|
||||
history.messages[messageParentId].childrenIds = history.messages[
|
||||
messageParentId
|
||||
].childrenIds.filter((id) => id !== messageId);
|
||||
|
||||
await tick();
|
||||
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
@@ -292,7 +305,7 @@
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
on:delete={() => deleteMessageHandler(message.id)}
|
||||
{user}
|
||||
{readOnly}
|
||||
{message}
|
||||
@@ -308,7 +321,7 @@
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
/>
|
||||
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
|
||||
{#key message.id}
|
||||
{#key message.id && history.currentId}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { config, user, models as _models } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { blur, fade } from 'svelte/transition';
|
||||
|
||||
import Suggestions from '../MessageInput/Suggestions.svelte';
|
||||
import { sanitizeResponseContent } from '$lib/utils';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -29,7 +32,7 @@
|
||||
</script>
|
||||
|
||||
{#key mounted}
|
||||
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-16">
|
||||
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-10">
|
||||
<div class="flex justify-start">
|
||||
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
|
||||
{#each models as model, modelIdx}
|
||||
@@ -65,8 +68,12 @@
|
||||
|
||||
<div in:fade={{ duration: 200, delay: 200 }}>
|
||||
{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
|
||||
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3">
|
||||
{models[selectedModelIdx]?.info?.meta?.description}
|
||||
<div
|
||||
class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
)}
|
||||
</div>
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user}
|
||||
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
|
||||
@@ -85,7 +92,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" font-medium text-gray-400 dark:text-gray-500">
|
||||
<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1">
|
||||
{$i18n.t('How can I help you today?')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
selectedReason = message.annotation.reason;
|
||||
comment = message.annotation.comment;
|
||||
selectedReason = message?.annotation?.reason ?? '';
|
||||
comment = message?.annotation?.comment ?? '';
|
||||
loadReasons();
|
||||
});
|
||||
|
||||
|
||||
@@ -211,93 +211,98 @@
|
||||
speaking = null;
|
||||
speakingIdx = null;
|
||||
} else {
|
||||
speaking = true;
|
||||
if ((message?.content ?? '').trim() !== '') {
|
||||
speaking = true;
|
||||
|
||||
if ($config.audio.tts.engine === 'openai') {
|
||||
loadingSpeech = true;
|
||||
if ($config.audio.tts.engine === 'openai') {
|
||||
loadingSpeech = true;
|
||||
|
||||
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
|
||||
const lastIndex = mergedTexts.length - 1;
|
||||
if (lastIndex >= 0) {
|
||||
const previousText = mergedTexts[lastIndex];
|
||||
const wordCount = previousText.split(/\s+/).length;
|
||||
if (wordCount < 2) {
|
||||
mergedTexts[lastIndex] = previousText + ' ' + currentText;
|
||||
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
|
||||
const lastIndex = mergedTexts.length - 1;
|
||||
if (lastIndex >= 0) {
|
||||
const previousText = mergedTexts[lastIndex];
|
||||
const wordCount = previousText.split(/\s+/).length;
|
||||
if (wordCount < 2) {
|
||||
mergedTexts[lastIndex] = previousText + ' ' + currentText;
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
}
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
}
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
return mergedTexts;
|
||||
}, []);
|
||||
|
||||
console.log(sentences);
|
||||
|
||||
sentencesAudio = sentences.reduce((a, e, i, arr) => {
|
||||
a[i] = null;
|
||||
return a;
|
||||
}, {});
|
||||
|
||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||
|
||||
for (const [idx, sentence] of sentences.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
speaking = null;
|
||||
loadingSpeech = false;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
sentencesAudio[idx] = audio;
|
||||
loadingSpeech = false;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
}
|
||||
return mergedTexts;
|
||||
}, []);
|
||||
} else {
|
||||
let voices = [];
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
|
||||
console.log(sentences);
|
||||
const voice =
|
||||
voices
|
||||
?.filter(
|
||||
(v) =>
|
||||
v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
)
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
sentencesAudio = sentences.reduce((a, e, i, arr) => {
|
||||
a[i] = null;
|
||||
return a;
|
||||
}, {});
|
||||
console.log(voice);
|
||||
|
||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||
const speak = new SpeechSynthesisUtterance(message.content);
|
||||
|
||||
for (const [idx, sentence] of sentences.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
console.log(speak);
|
||||
|
||||
speaking = null;
|
||||
loadingSpeech = false;
|
||||
speak.onend = () => {
|
||||
speaking = null;
|
||||
if ($settings.conversationMode) {
|
||||
document.getElementById('voice-input-button')?.click();
|
||||
}
|
||||
};
|
||||
|
||||
return null;
|
||||
});
|
||||
if (voice) {
|
||||
speak.voice = voice;
|
||||
}
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
sentencesAudio[idx] = audio;
|
||||
loadingSpeech = false;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
speechSynthesis.speak(speak);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
let voices = [];
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
|
||||
const voice =
|
||||
voices
|
||||
?.filter(
|
||||
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
)
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
console.log(voice);
|
||||
|
||||
const speak = new SpeechSynthesisUtterance(message.content);
|
||||
|
||||
console.log(speak);
|
||||
|
||||
speak.onend = () => {
|
||||
speaking = null;
|
||||
if ($settings.conversationMode) {
|
||||
document.getElementById('voice-input-button')?.click();
|
||||
}
|
||||
};
|
||||
|
||||
if (voice) {
|
||||
speak.voice = voice;
|
||||
}
|
||||
|
||||
speechSynthesis.speak(speak);
|
||||
}
|
||||
}, 100);
|
||||
toast.error('No content to speak');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -415,26 +420,29 @@
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
<div>
|
||||
{#if message?.status}
|
||||
{#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
|
||||
{@const status = (
|
||||
message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
|
||||
).at(-1)}
|
||||
<div class="flex items-center gap-2 pt-1 pb-1">
|
||||
{#if message?.status?.done === false}
|
||||
{#if status.done === false}
|
||||
<div class="">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if message?.status?.action === 'web_search' && message?.status?.urls}
|
||||
<WebSearchResults urls={message?.status?.urls}>
|
||||
{#if status?.action === 'web_search' && status?.urls}
|
||||
<WebSearchResults {status}>
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class="text-base line-clamp-1 text-wrap">
|
||||
{message.status.description}
|
||||
{status?.description}
|
||||
</div>
|
||||
</div>
|
||||
</WebSearchResults>
|
||||
{:else}
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
|
||||
{message.status.description}
|
||||
{status?.description}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -868,8 +876,8 @@
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
|
||||
?.annotation?.rating === 1
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
|
||||
?.annotation?.rating ?? null) === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
@@ -903,8 +911,8 @@
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
|
||||
?.annotation?.rating === -1
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
|
||||
?.annotation?.rating ?? null) === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
@@ -932,67 +940,68 @@
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if isLastMessage && !readOnly}
|
||||
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? '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();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
{#if isLastMessage}
|
||||
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? '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();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? '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={() => {
|
||||
regenerateResponse(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? '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={() => {
|
||||
showRateComment = false;
|
||||
regenerateResponse(message);
|
||||
}}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||
import { Collapsible } from 'bits-ui';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let urls = [];
|
||||
export let status = { urls: [], query: '' };
|
||||
let state = false;
|
||||
</script>
|
||||
|
||||
@@ -27,11 +28,45 @@
|
||||
class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
|
||||
transition={slide}
|
||||
>
|
||||
{#each urls as url, urlIdx}
|
||||
{#if status?.query}
|
||||
<a
|
||||
href="https://www.google.com/search?q={status.query}"
|
||||
target="_blank"
|
||||
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<MagnifyingGlass />
|
||||
|
||||
<div class=" line-clamp-1">
|
||||
{status.query}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition"
|
||||
>
|
||||
<!-- -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#each status.urls as url, urlIdx}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
class="flex w-full items-center p-3 px-4 {urlIdx === urls.length - 1
|
||||
class="flex w-full items-center p-3 px-4 {urlIdx === status.urls.length - 1
|
||||
? ''
|
||||
: 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
@@ -152,6 +153,7 @@
|
||||
|
||||
toast.error(error);
|
||||
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,9 +334,12 @@
|
||||
|
||||
{#if item.model?.info?.meta?.description}
|
||||
<Tooltip
|
||||
content={`${sanitizeResponseContent(
|
||||
item.model?.info?.meta?.description
|
||||
).replaceAll('\n', '<br>')}`}
|
||||
content={`${marked.parse(
|
||||
sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll(
|
||||
'\n',
|
||||
'<br>'
|
||||
)
|
||||
)}`}
|
||||
>
|
||||
<div class="">
|
||||
<svg
|
||||
|
||||
@@ -5,21 +5,25 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let admin = false;
|
||||
|
||||
export let params = {
|
||||
// Advanced
|
||||
seed: 0,
|
||||
seed: null,
|
||||
stop: null,
|
||||
temperature: '',
|
||||
frequency_penalty: '',
|
||||
repeat_last_n: '',
|
||||
mirostat: '',
|
||||
mirostat_eta: '',
|
||||
mirostat_tau: '',
|
||||
top_k: '',
|
||||
top_p: '',
|
||||
tfs_z: '',
|
||||
num_ctx: '',
|
||||
max_tokens: '',
|
||||
temperature: null,
|
||||
frequency_penalty: null,
|
||||
repeat_last_n: null,
|
||||
mirostat: null,
|
||||
mirostat_eta: null,
|
||||
mirostat_tau: null,
|
||||
top_k: null,
|
||||
top_p: null,
|
||||
tfs_z: null,
|
||||
num_ctx: null,
|
||||
num_batch: null,
|
||||
num_keep: null,
|
||||
max_tokens: null,
|
||||
use_mmap: null,
|
||||
use_mlock: null,
|
||||
num_thread: null,
|
||||
@@ -112,10 +116,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.temperature = (params?.temperature ?? '') === '' ? 0.8 : '';
|
||||
params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.temperature ?? '') === ''}
|
||||
{#if (params?.temperature ?? null) === null}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
@@ -123,7 +127,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.temperature ?? '') !== ''}
|
||||
{#if (params?.temperature ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -143,7 +147,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,10 +162,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.mirostat = (params?.mirostat ?? '') === '' ? 0 : '';
|
||||
params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.mirostat ?? '') === ''}
|
||||
{#if (params?.mirostat ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -169,7 +173,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.mirostat ?? '') !== ''}
|
||||
{#if (params?.mirostat ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -204,10 +208,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.mirostat_eta = (params?.mirostat_eta ?? '') === '' ? 0.1 : '';
|
||||
params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.mirostat_eta ?? '') === ''}
|
||||
{#if (params?.mirostat_eta ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -215,7 +219,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.mirostat_eta ?? '') !== ''}
|
||||
{#if (params?.mirostat_eta ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -235,7 +239,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,10 +254,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.mirostat_tau = (params?.mirostat_tau ?? '') === '' ? 5.0 : '';
|
||||
params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.mirostat_tau ?? '') === ''}
|
||||
{#if (params?.mirostat_tau ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -261,7 +265,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.mirostat_tau ?? '') !== ''}
|
||||
{#if (params?.mirostat_tau ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -281,7 +285,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.5"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,10 +300,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.top_k = (params?.top_k ?? '') === '' ? 40 : '';
|
||||
params.top_k = (params?.top_k ?? null) === null ? 40 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.top_k ?? '') === ''}
|
||||
{#if (params?.top_k ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -307,7 +311,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.top_k ?? '') !== ''}
|
||||
{#if (params?.top_k ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -327,7 +331,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.5"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,10 +346,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.top_p = (params?.top_p ?? '') === '' ? 0.9 : '';
|
||||
params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.top_p ?? '') === ''}
|
||||
{#if (params?.top_p ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -353,7 +357,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.top_p ?? '') !== ''}
|
||||
{#if (params?.top_p ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -373,7 +377,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -388,10 +392,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.frequency_penalty = (params?.frequency_penalty ?? '') === '' ? 1.1 : '';
|
||||
params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.frequency_penalty ?? '') === ''}
|
||||
{#if (params?.frequency_penalty ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -399,7 +403,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.frequency_penalty ?? '') !== ''}
|
||||
{#if (params?.frequency_penalty ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -419,7 +423,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.05"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,10 +438,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.repeat_last_n = (params?.repeat_last_n ?? '') === '' ? 64 : '';
|
||||
params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.repeat_last_n ?? '') === ''}
|
||||
{#if (params?.repeat_last_n ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -445,7 +449,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.repeat_last_n ?? '') !== ''}
|
||||
{#if (params?.repeat_last_n ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -480,10 +484,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.tfs_z = (params?.tfs_z ?? '') === '' ? 1 : '';
|
||||
params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.tfs_z ?? '') === ''}
|
||||
{#if (params?.tfs_z ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -491,7 +495,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.tfs_z ?? '') !== ''}
|
||||
{#if (params?.tfs_z ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -511,7 +515,7 @@
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.05"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,10 +530,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_ctx = (params?.num_ctx ?? '') === '' ? 2048 : '';
|
||||
params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.num_ctx ?? '') === ''}
|
||||
{#if (params?.num_ctx ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -537,7 +541,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.num_ctx ?? '') !== ''}
|
||||
{#if (params?.num_ctx ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -563,6 +567,98 @@
|
||||
{/if}
|
||||
</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('Batch Size (num_batch)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_batch = (params?.num_batch ?? null) === null ? 512 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.num_batch ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.num_batch ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="256"
|
||||
max="8192"
|
||||
step="256"
|
||||
bind:value={params.num_batch}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<input
|
||||
bind:value={params.num_batch}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="256"
|
||||
step="256"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</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('Tokens To Keep On Context Refresh (num_keep)')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_keep = (params?.num_keep ?? null) === null ? 24 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.num_keep ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.num_keep ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="-1"
|
||||
max="10240000"
|
||||
step="1"
|
||||
bind:value={params.num_keep}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<input
|
||||
bind:value={params.num_keep}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="-1"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</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('Max Tokens (num_predict)')}</div>
|
||||
@@ -571,10 +667,10 @@
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.max_tokens = (params?.max_tokens ?? '') === '' ? 128 : '';
|
||||
params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.max_tokens ?? '') === ''}
|
||||
{#if (params?.max_tokens ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
@@ -582,7 +678,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.max_tokens ?? '') !== ''}
|
||||
{#if (params?.max_tokens ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
@@ -609,122 +705,124 @@
|
||||
{/if}
|
||||
</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('use_mmap (Ollama)')}</div>
|
||||
{#if admin}
|
||||
<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('use_mmap (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.use_mmap ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</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('use_mlock (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.use_mlock ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</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('num_thread (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.num_thread ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.num_thread ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="1"
|
||||
max="256"
|
||||
step="1"
|
||||
bind:value={params.num_thread}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<input
|
||||
bind:value={params.num_thread}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="1"
|
||||
max="256"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.use_mmap ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</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('Template')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.template = (params?.template ?? null) === null ? '' : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.template ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.template ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
|
||||
placeholder="Write your model template content here"
|
||||
rows="4"
|
||||
bind:value={params.template}
|
||||
/>
|
||||
</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('use_mlock (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.use_mlock ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</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('num_thread (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.num_thread ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.num_thread ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="1"
|
||||
max="256"
|
||||
step="1"
|
||||
bind:value={params.num_thread}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<input
|
||||
bind:value={params.num_thread}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="1"
|
||||
max="256"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</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('Template')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.template = (params?.template ?? null) === null ? '' : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.template ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.template ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
|
||||
placeholder="Write your model template content here"
|
||||
rows="4"
|
||||
bind:value={params.template}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div> -->
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { getLanguages } from '$lib/i18n';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { models, settings, theme } from '$lib/stores';
|
||||
import { models, settings, theme, user } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -43,19 +43,21 @@
|
||||
|
||||
let params = {
|
||||
// Advanced
|
||||
seed: 0,
|
||||
temperature: '',
|
||||
frequency_penalty: '',
|
||||
repeat_last_n: '',
|
||||
mirostat: '',
|
||||
mirostat_eta: '',
|
||||
mirostat_tau: '',
|
||||
top_k: '',
|
||||
top_p: '',
|
||||
seed: null,
|
||||
temperature: null,
|
||||
frequency_penalty: null,
|
||||
repeat_last_n: null,
|
||||
mirostat: null,
|
||||
mirostat_eta: null,
|
||||
mirostat_tau: null,
|
||||
top_k: null,
|
||||
top_p: null,
|
||||
stop: null,
|
||||
tfs_z: '',
|
||||
num_ctx: '',
|
||||
max_tokens: ''
|
||||
tfs_z: null,
|
||||
num_ctx: null,
|
||||
num_batch: null,
|
||||
num_keep: null,
|
||||
max_tokens: null
|
||||
};
|
||||
|
||||
const toggleRequestFormat = async () => {
|
||||
@@ -79,12 +81,6 @@
|
||||
requestFormat = $settings.requestFormat ?? '';
|
||||
keepAlive = $settings.keepAlive ?? null;
|
||||
|
||||
params.seed = $settings.seed ?? 0;
|
||||
params.temperature = $settings.temperature ?? '';
|
||||
params.frequency_penalty = $settings.frequency_penalty ?? '';
|
||||
params.top_k = $settings.top_k ?? '';
|
||||
params.top_p = $settings.top_p ?? '';
|
||||
params.num_ctx = $settings.num_ctx ?? '';
|
||||
params = { ...params, ...$settings.params };
|
||||
params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
|
||||
});
|
||||
@@ -146,6 +142,7 @@
|
||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
|
||||
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||
<option value="her">🌷 Her</option>
|
||||
<!-- <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
||||
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> -->
|
||||
</select>
|
||||
@@ -227,7 +224,7 @@
|
||||
</div>
|
||||
|
||||
{#if showAdvanced}
|
||||
<AdvancedParams bind:params />
|
||||
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" py-1 w-full justify-between">
|
||||
@@ -300,20 +297,25 @@
|
||||
saveSettings({
|
||||
system: system !== '' ? system : undefined,
|
||||
params: {
|
||||
seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined,
|
||||
seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
|
||||
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
|
||||
temperature: params.temperature !== '' ? params.temperature : undefined,
|
||||
temperature: params.temperature !== null ? params.temperature : undefined,
|
||||
frequency_penalty:
|
||||
params.frequency_penalty !== '' ? params.frequency_penalty : undefined,
|
||||
repeat_last_n: params.repeat_last_n !== '' ? params.repeat_last_n : undefined,
|
||||
mirostat: params.mirostat !== '' ? params.mirostat : undefined,
|
||||
mirostat_eta: params.mirostat_eta !== '' ? params.mirostat_eta : undefined,
|
||||
mirostat_tau: params.mirostat_tau !== '' ? params.mirostat_tau : undefined,
|
||||
top_k: params.top_k !== '' ? params.top_k : undefined,
|
||||
top_p: params.top_p !== '' ? params.top_p : undefined,
|
||||
tfs_z: params.tfs_z !== '' ? params.tfs_z : undefined,
|
||||
num_ctx: params.num_ctx !== '' ? params.num_ctx : undefined,
|
||||
max_tokens: params.max_tokens !== '' ? params.max_tokens : undefined
|
||||
params.frequency_penalty !== null ? params.frequency_penalty : undefined,
|
||||
repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined,
|
||||
mirostat: params.mirostat !== null ? params.mirostat : undefined,
|
||||
mirostat_eta: params.mirostat_eta !== null ? params.mirostat_eta : undefined,
|
||||
mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined,
|
||||
top_k: params.top_k !== null ? params.top_k : undefined,
|
||||
top_p: params.top_p !== null ? params.top_p : undefined,
|
||||
tfs_z: params.tfs_z !== null ? params.tfs_z : undefined,
|
||||
num_ctx: params.num_ctx !== null ? params.num_ctx : undefined,
|
||||
num_batch: params.num_batch !== null ? params.num_batch : undefined,
|
||||
num_keep: params.num_keep !== null ? params.num_keep : undefined,
|
||||
max_tokens: params.max_tokens !== null ? params.max_tokens : undefined,
|
||||
use_mmap: params.use_mmap !== null ? params.use_mmap : undefined,
|
||||
use_mlock: params.use_mlock !== null ? params.use_mlock : undefined,
|
||||
num_thread: params.num_thread !== null ? params.num_thread : undefined
|
||||
},
|
||||
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { updateUserInfo } from '$lib/apis/users';
|
||||
import { getUserPosition } from '$lib/utils';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -16,13 +18,17 @@
|
||||
let responseAutoCopy = false;
|
||||
let widescreenMode = false;
|
||||
let splitLargeChunks = false;
|
||||
let userLocation = false;
|
||||
|
||||
// Interface
|
||||
let defaultModelId = '';
|
||||
let showUsername = false;
|
||||
|
||||
let chatBubble = true;
|
||||
let chatDirection: 'LTR' | 'RTL' = 'LTR';
|
||||
|
||||
let showEmojiInCall = false;
|
||||
|
||||
const toggleSplitLargeChunks = async () => {
|
||||
splitLargeChunks = !splitLargeChunks;
|
||||
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||
@@ -43,6 +49,31 @@
|
||||
saveSettings({ showUsername: showUsername });
|
||||
};
|
||||
|
||||
const toggleEmojiInCall = async () => {
|
||||
showEmojiInCall = !showEmojiInCall;
|
||||
saveSettings({ showEmojiInCall: showEmojiInCall });
|
||||
};
|
||||
|
||||
const toggleUserLocation = async () => {
|
||||
userLocation = !userLocation;
|
||||
|
||||
if (userLocation) {
|
||||
const position = await getUserPosition().catch((error) => {
|
||||
toast.error(error.message);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (position) {
|
||||
await updateUserInfo(localStorage.token, { location: position });
|
||||
toast.success('User location successfully retrieved.');
|
||||
} else {
|
||||
userLocation = false;
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings({ userLocation });
|
||||
};
|
||||
|
||||
const toggleTitleAutoGenerate = async () => {
|
||||
titleAutoGenerate = !titleAutoGenerate;
|
||||
saveSettings({
|
||||
@@ -88,12 +119,17 @@
|
||||
|
||||
onMount(async () => {
|
||||
titleAutoGenerate = $settings?.title?.auto ?? true;
|
||||
|
||||
responseAutoCopy = $settings.responseAutoCopy ?? false;
|
||||
showUsername = $settings.showUsername ?? false;
|
||||
|
||||
showEmojiInCall = $settings.showEmojiInCall ?? false;
|
||||
|
||||
chatBubble = $settings.chatBubble ?? true;
|
||||
widescreenMode = $settings.widescreenMode ?? false;
|
||||
splitLargeChunks = $settings.splitLargeChunks ?? false;
|
||||
chatDirection = $settings.chatDirection ?? 'LTR';
|
||||
userLocation = $settings.userLocation ?? false;
|
||||
|
||||
defaultModelId = ($settings?.models ?? ['']).at(0);
|
||||
});
|
||||
@@ -130,6 +166,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
togglewidescreenMode();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if widescreenMode === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
|
||||
@@ -174,16 +230,36 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow User Location')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
togglewidescreenMode();
|
||||
toggleUserLocation();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if widescreenMode === true}
|
||||
{#if userLocation === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Display Emoji in Call')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleEmojiInCall();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if showEmojiInCall === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { addNewMemory } from '$lib/apis/memories';
|
||||
import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let loading = false;
|
||||
@@ -38,7 +37,9 @@
|
||||
<Modal bind:show size="sm">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Add Memory')}</div>
|
||||
<div class=" text-lg font-medium self-center">
|
||||
{$i18n.t('Add Memory')}
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { updateMemoryById } from '$lib/apis/memories';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show;
|
||||
export let memory = {};
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let loading = false;
|
||||
let content = '';
|
||||
|
||||
$: if (show) {
|
||||
setContent();
|
||||
}
|
||||
|
||||
const setContent = () => {
|
||||
content = memory.content;
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
const res = await updateMemoryById(localStorage.token, memory.id, content).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
toast.success('Memory updated successfully');
|
||||
dispatch('save');
|
||||
show = false;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="sm">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">
|
||||
{$i18n.t('Edit Memory')}
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="">
|
||||
<textarea
|
||||
bind:value={content}
|
||||
class=" bg-transparent w-full text-sm resize-none rounded-xl p-3 outline outline-1 outline-gray-100 dark:outline-gray-800"
|
||||
rows="3"
|
||||
placeholder={$i18n.t('Enter a detail about yourself for your LLMs to recall')}
|
||||
/>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
ⓘ {$i18n.t('Refer to yourself as "User" (e.g., "User is learning Spanish")')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-1 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{$i18n.t('Update')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -10,18 +10,24 @@
|
||||
import { deleteMemoriesByUserId, deleteMemoryById, getMemories } from '$lib/apis/memories';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import EditMemoryModal from './EditMemoryModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
|
||||
let memories = [];
|
||||
let loading = true;
|
||||
|
||||
let showAddMemoryModal = false;
|
||||
let showEditMemoryModal = false;
|
||||
|
||||
$: if (show) {
|
||||
let selectedMemory = null;
|
||||
|
||||
$: if (show && memories.length === 0 && loading) {
|
||||
(async () => {
|
||||
memories = await getMemories(localStorage.token);
|
||||
loading = false;
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
@@ -62,7 +68,9 @@
|
||||
>
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
||||
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created At')} </th>
|
||||
<th scope="col" class="px-3 py-2 hidden md:flex">
|
||||
{$i18n.t('Last Modified')}
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 text-right" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -76,11 +84,38 @@
|
||||
</td>
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
|
||||
<div class="my-auto whitespace-nowrap">
|
||||
{dayjs(memory.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||
{dayjs(memory.updated_at * 1000).format(
|
||||
$i18n.t('MMMM DD, YYYY hh:mm:ss A')
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-1">
|
||||
<div class="flex justify-end w-full">
|
||||
<Tooltip content="Edit">
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
on:click={() => {
|
||||
selectedMemory = memory;
|
||||
showEditMemoryModal = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4 s-FoVA_WMOgxUD"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
|
||||
class="s-FoVA_WMOgxUD"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Delete">
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
@@ -163,3 +198,11 @@
|
||||
memories = await getMemories(localStorage.token);
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditMemoryModal
|
||||
bind:show={showEditMemoryModal}
|
||||
memory={selectedMemory}
|
||||
on:save={async () => {
|
||||
memories = await getMemories(localStorage.token);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts">
|
||||
import { basicSetup, EditorView } from 'codemirror';
|
||||
import { keymap, placeholder } from '@codemirror/view';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
|
||||
import { acceptCompletion } from '@codemirror/autocomplete';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
|
||||
import { indentUnit } from '@codemirror/language';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { formatPythonCode } from '$lib/apis/utils';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let boilerplate = '';
|
||||
export let value = '';
|
||||
|
||||
let codeEditor;
|
||||
|
||||
let isDarkMode = false;
|
||||
let editorTheme = new Compartment();
|
||||
|
||||
export const formatPythonCodeHandler = async () => {
|
||||
if (codeEditor) {
|
||||
const res = await formatPythonCode(value).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res && res.code) {
|
||||
const formattedCode = res.code;
|
||||
codeEditor.dispatch({
|
||||
changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }]
|
||||
});
|
||||
|
||||
toast.success('Code formatted successfully');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let extensions = [
|
||||
basicSetup,
|
||||
keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
|
||||
python(),
|
||||
indentUnit.of(' '),
|
||||
placeholder('Enter your code here...'),
|
||||
EditorView.updateListener.of((e) => {
|
||||
if (e.docChanged) {
|
||||
value = e.state.doc.toString();
|
||||
}
|
||||
}),
|
||||
editorTheme.of([])
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
console.log(value);
|
||||
if (value === '') {
|
||||
value = boilerplate;
|
||||
}
|
||||
|
||||
// Check if html class has dark mode
|
||||
isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
// python code editor, highlight python code
|
||||
codeEditor = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: value,
|
||||
extensions: extensions
|
||||
}),
|
||||
parent: document.getElementById('code-textarea')
|
||||
});
|
||||
|
||||
if (isDarkMode) {
|
||||
codeEditor.dispatch({
|
||||
effects: editorTheme.reconfigure(oneDark)
|
||||
});
|
||||
}
|
||||
|
||||
// listen to html class changes this should fire only when dark mode is toggled
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
const _isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
if (_isDarkMode !== isDarkMode) {
|
||||
isDarkMode = _isDarkMode;
|
||||
if (_isDarkMode) {
|
||||
codeEditor.dispatch({
|
||||
effects: editorTheme.reconfigure(oneDark)
|
||||
});
|
||||
} else {
|
||||
codeEditor.dispatch({
|
||||
effects: editorTheme.reconfigure()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
|
||||
const keydownHandler = async (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
dispatch('save');
|
||||
}
|
||||
|
||||
// Format code when Ctrl + Shift + F is pressed
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') {
|
||||
e.preventDefault();
|
||||
await formatPythonCodeHandler();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keydownHandler);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
document.removeEventListener('keydown', keydownHandler);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="code-textarea" class="h-full w-full" />
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let title = 'Confirm your action';
|
||||
export let message = 'This action cannot be undone. Do you wish to continue?';
|
||||
|
||||
export let cancelLabel = 'Cancel';
|
||||
export let confirmLabel = 'Confirm';
|
||||
|
||||
export let show = false;
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('Escape');
|
||||
show = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$: if (mounted) {
|
||||
if (show) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- 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"
|
||||
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"
|
||||
in:flyAndScale
|
||||
on:mousedown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div class="px-[1.75rem] py-6">
|
||||
<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">{title}</div>
|
||||
|
||||
<slot>
|
||||
<div class=" text-sm text-gray-500">
|
||||
{message}
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div class="mt-6 flex justify-between gap-1.5">
|
||||
<button
|
||||
class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2.5 rounded-lg transition"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
dispatch('confirm');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal-content {
|
||||
animation: scaleUp 0.1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes scaleUp {
|
||||
from {
|
||||
transform: scale(0.985);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
export let placement = 'top';
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
export let saveHandler: Function;
|
||||
|
||||
let webConfig = null;
|
||||
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper'];
|
||||
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper', 'serply'];
|
||||
|
||||
let youtubeLanguage = 'en';
|
||||
let youtubeTranslation = null;
|
||||
|
||||
@@ -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="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
user,
|
||||
@@ -11,10 +12,11 @@
|
||||
mobile,
|
||||
showArchivedChats
|
||||
} from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import {
|
||||
deleteChatById,
|
||||
getChatList,
|
||||
@@ -25,37 +27,25 @@
|
||||
archiveChatById,
|
||||
cloneChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import ChatMenu from './Sidebar/ChatMenu.svelte';
|
||||
import ShareChatModal from '../chat/ShareChatModal.svelte';
|
||||
import ArchiveBox from '../icons/ArchiveBox.svelte';
|
||||
|
||||
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
|
||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import ChatItem from './Sidebar/ChatItem.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
let show = false;
|
||||
let navElement;
|
||||
|
||||
let title: string = 'UI';
|
||||
let search = '';
|
||||
|
||||
let shareChatId = null;
|
||||
let shiftKey = false;
|
||||
|
||||
let selectedChatId = null;
|
||||
let deleteChat = null;
|
||||
|
||||
let chatDeleteId = null;
|
||||
let chatTitleEditId = null;
|
||||
let chatTitle = '';
|
||||
|
||||
let showShareChatModal = false;
|
||||
let showDeleteConfirm = false;
|
||||
let showDropdown = false;
|
||||
let isEditing = false;
|
||||
|
||||
let filteredChatList = [];
|
||||
|
||||
$: filteredChatList = $chats.filter((chat) => {
|
||||
@@ -78,13 +68,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
mobile;
|
||||
const onResize = () => {
|
||||
if ($showSidebar && window.innerWidth < BREAKPOINT) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
mobile.subscribe((e) => {
|
||||
if ($showSidebar && e) {
|
||||
@@ -125,12 +108,43 @@
|
||||
checkDirection();
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
shiftKey = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyUp = (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
shiftKey = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {};
|
||||
|
||||
const onBlur = () => {
|
||||
shiftKey = false;
|
||||
selectedChatId = null;
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
|
||||
window.addEventListener('touchstart', onTouchStart);
|
||||
window.addEventListener('touchend', onTouchEnd);
|
||||
|
||||
window.addEventListener('focus', onFocus);
|
||||
window.addEventListener('blur', onBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
|
||||
window.removeEventListener('touchstart', onTouchStart);
|
||||
window.removeEventListener('touchend', onTouchEnd);
|
||||
|
||||
window.removeEventListener('focus', onFocus);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -149,69 +163,29 @@
|
||||
await chats.set(enrichedChats);
|
||||
};
|
||||
|
||||
const loadChat = async (id) => {
|
||||
goto(`/c/${id}`);
|
||||
};
|
||||
|
||||
const editChatTitle = async (id, _title) => {
|
||||
if (_title === '') {
|
||||
toast.error($i18n.t('Title cannot be an empty string.'));
|
||||
} else {
|
||||
title = _title;
|
||||
|
||||
await updateChatById(localStorage.token, id, {
|
||||
title: _title
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const deleteChat = async (id) => {
|
||||
const res = await deleteChatById(localStorage.token, id).catch((error) => {
|
||||
toast.error(error);
|
||||
chatDeleteId = null;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
if ($chatId === id) {
|
||||
goto('/');
|
||||
}
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const cloneChatHandler = async (id) => {
|
||||
const res = await cloneChatById(localStorage.token, id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
goto(`/c/${res.id}`);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async (updated) => {
|
||||
await settings.set({ ...$settings, ...updated });
|
||||
await updateUserSettings(localStorage.token, { ui: $settings });
|
||||
location.href = '/';
|
||||
};
|
||||
|
||||
const archiveChatHandler = async (id) => {
|
||||
await archiveChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
const deleteChatHandler = async (id) => {
|
||||
const res = await deleteChatById(localStorage.token, id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
node.focus();
|
||||
if (res) {
|
||||
if ($chatId === id) {
|
||||
await chatId.set('');
|
||||
await tick();
|
||||
goto('/');
|
||||
}
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
|
||||
<ArchivedChatsModal
|
||||
bind:show={$showArchivedChats}
|
||||
on:change={async () => {
|
||||
@@ -219,6 +193,18 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title="Delete chat?"
|
||||
on:confirm={() => {
|
||||
deleteChatHandler(deleteChat.id);
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500">
|
||||
This will delete <span class=" font-semibold">{deleteChat.title}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
{#if $showSidebar}
|
||||
@@ -252,12 +238,10 @@
|
||||
draggable="false"
|
||||
on:click={async () => {
|
||||
selectedChatId = null;
|
||||
|
||||
await goto('/');
|
||||
const newChatButton = document.getElementById('new-chat-button');
|
||||
setTimeout(() => {
|
||||
newChatButton?.click();
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
@@ -486,215 +470,25 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" w-full pr-2 relative group">
|
||||
{#if chatTitleEditId === chat.id}
|
||||
<div
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
|
||||
chat.id === chatTitleEditId ||
|
||||
chat.id === chatDeleteId
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: chat.id === selectedChatId
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<input
|
||||
use:focusEdit
|
||||
bind:value={chatTitle}
|
||||
class=" bg-transparent w-full outline-none mr-10"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
|
||||
chat.id === chatTitleEditId ||
|
||||
chat.id === chatDeleteId
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: chat.id === selectedChatId
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
href="/c/{chat.id}"
|
||||
on:click={() => {
|
||||
selectedChatId = chat.id;
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
on:dblclick={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitleEditId = chat.id;
|
||||
}}
|
||||
draggable="false"
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
<div class=" text-left self-center overflow-hidden w-full h-[20px]">
|
||||
{chat.title}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="
|
||||
|
||||
{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
|
||||
? 'from-gray-200 dark:from-gray-900'
|
||||
: chat.id === selectedChatId
|
||||
? 'from-gray-100 dark:from-gray-950'
|
||||
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
|
||||
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
|
||||
|
||||
to-transparent"
|
||||
>
|
||||
{#if chatTitleEditId === chat.id}
|
||||
<div class="flex self-center space-x-1.5 z-10">
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
editChatTitle(chat.id, chatTitle);
|
||||
chatTitleEditId = null;
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
chatTitleEditId = null;
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{:else if chatDeleteId === chat.id}
|
||||
<div class="flex self-center space-x-1.5 z-10">
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
deleteChat(chat.id);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
chatDeleteId = null;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex self-center space-x-1 z-10">
|
||||
<ChatMenu
|
||||
chatId={chat.id}
|
||||
cloneChatHandler={() => {
|
||||
cloneChatHandler(chat.id);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareChatId = selectedChatId;
|
||||
showShareChatModal = true;
|
||||
}}
|
||||
archiveChatHandler={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
}}
|
||||
renameHandler={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitleEditId = chat.id;
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
chatDeleteId = chat.id;
|
||||
}}
|
||||
onClose={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label="Chat Menu"
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</ChatMenu>
|
||||
|
||||
{#if chat.id === $chatId}
|
||||
<button
|
||||
id="delete-chat-button"
|
||||
class="hidden"
|
||||
on:click={() => {
|
||||
chatDeleteId = chat.id;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<ChatItem
|
||||
{chat}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
||||
import { onMount, getContext, createEventDispatcher, tick } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import {
|
||||
archiveChatById,
|
||||
cloneChatById,
|
||||
deleteChatById,
|
||||
getChatList,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { chatId, chats, mobile, showSidebar } from '$lib/stores';
|
||||
|
||||
import ChatMenu from './ChatMenu.svelte';
|
||||
import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
|
||||
export let chat;
|
||||
export let selected = false;
|
||||
export let shiftKey = false;
|
||||
|
||||
let mouseOver = false;
|
||||
|
||||
let showShareChatModal = false;
|
||||
let confirmEdit = false;
|
||||
|
||||
let chatTitle = chat.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
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const cloneChatHandler = async (id) => {
|
||||
const res = await cloneChatById(localStorage.token, id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
goto(`/c/${res.id}`);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const archiveChatHandler = async (id) => {
|
||||
await archiveChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
node.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={chat.id} />
|
||||
|
||||
<div class=" w-full pr-2 relative group">
|
||||
{#if confirmEdit}
|
||||
<div
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: selected
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<input
|
||||
use:focusEdit
|
||||
bind:value={chatTitle}
|
||||
class=" bg-transparent w-full outline-none mr-10"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: selected
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
href="/c/{chat.id}"
|
||||
on:click={() => {
|
||||
dispatch('select');
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
on:dblclick={() => {
|
||||
chatTitle = chat.title;
|
||||
confirmEdit = true;
|
||||
}}
|
||||
on:mouseenter={(e) => {
|
||||
mouseOver = true;
|
||||
}}
|
||||
on:mouseleave={(e) => {
|
||||
mouseOver = false;
|
||||
}}
|
||||
on:focus={(e) => {}}
|
||||
draggable="false"
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
<div class=" text-left self-center overflow-hidden w-full h-[20px]">
|
||||
{chat.title}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="
|
||||
{chat.id === $chatId || confirmEdit
|
||||
? 'from-gray-200 dark:from-gray-900'
|
||||
: selected
|
||||
? 'from-gray-100 dark:from-gray-950'
|
||||
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
|
||||
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
|
||||
|
||||
to-transparent"
|
||||
on:mouseenter={(e) => {
|
||||
mouseOver = true;
|
||||
}}
|
||||
on:mouseleave={(e) => {
|
||||
mouseOver = false;
|
||||
}}
|
||||
>
|
||||
{#if confirmEdit}
|
||||
<div class="flex self-center space-x-1.5 z-10">
|
||||
<Tooltip content="Confirm">
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
editChatTitle(chat.id, chatTitle);
|
||||
confirmEdit = false;
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Cancel">
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
confirmEdit = false;
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{:else if shiftKey && mouseOver}
|
||||
<div class=" flex items-center self-center space-x-1.5">
|
||||
<Tooltip content="Archive" className="flex items-center">
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ArchiveBox className="size-4 translate-y-[0.5px]" strokeWidth="2" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Delete">
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
dispatch('delete', 'shift');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex self-center space-x-1 z-10">
|
||||
<ChatMenu
|
||||
chatId={chat.id}
|
||||
cloneChatHandler={() => {
|
||||
cloneChatHandler(chat.id);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
showShareChatModal = true;
|
||||
}}
|
||||
archiveChatHandler={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
}}
|
||||
renameHandler={() => {
|
||||
chatTitle = chat.title;
|
||||
|
||||
confirmEdit = true;
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
dispatch('delete');
|
||||
}}
|
||||
onClose={() => {
|
||||
dispatch('unselect');
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label="Chat Menu"
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
dispatch('select');
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</ChatMenu>
|
||||
|
||||
{#if chat.id === $chatId}
|
||||
<!-- Shortcut support using "delete-chat-button" id -->
|
||||
<button
|
||||
id="delete-chat-button"
|
||||
class="hidden"
|
||||
on:click={() => {
|
||||
dispatch('delete');
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,7 +491,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-xs mt-1">
|
||||
<div class=" text-gray-500 text-xs mt-1 mb-2">
|
||||
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,15 +17,19 @@
|
||||
|
||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||
import ModelMenu from './Models/ModelMenu.svelte';
|
||||
import ModelDeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let showModelDeleteConfirm = false;
|
||||
|
||||
let localModelfiles = [];
|
||||
|
||||
let importFiles;
|
||||
let modelsImportInputElement: HTMLInputElement;
|
||||
|
||||
let _models = [];
|
||||
let selectedModel = null;
|
||||
|
||||
let sortable = null;
|
||||
let searchValue = '';
|
||||
@@ -199,6 +203,13 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<ModelDeleteConfirmDialog
|
||||
bind:show={showModelDeleteConfirm}
|
||||
on:confirm={() => {
|
||||
deleteModelHandler(selectedModel);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
@@ -339,7 +350,8 @@
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
deleteModelHandler(model);
|
||||
selectedModel = model;
|
||||
showModelDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {}}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<div class=" text-xs dark:text-gray-500">
|
||||
To add documents here, upload them to the "Documents" workspace first.
|
||||
{$i18n.t('To add documents here, upload them to the "Documents" workspace first.')}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
@@ -97,7 +97,7 @@
|
||||
<Selector bind:knowledge>
|
||||
<button
|
||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
||||
type="button">Select Documents</button
|
||||
type="button">{$i18n.t('Select Documents')}</button
|
||||
>
|
||||
</Selector>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
|
||||
export let tools = [];
|
||||
|
||||
let _tools = {};
|
||||
|
||||
export let selectedToolIds = [];
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
onMount(() => {
|
||||
_tools = tools.reduce((acc, tool) => {
|
||||
acc[tool.id] = {
|
||||
...tool,
|
||||
selected: selectedToolIds.includes(tool.id)
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex w-full justify-between mb-1">
|
||||
<div class=" self-center text-sm font-semibold">{$i18n.t('Tools')}</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-xs dark:text-gray-500">
|
||||
{$i18n.t('To select toolkits here, add them to the "Tools" workspace first.')}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if tools.length > 0}
|
||||
<div class=" flex items-center gap-2 mt-2">
|
||||
{#each Object.keys(_tools) as tool, toolIdx}
|
||||
<div class=" flex items-center gap-2">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
state={_tools[tool].selected ? 'checked' : 'unchecked'}
|
||||
on:change={(e) => {
|
||||
_tools[tool].selected = e.detail === 'checked';
|
||||
selectedToolIds = Object.keys(_tools).filter((t) => _tools[t].selected);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
||||
{_tools[tool].name}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,358 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { WEBUI_NAME, prompts, tools } from '$lib/stores';
|
||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
createNewTool,
|
||||
deleteToolById,
|
||||
exportTools,
|
||||
getToolById,
|
||||
getTools
|
||||
} from '$lib/apis/tools';
|
||||
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import ConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let toolsImportInputElement: HTMLInputElement;
|
||||
let importFiles;
|
||||
|
||||
let showConfirm = false;
|
||||
let query = '';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$i18n.t('Tools')} | {$WEBUI_NAME}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-3 flex justify-between items-center">
|
||||
<div class=" text-lg font-semibold self-center">{$i18n.t('Tools')}</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Tools')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
|
||||
href="/workspace/tools/create"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850 my-2.5" />
|
||||
|
||||
<div class="my-3 mb-5">
|
||||
{#each $tools.filter((t) => query === '' || t.name
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())) as tool}
|
||||
<button
|
||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||
}}
|
||||
>
|
||||
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
||||
<a
|
||||
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
||||
class="flex items-center text-left"
|
||||
>
|
||||
<div class=" flex-1 self-center pl-5">
|
||||
<div class=" font-semibold flex items-center gap-1.5">
|
||||
<div>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div class=" text-gray-500 text-xs font-medium">{tool.id}</div>
|
||||
</div>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||
{tool.meta.description}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-1 self-center">
|
||||
<Tooltip content="Edit">
|
||||
<a
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Clone">
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
on:click={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (_tool) {
|
||||
sessionStorage.tool = JSON.stringify({
|
||||
..._tool,
|
||||
id: `${_tool.id}_clone`,
|
||||
name: `${_tool.name} (Clone)`
|
||||
});
|
||||
goto('/workspace/tools/create');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Export">
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
on:click={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (_tool) {
|
||||
let blob = new Blob([JSON.stringify([_tool])], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `tool-${_tool.id}-export-${Date.now()}.json`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowDownTray />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Delete">
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
on:click={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const res = await deleteToolById(localStorage.token, tool.id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success('Tool deleted successfully');
|
||||
tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-xs mt-1 mb-2">
|
||||
ⓘ {$i18n.t(
|
||||
'Admins have access to all tools at all times; users need tools assigned per model in the workspace.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-end w-full mb-2">
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
id="documents-import-input"
|
||||
bind:this={toolsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
hidden
|
||||
on:change={() => {
|
||||
console.log(importFiles);
|
||||
showConfirm = true;
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||
on:click={() => {
|
||||
toolsImportInputElement.click();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Tools')}</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
const _tools = await exportTools(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (_tools) {
|
||||
let blob = new Blob([JSON.stringify(_tools)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `tools-export-${Date.now()}.json`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Tools')}</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showConfirm}
|
||||
on:confirm={() => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
const _tools = JSON.parse(event.target.result);
|
||||
console.log(_tools);
|
||||
|
||||
for (const tool of _tools) {
|
||||
const res = await createNewTool(localStorage.token, tool).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
toast.success('Tool imported successfully');
|
||||
tools.set(await getTools(localStorage.token));
|
||||
};
|
||||
|
||||
reader.readAsText(importFiles[0]);
|
||||
}}
|
||||
>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
|
||||
<div>Please carefully review the following warnings:</div>
|
||||
|
||||
<ul class=" mt-1 list-disc pl-4 text-xs">
|
||||
<li>Tools have a function calling system that allows arbitrary code execution.</li>
|
||||
<li>Do not install tools from sources you do not fully trust.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
I acknowledge that I have read and I understand the implications of my action. I am aware of
|
||||
the risks associated with executing arbitrary code and I have verified the trustworthiness of
|
||||
the source.
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let value = '';
|
||||
|
||||
let codeEditor;
|
||||
let boilerplate = `import os
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Tools:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
# Add your custom tools using pure Python code here, make sure to add type hints
|
||||
# Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications
|
||||
# Please refer to function_calling_filter_pipeline.py file from pipelines project for an example
|
||||
|
||||
def get_user_name_and_email_and_id(self, __user__: dict = {}) -> str:
|
||||
"""
|
||||
Get the user name, Email and ID from the user object.
|
||||
"""
|
||||
|
||||
# Do not include :param for __user__ in the docstring as it should not be shown in the tool's specification
|
||||
# The session user object will be passed as a parameter when the function is called
|
||||
|
||||
print(__user__)
|
||||
result = ""
|
||||
|
||||
if "name" in __user__:
|
||||
result += f"User: {__user__['name']}"
|
||||
if "id" in __user__:
|
||||
result += f" (ID: {__user__['id']})"
|
||||
if "email" in __user__:
|
||||
result += f" (Email: {__user__['email']})"
|
||||
|
||||
if result == "":
|
||||
result = "User: Unknown"
|
||||
|
||||
return result
|
||||
|
||||
def get_current_time(self) -> str:
|
||||
"""
|
||||
Get the current time in a more human-readable format.
|
||||
:return: The current time.
|
||||
"""
|
||||
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%I:%M:%S %p") # Using 12-hour format with AM/PM
|
||||
current_date = now.strftime(
|
||||
"%A, %B %d, %Y"
|
||||
) # Full weekday, month name, day, and year
|
||||
|
||||
return f"Current Date and Time = {current_date}, {current_time}"
|
||||
|
||||
def calculator(self, equation: str) -> str:
|
||||
"""
|
||||
Calculate the result of an equation.
|
||||
:param equation: The equation to calculate.
|
||||
"""
|
||||
|
||||
# Avoid using eval in production code
|
||||
# https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
|
||||
try:
|
||||
result = eval(equation)
|
||||
return f"{equation} = {result}"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return "Invalid equation"
|
||||
|
||||
def get_current_weather(self, city: str) -> str:
|
||||
"""
|
||||
Get the current weather for a given city.
|
||||
:param city: The name of the city to get the weather for.
|
||||
:return: The current weather information or an error message.
|
||||
"""
|
||||
api_key = os.getenv("OPENWEATHER_API_KEY")
|
||||
if not api_key:
|
||||
return (
|
||||
"API key is not set in the environment variable 'OPENWEATHER_API_KEY'."
|
||||
)
|
||||
|
||||
base_url = "http://api.openweathermap.org/data/2.5/weather"
|
||||
params = {
|
||||
"q": city,
|
||||
"appid": api_key,
|
||||
"units": "metric", # Optional: Use 'imperial' for Fahrenheit
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(base_url, params=params)
|
||||
response.raise_for_status() # Raise HTTPError for bad responses (4xx and 5xx)
|
||||
data = response.json()
|
||||
|
||||
if data.get("cod") != 200:
|
||||
return f"Error fetching weather data: {data.get('message')}"
|
||||
|
||||
weather_description = data["weather"][0]["description"]
|
||||
temperature = data["main"]["temp"]
|
||||
humidity = data["main"]["humidity"]
|
||||
wind_speed = data["wind"]["speed"]
|
||||
|
||||
return f"Weather in {city}: {temperature}°C"
|
||||
except requests.RequestException as e:
|
||||
return f"Error fetching weather data: {str(e)}"
|
||||
`;
|
||||
|
||||
export const formatHandler = async () => {
|
||||
if (codeEditor) {
|
||||
return await codeEditor.formatPythonCodeHandler();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<CodeEditor
|
||||
bind:value
|
||||
{boilerplate}
|
||||
bind:this={codeEditor}
|
||||
on:save={() => {
|
||||
dispatch('save');
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import CodeEditor from './CodeEditor.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let formElement = null;
|
||||
let loading = false;
|
||||
let showConfirm = false;
|
||||
|
||||
export let edit = false;
|
||||
export let clone = false;
|
||||
|
||||
export let id = '';
|
||||
export let name = '';
|
||||
export let meta = {
|
||||
description: ''
|
||||
};
|
||||
export let content = '';
|
||||
|
||||
$: if (name && !edit && !clone) {
|
||||
id = name.replace(/\s+/g, '_').toLowerCase();
|
||||
}
|
||||
|
||||
let codeEditor;
|
||||
|
||||
const saveHandler = async () => {
|
||||
loading = true;
|
||||
dispatch('save', {
|
||||
id,
|
||||
name,
|
||||
meta,
|
||||
content
|
||||
});
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (codeEditor) {
|
||||
const res = await codeEditor.formatHandler();
|
||||
|
||||
if (res) {
|
||||
console.log('Code formatted successfully');
|
||||
saveHandler();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||
<div class="mx-auto w-full md:px-0 h-full">
|
||||
<form
|
||||
bind:this={formElement}
|
||||
class=" flex flex-col max-h-[100dvh] h-full"
|
||||
on:submit|preventDefault={() => {
|
||||
if (edit) {
|
||||
submitHandler();
|
||||
} else {
|
||||
showConfirm = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="mb-2.5">
|
||||
<button
|
||||
class="flex space-x-1"
|
||||
on:click={() => {
|
||||
goto('/workspace/tools');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
||||
<div class="w-full mb-2 flex flex-col gap-1.5">
|
||||
<div class="flex gap-2 w-full">
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Toolkit Name (e.g. My ToolKit)"
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Toolkit ID (e.g. my_toolkit)"
|
||||
bind:value={id}
|
||||
required
|
||||
disabled={edit}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Toolkit Description (e.g. A toolkit for performing various operations)"
|
||||
bind:value={meta.description}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||
<CodeEditor
|
||||
bind:value={content}
|
||||
bind:this={codeEditor}
|
||||
on:save={() => {
|
||||
if (formElement) {
|
||||
formElement.requestSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pb-3 flex justify-between">
|
||||
<div class="flex-1 pr-3">
|
||||
<div class="text-xs text-gray-500 line-clamp-2">
|
||||
<span class=" font-semibold dark:text-gray-200">Warning:</span> Tools are a function
|
||||
calling system with arbitrary code execution <br />—
|
||||
<span class=" font-medium dark:text-gray-400"
|
||||
>don't install random tools from sources you don't trust.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showConfirm}
|
||||
on:confirm={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
|
||||
<div>Please carefully review the following warnings:</div>
|
||||
|
||||
<ul class=" mt-1 list-disc pl-4 text-xs">
|
||||
<li>Tools have a function calling system that allows arbitrary code execution.</li>
|
||||
<li>Do not install tools from sources you do not fully trust.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
I acknowledge that I have read and I understand the implications of my action. I am aware of
|
||||
the risks associated with executing arbitrary code and I have verified the trustworthiness of
|
||||
the source.
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "لوحة التحكم",
|
||||
"Admin Settings": "اعدادات المشرف",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "التعليمات المتقدمة",
|
||||
"Advanced Params": "المعلمات المتقدمة",
|
||||
"all": "الكل",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "يسمح",
|
||||
"Allow Chat Deletion": "يستطيع حذف المحادثات",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات",
|
||||
"Already have an account?": "هل تملك حساب ؟",
|
||||
"an assistant": "مساعد",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "استجابة خطاء",
|
||||
"Banners": "لافتات",
|
||||
"Base Model (From)": "النموذج الأساسي (من)",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "قبل",
|
||||
"Being lazy": "كون كسول",
|
||||
"Brave Search API Key": "مفتاح واجهة برمجة تطبيقات البحث الشجاع",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
|
||||
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
||||
"Document": "المستند",
|
||||
"Document Settings": "أعدادات المستند",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.",
|
||||
"Edit": "تعديل",
|
||||
"Edit Doc": "تعديل الملف",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "تعديل المستخدم",
|
||||
"Email": "البريد",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "أدخل النتيجة",
|
||||
"Enter Searxng Query URL": "أدخل عنوان URL لاستعلام Searxng",
|
||||
"Enter Serper API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serper",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||
"Enter stop sequence": "أدخل تسلسل التوقف",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "أدخل Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "تصدير وثائق الخرائط",
|
||||
"Export Models": "نماذج التصدير",
|
||||
"Export Prompts": "مطالبات التصدير",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
|
||||
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "استيراد خرائط المستندات",
|
||||
"Import Models": "استيراد النماذج",
|
||||
"Import Prompts": "مطالبات الاستيراد",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "قم بتضمين علامة `-api` عند تشغيل Stable-diffusion-webui",
|
||||
"Info": "معلومات",
|
||||
"Input commands": "إدخال الأوامر",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "اللغة",
|
||||
"Last Active": "آخر نشاط",
|
||||
"Last Modified": "",
|
||||
"Light": "فاتح",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Mirostat Tau",
|
||||
"MMMM DD, YYYY": "MMMM DD, YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "تم تحميل النموذج '{{modelName}}' بنجاح",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل",
|
||||
"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
|
||||
@@ -419,13 +430,14 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "عدد نتائج البحث",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_zero": "تم البحث في {{count}} sites_zero",
|
||||
"Searched {{count}} sites_one": "تم البحث في {{count}} sites_one",
|
||||
"Searched {{count}} sites_two": "تم البحث في {{count}} sites_two",
|
||||
"Searched {{count}} sites_few": "تم البحث في {{count}} sites_few",
|
||||
"Searched {{count}} sites_many": "تم البحث في {{count}} sites_many",
|
||||
"Searched {{count}} sites_other": "تم البحث في {{count}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "البحث في الويب عن \"{{searchQuery}}\"",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "عنوان URL لاستعلام Searxng",
|
||||
"See readme.md for instructions": "readme.md للحصول على التعليمات",
|
||||
"See what's new": "ما الجديد",
|
||||
@@ -437,6 +449,7 @@
|
||||
"Select a pipeline": "حدد مسارا",
|
||||
"Select a pipeline url": "حدد عنوان URL لخط الأنابيب",
|
||||
"Select an Ollama instance": "أختار سيرفر ",
|
||||
"Select Documents": "",
|
||||
"Select model": " أختار موديل",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
|
||||
@@ -445,6 +458,7 @@
|
||||
"Send message": "يُرجى إدخال طلبك هنا.",
|
||||
"September": "سبتمبر",
|
||||
"Serper API Key": "مفتاح واجهة برمجة تطبيقات سيربر",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||
"Server connection verified": "تم التحقق من اتصال الخادم",
|
||||
"Set as default": "الافتراضي",
|
||||
@@ -485,6 +499,8 @@
|
||||
"System": "النظام",
|
||||
"System Prompt": "محادثة النظام",
|
||||
"Tags": "الوسوم",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "أخبرنا المزيد:",
|
||||
"Temperature": "درجة حرارة",
|
||||
"Template": "نموذج",
|
||||
@@ -509,10 +525,14 @@
|
||||
"To access the available model names for downloading,": "للوصول إلى أسماء الموديلات المتاحة للتنزيل،",
|
||||
"To access the GGUF models available for downloading,": "للوصول إلى الموديلات GGUF المتاحة للتنزيل،",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "الى كتابة المحادثه",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "اليوم",
|
||||
"Toggle settings": "فتح وأغلاق الاعدادات",
|
||||
"Toggle sidebar": "فتح وأغلاق الشريط الجانبي",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
|
||||
@@ -523,8 +543,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع ملف غير معروف '{{file_type}}', ولكن القبول والتعامل كنص عادي ",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "تحديث ونسخ الرابط",
|
||||
"Update password": "تحديث كلمة المرور",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "GGUF رفع موديل نوع",
|
||||
"Upload Files": "تحميل الملفات",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "Панел на Администратор",
|
||||
"Admin Settings": "Настройки на Администратор",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Разширени Параметри",
|
||||
"Advanced Params": "Разширени параметри",
|
||||
"all": "всички",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "Позволи",
|
||||
"Allow Chat Deletion": "Позволи Изтриване на Чат",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "алфанумерични знаци и тире",
|
||||
"Already have an account?": "Вече имате акаунт? ",
|
||||
"an assistant": "асистент",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "Невалиден отговор от API",
|
||||
"Banners": "Банери",
|
||||
"Base Model (From)": "Базов модел (от)",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "преди",
|
||||
"Being lazy": "Да бъдеш мързелив",
|
||||
"Brave Search API Key": "Смел ключ за API за търсене",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "Откриване, сваляне и преглед на персонализирани промптове",
|
||||
"Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
|
||||
"Document": "Документ",
|
||||
"Document Settings": "Документ Настройки",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.",
|
||||
"Edit": "Редактиране",
|
||||
"Edit Doc": "Редактиране на документ",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "Редактиране на потребител",
|
||||
"Email": "Имейл",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "Въведете оценка",
|
||||
"Enter Searxng Query URL": "Въведете URL адреса на заявката на Searxng",
|
||||
"Enter Serper API Key": "Въведете Serper API ключ",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "Въведете Serpstack API ключ",
|
||||
"Enter stop sequence": "Въведете стоп последователност",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "Въведете Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Въведете URL (напр. http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "Въведете URL (напр. http://localhost:11434)",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "Експортване на документен мапинг",
|
||||
"Export Models": "Експортиране на модели",
|
||||
"Export Prompts": "Експортване на промптове",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "Неуспешно създаване на API ключ.",
|
||||
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "Импортване на документен мапинг",
|
||||
"Import Models": "Импортиране на модели",
|
||||
"Import Prompts": "Импортване на промптове",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Включете флага `--api`, когато стартирате stable-diffusion-webui",
|
||||
"Info": "Информация",
|
||||
"Input commands": "Въведете команди",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "Език",
|
||||
"Last Active": "Последни активни",
|
||||
"Last Modified": "",
|
||||
"Light": "Светъл",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Mirostat Tau",
|
||||
"MMMM DD, YYYY": "MMMM DD, YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "Моделът '{{modelName}}' беше успешно свален.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.",
|
||||
"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "Брой резултати от търсенето",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "Търси се в {{count}} sites_one",
|
||||
"Searched {{count}} sites_other": "Търси се в {{count}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "Търсене в уеб за '{{searchQuery}}'",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "URL адрес на заявка на Searxng",
|
||||
"See readme.md for instructions": "Виж readme.md за инструкции",
|
||||
"See what's new": "Виж какво е новото",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "Изберете тръбопровод",
|
||||
"Select a pipeline url": "Избор на URL адрес на канал",
|
||||
"Select an Ollama instance": "Изберете Ollama инстанция",
|
||||
"Select Documents": "",
|
||||
"Select model": "Изберете модел",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "Изпращане на съобщение",
|
||||
"September": "Септември",
|
||||
"Serper API Key": "Serper API ключ",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack API ключ",
|
||||
"Server connection verified": "Server connection verified",
|
||||
"Set as default": "Задай по подразбиране",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "Система",
|
||||
"System Prompt": "Системен Промпт",
|
||||
"Tags": "Тагове",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "Повече информация:",
|
||||
"Temperature": "Температура",
|
||||
"Template": "Шаблон",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,",
|
||||
"To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "към чат входа.",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "днес",
|
||||
"Toggle settings": "Toggle settings",
|
||||
"Toggle sidebar": "Toggle sidebar",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Проблеми с достъпът до Ollama?",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Въведете Hugging Face Resolve (Download) URL",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "О, не! Възникна проблем при свързването с {{provider}}.",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Непознат файлов тип '{{file_type}}', но се приема и обработва като текст",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "Обнови и копирай връзка",
|
||||
"Update password": "Обновяване на парола",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "Качване на GGUF модел",
|
||||
"Upload Files": "Качване на файлове",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "এডমিন প্যানেল",
|
||||
"Admin Settings": "এডমিন সেটিংস",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "এডভান্সড প্যারামিটার্স",
|
||||
"Advanced Params": "অ্যাডভান্সড প্যারাম",
|
||||
"all": "সব",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "অনুমোদন",
|
||||
"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন",
|
||||
"Already have an account?": "আগে থেকেই একাউন্ট আছে?",
|
||||
"an assistant": "একটা এসিস্ট্যান্ট",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "খারাপ প্রতিক্রিয়া",
|
||||
"Banners": "ব্যানার",
|
||||
"Base Model (From)": "বেস মডেল (থেকে)",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "পূর্ববর্তী",
|
||||
"Being lazy": "অলস হওয়া",
|
||||
"Brave Search API Key": "সাহসী অনুসন্ধান API কী",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "কাস্টম প্রম্পটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন",
|
||||
"Discover, download, and explore model presets": "মডেল প্রিসেটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "চ্যাটে 'আপনি'-র পরবর্তে ইউজারনেম দেখান",
|
||||
"Document": "ডকুমেন্ট",
|
||||
"Document Settings": "ডকুমেন্ট সেটিংসমূহ",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "যেমন '30s','10m'. সময়ের অনুমোদিত অনুমোদিত এককগুলি হচ্ছে 's', 'm', 'h'.",
|
||||
"Edit": "এডিট করুন",
|
||||
"Edit Doc": "ডকুমেন্ট এডিট করুন",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "ইউজার এডিট করুন",
|
||||
"Email": "ইমেইল",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "স্কোর দিন",
|
||||
"Enter Searxng Query URL": "Searxng ক্যোয়ারী URL লিখুন",
|
||||
"Enter Serper API Key": "Serper API কী লিখুন",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "Serpstack API কী লিখুন",
|
||||
"Enter stop sequence": "স্টপ সিকোয়েন্স লিখুন",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "Top K লিখুন",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "ইউআরএল দিন (যেমন http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "ইউআরএল দিন (যেমন http://localhost:11434)",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং এক্সপোর্ট করুন",
|
||||
"Export Models": "রপ্তানি মডেল",
|
||||
"Export Prompts": "প্রম্পটগুলো একপোর্ট করুন",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "API Key তৈরি করা যায়নি।",
|
||||
"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং ইমপোর্ট করুন",
|
||||
"Import Models": "মডেল আমদানি করুন",
|
||||
"Import Prompts": "প্রম্পটগুলো ইমপোর্ট করুন",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui চালু করার সময় `--api` ফ্ল্যাগ সংযুক্ত করুন",
|
||||
"Info": "তথ্য",
|
||||
"Input commands": "ইনপুট কমান্ডস",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "ভাষা",
|
||||
"Last Active": "সর্বশেষ সক্রিয়",
|
||||
"Last Modified": "",
|
||||
"Light": "লাইট",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "LLM ভুল করতে পারে। গুরুত্বপূর্ণ তথ্য যাচাই করে নিন।",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Mirostat Tau",
|
||||
"MMMM DD, YYYY": "MMMM DD, YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' মডেল সফলভাবে ডাউনলোড হয়েছে।",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "{{modelTag}} ডাউনলোডের জন্য আগে থেকেই অপেক্ষমান আছে।",
|
||||
"Model {{modelId}} not found": "{{modelId}} মডেল পাওয়া যায়নি",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "অনুসন্ধানের ফলাফল গণনা",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "{{কাউন্ট}} অনুসন্ধান করা হয়েছে sites_one",
|
||||
"Searched {{count}} sites_other": "{{কাউন্ট}} অনুসন্ধান করা হয়েছে sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "'{{searchQuery}}' এর জন্য ওয়েবে অনুসন্ধান করা হচ্ছে",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "Searxng ক্যোয়ারী URL",
|
||||
"See readme.md for instructions": "নির্দেশিকার জন্য readme.md দেখুন",
|
||||
"See what's new": "নতুন কী আছে দেখুন",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "একটি পাইপলাইন নির্বাচন করুন",
|
||||
"Select a pipeline url": "একটি পাইপলাইন URL নির্বাচন করুন",
|
||||
"Select an Ollama instance": "একটি Ollama ইন্সট্যান্স নির্বাচন করুন",
|
||||
"Select Documents": "",
|
||||
"Select model": "মডেল নির্বাচন করুন",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "মেসেজ পাঠান",
|
||||
"September": "সেপ্টেম্বর",
|
||||
"Serper API Key": "Serper API Key",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack API Key",
|
||||
"Server connection verified": "সার্ভার কানেকশন যাচাই করা হয়েছে",
|
||||
"Set as default": "ডিফল্ট হিসেবে নির্ধারণ করুন",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "সিস্টেম",
|
||||
"System Prompt": "সিস্টেম প্রম্পট",
|
||||
"Tags": "ট্যাগসমূহ",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "আরও বলুন:",
|
||||
"Temperature": "তাপমাত্রা",
|
||||
"Template": "টেম্পলেট",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "ডাউনলোডের জন্য এভেইলএবল মডেলের নামগুলো এক্সেস করতে,",
|
||||
"To access the GGUF models available for downloading,": "ডাউলোডের জন্য এভেইলএবল GGUF মডেলগুলো এক্সেস করতে,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "চ্যাট ইনপুটে",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "আজ",
|
||||
"Toggle settings": "সেটিংস টোগল",
|
||||
"Toggle sidebar": "সাইডবার টোগল",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Ollama এক্সেস করতে সমস্যা হচ্ছে?",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Hugging Face থেকে ডাউনলোড করার ইউআরএল টাইপ করুন",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "ওহ-হো! {{provider}} এর সাথে কানেকশনে সমস্যা হয়েছে।",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "অপরিচিত ফাইল ফরম্যাট '{{file_type}}', তবে প্লেইন টেক্সট হিসেবে গ্রহণ করা হলো",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "আপডেট এবং লিংক কপি করুন",
|
||||
"Update password": "পাসওয়ার্ড আপডেট করুন",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "একটি GGUF মডেল আপলোড করুন",
|
||||
"Upload Files": "ফাইল আপলোড করুন",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "Panell d'Administració",
|
||||
"Admin Settings": "Configuració d'Administració",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Paràmetres Avançats",
|
||||
"Advanced Params": "Paràmetres avançats",
|
||||
"all": "tots",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "Permet",
|
||||
"Allow Chat Deletion": "Permet la Supressió del Xat",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "caràcters alfanumèrics i guions",
|
||||
"Already have an account?": "Ja tens un compte?",
|
||||
"an assistant": "un assistent",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "Resposta Erroni",
|
||||
"Banners": "Banners",
|
||||
"Base Model (From)": "Model base (des de)",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "abans",
|
||||
"Being lazy": "Ser l'estupidez",
|
||||
"Brave Search API Key": "Clau API Brave Search",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "Descobreix, descarrega i explora prompts personalitzats",
|
||||
"Discover, download, and explore model presets": "Descobreix, descarrega i explora presets de models",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "Mostra el nom d'usuari en lloc de 'Tu' al Xat",
|
||||
"Document": "Document",
|
||||
"Document Settings": "Configuració de Documents",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
|
||||
"Edit": "Editar",
|
||||
"Edit Doc": "Edita Document",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "Edita Usuari",
|
||||
"Email": "Correu electrònic",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "Introdueix el Puntuació",
|
||||
"Enter Searxng Query URL": "Introduïu l'URL de consulta de Searxng",
|
||||
"Enter Serper API Key": "Introduïu la clau de l'API Serper",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "Introduïu la clau de l'API Serpstack",
|
||||
"Enter stop sequence": "Introdueix la seqüència de parada",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "Introdueix Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Introdueix l'URL (p. ex. http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "Introdueix l'URL (p. ex. http://localhost:11434)",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "Exporta el Mapatge de Documents",
|
||||
"Export Models": "Models d'exportació",
|
||||
"Export Prompts": "Exporta Prompts",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "No s'ha pogut crear la clau d'API.",
|
||||
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "Importa el Mapa de Documents",
|
||||
"Import Models": "Models d'importació",
|
||||
"Import Prompts": "Importa Prompts",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Inclou la bandera `--api` quan executis stable-diffusion-webui",
|
||||
"Info": "Informació",
|
||||
"Input commands": "Entra ordres",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "Idioma",
|
||||
"Last Active": "Últim Actiu",
|
||||
"Last Modified": "",
|
||||
"Light": "Clar",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "Els LLMs poden cometre errors. Verifica la informació important.",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Tau de Mirostat",
|
||||
"MMMM DD, YYYY": "DD de MMMM, YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "DD de MMMM, YYYY HH:mm",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat amb èxit.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
|
||||
"Model {{modelId}} not found": "Model {{modelId}} no trobat",
|
||||
@@ -419,10 +430,11 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "Recompte de resultats de cerca",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "Cercat {{count}} sites_one",
|
||||
"Searched {{count}} sites_many": "Cercat {{recompte}} sites_many",
|
||||
"Searched {{count}} sites_other": "Cercat {{recompte}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "Cerca a la web de '{{searchQuery}}'",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "Searxng URL de consulta",
|
||||
"See readme.md for instructions": "Consulta el readme.md per a instruccions",
|
||||
"See what's new": "Veure novetats",
|
||||
@@ -434,6 +446,7 @@
|
||||
"Select a pipeline": "Seleccioneu una canonada",
|
||||
"Select a pipeline url": "Seleccionar un URL de canonada",
|
||||
"Select an Ollama instance": "Selecciona una instància d'Ollama",
|
||||
"Select Documents": "",
|
||||
"Select model": "Selecciona un model",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Els models seleccionats no admeten l'entrada d'imatges",
|
||||
@@ -442,6 +455,7 @@
|
||||
"Send message": "Envia missatge",
|
||||
"September": "Setembre",
|
||||
"Serper API Key": "Clau API Serper",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack API Key",
|
||||
"Server connection verified": "Connexió al servidor verificada",
|
||||
"Set as default": "Estableix com a predeterminat",
|
||||
@@ -482,6 +496,8 @@
|
||||
"System": "Sistema",
|
||||
"System Prompt": "Prompt del Sistema",
|
||||
"Tags": "Etiquetes",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "Dóna'ns més informació:",
|
||||
"Temperature": "Temperatura",
|
||||
"Template": "Plantilla",
|
||||
@@ -506,10 +522,14 @@
|
||||
"To access the available model names for downloading,": "Per accedir als noms dels models disponibles per descarregar,",
|
||||
"To access the GGUF models available for downloading,": "Per accedir als models GGUF disponibles per descarregar,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "a l'entrada del xat.",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "Avui",
|
||||
"Toggle settings": "Commuta configuracions",
|
||||
"Toggle sidebar": "Commuta barra lateral",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Problemes accedint a Ollama?",
|
||||
@@ -520,8 +540,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Escriu URL de Resolució (Descàrrega) de Hugging Face",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "Uf! Hi va haver un problema connectant-se a {{provider}}.",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipus d'Arxiu Desconegut '{{file_type}}', però acceptant i tractant com a text pla",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "Actualitza i Copia enllaç",
|
||||
"Update password": "Actualitza contrasenya",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "Puja un model GGUF",
|
||||
"Upload Files": "Pujar fitxers",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "Admin Panel",
|
||||
"Admin Settings": "Mga setting sa administratibo",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "advanced settings",
|
||||
"Advanced Params": "",
|
||||
"all": "tanan",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "Sa pagtugot",
|
||||
"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "alphanumeric nga mga karakter ug hyphen",
|
||||
"Already have an account?": "Naa na kay account ?",
|
||||
"an assistant": "usa ka katabang",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "",
|
||||
"Being lazy": "",
|
||||
"Brave Search API Key": "",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "Pagdiskubre, pag-download ug pagsuhid sa mga naandan nga pag-aghat",
|
||||
"Discover, download, and explore model presets": "Pagdiskobre, pag-download, ug pagsuhid sa mga preset sa template",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "Ipakita ang username imbes nga 'Ikaw' sa Panaghisgutan",
|
||||
"Document": "Dokumento",
|
||||
"Document Settings": "Mga Setting sa Dokumento",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ",
|
||||
"Edit": "",
|
||||
"Edit Doc": "I-edit ang dokumento",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "I-edit ang tiggamit",
|
||||
"Email": "E-mail",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter stop sequence": "Pagsulod sa katapusan nga han-ay",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "Pagsulod sa Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Pagsulod sa URL (e.g. http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "I-export ang pagmapa sa dokumento",
|
||||
"Export Models": "",
|
||||
"Export Prompts": "Export prompts",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "",
|
||||
"Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "Import nga pagmapa sa dokumento",
|
||||
"Import Models": "",
|
||||
"Import Prompts": "Import prompt",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Iapil ang `--api` nga bandila kung nagdagan nga stable-diffusion-webui",
|
||||
"Info": "",
|
||||
"Input commands": "Pagsulod sa input commands",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "Pinulongan",
|
||||
"Last Active": "",
|
||||
"Last Modified": "",
|
||||
"Light": "Kahayag",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "Ang mga LLM mahimong masayop. ",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Mirostat Tau",
|
||||
"MMMM DD, YYYY": "MMMM DD, YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "Ang modelo'{{modelName}}' malampuson nga na-download.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "Ang modelo'{{modelTag}}' naa na sa pila para ma-download.",
|
||||
"Model {{modelId}} not found": "Modelo {{modelId}} wala makit-an",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "",
|
||||
"See readme.md for instructions": "Tan-awa ang readme.md alang sa mga panudlo",
|
||||
"See what's new": "Tan-awa unsay bag-o",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select an Ollama instance": "Pagpili usa ka pananglitan sa Ollama",
|
||||
"Select Documents": "",
|
||||
"Select model": "Pagpili og modelo",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "Magpadala ug mensahe",
|
||||
"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",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "Sistema",
|
||||
"System Prompt": "Madasig nga Sistema",
|
||||
"Tags": "Mga tag",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "",
|
||||
"Temperature": "Temperatura",
|
||||
"Template": "Modelo",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "Aron ma-access ang mga ngalan sa modelo nga ma-download,",
|
||||
"To access the GGUF models available for downloading,": "Aron ma-access ang mga modelo sa GGUF nga magamit alang sa pag-download,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "sa entrada sa iring.",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "",
|
||||
"Toggle settings": "I-toggle ang mga setting",
|
||||
"Toggle sidebar": "I-toggle ang sidebar",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Ibabaw nga P",
|
||||
"Trouble accessing Ollama?": "Adunay mga problema sa pag-access sa Ollama?",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Pagsulod sa resolusyon (pag-download) URL Hugging Face",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! {{provider}}.",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Wala mailhi nga tipo sa file '{{file_type}}', apan gidawat ug gitratar ingon yano nga teksto",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update password": "I-update ang password",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "Pag-upload ug modelo sa GGUF",
|
||||
"Upload Files": "",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "Admin Panel",
|
||||
"Admin Settings": "Admin Einstellungen",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Erweiterte Parameter",
|
||||
"Advanced Params": "Erweiterte Parameter",
|
||||
"all": "Alle",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "Erlauben",
|
||||
"Allow Chat Deletion": "Chat Löschung erlauben",
|
||||
"Allow non-local voices": "Nicht-lokale Stimmen erlauben",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "alphanumerische Zeichen und Bindestriche",
|
||||
"Already have an account?": "Hast du vielleicht schon ein Account?",
|
||||
"an assistant": "ein Assistent",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "Schlechte Antwort",
|
||||
"Banners": "Banner",
|
||||
"Base Model (From)": "Basismodell (von)",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "bereits geteilt",
|
||||
"Being lazy": "Faul sein",
|
||||
"Brave Search API Key": "API-Schlüssel für die Brave-Suche",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "Benutzerdefinierte Prompts entdecken, herunterladen und erkunden",
|
||||
"Discover, download, and explore model presets": "Modellvorgaben entdecken, herunterladen und erkunden",
|
||||
"Dismissible": "ausblendbar",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "Den Benutzernamen anstelle von 'du' im Chat anzeigen",
|
||||
"Document": "Dokument",
|
||||
"Document Settings": "Dokumenteinstellungen",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z.B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit Doc": "Dokument bearbeiten",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "Benutzer bearbeiten",
|
||||
"Email": "E-Mail",
|
||||
"Embedding Batch Size": "Embedding Batch Größe",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "Score eingeben",
|
||||
"Enter Searxng Query URL": "Geben Sie die Searxng-Abfrage-URL ein",
|
||||
"Enter Serper API Key": "Serper-API-Schlüssel eingeben",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "Geben Sie den Serpstack-API-Schlüssel ein",
|
||||
"Enter stop sequence": "Stop-Sequenz eingeben",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "Gib Top K ein",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Gib die URL ein (z.B. http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "Gib die URL ein (z.B. http://localhost:11434)",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "Dokumentenmapping exportieren",
|
||||
"Export Models": "Modelle exportieren",
|
||||
"Export Prompts": "Prompts exportieren",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "API Key erstellen fehlgeschlagen",
|
||||
"Failed to read clipboard contents": "Fehler beim Lesen des Zwischenablageninhalts",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "Dokumentenmapping importieren",
|
||||
"Import Models": "Modelle importieren",
|
||||
"Import Prompts": "Prompts importieren",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Füge das `--api`-Flag hinzu, wenn du stable-diffusion-webui nutzt",
|
||||
"Info": "Info",
|
||||
"Input commands": "Eingabebefehle",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "Sprache",
|
||||
"Last Active": "Zuletzt aktiv",
|
||||
"Last Modified": "",
|
||||
"Light": "Hell",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Mirostat Tau",
|
||||
"MMMM DD, YYYY": "DD MMMM YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "DD MMMM YYYY HH:mm",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "Modell '{{modelName}}' wurde erfolgreich heruntergeladen.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "Modell '{{modelTag}}' befindet sich bereits in der Warteschlange zum Herunterladen.",
|
||||
"Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "Anzahl der Suchergebnisse",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "Gesucht {{count}} sites_one",
|
||||
"Searched {{count}} sites_other": "Gesucht {{count}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "Suche im Web nach '{{searchQuery}}'",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "Searxng-Abfrage-URL",
|
||||
"See readme.md for instructions": "Anleitung in readme.md anzeigen",
|
||||
"See what's new": "Was gibt's Neues",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "Wählen Sie eine Pipeline aus",
|
||||
"Select a pipeline url": "Auswählen einer Pipeline-URL",
|
||||
"Select an Ollama instance": "Eine Ollama Instanz auswählen",
|
||||
"Select Documents": "",
|
||||
"Select model": "Modell auswählen",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Ausgewählte Modelle unterstützen keine Bildeingaben",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "Nachricht senden",
|
||||
"September": "September",
|
||||
"Serper API Key": "Serper-API-Schlüssel",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack-API-Schlüssel",
|
||||
"Server connection verified": "Serververbindung überprüft",
|
||||
"Set as default": "Als Standard festlegen",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "System",
|
||||
"System Prompt": "System-Prompt",
|
||||
"Tags": "Tags",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "Erzähl uns mehr",
|
||||
"Temperature": "Temperatur",
|
||||
"Template": "Vorlage",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "Um auf die verfügbaren Modellnamen zum Herunterladen zuzugreifen,",
|
||||
"To access the GGUF models available for downloading,": "Um auf die verfügbaren GGUF Modelle zum Herunterladen zuzugreifen",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "to chat input.",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "Heute",
|
||||
"Toggle settings": "Einstellungen umschalten",
|
||||
"Toggle sidebar": "Seitenleiste umschalten",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Probleme beim Zugriff auf Ollama?",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Gib die Hugging Face Resolve (Download) URL ein",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "Ups! Es gab ein Problem bei der Verbindung mit {{provider}}.",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unbekannter Dateityp '{{file_type}}', wird jedoch akzeptiert und als einfacher Text behandelt.",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "Erneuern und kopieren",
|
||||
"Update password": "Passwort aktualisieren",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "GGUF Model hochladen",
|
||||
"Upload Files": "Dateien hochladen",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "Admin Panel",
|
||||
"Admin Settings": "Admin Settings",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Advanced Parameters",
|
||||
"Advanced Params": "",
|
||||
"all": "all",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "Allow",
|
||||
"Allow Chat Deletion": "Allow Delete Chats",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "so alpha, many hyphen",
|
||||
"Already have an account?": "Such account exists?",
|
||||
"an assistant": "such assistant",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "",
|
||||
"Being lazy": "",
|
||||
"Brave Search API Key": "",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "Discover, download, and explore custom prompts",
|
||||
"Discover, download, and explore model presets": "Discover, download, and explore model presets",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "Display username instead of You in Chat",
|
||||
"Document": "Document",
|
||||
"Document Settings": "Document Settings",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Much time units are 's', 'm', 'h'.",
|
||||
"Edit": "",
|
||||
"Edit Doc": "Edit Doge",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "Edit Wowser",
|
||||
"Email": "Email",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter stop sequence": "Enter stop bark",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "Enter Top Wow",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Enter URL (e.g. http://127.0.0.1:7860/)",
|
||||
"Enter URL (e.g. http://localhost:11434)": "",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "Export Mappings of Dogos",
|
||||
"Export Models": "",
|
||||
"Export Prompts": "Export Promptos",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "",
|
||||
"Failed to read clipboard contents": "Failed to read clipboard borks",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "Import Doge Mapping",
|
||||
"Import Models": "",
|
||||
"Import Prompts": "Import Promptos",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui",
|
||||
"Info": "",
|
||||
"Input commands": "Input commands",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "Doge Speak",
|
||||
"Last Active": "",
|
||||
"Last Modified": "",
|
||||
"Light": "Light",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "LLMs can make borks. Verify important info.",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "Mirostat Tau",
|
||||
"MMMM DD, YYYY": "MMMM DD, YYYY",
|
||||
"MMMM DD, YYYY HH:mm": "",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' has been successfully downloaded.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.",
|
||||
"Model {{modelId}} not found": "Model {{modelId}} not found",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "",
|
||||
"See readme.md for instructions": "See readme.md for instructions wow",
|
||||
"See what's new": "See what's new so amaze",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select an Ollama instance": "Select an Ollama instance very choose",
|
||||
"Select Documents": "",
|
||||
"Select model": "Select model much choice",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "Send message very send",
|
||||
"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",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "System very system",
|
||||
"System Prompt": "System Prompt much prompt",
|
||||
"Tags": "Tags very tags",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "",
|
||||
"Temperature": "Temperature very temp",
|
||||
"Template": "Template much template",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "To access the available model names for downloading, much access",
|
||||
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading, much access",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "to chat input. Very chat.",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "",
|
||||
"Toggle settings": "Toggle settings much toggle",
|
||||
"Toggle sidebar": "Toggle sidebar much toggle",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "Top K very top",
|
||||
"Top P": "Top P very top",
|
||||
"Trouble accessing Ollama?": "Trouble accessing Ollama? Much trouble?",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL much download",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! There was an issue connecting to {{provider}}. Much uh-oh!",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unknown File Type '{{file_type}}', but accepting and treating as plain text very unknown",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update password": "Update password much change",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "Upload a GGUF model very upload",
|
||||
"Upload Files": "",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "",
|
||||
"Admin Settings": "",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "",
|
||||
"Advanced Params": "",
|
||||
"all": "",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "",
|
||||
"Allow Chat Deletion": "",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "",
|
||||
"Already have an account?": "",
|
||||
"an assistant": "",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "",
|
||||
"Being lazy": "",
|
||||
"Brave Search API Key": "",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "",
|
||||
"Discover, download, and explore model presets": "",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "",
|
||||
"Document": "",
|
||||
"Document Settings": "",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
|
||||
"Edit": "",
|
||||
"Edit Doc": "",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "",
|
||||
"Email": "",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter stop sequence": "",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
|
||||
"Enter URL (e.g. http://localhost:11434)": "",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "",
|
||||
"Export Models": "",
|
||||
"Export Prompts": "",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "",
|
||||
"Failed to read clipboard contents": "",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "",
|
||||
"Import Models": "",
|
||||
"Import Prompts": "",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "",
|
||||
"Info": "",
|
||||
"Input commands": "",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "",
|
||||
"Last Active": "",
|
||||
"Last Modified": "",
|
||||
"Light": "",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "",
|
||||
"MMMM DD, YYYY": "",
|
||||
"MMMM DD, YYYY HH:mm": "",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "",
|
||||
"Model {{modelId}} not found": "",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "",
|
||||
"See readme.md for instructions": "",
|
||||
"See what's new": "",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select an Ollama instance": "",
|
||||
"Select Documents": "",
|
||||
"Select model": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "",
|
||||
"September": "",
|
||||
"Serper API Key": "",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Server connection verified": "",
|
||||
"Set as default": "",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "",
|
||||
"System Prompt": "",
|
||||
"Tags": "",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "",
|
||||
"Temperature": "",
|
||||
"Template": "",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "",
|
||||
"To access the GGUF models available for downloading,": "",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "",
|
||||
"Toggle settings": "",
|
||||
"Toggle sidebar": "",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "",
|
||||
"Top P": "",
|
||||
"Trouble accessing Ollama?": "",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update password": "",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "",
|
||||
"Upload Files": "",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Admin": "",
|
||||
"Admin Panel": "",
|
||||
"Admin Settings": "",
|
||||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "",
|
||||
"Advanced Params": "",
|
||||
"all": "",
|
||||
@@ -41,6 +42,7 @@
|
||||
"Allow": "",
|
||||
"Allow Chat Deletion": "",
|
||||
"Allow non-local voices": "",
|
||||
"Allow User Location": "",
|
||||
"alphanumeric characters and hyphens": "",
|
||||
"Already have an account?": "",
|
||||
"an assistant": "",
|
||||
@@ -68,6 +70,7 @@
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Batch Size (num_batch)": "",
|
||||
"before": "",
|
||||
"Being lazy": "",
|
||||
"Brave Search API Key": "",
|
||||
@@ -159,6 +162,7 @@
|
||||
"Discover, download, and explore custom prompts": "",
|
||||
"Discover, download, and explore model presets": "",
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "",
|
||||
"Document": "",
|
||||
"Document Settings": "",
|
||||
@@ -175,6 +179,7 @@
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
|
||||
"Edit": "",
|
||||
"Edit Doc": "",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "",
|
||||
"Email": "",
|
||||
"Embedding Batch Size": "",
|
||||
@@ -201,8 +206,10 @@
|
||||
"Enter Score": "",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter stop sequence": "",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Top K": "",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
|
||||
"Enter URL (e.g. http://localhost:11434)": "",
|
||||
@@ -219,6 +226,7 @@
|
||||
"Export Documents Mapping": "",
|
||||
"Export Models": "",
|
||||
"Export Prompts": "",
|
||||
"Export Tools": "",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "",
|
||||
"Failed to read clipboard contents": "",
|
||||
@@ -256,6 +264,7 @@
|
||||
"Import Documents Mapping": "",
|
||||
"Import Models": "",
|
||||
"Import Prompts": "",
|
||||
"Import Tools": "",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "",
|
||||
"Info": "",
|
||||
"Input commands": "",
|
||||
@@ -276,6 +285,7 @@
|
||||
"Knowledge": "",
|
||||
"Language": "",
|
||||
"Last Active": "",
|
||||
"Last Modified": "",
|
||||
"Light": "",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "",
|
||||
@@ -300,6 +310,7 @@
|
||||
"Mirostat Tau": "",
|
||||
"MMMM DD, YYYY": "",
|
||||
"MMMM DD, YYYY HH:mm": "",
|
||||
"MMMM DD, YYYY hh:mm:ss A": "",
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "",
|
||||
"Model {{modelId}} not found": "",
|
||||
@@ -419,9 +430,10 @@
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "",
|
||||
"Search Tools": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searching \"{{searchQuery}}\"": "",
|
||||
"Searxng Query URL": "",
|
||||
"See readme.md for instructions": "",
|
||||
"See what's new": "",
|
||||
@@ -433,6 +445,7 @@
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select an Ollama instance": "",
|
||||
"Select Documents": "",
|
||||
"Select model": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
@@ -441,6 +454,7 @@
|
||||
"Send message": "",
|
||||
"September": "",
|
||||
"Serper API Key": "",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Server connection verified": "",
|
||||
"Set as default": "",
|
||||
@@ -481,6 +495,8 @@
|
||||
"System": "",
|
||||
"System Prompt": "",
|
||||
"Tags": "",
|
||||
"Tap to interrupt": "",
|
||||
"Tavily API Key": "",
|
||||
"Tell us more:": "",
|
||||
"Temperature": "",
|
||||
"Template": "",
|
||||
@@ -505,10 +521,14 @@
|
||||
"To access the available model names for downloading,": "",
|
||||
"To access the GGUF models available for downloading,": "",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "",
|
||||
"Toggle settings": "",
|
||||
"Toggle sidebar": "",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "",
|
||||
"Tools": "",
|
||||
"Top K": "",
|
||||
"Top P": "",
|
||||
"Trouble accessing Ollama?": "",
|
||||
@@ -519,8 +539,10 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update password": "",
|
||||
"Updated at": "",
|
||||
"Upload a GGUF model": "",
|
||||
"Upload Files": "",
|
||||
"Upload Pipeline": "",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user