Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46774aa5cd | ||
|
|
62392aa88a | ||
|
|
c32d41b47f | ||
|
|
9404b6135a | ||
|
|
8caad59cb4 | ||
|
|
21132420d8 | ||
|
|
c89070797c | ||
|
|
1f85354856 | ||
|
|
bfd066cc2f | ||
|
|
000bea84ae | ||
|
|
944efd2cd8 | ||
|
|
4a09342a71 | ||
|
|
aad13607c0 | ||
|
|
1509c2c2ee | ||
|
|
d94bc21ac8 | ||
|
|
947c392f72 | ||
|
|
15d24a6a11 | ||
|
|
a3d6343dbe | ||
|
|
14fcf4d267 | ||
|
|
83ad488e31 | ||
|
|
fa61e738c3 | ||
|
|
0aa4d305c7 | ||
|
|
1066096b33 | ||
|
|
398b2076cd | ||
|
|
fb516ea694 | ||
|
|
b35cc36e19 | ||
|
|
6d1ce99537 | ||
|
|
7c5f6d71b3 | ||
|
|
e49e04c56a | ||
|
|
da8646cae9 | ||
|
|
ba0523cd69 | ||
|
|
38c34b4444 | ||
|
|
b11af9ea0e | ||
|
|
56369fea3a | ||
|
|
24fb77759e | ||
|
|
f9106cf394 | ||
|
|
807a60cccf | ||
|
|
ac4675900e | ||
|
|
f7a1fc207a | ||
|
|
b9b1d64bd5 | ||
|
|
7e6a00b009 | ||
|
|
5aad328040 | ||
|
|
03a7f88dca | ||
|
|
f61869d90d | ||
|
|
95505bc4c1 | ||
|
|
eca59f759d | ||
|
|
9975cb17a9 | ||
|
|
0b823f90e6 | ||
|
|
c0fff4c69f | ||
|
|
180dd3966b | ||
|
|
e4d101e550 | ||
|
|
88c5b7f495 | ||
|
|
4e9ca31146 | ||
|
|
48e7376336 | ||
|
|
488301f150 | ||
|
|
287c95e72d | ||
|
|
af5a7a35c0 | ||
|
|
5faa05a117 | ||
|
|
e273a99153 | ||
|
|
151055590d | ||
|
|
7978adbf45 | ||
|
|
94976e5ed3 | ||
|
|
3a1d747677 | ||
|
|
7121465eda | ||
|
|
52e83891b4 | ||
|
|
456a78ada9 | ||
|
|
cd60dbe08c | ||
|
|
a0d932b986 | ||
|
|
10cad60566 | ||
|
|
c6296e5108 | ||
|
|
dd9cbd0ab9 | ||
|
|
865c268312 | ||
|
|
dcac8a773b | ||
|
|
96d75dfc1b | ||
|
|
49428eb7ef | ||
|
|
6c0e3972dc | ||
|
|
458c317736 | ||
|
|
98f68b8b08 | ||
|
|
b76e934140 | ||
|
|
0cdcc2deb6 | ||
|
|
58992d592c | ||
|
|
04e172deac | ||
|
|
a363c1f2f1 | ||
|
|
6425e794f6 | ||
|
|
f340178bb7 | ||
|
|
aa6c8b1bfd | ||
|
|
d1ca6b50d9 | ||
|
|
57530d87f6 | ||
|
|
2c030218f8 | ||
|
|
5558514ff1 | ||
|
|
83c7633acb | ||
|
|
6c3f2f8d38 | ||
|
|
d72653cdea | ||
|
|
6c96361402 | ||
|
|
554e56687c | ||
|
|
77b1edcd0f | ||
|
|
d667faea33 | ||
|
|
4cb2cc62d2 | ||
|
|
698bfcf87f | ||
|
|
edeff20e1d | ||
|
|
eda157e303 | ||
|
|
653a0ff02f | ||
|
|
8175a323e9 | ||
|
|
11850b75c8 | ||
|
|
562e40a7bd | ||
|
|
0e3b7a11e3 | ||
|
|
196f91d68c | ||
|
|
bfbfdae1c5 | ||
|
|
a6c154d839 | ||
|
|
7563619e26 | ||
|
|
1b5c92d66c | ||
|
|
2f23325c90 | ||
|
|
cef676429f | ||
|
|
a0935dec60 | ||
|
|
587a8c5992 | ||
|
|
4ddb7e124e | ||
|
|
68cfdd89c4 | ||
|
|
144c9059a3 | ||
|
|
ee38b3c84d | ||
|
|
86aa2ca6cb | ||
|
|
1d7ca44017 | ||
|
|
174c082c69 | ||
|
|
bbb63104ea | ||
|
|
43832d165f | ||
|
|
d630485051 | ||
|
|
784572590f | ||
|
|
90a03c331d | ||
|
|
b391ad12ae | ||
|
|
6c2eb814ed | ||
|
|
3b0cb7945f | ||
|
|
46a181042b | ||
|
|
5af8d0612a | ||
|
|
d4fabeee3c | ||
|
|
e47d6ce1f6 | ||
|
|
15f4153fda | ||
|
|
1371e66ed9 | ||
|
|
2fbb92aceb | ||
|
|
f96beeee8f | ||
|
|
0c367412c3 | ||
|
|
150152ddbd | ||
|
|
12287f8680 | ||
|
|
047c9fe82c | ||
|
|
ce20f05ef1 | ||
|
|
242311aee1 | ||
|
|
bbc34d9be5 | ||
|
|
09da5e71e2 | ||
|
|
74638bbd6e | ||
|
|
f34fe49bdd | ||
|
|
50089710f3 | ||
|
|
4bf38e5287 | ||
|
|
50f6addd6f | ||
|
|
c42cb438f9 | ||
|
|
b93bdfb222 | ||
|
|
c85a2e8cd4 | ||
|
|
46e3208cde | ||
|
|
c6bb0ded5e | ||
|
|
d187b1615a | ||
|
|
5e24e03fe5 | ||
|
|
2fcc4aec4b | ||
|
|
67865fc5ad | ||
|
|
9ad097d265 | ||
|
|
572eefe181 | ||
|
|
cf44209730 | ||
|
|
dfeadf9595 | ||
|
|
25c71d8ac2 | ||
|
|
19d2121252 | ||
|
|
c24d9d3bea | ||
|
|
976c714063 | ||
|
|
f87d5d3d9b | ||
|
|
29f13f34d3 | ||
|
|
7d45d2762f | ||
|
|
e5408995d8 | ||
|
|
d69dfb7c8b | ||
|
|
addee2f248 | ||
|
|
0c021e42f5 | ||
|
|
64e6003627 | ||
|
|
26121c5d88 | ||
|
|
d245b756a3 | ||
|
|
b4b56f9c85 | ||
|
|
81e928030f | ||
|
|
0b62bbb52e | ||
|
|
cb364f0ac7 | ||
|
|
d12a7247bc | ||
|
|
941bb7c603 | ||
|
|
3edc547389 | ||
|
|
427ca4e3f5 | ||
|
|
5eac5c54f8 | ||
|
|
a1fc2f4df0 | ||
|
|
3688955c77 | ||
|
|
6307adfba1 | ||
|
|
4f78acaa5c | ||
|
|
7f405c74cd | ||
|
|
5d6cee0c58 | ||
|
|
af366cf732 | ||
|
|
32cb5f3ceb | ||
|
|
eecc7f3529 | ||
|
|
8962d948cd | ||
|
|
c91a5d8b1f | ||
|
|
77f4ffddc1 | ||
|
|
784a6ec85e | ||
|
|
eb004ccfc2 | ||
|
|
c6f9e74477 | ||
|
|
302bd11b33 | ||
|
|
c19ea89a1d | ||
|
|
9560f9196d | ||
|
|
42bb1ec089 | ||
|
|
ac294a74e7 | ||
|
|
4c9598918a | ||
|
|
ffa168211e | ||
|
|
371dfc1143 | ||
|
|
a1faa307b1 | ||
|
|
a4c4a0370e | ||
|
|
3e0aa29683 | ||
|
|
df21a92542 | ||
|
|
b218b02d93 | ||
|
|
ff8a55a861 | ||
|
|
7e0ea8f77d | ||
|
|
c91c95431d | ||
|
|
1d64d31275 | ||
|
|
c2d6d3230b | ||
|
|
8d108060d5 | ||
|
|
5cb6e9b0b4 | ||
|
|
cea74eaf52 | ||
|
|
adf9ccb5eb | ||
|
|
98624a406f | ||
|
|
862c96fcef | ||
|
|
cbaaacfb75 | ||
|
|
3e0d9ad74f | ||
|
|
e008738f30 | ||
|
|
244f34c24e | ||
|
|
642ca5a94a | ||
|
|
85187deb4b | ||
|
|
3305ac5928 | ||
|
|
51a4e49ae1 | ||
|
|
6c9f552733 | ||
|
|
7e6d499486 | ||
|
|
d291821bf3 | ||
|
|
45311bfa15 | ||
|
|
b03f4371d6 | ||
|
|
f662aab9e8 | ||
|
|
129253d90d | ||
|
|
2d1b67561b | ||
|
|
dca3dbbce6 | ||
|
|
e9f1a714e8 | ||
|
|
d5b5155c20 | ||
|
|
6e79530e0e | ||
|
|
b0a2eb31ee | ||
|
|
c0549498bf | ||
|
|
f8a922e966 | ||
|
|
5c1e3a460e | ||
|
|
bc04553a7c | ||
|
|
c296e9061e | ||
|
|
e14ad508c9 | ||
|
|
e9a198d8ed | ||
|
|
cb0e858c04 | ||
|
|
848d2382bc | ||
|
|
106727109e | ||
|
|
c97051e6eb | ||
|
|
4140e1b84a | ||
|
|
fe8a3ac2d7 | ||
|
|
b5111c4b34 | ||
|
|
92cc87d908 | ||
|
|
9e37d6edcc | ||
|
|
77ec296ac3 | ||
|
|
bc80f1438c | ||
|
|
af4caec4f5 | ||
|
|
c5e9ceeb06 | ||
|
|
47a04a7e3c | ||
|
|
c54fb92263 | ||
|
|
f3c113cb04 | ||
|
|
5f26b2a9e3 | ||
|
|
8935dca279 | ||
|
|
f675a18b05 | ||
|
|
f74f2ea765 | ||
|
|
5a90e90c0b | ||
|
|
2fa94956f4 | ||
|
|
8e52ba8b91 | ||
|
|
ed0d4b5a2a | ||
|
|
a1412d0b55 | ||
|
|
1a240e103e | ||
|
|
93c90dc186 | ||
|
|
8cb7127fbc | ||
|
|
4b91ac352a | ||
|
|
09dc3b7b67 | ||
|
|
bdc51b45ec | ||
|
|
6313a98287 | ||
|
|
ae347bb5e7 | ||
|
|
fa65be2ad2 | ||
|
|
2481e48a3a | ||
|
|
2c6e2d5e8a | ||
|
|
e6dd0bfbe0 | ||
|
|
b22ed64770 | ||
|
|
16fe0ee167 | ||
|
|
87344adf29 | ||
|
|
349fd0c87d | ||
|
|
d4a11ca33f | ||
|
|
bd84ee0c6a | ||
|
|
a9e411fdb9 | ||
|
|
de8de7e008 | ||
|
|
63edaae8d8 | ||
|
|
d3cf9a7535 | ||
|
|
7c8727151e | ||
|
|
c3f805b809 | ||
|
|
8508319c23 | ||
|
|
b9382f7051 | ||
|
|
53fb3d9c3e | ||
|
|
b21091bc01 | ||
|
|
4de591b930 | ||
|
|
492d4f3403 | ||
|
|
0b163c2c2d | ||
|
|
7bb8e91edb | ||
|
|
49faff497e | ||
|
|
33888ffaf8 | ||
|
|
b10eb0dacc | ||
|
|
7f197a70f9 | ||
|
|
61b4ab1e63 | ||
|
|
51342179ad | ||
|
|
f1925841e2 | ||
|
|
e86db74333 | ||
|
|
c569280806 | ||
|
|
bc5d322cbb | ||
|
|
d865b9fe59 | ||
|
|
b188143f01 | ||
|
|
c6e14ce327 | ||
|
|
04e3b168a6 | ||
|
|
ae97a96379 |
No files matched your search
@@ -1,27 +1,39 @@
|
|||||||
name: Python CI
|
name: Python CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: 'Format Backend'
|
name: 'Format Backend'
|
||||||
env:
|
|
||||||
PUBLIC_API_BASE_URL: ''
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
python-version: [3.11]
|
||||||
- latest
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Python
|
|
||||||
uses: actions/setup-python@v4
|
- name: Set up Python
|
||||||
- name: Use Bun
|
uses: actions/setup-python@v2
|
||||||
uses: oven-sh/setup-bun@v1
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install yapf
|
pip install black
|
||||||
|
|
||||||
- name: Format backend
|
- name: Format backend
|
||||||
run: bun run format:backend
|
run: npm run format:backend
|
||||||
|
|
||||||
|
- name: Check for changes after format
|
||||||
|
run: git diff --exit-code
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
name: Bun CI
|
name: Frontend Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: 'Format & Build Frontend'
|
name: 'Format & Build Frontend'
|
||||||
env:
|
|
||||||
PUBLIC_API_BASE_URL: ''
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout Repository
|
||||||
- name: Use Bun
|
uses: actions/checkout@v4
|
||||||
uses: oven-sh/setup-bun@v1
|
|
||||||
- run: bun --version
|
- name: Setup Node.js
|
||||||
- name: Install frontend dependencies
|
uses: actions/setup-node@v3
|
||||||
run: bun install
|
with:
|
||||||
- name: Format frontend
|
node-version: '20' # Or specify any other version you want to use
|
||||||
run: bun run format
|
|
||||||
- name: Build frontend
|
- name: Install Dependencies
|
||||||
run: bun run build
|
run: npm install
|
||||||
|
|
||||||
|
- name: Format Frontend
|
||||||
|
run: npm run format
|
||||||
|
|
||||||
|
- name: Check for Changes After Format
|
||||||
|
run: git diff --exit-code
|
||||||
|
|
||||||
|
- name: Build Frontend
|
||||||
|
run: npm run build
|
||||||
+1
-1
@@ -166,7 +166,7 @@ cython_debug/
|
|||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
@@ -5,6 +5,76 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.117] - 2024-04-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
|
||||||
|
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
|
||||||
|
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
|
||||||
|
- 📝 **Improved Logging**: Enhancements to logging functionality.
|
||||||
|
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
|
||||||
|
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
|
||||||
|
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
|
||||||
|
- 🔒 **Security Patches**: Applied necessary security patches.
|
||||||
|
|
||||||
|
## [0.1.116] - 2024-03-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
|
||||||
|
- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
|
||||||
|
- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
|
||||||
|
- **🌐 Localization**: Korean translation (I18n) now available.
|
||||||
|
- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
|
||||||
|
- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
|
||||||
|
- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
|
||||||
|
- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
|
||||||
|
- **✅ Spelling**: Corrected various spelling issues for improved readability.
|
||||||
|
|
||||||
|
## [0.1.115] - 2024-03-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature.
|
||||||
|
- **🛑 Cancel Model Download**: Added the ability to cancel model downloads.
|
||||||
|
- **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI.
|
||||||
|
- **🌟 Updated Light Theme**: Updated the light theme for a fresh look.
|
||||||
|
- **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings.
|
||||||
|
|
||||||
|
## [0.1.114] - 2024-03-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL.
|
||||||
|
- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy.
|
||||||
|
- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields.
|
||||||
|
- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization.
|
||||||
|
|
||||||
## [0.1.113] - 2024-03-18
|
## [0.1.113] - 2024-03-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ ENV OPENAI_API_BASE_URL ""
|
|||||||
ENV OPENAI_API_KEY ""
|
ENV OPENAI_API_KEY ""
|
||||||
|
|
||||||
ENV WEBUI_SECRET_KEY ""
|
ENV WEBUI_SECRET_KEY ""
|
||||||
|
ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER ""
|
||||||
|
|
||||||
ENV SCARF_NO_ANALYTICS true
|
ENV SCARF_NO_ANALYTICS true
|
||||||
ENV DO_NOT_TRACK true
|
ENV DO_NOT_TRACK true
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ remove:
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
@docker-compose start
|
@docker-compose start
|
||||||
|
startAndBuild:
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@docker-compose stop
|
@docker-compose stop
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
FastAPI,
|
FastAPI,
|
||||||
Request,
|
Request,
|
||||||
@@ -21,7 +22,16 @@ from utils.utils import (
|
|||||||
)
|
)
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
from config import CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
CACHE_DIR,
|
||||||
|
UPLOAD_DIR,
|
||||||
|
WHISPER_MODEL,
|
||||||
|
WHISPER_MODEL_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -38,7 +48,7 @@ def transcribe(
|
|||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
print(file.content_type)
|
log.info(f"file.content_type: {file.content_type}")
|
||||||
|
|
||||||
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -62,7 +72,7 @@ def transcribe(
|
|||||||
)
|
)
|
||||||
|
|
||||||
segments, info = model.transcribe(file_path, beam_size=5)
|
segments, info = model.transcribe(file_path, beam_size=5)
|
||||||
print(
|
log.info(
|
||||||
"Detected language '%s' with probability %f"
|
"Detected language '%s' with probability %f"
|
||||||
% (info.language, info.language_probability)
|
% (info.language, info.language_probability)
|
||||||
)
|
)
|
||||||
@@ -72,7 +82,7 @@ def transcribe(
|
|||||||
return {"text": transcript.strip()}
|
return {"text": transcript.strip()}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
|||||||
+108
-14
@@ -18,6 +18,8 @@ from utils.utils import (
|
|||||||
get_current_user,
|
get_current_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -25,10 +27,14 @@ from pathlib import Path
|
|||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from config import CACHE_DIR, AUTOMATIC1111_BASE_URL
|
from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
||||||
|
|
||||||
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
||||||
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -49,6 +55,8 @@ app.state.MODEL = ""
|
|||||||
|
|
||||||
|
|
||||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
|
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
app.state.IMAGE_SIZE = "512x512"
|
app.state.IMAGE_SIZE = "512x512"
|
||||||
app.state.IMAGE_STEPS = 50
|
app.state.IMAGE_STEPS = 50
|
||||||
@@ -71,32 +79,48 @@ async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user
|
|||||||
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
|
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
|
||||||
|
|
||||||
|
|
||||||
class UrlUpdateForm(BaseModel):
|
class EngineUrlUpdateForm(BaseModel):
|
||||||
url: str
|
AUTOMATIC1111_BASE_URL: Optional[str] = None
|
||||||
|
COMFYUI_BASE_URL: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/url")
|
@app.get("/url")
|
||||||
async def get_automatic1111_url(user=Depends(get_admin_user)):
|
async def get_engine_url(user=Depends(get_admin_user)):
|
||||||
return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL}
|
return {
|
||||||
|
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||||
|
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/url/update")
|
@app.post("/url/update")
|
||||||
async def update_automatic1111_url(
|
async def update_engine_url(
|
||||||
form_data: UrlUpdateForm, user=Depends(get_admin_user)
|
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
if form_data.url == "":
|
if form_data.AUTOMATIC1111_BASE_URL == None:
|
||||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
else:
|
else:
|
||||||
url = form_data.url.strip("/")
|
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
||||||
try:
|
try:
|
||||||
r = requests.head(url)
|
r = requests.head(url)
|
||||||
app.state.AUTOMATIC1111_BASE_URL = url
|
app.state.AUTOMATIC1111_BASE_URL = url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
|
|
||||||
|
if form_data.COMFYUI_BASE_URL == None:
|
||||||
|
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
|
else:
|
||||||
|
url = form_data.COMFYUI_BASE_URL.strip("/")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.head(url)
|
||||||
|
app.state.COMFYUI_BASE_URL = url
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||||
|
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
|
||||||
"status": True,
|
"status": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +210,18 @@ def get_models(user=Depends(get_current_user)):
|
|||||||
{"id": "dall-e-2", "name": "DALL·E 2"},
|
{"id": "dall-e-2", "name": "DALL·E 2"},
|
||||||
{"id": "dall-e-3", "name": "DALL·E 3"},
|
{"id": "dall-e-3", "name": "DALL·E 3"},
|
||||||
]
|
]
|
||||||
|
elif app.state.ENGINE == "comfyui":
|
||||||
|
|
||||||
|
r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
|
||||||
|
info = r.json()
|
||||||
|
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda model: {"id": model, "name": model},
|
||||||
|
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
|
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
|
||||||
@@ -207,6 +243,8 @@ async def get_default_model(user=Depends(get_admin_user)):
|
|||||||
try:
|
try:
|
||||||
if app.state.ENGINE == "openai":
|
if app.state.ENGINE == "openai":
|
||||||
return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
|
return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
|
||||||
|
elif app.state.ENGINE == "comfyui":
|
||||||
|
return {"model": app.state.MODEL if app.state.MODEL else ""}
|
||||||
else:
|
else:
|
||||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||||
options = r.json()
|
options = r.json()
|
||||||
@@ -221,10 +259,12 @@ class UpdateModelForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def set_model_handler(model: str):
|
def set_model_handler(model: str):
|
||||||
|
|
||||||
if app.state.ENGINE == "openai":
|
if app.state.ENGINE == "openai":
|
||||||
app.state.MODEL = model
|
app.state.MODEL = model
|
||||||
return app.state.MODEL
|
return app.state.MODEL
|
||||||
|
if app.state.ENGINE == "comfyui":
|
||||||
|
app.state.MODEL = model
|
||||||
|
return app.state.MODEL
|
||||||
else:
|
else:
|
||||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||||
options = r.json()
|
options = r.json()
|
||||||
@@ -268,7 +308,24 @@ def save_b64_image(b64_str):
|
|||||||
|
|
||||||
return image_id
|
return image_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving image: {e}")
|
log.error(f"Error saving image: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_url_image(url):
|
||||||
|
image_id = str(uuid.uuid4())
|
||||||
|
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(url)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
with open(file_path, "wb") as image_file:
|
||||||
|
image_file.write(r.content)
|
||||||
|
|
||||||
|
return image_id
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error saving image: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -278,6 +335,8 @@ def generate_image(
|
|||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
||||||
|
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
if app.state.ENGINE == "openai":
|
if app.state.ENGINE == "openai":
|
||||||
@@ -315,12 +374,47 @@ def generate_image(
|
|||||||
|
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
elif app.state.ENGINE == "comfyui":
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"prompt": form_data.prompt,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"n": form_data.n,
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.state.IMAGE_STEPS != None:
|
||||||
|
data["steps"] = app.state.IMAGE_STEPS
|
||||||
|
|
||||||
|
if form_data.negative_prompt != None:
|
||||||
|
data["negative_prompt"] = form_data.negative_prompt
|
||||||
|
|
||||||
|
data = ImageGenerationPayload(**data)
|
||||||
|
|
||||||
|
res = comfyui_generate_image(
|
||||||
|
app.state.MODEL,
|
||||||
|
data,
|
||||||
|
user.id,
|
||||||
|
app.state.COMFYUI_BASE_URL,
|
||||||
|
)
|
||||||
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
|
images = []
|
||||||
|
|
||||||
|
for image in res["data"]:
|
||||||
|
image_id = save_url_image(image["url"])
|
||||||
|
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||||
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
|
with open(file_body_path, "w") as f:
|
||||||
|
json.dump(data.model_dump(exclude_none=True), f)
|
||||||
|
|
||||||
|
log.debug(f"images: {images}")
|
||||||
|
return images
|
||||||
else:
|
else:
|
||||||
if form_data.model:
|
if form_data.model:
|
||||||
set_model_handler(form_data.model)
|
set_model_handler(form_data.model)
|
||||||
|
|
||||||
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"prompt": form_data.prompt,
|
"prompt": form_data.prompt,
|
||||||
"batch_size": form_data.n,
|
"batch_size": form_data.n,
|
||||||
@@ -341,7 +435,7 @@ def generate_image(
|
|||||||
|
|
||||||
res = r.json()
|
res = r.json()
|
||||||
|
|
||||||
print(res)
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
COMFYUI_DEFAULT_PROMPT = """
|
||||||
|
{
|
||||||
|
"3": {
|
||||||
|
"inputs": {
|
||||||
|
"seed": 0,
|
||||||
|
"steps": 20,
|
||||||
|
"cfg": 8,
|
||||||
|
"sampler_name": "euler",
|
||||||
|
"scheduler": "normal",
|
||||||
|
"denoise": 1,
|
||||||
|
"model": [
|
||||||
|
"4",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"positive": [
|
||||||
|
"6",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"negative": [
|
||||||
|
"7",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent_image": [
|
||||||
|
"5",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "KSampler",
|
||||||
|
"_meta": {
|
||||||
|
"title": "KSampler"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"inputs": {
|
||||||
|
"ckpt_name": "model.safetensors"
|
||||||
|
},
|
||||||
|
"class_type": "CheckpointLoaderSimple",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Load Checkpoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"inputs": {
|
||||||
|
"width": 512,
|
||||||
|
"height": 512,
|
||||||
|
"batch_size": 1
|
||||||
|
},
|
||||||
|
"class_type": "EmptyLatentImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Empty Latent Image"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"inputs": {
|
||||||
|
"text": "Prompt",
|
||||||
|
"clip": [
|
||||||
|
"4",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "CLIP Text Encode (Prompt)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"inputs": {
|
||||||
|
"text": "Negative Prompt",
|
||||||
|
"clip": [
|
||||||
|
"4",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "CLIP Text Encode (Prompt)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"inputs": {
|
||||||
|
"samples": [
|
||||||
|
"3",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"4",
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "VAEDecode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "VAE Decode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"inputs": {
|
||||||
|
"filename_prefix": "ComfyUI",
|
||||||
|
"images": [
|
||||||
|
"8",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "SaveImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Save Image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def queue_prompt(prompt, client_id, base_url):
|
||||||
|
log.info("queue_prompt")
|
||||||
|
p = {"prompt": prompt, "client_id": client_id}
|
||||||
|
data = json.dumps(p).encode("utf-8")
|
||||||
|
req = urllib.request.Request(f"{base_url}/prompt", data=data)
|
||||||
|
return json.loads(urllib.request.urlopen(req).read())
|
||||||
|
|
||||||
|
|
||||||
|
def get_image(filename, subfolder, folder_type, base_url):
|
||||||
|
log.info("get_image")
|
||||||
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
|
url_values = urllib.parse.urlencode(data)
|
||||||
|
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
|
||||||
|
return response.read()
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_url(filename, subfolder, folder_type, base_url):
|
||||||
|
log.info("get_image")
|
||||||
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
|
url_values = urllib.parse.urlencode(data)
|
||||||
|
return f"{base_url}/view?{url_values}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_history(prompt_id, base_url):
|
||||||
|
log.info("get_history")
|
||||||
|
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
|
||||||
|
return json.loads(response.read())
|
||||||
|
|
||||||
|
|
||||||
|
def get_images(ws, prompt, client_id, base_url):
|
||||||
|
prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"]
|
||||||
|
output_images = []
|
||||||
|
while True:
|
||||||
|
out = ws.recv()
|
||||||
|
if isinstance(out, str):
|
||||||
|
message = json.loads(out)
|
||||||
|
if message["type"] == "executing":
|
||||||
|
data = message["data"]
|
||||||
|
if data["node"] is None and data["prompt_id"] == prompt_id:
|
||||||
|
break # Execution is done
|
||||||
|
else:
|
||||||
|
continue # previews are binary data
|
||||||
|
|
||||||
|
history = get_history(prompt_id, base_url)[prompt_id]
|
||||||
|
for o in history["outputs"]:
|
||||||
|
for node_id in history["outputs"]:
|
||||||
|
node_output = history["outputs"][node_id]
|
||||||
|
if "images" in node_output:
|
||||||
|
for image in node_output["images"]:
|
||||||
|
url = get_image_url(
|
||||||
|
image["filename"], image["subfolder"], image["type"], base_url
|
||||||
|
)
|
||||||
|
output_images.append({"url": url})
|
||||||
|
return {"data": output_images}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageGenerationPayload(BaseModel):
|
||||||
|
prompt: str
|
||||||
|
negative_prompt: Optional[str] = ""
|
||||||
|
steps: Optional[int] = None
|
||||||
|
seed: Optional[int] = None
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
n: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_generate_image(
|
||||||
|
model: str, payload: ImageGenerationPayload, client_id, base_url
|
||||||
|
):
|
||||||
|
host = base_url.replace("http://", "").replace("https://", "")
|
||||||
|
|
||||||
|
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
||||||
|
|
||||||
|
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
||||||
|
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
||||||
|
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
||||||
|
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
||||||
|
|
||||||
|
# set the text prompt for our positive CLIPTextEncode
|
||||||
|
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
||||||
|
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
||||||
|
|
||||||
|
if payload.steps:
|
||||||
|
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
||||||
|
|
||||||
|
comfyui_prompt["3"]["inputs"]["seed"] = (
|
||||||
|
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
ws.connect(f"ws://{host}/ws?clientId={client_id}")
|
||||||
|
log.info("WebSocket connection established.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Failed to connect to WebSocket server: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error while receiving images: {e}")
|
||||||
|
images = None
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
return images
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from litellm.proxy.proxy_server import ProxyConfig, initialize
|
from litellm.proxy.proxy_server import ProxyConfig, initialize
|
||||||
from litellm.proxy.proxy_server import app
|
from litellm.proxy.proxy_server import app
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status
|
from fastapi import FastAPI, Request, Depends, status, Response
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
import json
|
||||||
|
|
||||||
from utils.utils import get_http_authorization_cred, get_current_user
|
from utils.utils import get_http_authorization_cred, get_current_user
|
||||||
from config import ENV
|
from config import SRC_LOG_LEVELS, ENV
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
||||||
|
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
MODEL_FILTER_ENABLED,
|
||||||
|
MODEL_FILTER_LIST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
proxy_config = ProxyConfig()
|
||||||
|
|
||||||
@@ -26,16 +43,58 @@ async def on_startup():
|
|||||||
await startup()
|
await startup()
|
||||||
|
|
||||||
|
|
||||||
|
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||||
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def auth_middleware(request: Request, call_next):
|
async def auth_middleware(request: Request, call_next):
|
||||||
auth_header = request.headers.get("Authorization", "")
|
auth_header = request.headers.get("Authorization", "")
|
||||||
|
request.state.user = None
|
||||||
|
|
||||||
if ENV != "dev":
|
try:
|
||||||
try:
|
user = get_current_user(get_http_authorization_cred(auth_header))
|
||||||
user = get_current_user(get_http_authorization_cred(auth_header))
|
log.debug(f"user: {user}")
|
||||||
print(user)
|
request.state.user = user
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(status_code=400, content={"detail": str(e)})
|
return JSONResponse(status_code=400, content={"detail": str(e)})
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ModifyModelsResponseMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(
|
||||||
|
self, request: Request, call_next: RequestResponseEndpoint
|
||||||
|
) -> Response:
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
user = request.state.user
|
||||||
|
|
||||||
|
if "/models" in request.url.path:
|
||||||
|
if isinstance(response, StreamingResponse):
|
||||||
|
# Read the content of the streaming response
|
||||||
|
body = b""
|
||||||
|
async for chunk in response.body_iterator:
|
||||||
|
body += chunk
|
||||||
|
|
||||||
|
data = json.loads(body.decode("utf-8"))
|
||||||
|
|
||||||
|
if app.state.MODEL_FILTER_ENABLED:
|
||||||
|
if user and user.role == "user":
|
||||||
|
data["data"] = list(
|
||||||
|
filter(
|
||||||
|
lambda model: model["id"]
|
||||||
|
in app.state.MODEL_FILTER_LIST,
|
||||||
|
data["data"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Modified Flag
|
||||||
|
data["modified"] = True
|
||||||
|
return JSONResponse(content=data)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
app.add_middleware(ModifyModelsResponseMiddleware)
|
||||||
+304
-39
@@ -1,24 +1,49 @@
|
|||||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends, status
|
from fastapi import (
|
||||||
|
FastAPI,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
HTTPException,
|
||||||
|
Depends,
|
||||||
|
status,
|
||||||
|
UploadFile,
|
||||||
|
File,
|
||||||
|
BackgroundTasks,
|
||||||
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from fastapi.concurrency import run_in_threadpool
|
from fastapi.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
import os
|
||||||
|
import copy
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from typing import Optional, List, Union
|
||||||
|
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils.utils import decode_token, get_current_user, get_admin_user
|
from utils.utils import decode_token, get_current_user, get_admin_user
|
||||||
from config import OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST
|
|
||||||
|
|
||||||
from typing import Optional, List, Union
|
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
OLLAMA_BASE_URLS,
|
||||||
|
MODEL_FILTER_ENABLED,
|
||||||
|
MODEL_FILTER_LIST,
|
||||||
|
UPLOAD_DIR,
|
||||||
|
)
|
||||||
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -56,6 +81,12 @@ async def check_url(request: Request, call_next):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.head("/")
|
||||||
|
@app.get("/")
|
||||||
|
async def get_status():
|
||||||
|
return {"status": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/urls")
|
@app.get("/urls")
|
||||||
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||||
@@ -69,7 +100,7 @@ class UrlUpdateForm(BaseModel):
|
|||||||
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
||||||
app.state.OLLAMA_BASE_URLS = form_data.urls
|
app.state.OLLAMA_BASE_URLS = form_data.urls
|
||||||
|
|
||||||
print(app.state.OLLAMA_BASE_URLS)
|
log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}")
|
||||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||||
|
|
||||||
|
|
||||||
@@ -90,7 +121,7 @@ async def fetch_url(url):
|
|||||||
return await response.json()
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.error(f"Connection error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -114,7 +145,7 @@ def merge_models_lists(model_lists):
|
|||||||
|
|
||||||
|
|
||||||
async def get_all_models():
|
async def get_all_models():
|
||||||
print("get_all_models")
|
log.info("get_all_models()")
|
||||||
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
|
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
@@ -155,7 +186,7 @@ async def get_ollama_tags(
|
|||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -201,7 +232,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
|||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -227,18 +258,33 @@ async def pull_model(
|
|||||||
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
|
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
def get_request():
|
def get_request():
|
||||||
nonlocal url
|
nonlocal url
|
||||||
nonlocal r
|
nonlocal r
|
||||||
|
|
||||||
|
request_id = str(uuid.uuid4())
|
||||||
try:
|
try:
|
||||||
|
REQUEST_POOL.append(request_id)
|
||||||
|
|
||||||
def stream_content():
|
def stream_content():
|
||||||
for chunk in r.iter_content(chunk_size=8192):
|
try:
|
||||||
yield chunk
|
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||||
|
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
if request_id in REQUEST_POOL:
|
||||||
|
yield chunk
|
||||||
|
else:
|
||||||
|
log.warning("User: canceled request")
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
if hasattr(r, "close"):
|
||||||
|
r.close()
|
||||||
|
if request_id in REQUEST_POOL:
|
||||||
|
REQUEST_POOL.remove(request_id)
|
||||||
|
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
@@ -259,8 +305,9 @@ async def pull_model(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await run_in_threadpool(get_request)
|
return await run_in_threadpool(get_request)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -299,7 +346,7 @@ async def push_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.debug(f"url: {url}")
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
@@ -331,7 +378,7 @@ async def push_model(
|
|||||||
try:
|
try:
|
||||||
return await run_in_threadpool(get_request)
|
return await run_in_threadpool(get_request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -359,9 +406,9 @@ class CreateModelForm(BaseModel):
|
|||||||
async def create_model(
|
async def create_model(
|
||||||
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
|
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
print(form_data)
|
log.debug(f"form_data: {form_data}")
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
@@ -383,7 +430,7 @@ async def create_model(
|
|||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
print(r)
|
log.debug(f"r: {r}")
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream_content(),
|
stream_content(),
|
||||||
@@ -396,7 +443,7 @@ async def create_model(
|
|||||||
try:
|
try:
|
||||||
return await run_in_threadpool(get_request)
|
return await run_in_threadpool(get_request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -434,7 +481,7 @@ async def copy_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@@ -444,11 +491,11 @@ async def copy_model(
|
|||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
print(r.text)
|
log.debug(f"r.text: {r.text}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -481,7 +528,7 @@ async def delete_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@@ -491,11 +538,11 @@ async def delete_model(
|
|||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
print(r.text)
|
log.debug(f"r.text: {r.text}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -521,7 +568,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use
|
|||||||
|
|
||||||
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@@ -533,7 +580,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use
|
|||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -573,7 +620,7 @@ async def generate_embeddings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
@@ -585,7 +632,7 @@ async def generate_embeddings(
|
|||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -629,11 +676,11 @@ async def generate_completion(
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="error_detail",
|
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
@@ -654,7 +701,7 @@ async def generate_completion(
|
|||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
print("User: canceled request")
|
log.warning("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
@@ -709,7 +756,7 @@ class GenerateChatCompletionForm(BaseModel):
|
|||||||
format: Optional[str] = None
|
format: Optional[str] = None
|
||||||
options: Optional[dict] = None
|
options: Optional[dict] = None
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
stream: Optional[bool] = True
|
stream: Optional[bool] = None
|
||||||
keep_alive: Optional[Union[int, str]] = None
|
keep_alive: Optional[Union[int, str]] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -731,11 +778,15 @@ async def generate_chat_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
print(form_data.model_dump_json(exclude_none=True).encode())
|
log.debug(
|
||||||
|
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
||||||
|
form_data.model_dump_json(exclude_none=True).encode()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_request():
|
def get_request():
|
||||||
nonlocal form_data
|
nonlocal form_data
|
||||||
@@ -754,7 +805,7 @@ async def generate_chat_completion(
|
|||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
print("User: canceled request")
|
log.warning("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
@@ -777,7 +828,7 @@ async def generate_chat_completion(
|
|||||||
headers=dict(r.headers),
|
headers=dict(r.headers),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -831,7 +882,7 @@ async def generate_openai_chat_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
print(url)
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
@@ -854,7 +905,7 @@ async def generate_openai_chat_completion(
|
|||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
print("User: canceled request")
|
log.warning("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
@@ -897,6 +948,220 @@ async def generate_openai_chat_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UrlForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class UploadBlobForm(BaseModel):
|
||||||
|
filename: str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_huggingface_url(hf_url):
|
||||||
|
try:
|
||||||
|
# Parse the URL
|
||||||
|
parsed_url = urlparse(hf_url)
|
||||||
|
|
||||||
|
# Get the path and split it into components
|
||||||
|
path_components = parsed_url.path.split("/")
|
||||||
|
|
||||||
|
# Extract the desired output
|
||||||
|
user_repo = "/".join(path_components[1:3])
|
||||||
|
model_file = path_components[-1]
|
||||||
|
|
||||||
|
return model_file
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def download_file_stream(
|
||||||
|
ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024
|
||||||
|
):
|
||||||
|
done = False
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
current_size = os.path.getsize(file_path)
|
||||||
|
else:
|
||||||
|
current_size = 0
|
||||||
|
|
||||||
|
headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
async with session.get(file_url, headers=headers) as response:
|
||||||
|
total_size = int(response.headers.get("content-length", 0)) + current_size
|
||||||
|
|
||||||
|
with open(file_path, "ab+") as file:
|
||||||
|
async for data in response.content.iter_chunked(chunk_size):
|
||||||
|
current_size += len(data)
|
||||||
|
file.write(data)
|
||||||
|
|
||||||
|
done = current_size == total_size
|
||||||
|
progress = round((current_size / total_size) * 100, 2)
|
||||||
|
|
||||||
|
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
||||||
|
|
||||||
|
if done:
|
||||||
|
file.seek(0)
|
||||||
|
hashed = calculate_sha256(file)
|
||||||
|
file.seek(0)
|
||||||
|
|
||||||
|
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
||||||
|
response = requests.post(url, data=file)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
res = {
|
||||||
|
"done": done,
|
||||||
|
"blob": f"sha256:{hashed}",
|
||||||
|
"name": file_name,
|
||||||
|
}
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
yield f"data: {json.dumps(res)}\n\n"
|
||||||
|
else:
|
||||||
|
raise "Ollama: Could not create blob, Please try again."
|
||||||
|
|
||||||
|
|
||||||
|
# def number_generator():
|
||||||
|
# for i in range(1, 101):
|
||||||
|
# yield f"data: {i}\n"
|
||||||
|
|
||||||
|
|
||||||
|
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
|
||||||
|
@app.post("/models/download")
|
||||||
|
@app.post("/models/download/{url_idx}")
|
||||||
|
async def download_model(
|
||||||
|
form_data: UrlForm,
|
||||||
|
url_idx: Optional[int] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
||||||
|
|
||||||
|
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if url_idx == None:
|
||||||
|
url_idx = 0
|
||||||
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
|
||||||
|
file_name = parse_huggingface_url(form_data.url)
|
||||||
|
|
||||||
|
if file_name:
|
||||||
|
file_path = f"{UPLOAD_DIR}/{file_name}"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
download_file_stream(url, form_data.url, file_path, file_name),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/models/upload")
|
||||||
|
@app.post("/models/upload/{url_idx}")
|
||||||
|
def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
|
||||||
|
if url_idx == None:
|
||||||
|
url_idx = 0
|
||||||
|
ollama_url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
|
||||||
|
file_path = f"{UPLOAD_DIR}/{file.filename}"
|
||||||
|
|
||||||
|
# Save file in chunks
|
||||||
|
with open(file_path, "wb+") as f:
|
||||||
|
for chunk in file.file:
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
def file_process_stream():
|
||||||
|
nonlocal ollama_url
|
||||||
|
total_size = os.path.getsize(file_path)
|
||||||
|
chunk_size = 1024 * 1024
|
||||||
|
try:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
total = 0
|
||||||
|
done = False
|
||||||
|
|
||||||
|
while not done:
|
||||||
|
chunk = f.read(chunk_size)
|
||||||
|
if not chunk:
|
||||||
|
done = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
total += len(chunk)
|
||||||
|
progress = round((total / total_size) * 100, 2)
|
||||||
|
|
||||||
|
res = {
|
||||||
|
"progress": progress,
|
||||||
|
"total": total_size,
|
||||||
|
"completed": total,
|
||||||
|
}
|
||||||
|
yield f"data: {json.dumps(res)}\n\n"
|
||||||
|
|
||||||
|
if done:
|
||||||
|
f.seek(0)
|
||||||
|
hashed = calculate_sha256(f)
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
||||||
|
response = requests.post(url, data=f)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
res = {
|
||||||
|
"done": done,
|
||||||
|
"blob": f"sha256:{hashed}",
|
||||||
|
"name": file.filename,
|
||||||
|
}
|
||||||
|
os.remove(file_path)
|
||||||
|
yield f"data: {json.dumps(res)}\n\n"
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"Ollama: Could not create blob, Please try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
res = {"error": str(e)}
|
||||||
|
yield f"data: {json.dumps(res)}\n\n"
|
||||||
|
|
||||||
|
return StreamingResponse(file_process_stream(), media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
|
# async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
|
||||||
|
# if url_idx == None:
|
||||||
|
# url_idx = 0
|
||||||
|
# url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
|
||||||
|
# file_location = os.path.join(UPLOAD_DIR, file.filename)
|
||||||
|
# total_size = file.size
|
||||||
|
|
||||||
|
# async def file_upload_generator(file):
|
||||||
|
# print(file)
|
||||||
|
# try:
|
||||||
|
# async with aiofiles.open(file_location, "wb") as f:
|
||||||
|
# completed_size = 0
|
||||||
|
# while True:
|
||||||
|
# chunk = await file.read(1024*1024)
|
||||||
|
# if not chunk:
|
||||||
|
# break
|
||||||
|
# await f.write(chunk)
|
||||||
|
# completed_size += len(chunk)
|
||||||
|
# progress = (completed_size / total_size) * 100
|
||||||
|
|
||||||
|
# print(progress)
|
||||||
|
# yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n'
|
||||||
|
# except Exception as e:
|
||||||
|
# print(e)
|
||||||
|
# yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n"
|
||||||
|
# finally:
|
||||||
|
# await file.close()
|
||||||
|
# print("done")
|
||||||
|
# yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n'
|
||||||
|
|
||||||
|
# return StreamingResponse(
|
||||||
|
# file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream"
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||||
async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)):
|
async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||||
url = app.state.OLLAMA_BASE_URLS[0]
|
url = app.state.OLLAMA_BASE_URLS[0]
|
||||||
@@ -947,7 +1212,7 @@ async def deprecated_proxy(path: str, request: Request, user=Depends(get_current
|
|||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
print("User: canceled request")
|
log.warning("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import requests
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ from utils.utils import (
|
|||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
from config import (
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
@@ -31,6 +33,9 @@ from typing import List, Optional
|
|||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
@@ -134,7 +139,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -160,7 +165,7 @@ async def fetch_url(url, key):
|
|||||||
return await response.json()
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.error(f"Connection error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -182,7 +187,7 @@ def merge_models_lists(model_lists):
|
|||||||
|
|
||||||
|
|
||||||
async def get_all_models():
|
async def get_all_models():
|
||||||
print("get_all_models")
|
log.info("get_all_models()")
|
||||||
|
|
||||||
if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
|
if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
|
||||||
models = {"data": []}
|
models = {"data": []}
|
||||||
@@ -198,7 +203,9 @@ async def get_all_models():
|
|||||||
list(
|
list(
|
||||||
map(
|
map(
|
||||||
lambda response: (
|
lambda response: (
|
||||||
response["data"] if "data" in response else None
|
response["data"]
|
||||||
|
if response and "data" in response
|
||||||
|
else None
|
||||||
),
|
),
|
||||||
responses,
|
responses,
|
||||||
)
|
)
|
||||||
@@ -206,7 +213,7 @@ async def get_all_models():
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
print(models)
|
log.info(f"models: {models}")
|
||||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||||
|
|
||||||
return models
|
return models
|
||||||
@@ -244,7 +251,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
|
|||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -278,7 +285,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
if body.get("model") == "gpt-4-vision-preview":
|
if body.get("model") == "gpt-4-vision-preview":
|
||||||
if "max_tokens" not in body:
|
if "max_tokens" not in body:
|
||||||
body["max_tokens"] = 4000
|
body["max_tokens"] = 4000
|
||||||
print("Modified body_dict:", body)
|
log.debug("Modified body_dict:", body)
|
||||||
|
|
||||||
# Fix for ChatGPT calls failing because the num_ctx key is in body
|
# Fix for ChatGPT calls failing because the num_ctx key is in body
|
||||||
if "num_ctx" in body:
|
if "num_ctx" in body:
|
||||||
@@ -290,7 +297,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
# Convert the modified body back to JSON
|
# Convert the modified body back to JSON
|
||||||
body = json.dumps(body)
|
body = json.dumps(body)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print("Error loading request body into a dictionary:", e)
|
log.error("Error loading request body into a dictionary:", e)
|
||||||
|
|
||||||
url = app.state.OPENAI_API_BASE_URLS[idx]
|
url = app.state.OPENAI_API_BASE_URLS[idx]
|
||||||
key = app.state.OPENAI_API_KEYS[idx]
|
key = app.state.OPENAI_API_KEYS[idx]
|
||||||
@@ -328,7 +335,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
response_data = r.json()
|
response_data = r.json()
|
||||||
return response_data
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+165
-81
@@ -8,7 +8,7 @@ from fastapi import (
|
|||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os, shutil
|
import os, shutil, logging, re
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -21,6 +21,7 @@ from langchain_community.document_loaders import (
|
|||||||
TextLoader,
|
TextLoader,
|
||||||
PyPDFLoader,
|
PyPDFLoader,
|
||||||
CSVLoader,
|
CSVLoader,
|
||||||
|
BSHTMLLoader,
|
||||||
Docx2txtLoader,
|
Docx2txtLoader,
|
||||||
UnstructuredEPubLoader,
|
UnstructuredEPubLoader,
|
||||||
UnstructuredWordDocumentLoader,
|
UnstructuredWordDocumentLoader,
|
||||||
@@ -54,6 +55,7 @@ from utils.misc import (
|
|||||||
)
|
)
|
||||||
from utils.utils import get_current_user, get_admin_user
|
from utils.utils import get_current_user, get_admin_user
|
||||||
from config import (
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
RAG_EMBEDDING_MODEL,
|
RAG_EMBEDDING_MODEL,
|
||||||
@@ -66,6 +68,9 @@ from config import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
#
|
#
|
||||||
# if RAG_EMBEDDING_MODEL:
|
# if RAG_EMBEDDING_MODEL:
|
||||||
# sentence_transformer_ef = SentenceTransformer(
|
# sentence_transformer_ef = SentenceTransformer(
|
||||||
@@ -111,39 +116,6 @@ class StoreWebForm(CollectionNameForm):
|
|||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter(
|
|
||||||
chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP
|
|
||||||
)
|
|
||||||
docs = text_splitter.split_documents(data)
|
|
||||||
|
|
||||||
texts = [doc.page_content for doc in docs]
|
|
||||||
metadatas = [doc.metadata for doc in docs]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if overwrite:
|
|
||||||
for collection in CHROMA_CLIENT.list_collections():
|
|
||||||
if collection_name == collection.name:
|
|
||||||
print(f"deleting existing collection {collection_name}")
|
|
||||||
CHROMA_CLIENT.delete_collection(name=collection_name)
|
|
||||||
|
|
||||||
collection = CHROMA_CLIENT.create_collection(
|
|
||||||
name=collection_name,
|
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
|
||||||
)
|
|
||||||
|
|
||||||
collection.add(
|
|
||||||
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
if e.__class__.__name__ == "UniqueConstraintError":
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
return {
|
return {
|
||||||
@@ -274,7 +246,7 @@ def query_doc_handler(
|
|||||||
embedding_function=app.state.sentence_transformer_ef,
|
embedding_function=app.state.sentence_transformer_ef,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
@@ -318,13 +290,69 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
|||||||
"filename": form_data.url,
|
"filename": form_data.url,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=app.state.CHUNK_SIZE,
|
||||||
|
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||||
|
add_start_index=True,
|
||||||
|
)
|
||||||
|
docs = text_splitter.split_documents(data)
|
||||||
|
|
||||||
|
if len(docs) > 0:
|
||||||
|
return store_docs_in_vector_db(docs, collection_name, overwrite), None
|
||||||
|
else:
|
||||||
|
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
def store_text_in_vector_db(
|
||||||
|
text, metadata, collection_name, overwrite: bool = False
|
||||||
|
) -> bool:
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=app.state.CHUNK_SIZE,
|
||||||
|
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||||
|
add_start_index=True,
|
||||||
|
)
|
||||||
|
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
||||||
|
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
||||||
|
|
||||||
|
|
||||||
|
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
|
||||||
|
texts = [doc.page_content for doc in docs]
|
||||||
|
metadatas = [doc.metadata for doc in docs]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if overwrite:
|
||||||
|
for collection in CHROMA_CLIENT.list_collections():
|
||||||
|
if collection_name == collection.name:
|
||||||
|
log.info(f"deleting existing collection {collection_name}")
|
||||||
|
CHROMA_CLIENT.delete_collection(name=collection_name)
|
||||||
|
|
||||||
|
collection = CHROMA_CLIENT.create_collection(
|
||||||
|
name=collection_name,
|
||||||
|
embedding_function=app.state.sentence_transformer_ef,
|
||||||
|
)
|
||||||
|
|
||||||
|
collection.add(
|
||||||
|
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
if e.__class__.__name__ == "UniqueConstraintError":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_loader(filename: str, file_content_type: str, file_path: str):
|
def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||||
file_ext = filename.split(".")[-1].lower()
|
file_ext = filename.split(".")[-1].lower()
|
||||||
known_type = True
|
known_type = True
|
||||||
@@ -382,6 +410,8 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
||||||
elif file_ext == "xml":
|
elif file_ext == "xml":
|
||||||
loader = UnstructuredXMLLoader(file_path)
|
loader = UnstructuredXMLLoader(file_path)
|
||||||
|
elif file_ext in ["htm", "html"]:
|
||||||
|
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
||||||
elif file_ext == "md":
|
elif file_ext == "md":
|
||||||
loader = UnstructuredMarkdownLoader(file_path)
|
loader = UnstructuredMarkdownLoader(file_path)
|
||||||
elif file_content_type == "application/epub+zip":
|
elif file_content_type == "application/epub+zip":
|
||||||
@@ -416,10 +446,27 @@ def store_doc(
|
|||||||
):
|
):
|
||||||
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
||||||
|
|
||||||
print(file.content_type)
|
log.info(f"file.content_type: {file.content_type}")
|
||||||
try:
|
try:
|
||||||
|
is_valid_filename = True
|
||||||
|
unsanitized_filename = file.filename
|
||||||
|
if re.search(r'[\\/:"\*\?<>|\n\t ]', unsanitized_filename) is not None:
|
||||||
|
is_valid_filename = False
|
||||||
|
|
||||||
|
unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}"
|
||||||
|
dereferenced_file_path = str(Path(unvalidated_file_path).resolve(strict=False))
|
||||||
|
if not dereferenced_file_path.startswith(UPLOAD_DIR):
|
||||||
|
is_valid_filename = False
|
||||||
|
|
||||||
|
if is_valid_filename:
|
||||||
|
file_path = dereferenced_file_path
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
|
)
|
||||||
|
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
|
||||||
contents = file.file.read()
|
contents = file.file.read()
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -432,22 +479,24 @@ def store_doc(
|
|||||||
|
|
||||||
loader, known_type = get_loader(file.filename, file.content_type, file_path)
|
loader, known_type = get_loader(file.filename, file.content_type, file_path)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
|
||||||
|
|
||||||
if result:
|
try:
|
||||||
return {
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
"status": True,
|
|
||||||
"collection_name": collection_name,
|
if result:
|
||||||
"filename": filename,
|
return {
|
||||||
"known_type": known_type,
|
"status": True,
|
||||||
}
|
"collection_name": collection_name,
|
||||||
else:
|
"filename": filename,
|
||||||
|
"known_type": known_type,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(),
|
detail=e,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
if "No pandoc was found" in str(e):
|
if "No pandoc was found" in str(e):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -460,6 +509,37 @@ def store_doc(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TextRAGForm(BaseModel):
|
||||||
|
name: str
|
||||||
|
content: str
|
||||||
|
collection_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/text")
|
||||||
|
def store_text(
|
||||||
|
form_data: TextRAGForm,
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
|
||||||
|
collection_name = form_data.collection_name
|
||||||
|
if collection_name == None:
|
||||||
|
collection_name = calculate_sha256_string(form_data.content)
|
||||||
|
|
||||||
|
result = store_text_in_vector_db(
|
||||||
|
form_data.content,
|
||||||
|
metadata={"name": form_data.name, "created_by": user.id},
|
||||||
|
collection_name=collection_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {"status": True, "collection_name": collection_name}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/scan")
|
@app.get("/scan")
|
||||||
def scan_docs_dir(user=Depends(get_admin_user)):
|
def scan_docs_dir(user=Depends(get_admin_user)):
|
||||||
for path in Path(DOCS_DIR).rglob("./**/*"):
|
for path in Path(DOCS_DIR).rglob("./**/*"):
|
||||||
@@ -478,41 +558,45 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
|||||||
)
|
)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
|
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
try:
|
||||||
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
sanitized_filename = sanitize_filename(filename)
|
sanitized_filename = sanitize_filename(filename)
|
||||||
doc = Documents.get_doc_by_name(sanitized_filename)
|
doc = Documents.get_doc_by_name(sanitized_filename)
|
||||||
|
|
||||||
if doc == None:
|
if doc == None:
|
||||||
doc = Documents.insert_new_doc(
|
doc = Documents.insert_new_doc(
|
||||||
user.id,
|
user.id,
|
||||||
DocumentForm(
|
DocumentForm(
|
||||||
**{
|
**{
|
||||||
"name": sanitized_filename,
|
"name": sanitized_filename,
|
||||||
"title": filename,
|
"title": filename,
|
||||||
"collection_name": collection_name,
|
"collection_name": collection_name,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"content": (
|
"content": (
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"tags": list(
|
"tags": list(
|
||||||
map(
|
map(
|
||||||
lambda name: {"name": name},
|
lambda name: {"name": name},
|
||||||
tags,
|
tags,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
)
|
if len(tags)
|
||||||
if len(tags)
|
else "{}"
|
||||||
else "{}"
|
),
|
||||||
),
|
}
|
||||||
}
|
),
|
||||||
),
|
)
|
||||||
)
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
pass
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -533,11 +617,11 @@ def reset(user=Depends(get_admin_user)) -> bool:
|
|||||||
elif os.path.isdir(file_path):
|
elif os.path.isdir(file_path):
|
||||||
shutil.rmtree(file_path)
|
shutil.rmtree(file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Failed to delete %s. Reason: %s" % (file_path, e))
|
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
CHROMA_CLIENT.reset()
|
CHROMA_CLIENT.reset()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from config import CHROMA_CLIENT
|
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
def query_doc(collection_name: str, query: str, k: int, embedding_function):
|
def query_doc(collection_name: str, query: str, k: int, embedding_function):
|
||||||
@@ -97,7 +101,7 @@ def rag_template(template: str, context: str, query: str):
|
|||||||
|
|
||||||
|
|
||||||
def rag_messages(docs, messages, template, k, embedding_function):
|
def rag_messages(docs, messages, template, k, embedding_function):
|
||||||
print(docs)
|
log.debug(f"docs: {docs}")
|
||||||
|
|
||||||
last_user_message_idx = None
|
last_user_message_idx = None
|
||||||
for i in range(len(messages) - 1, -1, -1):
|
for i in range(len(messages) - 1, -1, -1):
|
||||||
@@ -137,6 +141,8 @@ def rag_messages(docs, messages, template, k, embedding_function):
|
|||||||
k=k,
|
k=k,
|
||||||
embedding_function=embedding_function,
|
embedding_function=embedding_function,
|
||||||
)
|
)
|
||||||
|
elif doc["type"] == "text":
|
||||||
|
context = doc["content"]
|
||||||
else:
|
else:
|
||||||
context = query_doc(
|
context = query_doc(
|
||||||
collection_name=doc["collection_name"],
|
collection_name=doc["collection_name"],
|
||||||
@@ -145,11 +151,13 @@ def rag_messages(docs, messages, template, k, embedding_function):
|
|||||||
embedding_function=embedding_function,
|
embedding_function=embedding_function,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
relevant_contexts.append(context)
|
relevant_contexts.append(context)
|
||||||
|
|
||||||
|
log.debug(f"relevant_contexts: {relevant_contexts}")
|
||||||
|
|
||||||
context_string = ""
|
context_string = ""
|
||||||
for context in relevant_contexts:
|
for context in relevant_contexts:
|
||||||
if context:
|
if context:
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
from peewee import *
|
from peewee import *
|
||||||
from config import DATA_DIR
|
from peewee_migrate import Router
|
||||||
|
from config import SRC_LOG_LEVELS, DATA_DIR
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["DB"])
|
||||||
|
|
||||||
# Check if the file exists
|
# Check if the file exists
|
||||||
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
||||||
# Rename the file
|
# Rename the file
|
||||||
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
||||||
print("File renamed successfully.")
|
log.info("File renamed successfully.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
||||||
DB.connect()
|
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
|
||||||
|
router.run()
|
||||||
|
DB.connect(reuse_if_open=True)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Peewee migrations -- 001_initial_schema.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 Auth(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
email = pw.CharField(max_length=255)
|
||||||
|
password = pw.CharField(max_length=255)
|
||||||
|
active = pw.BooleanField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "auth"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Chat(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
title = pw.CharField()
|
||||||
|
chat = pw.TextField()
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "chat"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class ChatIdTag(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
tag_name = pw.CharField(max_length=255)
|
||||||
|
chat_id = pw.CharField(max_length=255)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "chatidtag"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Document(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
collection_name = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255, unique=True)
|
||||||
|
title = pw.CharField()
|
||||||
|
filename = pw.CharField()
|
||||||
|
content = pw.TextField(null=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "document"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Modelfile(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
tag_name = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
modelfile = pw.TextField()
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "modelfile"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Prompt(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
command = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
title = pw.CharField()
|
||||||
|
content = pw.TextField()
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "prompt"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Tag(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
data = pw.TextField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "tag"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class User(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255)
|
||||||
|
email = pw.CharField(max_length=255)
|
||||||
|
role = pw.CharField(max_length=255)
|
||||||
|
profile_image_url = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "user"
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_model("user")
|
||||||
|
|
||||||
|
migrator.remove_model("tag")
|
||||||
|
|
||||||
|
migrator.remove_model("prompt")
|
||||||
|
|
||||||
|
migrator.remove_model("modelfile")
|
||||||
|
|
||||||
|
migrator.remove_model("document")
|
||||||
|
|
||||||
|
migrator.remove_model("chatidtag")
|
||||||
|
|
||||||
|
migrator.remove_model("chat")
|
||||||
|
|
||||||
|
migrator.remove_model("auth")
|
||||||
@@ -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."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
"chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields("chat", "share_id")
|
||||||
@@ -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."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
"user", api_key=pw.CharField(max_length=255, null=True, unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields("user", "api_key")
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Database Migrations
|
||||||
|
|
||||||
|
This directory contains all the database migrations for the web app.
|
||||||
|
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
|
||||||
|
|
||||||
|
Migrations are automatically ran at app startup.
|
||||||
|
|
||||||
|
## Creating a migration
|
||||||
|
|
||||||
|
Have you made a change to the schema of an existing model?
|
||||||
|
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
|
||||||
|
|
||||||
|
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
|
||||||
|
2. Make your changes to the models.
|
||||||
|
3. From the `backend` directory, run the following command:
|
||||||
|
```bash
|
||||||
|
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
|
||||||
|
```
|
||||||
|
- `$SQLITE_DB` should be the path to the database file.
|
||||||
|
- `$MIGRATION_NAME` should be a descriptive name for the migration.
|
||||||
|
4. The migration file will be created in the `apps/web/internal/migrations` directory.
|
||||||
@@ -19,6 +19,8 @@ from config import (
|
|||||||
DEFAULT_USER_ROLE,
|
DEFAULT_USER_ROLE,
|
||||||
ENABLE_SIGNUP,
|
ENABLE_SIGNUP,
|
||||||
USER_PERMISSIONS,
|
USER_PERMISSIONS,
|
||||||
|
WEBHOOK_URL,
|
||||||
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -32,7 +34,8 @@ app.state.DEFAULT_MODELS = DEFAULT_MODELS
|
|||||||
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||||
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||||
app.state.USER_PERMISSIONS = USER_PERMISSIONS
|
app.state.USER_PERMISSIONS = USER_PERMISSIONS
|
||||||
|
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import logging
|
||||||
from peewee import *
|
from peewee import *
|
||||||
|
|
||||||
from apps.web.models.users import UserModel, Users
|
from apps.web.models.users import UserModel, Users
|
||||||
@@ -9,6 +10,11 @@ from utils.utils import verify_password
|
|||||||
|
|
||||||
from apps.web.internal.db import DB
|
from apps.web.internal.db import DB
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# DB MODEL
|
# DB MODEL
|
||||||
####################
|
####################
|
||||||
@@ -41,6 +47,10 @@ class Token(BaseModel):
|
|||||||
token_type: str
|
token_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKey(BaseModel):
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
email: str
|
email: str
|
||||||
@@ -86,7 +96,7 @@ class AuthsTable:
|
|||||||
def insert_new_auth(
|
def insert_new_auth(
|
||||||
self, email: str, password: str, name: str, role: str = "pending"
|
self, email: str, password: str, name: str, role: str = "pending"
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
print("insert_new_auth")
|
log.info("insert_new_auth")
|
||||||
|
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -103,7 +113,7 @@ class AuthsTable:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
||||||
print("authenticate_user", email)
|
log.info(f"authenticate_user: {email}")
|
||||||
try:
|
try:
|
||||||
auth = Auth.get(Auth.email == email, Auth.active == True)
|
auth = Auth.get(Auth.email == email, Auth.active == True)
|
||||||
if auth:
|
if auth:
|
||||||
@@ -117,6 +127,28 @@ class AuthsTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||||
|
log.info(f"authenticate_user_by_api_key: {api_key}")
|
||||||
|
# if no api_key, return None
|
||||||
|
if not api_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = Users.get_user_by_api_key(api_key)
|
||||||
|
return user if user else None
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
|
||||||
|
log.info(f"authenticate_user_by_trusted_header: {email}")
|
||||||
|
try:
|
||||||
|
auth = Auth.get(Auth.email == email, Auth.active == True)
|
||||||
|
if auth:
|
||||||
|
user = Users.get_user_by_id(auth.id)
|
||||||
|
return user
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Auth.update(password=new_password).where(Auth.id == id)
|
query = Auth.update(password=new_password).where(Auth.id == id)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Chat(Model):
|
|||||||
title = CharField()
|
title = CharField()
|
||||||
chat = TextField() # Save Chat JSON as Text
|
chat = TextField() # Save Chat JSON as Text
|
||||||
timestamp = DateField()
|
timestamp = DateField()
|
||||||
|
share_id = CharField(null=True, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
@@ -31,6 +32,7 @@ class ChatModel(BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
chat: str
|
chat: str
|
||||||
timestamp: int # timestamp in epoch
|
timestamp: int # timestamp in epoch
|
||||||
|
share_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
@@ -52,6 +54,7 @@ class ChatResponse(BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
chat: dict
|
chat: dict
|
||||||
timestamp: int # timestamp in epoch
|
timestamp: int # timestamp in epoch
|
||||||
|
share_id: Optional[str] = None # id of the chat to be shared
|
||||||
|
|
||||||
|
|
||||||
class ChatTitleIdResponse(BaseModel):
|
class ChatTitleIdResponse(BaseModel):
|
||||||
@@ -95,12 +98,63 @@ class ChatTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||||
|
# Get the existing chat to share
|
||||||
|
chat = Chat.get(Chat.id == chat_id)
|
||||||
|
# Check if the chat is already shared
|
||||||
|
if chat.share_id:
|
||||||
|
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
|
||||||
|
# Create a new chat with the same data, but with a new ID
|
||||||
|
shared_chat = ChatModel(
|
||||||
|
**{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"user_id": f"shared-{chat_id}",
|
||||||
|
"title": chat.title,
|
||||||
|
"chat": chat.chat,
|
||||||
|
"timestamp": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
shared_result = Chat.create(**shared_chat.model_dump())
|
||||||
|
# Update the original chat with the share_id
|
||||||
|
result = (
|
||||||
|
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return shared_chat if (shared_result and result) else None
|
||||||
|
|
||||||
|
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
print("update_shared_chat_by_id")
|
||||||
|
chat = Chat.get(Chat.id == chat_id)
|
||||||
|
print(chat)
|
||||||
|
|
||||||
|
query = Chat.update(
|
||||||
|
title=chat.title,
|
||||||
|
chat=chat.chat,
|
||||||
|
).where(Chat.id == chat.share_id)
|
||||||
|
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
chat = Chat.get(Chat.id == chat.share_id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
|
||||||
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_chat_share_id_by_id(
|
||||||
|
self, id: str, share_id: Optional[str]
|
||||||
|
) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
query = Chat.update(
|
query = Chat.update(
|
||||||
chat=json.dumps(chat),
|
share_id=share_id,
|
||||||
title=chat["title"] if "title" in chat else "New Chat",
|
|
||||||
timestamp=int(time.time()),
|
|
||||||
).where(Chat.id == id)
|
).where(Chat.id == id)
|
||||||
query.execute()
|
query.execute()
|
||||||
|
|
||||||
@@ -145,6 +199,13 @@ class ChatTable:
|
|||||||
.order_by(Chat.timestamp.desc())
|
.order_by(Chat.timestamp.desc())
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
chat = Chat.get(Chat.id == id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
|
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
|
||||||
@@ -163,12 +224,15 @@ class ChatTable:
|
|||||||
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
return True
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
self.delete_shared_chats_by_user_id(user_id)
|
||||||
|
|
||||||
query = Chat.delete().where(Chat.user_id == user_id)
|
query = Chat.delete().where(Chat.user_id == user_id)
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
@@ -176,5 +240,19 @@ class ChatTable:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
shared_chat_ids = [
|
||||||
|
f"shared-{chat.id}"
|
||||||
|
for chat in Chat.select().where(Chat.user_id == user_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
|
||||||
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Chats = ChatTable(DB)
|
Chats = ChatTable(DB)
|
||||||
@@ -3,6 +3,7 @@ from peewee import *
|
|||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
from utils.utils import decode_token
|
from utils.utils import decode_token
|
||||||
from utils.misc import get_gravatar_url
|
from utils.misc import get_gravatar_url
|
||||||
@@ -11,6 +12,11 @@ from apps.web.internal.db import DB
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Documents DB Schema
|
# Documents DB Schema
|
||||||
####################
|
####################
|
||||||
@@ -118,7 +124,7 @@ class DocumentsTable:
|
|||||||
doc = Document.get(Document.name == form_data.name)
|
doc = Document.get(Document.name == form_data.name)
|
||||||
return DocumentModel(**model_to_dict(doc))
|
return DocumentModel(**model_to_dict(doc))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_doc_content_by_name(
|
def update_doc_content_by_name(
|
||||||
@@ -138,7 +144,7 @@ class DocumentsTable:
|
|||||||
doc = Document.get(Document.name == name)
|
doc = Document.get(Document.name == name)
|
||||||
return DocumentModel(**model_to_dict(doc))
|
return DocumentModel(**model_to_dict(doc))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_doc_by_name(self, name: str) -> bool:
|
def delete_doc_by_name(self, name: str) -> bool:
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class ModelfilesTable:
|
|||||||
self.db.create_tables([Modelfile])
|
self.db.create_tables([Modelfile])
|
||||||
|
|
||||||
def insert_new_modelfile(
|
def insert_new_modelfile(
|
||||||
self, user_id: str,
|
self, user_id: str, form_data: ModelfileForm
|
||||||
form_data: ModelfileForm) -> Optional[ModelfileModel]:
|
) -> Optional[ModelfileModel]:
|
||||||
if "tagName" in form_data.modelfile:
|
if "tagName" in form_data.modelfile:
|
||||||
modelfile = ModelfileModel(
|
modelfile = ModelfileModel(
|
||||||
**{
|
**{
|
||||||
@@ -73,7 +73,8 @@ class ModelfilesTable:
|
|||||||
"tag_name": form_data.modelfile["tagName"],
|
"tag_name": form_data.modelfile["tagName"],
|
||||||
"modelfile": json.dumps(form_data.modelfile),
|
"modelfile": json.dumps(form_data.modelfile),
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = Modelfile.create(**modelfile.model_dump())
|
result = Modelfile.create(**modelfile.model_dump())
|
||||||
@@ -87,29 +88,28 @@ class ModelfilesTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_modelfile_by_tag_name(self,
|
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
|
||||||
tag_name: str) -> Optional[ModelfileModel]:
|
|
||||||
try:
|
try:
|
||||||
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
||||||
return ModelfileModel(**model_to_dict(modelfile))
|
return ModelfileModel(**model_to_dict(modelfile))
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_modelfiles(self,
|
def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50) -> List[ModelfileResponse]:
|
|
||||||
return [
|
return [
|
||||||
ModelfileResponse(
|
ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**model_to_dict(modelfile),
|
**model_to_dict(modelfile),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
}) for modelfile in Modelfile.select()
|
)
|
||||||
|
for modelfile in Modelfile.select()
|
||||||
# .limit(limit).offset(skip)
|
# .limit(limit).offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_modelfile_by_tag_name(
|
def update_modelfile_by_tag_name(
|
||||||
self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]:
|
self, tag_name: str, modelfile: dict
|
||||||
|
) -> Optional[ModelfileModel]:
|
||||||
try:
|
try:
|
||||||
query = Modelfile.update(
|
query = Modelfile.update(
|
||||||
modelfile=json.dumps(modelfile),
|
modelfile=json.dumps(modelfile),
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ class PromptsTable:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.db.create_tables([Prompt])
|
self.db.create_tables([Prompt])
|
||||||
|
|
||||||
def insert_new_prompt(self, user_id: str,
|
def insert_new_prompt(
|
||||||
form_data: PromptForm) -> Optional[PromptModel]:
|
self, user_id: str, form_data: PromptForm
|
||||||
|
) -> Optional[PromptModel]:
|
||||||
prompt = PromptModel(
|
prompt = PromptModel(
|
||||||
**{
|
**{
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
@@ -61,7 +62,8 @@ class PromptsTable:
|
|||||||
"title": form_data.title,
|
"title": form_data.title,
|
||||||
"content": form_data.content,
|
"content": form_data.content,
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = Prompt.create(**prompt.model_dump())
|
result = Prompt.create(**prompt.model_dump())
|
||||||
@@ -81,13 +83,14 @@ class PromptsTable:
|
|||||||
|
|
||||||
def get_prompts(self) -> List[PromptModel]:
|
def get_prompts(self) -> List[PromptModel]:
|
||||||
return [
|
return [
|
||||||
PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select()
|
PromptModel(**model_to_dict(prompt))
|
||||||
|
for prompt in Prompt.select()
|
||||||
# .limit(limit).offset(skip)
|
# .limit(limit).offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_prompt_by_command(
|
def update_prompt_by_command(
|
||||||
self, command: str,
|
self, command: str, form_data: PromptForm
|
||||||
form_data: PromptForm) -> Optional[PromptModel]:
|
) -> Optional[PromptModel]:
|
||||||
try:
|
try:
|
||||||
query = Prompt.update(
|
query = Prompt.update(
|
||||||
title=form_data.title,
|
title=form_data.title,
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ from playhouse.shortcuts import model_to_dict
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
from apps.web.internal.db import DB
|
from apps.web.internal.db import DB
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Tag DB Schema
|
# Tag DB Schema
|
||||||
####################
|
####################
|
||||||
@@ -173,7 +179,7 @@ class TagTable:
|
|||||||
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
|
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
|
||||||
)
|
)
|
||||||
res = query.execute() # Remove the rows, return number of rows removed.
|
res = query.execute() # Remove the rows, return number of rows removed.
|
||||||
print(res)
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
||||||
if tag_count == 0:
|
if tag_count == 0:
|
||||||
@@ -185,7 +191,7 @@ class TagTable:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("delete_tag", e)
|
log.error(f"delete_tag: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_tag_by_tag_name_and_chat_id_and_user_id(
|
def delete_tag_by_tag_name_and_chat_id_and_user_id(
|
||||||
@@ -198,7 +204,7 @@ class TagTable:
|
|||||||
& (ChatIdTag.user_id == user_id)
|
& (ChatIdTag.user_id == user_id)
|
||||||
)
|
)
|
||||||
res = query.execute() # Remove the rows, return number of rows removed.
|
res = query.execute() # Remove the rows, return number of rows removed.
|
||||||
print(res)
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
||||||
if tag_count == 0:
|
if tag_count == 0:
|
||||||
@@ -210,7 +216,7 @@ class TagTable:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("delete_tag", e)
|
log.error(f"delete_tag: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
|
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class User(Model):
|
|||||||
role = CharField()
|
role = CharField()
|
||||||
profile_image_url = CharField()
|
profile_image_url = CharField()
|
||||||
timestamp = DateField()
|
timestamp = DateField()
|
||||||
|
api_key = CharField(null=True, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
@@ -32,6 +33,7 @@ class UserModel(BaseModel):
|
|||||||
role: str = "pending"
|
role: str = "pending"
|
||||||
profile_image_url: str = "/user.png"
|
profile_image_url: str = "/user.png"
|
||||||
timestamp: int # timestamp in epoch
|
timestamp: int # timestamp in epoch
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
@@ -82,6 +84,13 @@ class UsersTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||||
|
try:
|
||||||
|
user = User.get(User.api_key == api_key)
|
||||||
|
return UserModel(**model_to_dict(user))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.email == email)
|
user = User.get(User.email == email)
|
||||||
@@ -149,5 +158,21 @@ class UsersTable:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
|
||||||
|
try:
|
||||||
|
query = User.update(api_key=api_key).where(User.id == id)
|
||||||
|
result = query.execute()
|
||||||
|
|
||||||
|
return True if result == 1 else False
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
user = User.get(User.id == id)
|
||||||
|
return user.api_key
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
Users = UsersTable(DB)
|
Users = UsersTable(DB)
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
from fastapi import Response, Request
|
from fastapi import Request
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import List, Union
|
|
||||||
|
|
||||||
from fastapi import APIRouter, status
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
from apps.web.models.auths import (
|
from apps.web.models.auths import (
|
||||||
SigninForm,
|
SigninForm,
|
||||||
@@ -17,6 +14,7 @@ from apps.web.models.auths import (
|
|||||||
UserResponse,
|
UserResponse,
|
||||||
SigninResponse,
|
SigninResponse,
|
||||||
Auths,
|
Auths,
|
||||||
|
ApiKey,
|
||||||
)
|
)
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
@@ -25,9 +23,12 @@ from utils.utils import (
|
|||||||
get_current_user,
|
get_current_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
create_token,
|
create_token,
|
||||||
|
create_api_key,
|
||||||
)
|
)
|
||||||
from utils.misc import parse_duration, validate_email_format
|
from utils.misc import parse_duration, validate_email_format
|
||||||
from constants import ERROR_MESSAGES
|
from utils.webhook import post_webhook
|
||||||
|
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||||
|
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -78,6 +79,8 @@ async def update_profile(
|
|||||||
async def update_password(
|
async def update_password(
|
||||||
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
|
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
|
||||||
):
|
):
|
||||||
|
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
|
||||||
|
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||||
if session_user:
|
if session_user:
|
||||||
user = Auths.authenticate_user(session_user.email, form_data.password)
|
user = Auths.authenticate_user(session_user.email, form_data.password)
|
||||||
|
|
||||||
@@ -97,7 +100,22 @@ async def update_password(
|
|||||||
|
|
||||||
@router.post("/signin", response_model=SigninResponse)
|
@router.post("/signin", response_model=SigninResponse)
|
||||||
async def signin(request: Request, form_data: SigninForm):
|
async def signin(request: Request, form_data: SigninForm):
|
||||||
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
|
||||||
|
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
|
||||||
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
|
||||||
|
|
||||||
|
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
|
||||||
|
if not Users.get_user_by_email(trusted_email.lower()):
|
||||||
|
await signup(
|
||||||
|
request,
|
||||||
|
SignupForm(
|
||||||
|
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
|
||||||
|
),
|
||||||
|
)
|
||||||
|
user = Auths.authenticate_user_by_trusted_header(trusted_email)
|
||||||
|
else:
|
||||||
|
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
token = create_token(
|
token = create_token(
|
||||||
data={"id": user.id},
|
data={"id": user.id},
|
||||||
@@ -155,6 +173,17 @@ async def signup(request: Request, form_data: SignupForm):
|
|||||||
)
|
)
|
||||||
# response.set_cookie(key='token', value=token, httponly=True)
|
# response.set_cookie(key='token', value=token, httponly=True)
|
||||||
|
|
||||||
|
if request.app.state.WEBHOOK_URL:
|
||||||
|
post_webhook(
|
||||||
|
request.app.state.WEBHOOK_URL,
|
||||||
|
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||||
|
{
|
||||||
|
"action": "signup",
|
||||||
|
"message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||||
|
"user": user.model_dump_json(exclude_none=True),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"token": token,
|
"token": token,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
@@ -237,3 +266,40 @@ async def update_token_expires_duration(
|
|||||||
return request.app.state.JWT_EXPIRES_IN
|
return request.app.state.JWT_EXPIRES_IN
|
||||||
else:
|
else:
|
||||||
return request.app.state.JWT_EXPIRES_IN
|
return request.app.state.JWT_EXPIRES_IN
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# API Key
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
# create api key
|
||||||
|
@router.post("/api_key", response_model=ApiKey)
|
||||||
|
async def create_api_key_(user=Depends(get_current_user)):
|
||||||
|
api_key = create_api_key()
|
||||||
|
success = Users.update_user_api_key_by_id(user.id, api_key)
|
||||||
|
if success:
|
||||||
|
return {
|
||||||
|
"api_key": api_key,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
# delete api key
|
||||||
|
@router.delete("/api_key", response_model=bool)
|
||||||
|
async def delete_api_key(user=Depends(get_current_user)):
|
||||||
|
success = Users.update_user_api_key_by_id(user.id, None)
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
# get api key
|
||||||
|
@router.get("/api_key", response_model=ApiKey)
|
||||||
|
async def get_api_key(user=Depends(get_current_user)):
|
||||||
|
api_key = Users.get_user_api_key_by_id(user.id)
|
||||||
|
if api_key:
|
||||||
|
return {
|
||||||
|
"api_key": api_key,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||||
@@ -5,6 +5,7 @@ from utils.utils import get_current_user, get_admin_user
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
from apps.web.models.chats import (
|
from apps.web.models.chats import (
|
||||||
@@ -27,6 +28,11 @@ from apps.web.models.tags import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -78,7 +84,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
|
|||||||
chat = Chats.insert_new_chat(user.id, form_data)
|
chat = Chats.insert_new_chat(user.id, form_data)
|
||||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
@@ -95,7 +101,7 @@ async def get_all_tags(user=Depends(get_current_user)):
|
|||||||
tags = Tags.get_tags_by_user_id(user.id)
|
tags = Tags.get_tags_by_user_id(user.id)
|
||||||
return tags
|
return tags
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
@@ -183,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# ShareChatById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/share", response_model=Optional[ChatResponse])
|
||||||
|
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||||
|
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||||
|
if chat:
|
||||||
|
if chat.share_id:
|
||||||
|
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
|
||||||
|
return ChatResponse(
|
||||||
|
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
|
||||||
|
)
|
||||||
|
|
||||||
|
shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
|
||||||
|
if not shared_chat:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# DeletedSharedChatById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}/share", response_model=Optional[bool])
|
||||||
|
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||||
|
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||||
|
if chat:
|
||||||
|
if not chat.share_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = Chats.delete_shared_chat_by_chat_id(id)
|
||||||
|
update_result = Chats.update_chat_share_id_by_id(id, None)
|
||||||
|
|
||||||
|
return result and update_result != None
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetSharedChatById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
|
||||||
|
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
|
||||||
|
chat = Chats.get_chat_by_id(share_id)
|
||||||
|
|
||||||
|
if chat:
|
||||||
|
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetChatTagsById
|
# GetChatTagsById
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import uuid
|
|||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token
|
from utils.utils import (
|
||||||
|
get_password_hash,
|
||||||
|
get_current_user,
|
||||||
|
get_admin_user,
|
||||||
|
create_token,
|
||||||
|
)
|
||||||
from utils.misc import get_gravatar_url, validate_email_format
|
from utils.misc import get_gravatar_url, validate_email_format
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
@@ -43,7 +48,6 @@ async def set_global_default_models(
|
|||||||
return request.app.state.DEFAULT_MODELS
|
return request.app.state.DEFAULT_MODELS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
||||||
async def set_global_default_suggestions(
|
async def set_global_default_suggestions(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[ModelfileResponse])
|
@router.get("/", response_model=List[ModelfileResponse])
|
||||||
async def get_modelfiles(skip: int = 0,
|
async def get_modelfiles(
|
||||||
limit: int = 50,
|
skip: int = 0, limit: int = 50, user=Depends(get_current_user)
|
||||||
user=Depends(get_current_user)):
|
):
|
||||||
return Modelfiles.get_modelfiles(skip, limit)
|
return Modelfiles.get_modelfiles(skip, limit)
|
||||||
|
|
||||||
|
|
||||||
@@ -36,17 +36,16 @@ async def get_modelfiles(skip: int = 0,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[ModelfileResponse])
|
@router.post("/create", response_model=Optional[ModelfileResponse])
|
||||||
async def create_new_modelfile(form_data: ModelfileForm,
|
async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
|
||||||
user=Depends(get_admin_user)):
|
|
||||||
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
|
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
|
||||||
|
|
||||||
if modelfile:
|
if modelfile:
|
||||||
return ModelfileResponse(
|
return ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**modelfile.model_dump(),
|
**modelfile.model_dump(),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
})
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -60,17 +59,18 @@ async def create_new_modelfile(form_data: ModelfileForm,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Optional[ModelfileResponse])
|
@router.post("/", response_model=Optional[ModelfileResponse])
|
||||||
async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
async def get_modelfile_by_tag_name(
|
||||||
user=Depends(get_current_user)):
|
form_data: ModelfileTagNameForm, user=Depends(get_current_user)
|
||||||
|
):
|
||||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||||
|
|
||||||
if modelfile:
|
if modelfile:
|
||||||
return ModelfileResponse(
|
return ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**modelfile.model_dump(),
|
**modelfile.model_dump(),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
})
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -84,8 +84,9 @@ async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/update", response_model=Optional[ModelfileResponse])
|
@router.post("/update", response_model=Optional[ModelfileResponse])
|
||||||
async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
async def update_modelfile_by_tag_name(
|
||||||
user=Depends(get_admin_user)):
|
form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||||
if modelfile:
|
if modelfile:
|
||||||
updated_modelfile = {
|
updated_modelfile = {
|
||||||
@@ -94,14 +95,15 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelfile = Modelfiles.update_modelfile_by_tag_name(
|
modelfile = Modelfiles.update_modelfile_by_tag_name(
|
||||||
form_data.tag_name, updated_modelfile)
|
form_data.tag_name, updated_modelfile
|
||||||
|
)
|
||||||
|
|
||||||
return ModelfileResponse(
|
return ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**modelfile.model_dump(),
|
**modelfile.model_dump(),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
})
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -115,7 +117,8 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/delete", response_model=bool)
|
@router.delete("/delete", response_model=bool)
|
||||||
async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
async def delete_modelfile_by_tag_name(
|
||||||
user=Depends(get_admin_user)):
|
form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
|
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
|
||||||
return result
|
return result
|
||||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import logging
|
||||||
|
|
||||||
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
|
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
|
||||||
from apps.web.models.auths import Auths
|
from apps.web.models.auths import Auths
|
||||||
@@ -14,6 +15,11 @@ from apps.web.models.auths import Auths
|
|||||||
from utils.utils import get_current_user, get_password_hash, get_admin_user
|
from utils.utils import get_current_user, get_password_hash, get_admin_user
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -83,7 +89,7 @@ async def update_user_by_id(
|
|||||||
|
|
||||||
if form_data.password:
|
if form_data.password:
|
||||||
hashed = get_password_hash(form_data.password)
|
hashed = get_password_hash(form_data.password)
|
||||||
print(hashed)
|
log.debug(f"hashed: {hashed}")
|
||||||
Auths.update_user_password_by_id(user_id, hashed)
|
Auths.update_user_password_by_id(user_id, hashed)
|
||||||
|
|
||||||
Auths.update_email_by_id(user_id, form_data.email.lower())
|
Auths.update_email_by_id(user_id, form_data.email.lower())
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from starlette.responses import StreamingResponse, FileResponse
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
import markdown
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -21,155 +23,6 @@ from constants import ERROR_MESSAGES
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class UploadBlobForm(BaseModel):
|
|
||||||
filename: str
|
|
||||||
|
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
|
|
||||||
def parse_huggingface_url(hf_url):
|
|
||||||
try:
|
|
||||||
# Parse the URL
|
|
||||||
parsed_url = urlparse(hf_url)
|
|
||||||
|
|
||||||
# Get the path and split it into components
|
|
||||||
path_components = parsed_url.path.split("/")
|
|
||||||
|
|
||||||
# Extract the desired output
|
|
||||||
user_repo = "/".join(path_components[1:3])
|
|
||||||
model_file = path_components[-1]
|
|
||||||
|
|
||||||
return model_file
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024):
|
|
||||||
done = False
|
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
current_size = os.path.getsize(file_path)
|
|
||||||
else:
|
|
||||||
current_size = 0
|
|
||||||
|
|
||||||
headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
|
|
||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
||||||
async with session.get(url, headers=headers) as response:
|
|
||||||
total_size = int(response.headers.get("content-length", 0)) + current_size
|
|
||||||
|
|
||||||
with open(file_path, "ab+") as file:
|
|
||||||
async for data in response.content.iter_chunked(chunk_size):
|
|
||||||
current_size += len(data)
|
|
||||||
file.write(data)
|
|
||||||
|
|
||||||
done = current_size == total_size
|
|
||||||
progress = round((current_size / total_size) * 100, 2)
|
|
||||||
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
|
||||||
|
|
||||||
if done:
|
|
||||||
file.seek(0)
|
|
||||||
hashed = calculate_sha256(file)
|
|
||||||
file.seek(0)
|
|
||||||
|
|
||||||
url = f"{OLLAMA_BASE_URLS[0]}/api/blobs/sha256:{hashed}"
|
|
||||||
response = requests.post(url, data=file)
|
|
||||||
|
|
||||||
if response.ok:
|
|
||||||
res = {
|
|
||||||
"done": done,
|
|
||||||
"blob": f"sha256:{hashed}",
|
|
||||||
"name": file_name,
|
|
||||||
}
|
|
||||||
os.remove(file_path)
|
|
||||||
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
else:
|
|
||||||
raise "Ollama: Could not create blob, Please try again."
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/download")
|
|
||||||
async def download(
|
|
||||||
url: str,
|
|
||||||
):
|
|
||||||
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
|
|
||||||
file_name = parse_huggingface_url(url)
|
|
||||||
|
|
||||||
if file_name:
|
|
||||||
file_path = f"{UPLOAD_DIR}/{file_name}"
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
download_file_stream(url, file_path, file_name),
|
|
||||||
media_type="text/event-stream",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload")
|
|
||||||
def upload(file: UploadFile = File(...)):
|
|
||||||
file_path = f"{UPLOAD_DIR}/{file.filename}"
|
|
||||||
|
|
||||||
# Save file in chunks
|
|
||||||
with open(file_path, "wb+") as f:
|
|
||||||
for chunk in file.file:
|
|
||||||
f.write(chunk)
|
|
||||||
|
|
||||||
def file_process_stream():
|
|
||||||
total_size = os.path.getsize(file_path)
|
|
||||||
chunk_size = 1024 * 1024
|
|
||||||
try:
|
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
total = 0
|
|
||||||
done = False
|
|
||||||
|
|
||||||
while not done:
|
|
||||||
chunk = f.read(chunk_size)
|
|
||||||
if not chunk:
|
|
||||||
done = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
total += len(chunk)
|
|
||||||
progress = round((total / total_size) * 100, 2)
|
|
||||||
|
|
||||||
res = {
|
|
||||||
"progress": progress,
|
|
||||||
"total": total_size,
|
|
||||||
"completed": total,
|
|
||||||
}
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
|
|
||||||
if done:
|
|
||||||
f.seek(0)
|
|
||||||
hashed = calculate_sha256(f)
|
|
||||||
f.seek(0)
|
|
||||||
|
|
||||||
url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}"
|
|
||||||
response = requests.post(url, data=f)
|
|
||||||
|
|
||||||
if response.ok:
|
|
||||||
res = {
|
|
||||||
"done": done,
|
|
||||||
"blob": f"sha256:{hashed}",
|
|
||||||
"name": file.filename,
|
|
||||||
}
|
|
||||||
os.remove(file_path)
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
else:
|
|
||||||
raise Exception(
|
|
||||||
"Ollama: Could not create blob, Please try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
res = {"error": str(e)}
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(file_process_stream(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/gravatar")
|
@router.get("/gravatar")
|
||||||
async def get_gravatar(
|
async def get_gravatar(
|
||||||
email: str,
|
email: str,
|
||||||
@@ -177,6 +30,17 @@ async def get_gravatar(
|
|||||||
return get_gravatar_url(email)
|
return get_gravatar_url(email)
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownForm(BaseModel):
|
||||||
|
md: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/markdown")
|
||||||
|
async def get_html_from_markdown(
|
||||||
|
form_data: MarkdownForm,
|
||||||
|
):
|
||||||
|
return {"html": markdown.markdown(form_data.md)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/db/download")
|
@router.get("/db/download")
|
||||||
async def download_db(user=Depends(get_admin_user)):
|
async def download_db(user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
|||||||
+71
-10
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
import chromadb
|
import chromadb
|
||||||
from chromadb import Settings
|
from chromadb import Settings
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
@@ -21,9 +23,11 @@ try:
|
|||||||
|
|
||||||
load_dotenv(find_dotenv("../.env"))
|
load_dotenv(find_dotenv("../.env"))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("dotenv not installed, skipping...")
|
log.warning("dotenv not installed, skipping...")
|
||||||
|
|
||||||
|
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||||
|
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||||
|
|
||||||
WEBUI_NAME = "Open WebUI"
|
|
||||||
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -100,18 +104,60 @@ for version in soup.find_all("h2"):
|
|||||||
CHANGELOG = changelog_json
|
CHANGELOG = changelog_json
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# LOGGING
|
||||||
|
####################################
|
||||||
|
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
||||||
|
|
||||||
|
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
||||||
|
if GLOBAL_LOG_LEVEL in log_levels:
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
||||||
|
else:
|
||||||
|
GLOBAL_LOG_LEVEL = "INFO"
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
||||||
|
|
||||||
|
log_sources = [
|
||||||
|
"AUDIO",
|
||||||
|
"COMFYUI",
|
||||||
|
"CONFIG",
|
||||||
|
"DB",
|
||||||
|
"IMAGES",
|
||||||
|
"LITELLM",
|
||||||
|
"MAIN",
|
||||||
|
"MODELS",
|
||||||
|
"OLLAMA",
|
||||||
|
"OPENAI",
|
||||||
|
"RAG",
|
||||||
|
"WEBHOOK",
|
||||||
|
]
|
||||||
|
|
||||||
|
SRC_LOG_LEVELS = {}
|
||||||
|
|
||||||
|
for source in log_sources:
|
||||||
|
log_env_var = source + "_LOG_LEVEL"
|
||||||
|
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
||||||
|
if SRC_LOG_LEVELS[source] not in log_levels:
|
||||||
|
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
||||||
|
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
||||||
|
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# CUSTOM_NAME
|
# CUSTOM_NAME
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
||||||
|
|
||||||
if CUSTOM_NAME:
|
if CUSTOM_NAME:
|
||||||
try:
|
try:
|
||||||
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
|
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if r.ok:
|
if r.ok:
|
||||||
if "logo" in data:
|
if "logo" in data:
|
||||||
url = (
|
WEBUI_FAVICON_URL = url = (
|
||||||
f"https://api.openwebui.com{data['logo']}"
|
f"https://api.openwebui.com{data['logo']}"
|
||||||
if data["logo"][0] == "/"
|
if data["logo"][0] == "/"
|
||||||
else data["logo"]
|
else data["logo"]
|
||||||
@@ -125,9 +171,11 @@ if CUSTOM_NAME:
|
|||||||
|
|
||||||
WEBUI_NAME = data["name"]
|
WEBUI_NAME = data["name"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
if WEBUI_NAME != "Open WebUI":
|
||||||
|
WEBUI_NAME += " (Open WebUI)"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# DATA/FRONTEND BUILD DIR
|
# DATA/FRONTEND BUILD DIR
|
||||||
@@ -194,9 +242,9 @@ def create_config_file(file_path):
|
|||||||
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
||||||
|
|
||||||
if not os.path.exists(LITELLM_CONFIG_PATH):
|
if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||||
print("Config file doesn't exist. Creating...")
|
log.info("Config file doesn't exist. Creating...")
|
||||||
create_config_file(LITELLM_CONFIG_PATH)
|
create_config_file(LITELLM_CONFIG_PATH)
|
||||||
print("Config file created successfully.")
|
log.info("Config file created successfully.")
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -208,7 +256,7 @@ OLLAMA_API_BASE_URL = os.environ.get(
|
|||||||
)
|
)
|
||||||
|
|
||||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
||||||
|
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
||||||
|
|
||||||
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
||||||
OLLAMA_BASE_URL = (
|
OLLAMA_BASE_URL = (
|
||||||
@@ -221,6 +269,9 @@ if ENV == "prod":
|
|||||||
if OLLAMA_BASE_URL == "/ollama":
|
if OLLAMA_BASE_URL == "/ollama":
|
||||||
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
||||||
|
|
||||||
|
elif K8S_FLAG:
|
||||||
|
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
|
||||||
|
|
||||||
|
|
||||||
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
||||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
|
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
|
||||||
@@ -290,13 +341,19 @@ DEFAULT_PROMPT_SUGGESTIONS = (
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
|
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
|
||||||
USER_PERMISSIONS = {"chat": {"deletion": True}}
|
|
||||||
|
USER_PERMISSIONS_CHAT_DELETION = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
|
||||||
|
|
||||||
|
|
||||||
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", False)
|
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true"
|
||||||
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
||||||
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
||||||
|
|
||||||
|
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_VERSION
|
# WEBUI_VERSION
|
||||||
@@ -309,6 +366,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
WEBUI_AUTH = True
|
WEBUI_AUTH = True
|
||||||
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||||
|
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_SECRET_KEY
|
# WEBUI_SECRET_KEY
|
||||||
@@ -370,3 +430,4 @@ WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models"
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
||||||
|
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
||||||
+14
-2
@@ -5,6 +5,13 @@ class MESSAGES(str, Enum):
|
|||||||
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
||||||
|
|
||||||
|
|
||||||
|
class WEBHOOK_MESSAGES(str, Enum):
|
||||||
|
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
||||||
|
USER_SIGNUP = lambda username="": (
|
||||||
|
f"New user signed up: {username}" if username else "New user signed up"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ERROR_MESSAGES(str, Enum):
|
class ERROR_MESSAGES(str, Enum):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
@@ -13,6 +20,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
||||||
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
||||||
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
||||||
|
EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
|
||||||
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
|
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
|
||||||
USERNAME_TAKEN = (
|
USERNAME_TAKEN = (
|
||||||
"Uh-oh! This username is already registered. Please choose another username."
|
"Uh-oh! This username is already registered. Please choose another username."
|
||||||
@@ -29,6 +37,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
INVALID_PASSWORD = (
|
INVALID_PASSWORD = (
|
||||||
"The password provided is incorrect. Please check for typos and try again."
|
"The password provided is incorrect. Please check for typos and try again."
|
||||||
)
|
)
|
||||||
|
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
|
||||||
UNAUTHORIZED = "401 Unauthorized"
|
UNAUTHORIZED = "401 Unauthorized"
|
||||||
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
||||||
ACTION_PROHIBITED = (
|
ACTION_PROHIBITED = (
|
||||||
@@ -46,10 +55,13 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
|
|
||||||
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
||||||
INCORRECT_FORMAT = (
|
INCORRECT_FORMAT = (
|
||||||
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
|
lambda err="": f"Invalid format. Please use the correct format{err}"
|
||||||
)
|
)
|
||||||
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
||||||
|
|
||||||
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
||||||
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"
|
OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
|
||||||
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
||||||
|
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
|
||||||
|
|
||||||
|
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
||||||
+23
-34
@@ -1,35 +1,24 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.1",
|
"version": 0,
|
||||||
"ui": {
|
"ui": {
|
||||||
"prompt_suggestions": [
|
"default_locale": "en-US",
|
||||||
{
|
"prompt_suggestions": [
|
||||||
"title": [
|
{
|
||||||
"Help me study",
|
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||||
"vocabulary for a college entrance exam"
|
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
|
||||||
],
|
},
|
||||||
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
|
{
|
||||||
},
|
"title": ["Give me ideas", "for what to do with my kids' art"],
|
||||||
{
|
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
|
||||||
"title": [
|
},
|
||||||
"Give me ideas",
|
{
|
||||||
"for what to do with my kids' art"
|
"title": ["Tell me a fun fact", "about the Roman Empire"],
|
||||||
],
|
"content": "Tell me a random fun fact about the Roman Empire"
|
||||||
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
|
},
|
||||||
},
|
{
|
||||||
{
|
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||||
"title": [
|
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
||||||
"Tell me a fun fact",
|
}
|
||||||
"about the Roman Empire"
|
]
|
||||||
],
|
}
|
||||||
"content": "Tell me a random fun fact about the Roman Empire"
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": [
|
|
||||||
"Show me a code snippet",
|
|
||||||
"of a website's sticky header"
|
|
||||||
],
|
|
||||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+77
-7
@@ -4,6 +4,7 @@ import markdown
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status
|
from fastapi import FastAPI, Request, Depends, status
|
||||||
@@ -31,6 +32,7 @@ from utils.utils import get_admin_user
|
|||||||
from apps.rag.utils import rag_messages
|
from apps.rag.utils import rag_messages
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
|
CONFIG_DATA,
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
ENV,
|
ENV,
|
||||||
VERSION,
|
VERSION,
|
||||||
@@ -38,9 +40,16 @@ from config import (
|
|||||||
FRONTEND_BUILD_DIR,
|
FRONTEND_BUILD_DIR,
|
||||||
MODEL_FILTER_ENABLED,
|
MODEL_FILTER_ENABLED,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
|
GLOBAL_LOG_LEVEL,
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
WEBHOOK_URL,
|
||||||
)
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
class SPAStaticFiles(StaticFiles):
|
class SPAStaticFiles(StaticFiles):
|
||||||
async def get_response(self, path: str, scope):
|
async def get_response(self, path: str, scope):
|
||||||
@@ -53,11 +62,28 @@ class SPAStaticFiles(StaticFiles):
|
|||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"""
|
||||||
|
___ __ __ _ _ _ ___
|
||||||
|
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
|
||||||
|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
|
||||||
|
| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || |
|
||||||
|
\___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___|
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
v{VERSION} - building the best open-source AI user interface.
|
||||||
|
https://github.com/open-webui/open-webui
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +92,7 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
|||||||
if request.method == "POST" and (
|
if request.method == "POST" and (
|
||||||
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
|
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
|
||||||
):
|
):
|
||||||
print(request.url.path)
|
log.debug(f"request.url.path: {request.url.path}")
|
||||||
|
|
||||||
# Read the original request body
|
# Read the original request body
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
@@ -78,7 +104,6 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
|||||||
# Example: Add a new key-value pair or modify existing ones
|
# Example: Add a new key-value pair or modify existing ones
|
||||||
# data["modified"] = True # Example modification
|
# data["modified"] = True # Example modification
|
||||||
if "docs" in data:
|
if "docs" in data:
|
||||||
|
|
||||||
data = {**data}
|
data = {**data}
|
||||||
data["messages"] = rag_messages(
|
data["messages"] = rag_messages(
|
||||||
data["docs"],
|
data["docs"],
|
||||||
@@ -89,7 +114,7 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
|||||||
)
|
)
|
||||||
del data["docs"]
|
del data["docs"]
|
||||||
|
|
||||||
print(data["messages"])
|
log.debug(f"data['messages']: {data['messages']}")
|
||||||
|
|
||||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||||
|
|
||||||
@@ -153,14 +178,22 @@ app.mount("/rag/api/v1", rag_app)
|
|||||||
|
|
||||||
@app.get("/api/config")
|
@app.get("/api/config")
|
||||||
async def get_app_config():
|
async def get_app_config():
|
||||||
|
# Checking and Handling the Absence of 'ui' in CONFIG_DATA
|
||||||
|
|
||||||
|
default_locale = "en-US"
|
||||||
|
if "ui" in CONFIG_DATA:
|
||||||
|
default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US")
|
||||||
|
|
||||||
|
# The Rest of the Function Now Uses the Variables Defined Above
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"name": WEBUI_NAME,
|
"name": WEBUI_NAME,
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
|
"default_locale": default_locale,
|
||||||
"images": images_app.state.ENABLED,
|
"images": images_app.state.ENABLED,
|
||||||
"default_models": webui_app.state.DEFAULT_MODELS,
|
"default_models": webui_app.state.DEFAULT_MODELS,
|
||||||
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
|
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -178,10 +211,9 @@ class ModelFilterConfigForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/config/model/filter")
|
@app.post("/api/config/model/filter")
|
||||||
async def get_model_filter_config(
|
async def update_model_filter_config(
|
||||||
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = form_data.enabled
|
app.state.MODEL_FILTER_ENABLED = form_data.enabled
|
||||||
app.state.MODEL_FILTER_LIST = form_data.models
|
app.state.MODEL_FILTER_LIST = form_data.models
|
||||||
|
|
||||||
@@ -191,15 +223,39 @@ async def get_model_filter_config(
|
|||||||
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
||||||
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
||||||
|
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"enabled": app.state.MODEL_FILTER_ENABLED,
|
"enabled": app.state.MODEL_FILTER_ENABLED,
|
||||||
"models": app.state.MODEL_FILTER_LIST,
|
"models": app.state.MODEL_FILTER_LIST,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/webhook")
|
||||||
|
async def get_webhook_url(user=Depends(get_admin_user)):
|
||||||
|
return {
|
||||||
|
"url": app.state.WEBHOOK_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UrlForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/webhook")
|
||||||
|
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.WEBHOOK_URL = form_data.url
|
||||||
|
|
||||||
|
webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": app.state.WEBHOOK_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/version")
|
@app.get("/api/version")
|
||||||
async def get_app_config():
|
async def get_app_config():
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
}
|
}
|
||||||
@@ -207,7 +263,7 @@ async def get_app_config():
|
|||||||
|
|
||||||
@app.get("/api/changelog")
|
@app.get("/api/changelog")
|
||||||
async def get_app_changelog():
|
async def get_app_changelog():
|
||||||
return CHANGELOG
|
return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/version/updates")
|
@app.get("/api/version/updates")
|
||||||
@@ -227,6 +283,20 @@ async def get_app_latest_release_version():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/manifest.json")
|
||||||
|
async def get_manifest_json():
|
||||||
|
return {
|
||||||
|
"name": WEBUI_NAME,
|
||||||
|
"short_name": WEBUI_NAME,
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#343541",
|
||||||
|
"theme_color": "#343541",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
|
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ uuid
|
|||||||
requests
|
requests
|
||||||
aiohttp
|
aiohttp
|
||||||
peewee
|
peewee
|
||||||
|
peewee-migrate
|
||||||
bcrypt
|
bcrypt
|
||||||
|
|
||||||
litellm==1.30.7
|
litellm==1.30.7
|
||||||
|
boto3
|
||||||
|
|
||||||
argon2-cffi
|
argon2-cffi
|
||||||
apscheduler
|
apscheduler
|
||||||
google-generativeai
|
google-generativeai
|
||||||
@@ -45,3 +48,4 @@ PyJWT
|
|||||||
pyjwt[crypto]
|
pyjwt[crypto]
|
||||||
|
|
||||||
black
|
black
|
||||||
|
langfuse
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi import HTTPException, status, Depends
|
from fastapi import HTTPException, status, Depends
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
@@ -8,6 +10,7 @@ from passlib.context import CryptContext
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import requests
|
import requests
|
||||||
import jwt
|
import jwt
|
||||||
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
import config
|
import config
|
||||||
|
|
||||||
@@ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str):
|
|||||||
return auth_header[len("Bearer ") :]
|
return auth_header[len("Bearer ") :]
|
||||||
|
|
||||||
|
|
||||||
|
def create_api_key():
|
||||||
|
key = str(uuid.uuid4()).replace("-", "")
|
||||||
|
return f"sk-{key}"
|
||||||
|
|
||||||
|
|
||||||
def get_http_authorization_cred(auth_header: str):
|
def get_http_authorization_cred(auth_header: str):
|
||||||
try:
|
try:
|
||||||
scheme, credentials = auth_header.split(" ")
|
scheme, credentials = auth_header.split(" ")
|
||||||
@@ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str):
|
|||||||
def get_current_user(
|
def get_current_user(
|
||||||
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
||||||
):
|
):
|
||||||
|
# auth by api key
|
||||||
|
if auth_token.credentials.startswith("sk-"):
|
||||||
|
return get_current_user_by_api_key(auth_token.credentials)
|
||||||
|
# auth by jwt token
|
||||||
data = decode_token(auth_token.credentials)
|
data = decode_token(auth_token.credentials)
|
||||||
if data != None and "id" in data:
|
if data != None and "id" in data:
|
||||||
user = Users.get_user_by_id(data["id"])
|
user = Users.get_user_by_id(data["id"])
|
||||||
@@ -85,6 +97,16 @@ def get_current_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user_by_api_key(api_key: str):
|
||||||
|
user = Users.get_user_by_api_key(api_key)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_verified_user(user=Depends(get_current_user)):
|
def get_verified_user(user=Depends(get_current_user)):
|
||||||
if user.role not in {"user", "admin"}:
|
if user.role not in {"user", "admin"}:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
|
||||||
|
|
||||||
|
|
||||||
|
def post_webhook(url: str, message: str, event_data: dict) -> bool:
|
||||||
|
try:
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
# Slack and Google Chat Webhooks
|
||||||
|
if "https://hooks.slack.com" in url or "https://chat.googleapis.com" in url:
|
||||||
|
payload["text"] = message
|
||||||
|
# Discord Webhooks
|
||||||
|
elif "https://discord.com/api/webhooks" in url:
|
||||||
|
payload["content"] = message
|
||||||
|
# Microsoft Teams Webhooks
|
||||||
|
elif "webhook.office.com" in url:
|
||||||
|
action = event_data.get("action", "undefined")
|
||||||
|
facts = [
|
||||||
|
{"name": name, "value": value}
|
||||||
|
for name, value in json.loads(event_data.get("user", {})).items()
|
||||||
|
]
|
||||||
|
payload = {
|
||||||
|
"@type": "MessageCard",
|
||||||
|
"@context": "http://schema.org/extensions",
|
||||||
|
"themeColor": "0076D7",
|
||||||
|
"summary": message,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"activityTitle": message,
|
||||||
|
"activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}",
|
||||||
|
"activityImage": WEBUI_FAVICON_URL,
|
||||||
|
"facts": facts,
|
||||||
|
"markdown": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
# Default Payload
|
||||||
|
else:
|
||||||
|
payload = {**event_data}
|
||||||
|
|
||||||
|
log.debug(f"payload: {payload}")
|
||||||
|
r = requests.post(url, json=payload)
|
||||||
|
r.raise_for_status()
|
||||||
|
log.debug(f"r.text: {r.text}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
return False
|
||||||
@@ -88,7 +88,7 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.ollama.persistence.size | quote }}
|
storage: {{ .Values.ollama.persistence.size | quote }}
|
||||||
storageClass: {{ .Values.ollama.persistence.storageClass }}
|
storageClassName: {{ .Values.ollama.persistence.storageClass }}
|
||||||
{{- with .Values.ollama.persistence.selector }}
|
{{- with .Values.ollama.persistence.selector }}
|
||||||
selector:
|
selector:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.webui.persistence.size }}
|
storage: {{ .Values.webui.persistence.size }}
|
||||||
storageClass: {{ .Values.webui.persistence.storageClass }}
|
storageClassName: {{ .Values.webui.persistence.storageClass }}
|
||||||
{{- with .Values.webui.persistence.selector }}
|
{{- with .Values.webui.persistence.selector }}
|
||||||
selector:
|
selector:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: webui-volume
|
- name: webui-volume
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: ollama-webui-pvc
|
claimName: open-webui-pvc
|
||||||
@@ -2,8 +2,8 @@ apiVersion: v1
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: ollama-webui
|
app: open-webui
|
||||||
name: ollama-webui-pvc
|
name: open-webui-pvc
|
||||||
namespace: open-webui
|
namespace: open-webui
|
||||||
spec:
|
spec:
|
||||||
accessModes: ["ReadWriteOnce"]
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
|||||||
Generated
+1784
-4763
File diff suppressed because it is too large.
Load diff
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.113",
|
"version": "0.1.117",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"lint:types": "npm run check",
|
"lint:types": "npm run check",
|
||||||
"lint:backend": "pylint backend/",
|
"lint:backend": "pylint backend/",
|
||||||
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
||||||
"format:backend": "yapf --recursive backend -p -i",
|
"format:backend": "black . --exclude \"/venv/\"",
|
||||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -53,10 +53,11 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
|
"jspdf": "^2.5.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,3 +78,7 @@ select {
|
|||||||
/* for Chrome */
|
/* for Chrome */
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.katex-mathml {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
+5
-1
@@ -9,7 +9,11 @@
|
|||||||
<script>
|
<script>
|
||||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||||
(() => {
|
(() => {
|
||||||
if (
|
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else if (
|
||||||
localStorage.theme === 'light' ||
|
localStorage.theme === 'light' ||
|
||||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -318,3 +318,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createAPIKey = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
|
||||||
|
method: 'POST',
|
||||||
|
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.api_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAPIKey = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
|
||||||
|
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.api_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAPIKey = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getChatByShareId = async (token: string, share_id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shareChatById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSharedChatById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateChatById = async (token: string, id: string, chat: object) => {
|
export const updateChatById = async (token: string, id: string, chat: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
return res.OPENAI_API_KEY;
|
return res.OPENAI_API_KEY;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAUTOMATIC1111Url = async (token: string = '') => {
|
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
|
||||||
@@ -168,10 +168,10 @@ export const getAUTOMATIC1111Url = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.AUTOMATIC1111_BASE_URL;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => {
|
export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
|
||||||
@@ -182,7 +182,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: url
|
...urls
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -203,7 +203,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.AUTOMATIC1111_BASE_URL;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageSize = async (token: string = '') => {
|
export const getImageSize = async (token: string = '') => {
|
||||||
|
|||||||
@@ -139,3 +139,60 @@ export const updateModelFilterConfig = async (
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWebhookUrl = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, {
|
||||||
|
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;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateWebhookUrl = async (token: string, url: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.url;
|
||||||
|
};
|
||||||
@@ -33,7 +33,7 @@ export const getLiteLLMModels = async (token: string = '') => {
|
|||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name ?? model.id,
|
name: model.name ?? model.id,
|
||||||
external: true,
|
external: true,
|
||||||
source: 'litellm'
|
source: 'LiteLLM'
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) =
|
|||||||
return [res, controller];
|
return [res, controller];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cancelChatCompletion = async (token: string = '', requestId: string) => {
|
export const cancelOllamaRequest = async (token: string = '', requestId: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, {
|
||||||
@@ -390,6 +390,73 @@ export const pullModel = async (token: string, tagName: string, urlIdx: string |
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const downloadModel = async (
|
||||||
|
token: string,
|
||||||
|
download_url: string,
|
||||||
|
urlIdx: string | null = null
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${OLLAMA_API_BASE_URL}/models/download${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: download_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadModel = async (token: string, file: File, urlIdx: string | null = null) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${OLLAMA_API_BASE_URL}/models/upload${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
// export const pullModel = async (token: string, tagName: string) => {
|
// export const pullModel = async (token: string, tagName: string) => {
|
||||||
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
|
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
|
||||||
// method: 'POST',
|
// method: 'POST',
|
||||||
|
|||||||
@@ -263,3 +263,53 @@ export const synthesizeOpenAISpeech = async (
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateTitle = async (
|
||||||
|
token: string = '',
|
||||||
|
template: string,
|
||||||
|
model: string,
|
||||||
|
prompt: string,
|
||||||
|
url: string = OPENAI_API_BASE_URL
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
template = template.replace(/{{prompt}}/g, prompt);
|
||||||
|
|
||||||
|
console.log(template);
|
||||||
|
|
||||||
|
const res = await fetch(`${url}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: template
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res?.choices[0]?.message?.content ?? 'New Chat';
|
||||||
|
};
|
||||||
@@ -22,6 +22,31 @@ export const getGravatarUrl = async (email: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getHTMLFromMarkdown = async (md: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
md: md
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.html;
|
||||||
|
};
|
||||||
|
|
||||||
export const downloadDatabase = async (token: string) => {
|
export const downloadDatabase = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||||
import {
|
import {
|
||||||
getDefaultUserRole,
|
getDefaultUserRole,
|
||||||
getJWTExpiresDuration,
|
getJWTExpiresDuration,
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
let defaultUserRole = 'pending';
|
let defaultUserRole = 'pending';
|
||||||
let JWTExpiresIn = '';
|
let JWTExpiresIn = '';
|
||||||
|
|
||||||
|
let webhookUrl = '';
|
||||||
|
|
||||||
const toggleSignUpEnabled = async () => {
|
const toggleSignUpEnabled = async () => {
|
||||||
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
||||||
};
|
};
|
||||||
@@ -28,18 +31,23 @@
|
|||||||
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateWebhookUrlHandler = async () => {
|
||||||
|
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
||||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
||||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
||||||
|
webhookUrl = await getWebhookUrl(localStorage.token);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
// console.log('submit');
|
|
||||||
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
||||||
|
updateWebhookUrlHandler();
|
||||||
saveHandler();
|
saveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -108,6 +116,23 @@
|
|||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
|
<div class=" w-full justify-between">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2 space-x-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||||
|
type="text"
|
||||||
|
placeholder={`https://example.com/webhook`}
|
||||||
|
bind:value={webhookUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
<div class=" w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||||
|
|||||||
@@ -673,7 +673,7 @@
|
|||||||
? chatInputPlaceholder
|
? chatInputPlaceholder
|
||||||
: isRecording
|
: isRecording
|
||||||
? $i18n.t('Listening...')
|
? $i18n.t('Listening...')
|
||||||
: $i18n.t('Send a Messsage')}
|
: $i18n.t('Send a Message')}
|
||||||
bind:value={prompt}
|
bind:value={prompt}
|
||||||
on:keypress={(e) => {
|
on:keypress={(e) => {
|
||||||
if (e.keyCode == 13 && !e.shiftKey) {
|
if (e.keyCode == 13 && !e.shiftKey) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
|
export let readOnly = false;
|
||||||
export let sendPrompt: Function;
|
export let sendPrompt: Function;
|
||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
export let regenerateResponse: Function;
|
export let regenerateResponse: Function;
|
||||||
@@ -317,6 +318,7 @@
|
|||||||
<UserMessage
|
<UserMessage
|
||||||
on:delete={() => messageDeleteHandler(message.id)}
|
on:delete={() => messageDeleteHandler(message.id)}
|
||||||
user={$user}
|
user={$user}
|
||||||
|
{readOnly}
|
||||||
{message}
|
{message}
|
||||||
isFirstMessage={messageIdx === 0}
|
isFirstMessage={messageIdx === 0}
|
||||||
siblings={message.parentId !== null
|
siblings={message.parentId !== null
|
||||||
@@ -335,6 +337,7 @@
|
|||||||
modelfiles={selectedModelfiles}
|
modelfiles={selectedModelfiles}
|
||||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||||
isLastMessage={messageIdx + 1 === messages.length}
|
isLastMessage={messageIdx + 1 === messages.length}
|
||||||
|
{readOnly}
|
||||||
{confirmEditResponseMessage}
|
{confirmEditResponseMessage}
|
||||||
{showPreviousMessage}
|
{showPreviousMessage}
|
||||||
{showNextMessage}
|
{showNextMessage}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<img
|
<img
|
||||||
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
alt="modelfile"
|
alt="modelfile"
|
||||||
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
src={models.length === 1
|
src={models.length === 1
|
||||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||||
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,7 +17,11 @@
|
|||||||
import { config, settings } from '$lib/stores';
|
import { config, settings } from '$lib/stores';
|
||||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
import { extractSentences } from '$lib/utils';
|
import {
|
||||||
|
extractSentences,
|
||||||
|
revertSanitizedResponseContent,
|
||||||
|
sanitizeResponseContent
|
||||||
|
} from '$lib/utils';
|
||||||
|
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import ProfileImage from './ProfileImage.svelte';
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
@@ -33,6 +37,8 @@
|
|||||||
|
|
||||||
export let isLastMessage = true;
|
export let isLastMessage = true;
|
||||||
|
|
||||||
|
export let readOnly = false;
|
||||||
|
|
||||||
export let confirmEditResponseMessage: Function;
|
export let confirmEditResponseMessage: Function;
|
||||||
export let showPreviousMessage: Function;
|
export let showPreviousMessage: Function;
|
||||||
export let showNextMessage: Function;
|
export let showNextMessage: Function;
|
||||||
@@ -54,7 +60,7 @@
|
|||||||
let loadingSpeech = false;
|
let loadingSpeech = false;
|
||||||
let generatingImage = false;
|
let generatingImage = false;
|
||||||
|
|
||||||
$: tokens = marked.lexer(message.content);
|
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
@@ -128,7 +134,7 @@
|
|||||||
// • auto-render specific keys, e.g.:
|
// • auto-render specific keys, e.g.:
|
||||||
delimiters: [
|
delimiters: [
|
||||||
{ left: '$$', right: '$$', display: false },
|
{ left: '$$', right: '$$', display: false },
|
||||||
{ left: '$', right: '$', display: false },
|
{ left: '$ ', right: ' $', display: false },
|
||||||
{ left: '\\(', right: '\\)', display: false },
|
{ left: '\\(', right: '\\)', display: false },
|
||||||
{ left: '\\[', right: '\\]', display: false },
|
{ left: '\\[', right: '\\]', display: false },
|
||||||
{ left: '[ ', right: ' ]', display: false }
|
{ left: '[ ', right: ' ]', display: false }
|
||||||
@@ -403,8 +409,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#each tokens as token}
|
{#each tokens as token}
|
||||||
{#if token.type === 'code'}
|
{#if token.type === 'code'}
|
||||||
<!-- {token.text} -->
|
<CodeBlock
|
||||||
<CodeBlock lang={token.lang} code={token.text} />
|
lang={token.lang}
|
||||||
|
code={revertSanitizedResponseContent(token.text)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{@html marked.parse(token.raw, {
|
{@html marked.parse(token.raw, {
|
||||||
...defaults,
|
...defaults,
|
||||||
@@ -469,31 +477,33 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Edit" placement="bottom">
|
{#if !readOnly}
|
||||||
<button
|
<Tooltip content="Edit" placement="bottom">
|
||||||
class="{isLastMessage
|
<button
|
||||||
? 'visible'
|
class="{isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
? 'visible'
|
||||||
on:click={() => {
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
editMessageHandler();
|
on:click={() => {
|
||||||
}}
|
editMessageHandler();
|
||||||
>
|
}}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
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"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
class="w-4 h-4"
|
||||||
</Tooltip>
|
>
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Copy" placement="bottom">
|
<Tooltip content="Copy" placement="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -521,59 +531,61 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="Good Response" placement="bottom">
|
{#if !readOnly}
|
||||||
<button
|
<Tooltip content="Good Response" placement="bottom">
|
||||||
class="{isLastMessage
|
<button
|
||||||
? 'visible'
|
class="{isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
? 'visible'
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
||||||
: ''} dark:hover:text-white hover:text-black transition"
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
on:click={() => {
|
: ''} dark:hover:text-white hover:text-black transition"
|
||||||
rateMessage(message.id, 1);
|
on:click={() => {
|
||||||
}}
|
rateMessage(message.id, 1);
|
||||||
>
|
}}
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-4 h-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><path
|
|
||||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
|
||||||
/></svg
|
|
||||||
>
|
>
|
||||||
</button>
|
<svg
|
||||||
</Tooltip>
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="Bad Response" placement="bottom">
|
<Tooltip content="Bad Response" placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ''} dark:hover:text-white hover:text-black transition"
|
: ''} dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
rateMessage(message.id, -1);
|
rateMessage(message.id, -1);
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-4 h-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><path
|
|
||||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
|
||||||
/></svg
|
|
||||||
>
|
>
|
||||||
</button>
|
<svg
|
||||||
</Tooltip>
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Read Aloud" placement="bottom">
|
<Tooltip content="Read Aloud" placement="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -656,7 +668,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if $config.images}
|
{#if $config.images && !readOnly}
|
||||||
<Tooltip content="Generate Image" placement="bottom">
|
<Tooltip content="Generate Image" placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
@@ -752,7 +764,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isLastMessage}
|
{#if isLastMessage && !readOnly}
|
||||||
<Tooltip content="Continue Response" placement="bottom">
|
<Tooltip content="Continue Response" placement="bottom">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
export let message;
|
export let message;
|
||||||
export let siblings;
|
export let siblings;
|
||||||
export let isFirstMessage: boolean;
|
export let isFirstMessage: boolean;
|
||||||
|
export let readOnly: boolean;
|
||||||
|
|
||||||
export let confirmEditMessage: Function;
|
export let confirmEditMessage: Function;
|
||||||
export let showPreviousMessage: Function;
|
export let showPreviousMessage: Function;
|
||||||
@@ -250,29 +251,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Edit" placement="bottom">
|
{#if !readOnly}
|
||||||
<button
|
<Tooltip content="Edit" placement="bottom">
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
<button
|
||||||
on:click={() => {
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||||
editMessageHandler();
|
on:click={() => {
|
||||||
}}
|
editMessageHandler();
|
||||||
>
|
}}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
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"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
class="w-4 h-4"
|
||||||
</Tooltip>
|
>
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Copy" placement="bottom">
|
<Tooltip content="Copy" placement="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -298,7 +301,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if !isFirstMessage}
|
{#if !isFirstMessage && !readOnly}
|
||||||
<Tooltip content="Delete" placement="bottom">
|
<Tooltip content="Delete" placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Collapsible } from 'bits-ui';
|
||||||
|
|
||||||
import { setDefaultModels } from '$lib/apis/configs';
|
import { setDefaultModels } from '$lib/apis/configs';
|
||||||
import { models, showSettings, settings, user } from '$lib/stores';
|
import { models, showSettings, settings, user } from '$lib/stores';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Selector from './ModelSelector/Selector.svelte';
|
||||||
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -32,110 +36,76 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col my-2">
|
<div class="flex flex-col mt-0.5 w-full">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex">
|
<div class="flex w-full">
|
||||||
<select
|
<div class="overflow-hidden w-full">
|
||||||
id="models"
|
<div class="mr-0.5 max-w-full">
|
||||||
class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400"
|
<Selector
|
||||||
bind:value={selectedModel}
|
placeholder={$i18n.t('Select a model')}
|
||||||
{disabled}
|
items={$models
|
||||||
>
|
.filter((model) => model.name !== 'hr')
|
||||||
<option class=" text-gray-700" value="" selected disabled
|
.map((model) => ({
|
||||||
>{$i18n.t('Select a model')}</option
|
value: model.id,
|
||||||
>
|
label: model.name,
|
||||||
|
info: model
|
||||||
|
}))}
|
||||||
|
bind:value={selectedModel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#each $models as model}
|
{#if selectedModelIdx === 0}
|
||||||
{#if model.name === 'hr'}
|
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
|
||||||
<hr />
|
<Tooltip content="Add Model">
|
||||||
{:else}
|
<button
|
||||||
<option value={model.id} class="text-gray-700 text-lg"
|
class=" "
|
||||||
>{model.name +
|
{disabled}
|
||||||
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
on:click={() => {
|
||||||
|
selectedModels = [...selectedModels, ''];
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/if}
|
<svg
|
||||||
{/each}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</select>
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
{#if selectedModelIdx === 0}
|
stroke-width="1.5"
|
||||||
<button
|
stroke="currentColor"
|
||||||
class=" self-center {selectedModelIdx === 0
|
class="w-4 h-4"
|
||||||
? 'mr-3'
|
>
|
||||||
: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
||||||
{disabled}
|
</svg>
|
||||||
on:click={() => {
|
</button>
|
||||||
selectedModels = [...selectedModels, ''];
|
</Tooltip>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<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="M12 6v12m6-6H6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
|
||||||
class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx ===
|
<Tooltip content="Remove Model">
|
||||||
0
|
<button
|
||||||
? 'mr-3'
|
{disabled}
|
||||||
: 'mr-7'}"
|
on:click={() => {
|
||||||
{disabled}
|
selectedModels.splice(selectedModelIdx, 1);
|
||||||
on:click={() => {
|
selectedModels = selectedModels;
|
||||||
selectedModels.splice(selectedModelIdx, 1);
|
}}
|
||||||
selectedModels = selectedModels;
|
>
|
||||||
}}
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
stroke-width="1.5"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
stroke-width="1.5"
|
class="w-4 h-4"
|
||||||
stroke="currentColor"
|
>
|
||||||
class="w-4 h-4"
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||||
>
|
</svg>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
</button>
|
||||||
</svg>
|
</Tooltip>
|
||||||
</button>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedModelIdx === 0}
|
|
||||||
<button
|
|
||||||
class=" self-center dark:hover:text-gray-300"
|
|
||||||
id="open-settings-button"
|
|
||||||
on:click={async () => {
|
|
||||||
await showSettings.set(!$showSettings);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-left mt-1.5 text-xs text-gray-500">
|
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
||||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select } from 'bits-ui';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||||
|
|
||||||
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
import Check from '$lib/components/icons/Check.svelte';
|
||||||
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
|
|
||||||
|
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||||
|
|
||||||
|
import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
export let placeholder = 'Select a model';
|
||||||
|
export let searchEnabled = true;
|
||||||
|
export let searchPlaceholder = 'Search a model';
|
||||||
|
|
||||||
|
export let items = [{ value: 'mango', label: 'Mango' }];
|
||||||
|
|
||||||
|
let searchValue = '';
|
||||||
|
let ollamaVersion = null;
|
||||||
|
|
||||||
|
$: filteredItems = searchValue
|
||||||
|
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
|
||||||
|
: items;
|
||||||
|
|
||||||
|
const pullModelHandler = async () => {
|
||||||
|
const sanitizedModelTag = searchValue.trim();
|
||||||
|
|
||||||
|
console.log($MODEL_DOWNLOAD_POOL);
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
||||||
|
modelTag: sanitizedModelTag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const reader = res.body
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(splitStream('\n'))
|
||||||
|
.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
let lines = value.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line !== '') {
|
||||||
|
let data = JSON.parse(line);
|
||||||
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
throw data.error;
|
||||||
|
}
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
requestId: data.id,
|
||||||
|
reader,
|
||||||
|
done: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status) {
|
||||||
|
if (data.digest) {
|
||||||
|
let downloadProgress = 0;
|
||||||
|
if (data.completed) {
|
||||||
|
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
|
||||||
|
} else {
|
||||||
|
downloadProgress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
pullProgress: downloadProgress,
|
||||||
|
digest: data.digest
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success(data.status);
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
done: data.status === 'success'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
if (typeof error !== 'string') {
|
||||||
|
error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error);
|
||||||
|
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
|
||||||
|
toast.success(
|
||||||
|
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
|
||||||
|
modelName: sanitizedModelTag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
} else {
|
||||||
|
toast.error('Download canceled');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancelModelPullHandler = async (model: string) => {
|
||||||
|
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model];
|
||||||
|
if (reader) {
|
||||||
|
await reader.cancel();
|
||||||
|
|
||||||
|
await cancelOllamaRequest(localStorage.token, requestId);
|
||||||
|
delete $MODEL_DOWNLOAD_POOL[model];
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL
|
||||||
|
});
|
||||||
|
await deleteModel(localStorage.token, model);
|
||||||
|
toast.success(`${model} download has been canceled`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select.Root
|
||||||
|
{items}
|
||||||
|
onOpenChange={async () => {
|
||||||
|
searchValue = '';
|
||||||
|
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||||
|
}}
|
||||||
|
selected={items.find((item) => item.value === value) ?? ''}
|
||||||
|
onSelectedChange={(selectedItem) => {
|
||||||
|
value = selectedItem.value;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="relative w-full" aria-label={placeholder}>
|
||||||
|
<Select.Value
|
||||||
|
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
||||||
|
{placeholder}
|
||||||
|
/>
|
||||||
|
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content
|
||||||
|
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
||||||
|
transition={flyAndScale}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
{#if searchEnabled}
|
||||||
|
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
|
||||||
|
<Search className="size-4" strokeWidth="2.5" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="model-search-input"
|
||||||
|
bind:value={searchValue}
|
||||||
|
class="w-full text-sm bg-transparent outline-none"
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="px-3 my-2 max-h-72 overflow-y-auto">
|
||||||
|
{#each filteredItems as item}
|
||||||
|
<Select.Item
|
||||||
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
value={item.value}
|
||||||
|
label={item.label}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
{item.label}
|
||||||
|
|
||||||
|
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||||
|
>{item.info?.details?.parameter_size ?? ''}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- {JSON.stringify(item.info)} -->
|
||||||
|
|
||||||
|
{#if item.info.external}
|
||||||
|
<Tooltip content={item.info?.source ?? 'External'}>
|
||||||
|
<div class=" mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-3"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
<Tooltip
|
||||||
|
content={`${
|
||||||
|
item.info?.details?.quantization_level
|
||||||
|
? item.info?.details?.quantization_level + ' '
|
||||||
|
: ''
|
||||||
|
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
|
||||||
|
>
|
||||||
|
<div class=" mr-2">
|
||||||
|
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if value === item.value}
|
||||||
|
<div class="ml-auto">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Select.Item>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
|
||||||
|
<button
|
||||||
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
on:click={() => {
|
||||||
|
pullModelHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pull "{searchValue}" from Ollama.com
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||||
|
<div
|
||||||
|
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
||||||
|
<svg
|
||||||
|
class="size-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>
|
||||||
|
|
||||||
|
<div class="flex flex-col self-start">
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
|
||||||
|
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
|
||||||
|
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
|
||||||
|
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mr-2 translate-y-0.5">
|
||||||
|
<Tooltip content="Cancel">
|
||||||
|
<button
|
||||||
|
class="text-gray-800 dark:text-gray-100"
|
||||||
|
on:click={() => {
|
||||||
|
cancelModelPullHandler(model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-800 dark:text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18 17.94 6M18 18 6.06 6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
import { compareVersion } from '$lib/utils';
|
import { compareVersion } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let ollamaVersion = '';
|
let ollamaVersion = '';
|
||||||
@@ -51,8 +53,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||||
<div>
|
<div class="flex gap-1">
|
||||||
v{WEBUI_VERSION}
|
<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}>
|
||||||
|
v{WEBUI_VERSION}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||||
@@ -126,7 +130,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('Created by')}
|
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||||
|
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
|
||||||
|
{/if}{$i18n.t('Created by')}
|
||||||
<a
|
<a
|
||||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||||
href="https://github.com/tjbck"
|
href="https://github.com/tjbck"
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
import { updateUserProfile } from '$lib/apis/auths';
|
import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
|
||||||
|
|
||||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||||
import { getGravatarUrl } from '$lib/apis/utils';
|
import { getGravatarUrl } from '$lib/apis/utils';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -15,8 +17,14 @@
|
|||||||
|
|
||||||
let profileImageUrl = '';
|
let profileImageUrl = '';
|
||||||
let name = '';
|
let name = '';
|
||||||
|
|
||||||
let showJWTToken = false;
|
let showJWTToken = false;
|
||||||
let JWTTokenCopied = false;
|
let JWTTokenCopied = false;
|
||||||
|
|
||||||
|
let APIKey = '';
|
||||||
|
let showAPIKey = false;
|
||||||
|
let APIKeyCopied = false;
|
||||||
|
|
||||||
let profileImageInputElement: HTMLInputElement;
|
let profileImageInputElement: HTMLInputElement;
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
@@ -33,9 +41,23 @@
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
const createAPIKeyHandler = async () => {
|
||||||
|
APIKey = await createAPIKey(localStorage.token);
|
||||||
|
if (APIKey) {
|
||||||
|
toast.success($i18n.t('API Key created.'));
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to create API Key.'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
name = $user.name;
|
name = $user.name;
|
||||||
profileImageUrl = $user.profile_image_url;
|
profileImageUrl = $user.profile_image_url;
|
||||||
|
|
||||||
|
APIKey = await getAPIKey(localStorage.token).catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -170,41 +192,83 @@
|
|||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-4" />
|
<hr class=" dark:border-gray-700 my-4" />
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex w-full justify-between">
|
<div class="justify-between w-full">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
<div class="flex justify-between w-full">
|
||||||
</div>
|
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2">
|
<div class="flex mt-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||||
type={showJWTToken ? 'text' : 'password'}
|
type={showJWTToken ? 'text' : 'password'}
|
||||||
value={localStorage.token}
|
value={localStorage.token}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
|
||||||
|
on:click={() => {
|
||||||
|
showJWTToken = !showJWTToken;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if showJWTToken}
|
||||||
|
<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.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="dark:bg-gray-800 px-2 transition rounded-r-lg"
|
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showJWTToken = !showJWTToken;
|
copyToClipboard(localStorage.token);
|
||||||
|
JWTTokenCopied = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
JWTTokenCopied = false;
|
||||||
|
}, 2000);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if showJWTToken}
|
{#if JWTTokenCopied}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
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"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
<path
|
|
||||||
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
@@ -213,60 +277,156 @@
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justify-between w-full">
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="flex mt-2">
|
||||||
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
|
{#if APIKey}
|
||||||
on:click={() => {
|
<div class="flex w-full">
|
||||||
copyToClipboard(localStorage.token);
|
<input
|
||||||
JWTTokenCopied = true;
|
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||||
setTimeout(() => {
|
type={showAPIKey ? 'text' : 'password'}
|
||||||
JWTTokenCopied = false;
|
value={APIKey}
|
||||||
}, 2000);
|
disabled
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if JWTTokenCopied}
|
|
||||||
<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
|
||||||
|
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
|
||||||
|
on:click={() => {
|
||||||
|
showAPIKey = !showAPIKey;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if showAPIKey}
|
||||||
|
<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.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
copyToClipboard(APIKey);
|
||||||
|
APIKeyCopied = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
APIKeyCopied = false;
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if APIKeyCopied}
|
||||||
|
<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>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Tooltip content="Create new key">
|
||||||
|
<button
|
||||||
|
class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
createAPIKeyHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<button
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition"
|
||||||
viewBox="0 0 16 16"
|
on:click={() => {
|
||||||
fill="currentColor"
|
createAPIKeyHandler();
|
||||||
class="w-4 h-4"
|
}}
|
||||||
|
>
|
||||||
|
<Plus strokeWidth="2" className=" size-3.5" />
|
||||||
|
|
||||||
|
Create new secret key</button
|
||||||
>
|
>
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { resetVectorDB } from '$lib/apis/rag';
|
|
||||||
import { chats, user } from '$lib/stores';
|
import { chats, user } from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -330,38 +329,6 @@
|
|||||||
{$i18n.t('Export All Chats (All Users)')}
|
{$i18n.t('Export All Chats (All Users)')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
|
||||||
|
|
||||||
<button
|
|
||||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
|
||||||
on:click={() => {
|
|
||||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
|
||||||
toast.error(error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
toast.success($i18n.t('Success'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
|
||||||
// General
|
// General
|
||||||
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark'];
|
||||||
let selectedTheme = 'system';
|
let selectedTheme = 'system';
|
||||||
|
|
||||||
let languages = [];
|
let languages = [];
|
||||||
@@ -91,12 +91,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const applyTheme = (_theme: string) => {
|
const applyTheme = (_theme: string) => {
|
||||||
let themeToApply = _theme;
|
let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme;
|
||||||
|
|
||||||
if (_theme === 'system') {
|
if (_theme === 'system') {
|
||||||
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (themeToApply === 'dark' && !_theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-900', '#171717');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
|
||||||
|
}
|
||||||
|
|
||||||
themes
|
themes
|
||||||
.filter((e) => e !== themeToApply)
|
.filter((e) => e !== themeToApply)
|
||||||
.forEach((e) => {
|
.forEach((e) => {
|
||||||
@@ -115,7 +120,11 @@
|
|||||||
const themeChangeHandler = (_theme: string) => {
|
const themeChangeHandler = (_theme: string) => {
|
||||||
theme.set(_theme);
|
theme.set(_theme);
|
||||||
localStorage.setItem('theme', _theme);
|
localStorage.setItem('theme', _theme);
|
||||||
|
if (_theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
applyTheme(_theme);
|
applyTheme(_theme);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -136,6 +145,7 @@
|
|||||||
>
|
>
|
||||||
<option value="system">⚙️ {$i18n.t('System')}</option>
|
<option value="system">⚙️ {$i18n.t('System')}</option>
|
||||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
<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="light">☀️ {$i18n.t('Light')}</option>
|
||||||
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
||||||
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { config, user } from '$lib/stores';
|
import { config, user } from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
getAUTOMATIC1111Url,
|
|
||||||
getImageGenerationModels,
|
getImageGenerationModels,
|
||||||
getDefaultImageGenerationModel,
|
getDefaultImageGenerationModel,
|
||||||
updateDefaultImageGenerationModel,
|
updateDefaultImageGenerationModel,
|
||||||
getImageSize,
|
getImageSize,
|
||||||
getImageGenerationConfig,
|
getImageGenerationConfig,
|
||||||
updateImageGenerationConfig,
|
updateImageGenerationConfig,
|
||||||
updateAUTOMATIC1111Url,
|
getImageGenerationEngineUrls,
|
||||||
|
updateImageGenerationEngineUrls,
|
||||||
updateImageSize,
|
updateImageSize,
|
||||||
getImageSteps,
|
getImageSteps,
|
||||||
updateImageSteps,
|
updateImageSteps,
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
let enableImageGeneration = false;
|
let enableImageGeneration = false;
|
||||||
|
|
||||||
let AUTOMATIC1111_BASE_URL = '';
|
let AUTOMATIC1111_BASE_URL = '';
|
||||||
|
let COMFYUI_BASE_URL = '';
|
||||||
|
|
||||||
let OPENAI_API_KEY = '';
|
let OPENAI_API_KEY = '';
|
||||||
|
|
||||||
let selectedModel = '';
|
let selectedModel = '';
|
||||||
@@ -49,24 +51,47 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAUTOMATIC1111UrlHandler = async () => {
|
const updateUrlHandler = async () => {
|
||||||
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
|
if (imageGenerationEngine === 'comfyui') {
|
||||||
(error) => {
|
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
||||||
|
COMFYUI_BASE_URL: COMFYUI_BASE_URL
|
||||||
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
|
|
||||||
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
AUTOMATIC1111_BASE_URL = res;
|
COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
|
||||||
|
|
||||||
await getModels();
|
await getModels();
|
||||||
|
|
||||||
if (models) {
|
if (models) {
|
||||||
toast.success($i18n.t('Server connection verified'));
|
toast.success($i18n.t('Server connection verified'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
||||||
|
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
|
||||||
|
|
||||||
|
await getModels();
|
||||||
|
|
||||||
|
if (models) {
|
||||||
|
toast.success($i18n.t('Server connection verified'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({ AUTOMATIC1111_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const updateImageGeneration = async () => {
|
const updateImageGeneration = async () => {
|
||||||
@@ -101,7 +126,11 @@
|
|||||||
imageGenerationEngine = res.engine;
|
imageGenerationEngine = res.engine;
|
||||||
enableImageGeneration = res.enabled;
|
enableImageGeneration = res.enabled;
|
||||||
}
|
}
|
||||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
const URLS = await getImageGenerationEngineUrls(localStorage.token);
|
||||||
|
|
||||||
|
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
||||||
|
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
||||||
|
|
||||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||||
|
|
||||||
imageSize = await getImageSize(localStorage.token);
|
imageSize = await getImageSize(localStorage.token);
|
||||||
@@ -154,6 +183,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
|
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
|
||||||
|
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||||
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
|
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,6 +201,9 @@
|
|||||||
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
||||||
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
||||||
enableImageGeneration = false;
|
enableImageGeneration = false;
|
||||||
|
} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
|
||||||
|
toast.error($i18n.t('ComfyUI Base URL is required.'));
|
||||||
|
enableImageGeneration = false;
|
||||||
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
|
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
|
||||||
toast.error($i18n.t('OpenAI API Key is required.'));
|
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||||
enableImageGeneration = false;
|
enableImageGeneration = false;
|
||||||
@@ -204,12 +237,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded-lg transition"
|
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"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
// updateOllamaAPIUrlHandler();
|
updateUrlHandler();
|
||||||
|
|
||||||
updateAUTOMATIC1111UrlHandler();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -237,6 +268,37 @@
|
|||||||
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if imageGenerationEngine === 'comfyui'}
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
|
bind:value={COMFYUI_BASE_URL}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
updateUrlHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{:else if imageGenerationEngine === 'openai'}
|
{:else if imageGenerationEngine === 'openai'}
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
@@ -261,6 +323,7 @@
|
|||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
required
|
||||||
>
|
>
|
||||||
{#if !selectedModel}
|
{#if !selectedModel}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||||
import { config, models, user } from '$lib/stores';
|
import { config, models, settings, user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
let titleAutoGenerate = true;
|
let titleAutoGenerate = true;
|
||||||
let responseAutoCopy = false;
|
let responseAutoCopy = false;
|
||||||
let titleAutoGenerateModel = '';
|
let titleAutoGenerateModel = '';
|
||||||
|
let titleAutoGenerateModelExternal = '';
|
||||||
let fullScreenMode = false;
|
let fullScreenMode = false;
|
||||||
let titleGenerationPrompt = '';
|
let titleGenerationPrompt = '';
|
||||||
|
|
||||||
@@ -33,7 +34,12 @@
|
|||||||
|
|
||||||
const toggleTitleAutoGenerate = async () => {
|
const toggleTitleAutoGenerate = async () => {
|
||||||
titleAutoGenerate = !titleAutoGenerate;
|
titleAutoGenerate = !titleAutoGenerate;
|
||||||
saveSettings({ titleAutoGenerate: titleAutoGenerate });
|
saveSettings({
|
||||||
|
title: {
|
||||||
|
...$settings.title,
|
||||||
|
auto: titleAutoGenerate
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleResponseAutoCopy = async () => {
|
const toggleResponseAutoCopy = async () => {
|
||||||
@@ -65,8 +71,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveSettings({
|
saveSettings({
|
||||||
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
title: {
|
||||||
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
...$settings.title,
|
||||||
|
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
||||||
|
modelExternal:
|
||||||
|
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
|
||||||
|
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,16 +88,18 @@
|
|||||||
|
|
||||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||||
|
|
||||||
titleAutoGenerate = settings.titleAutoGenerate ?? true;
|
titleAutoGenerate = settings?.title?.auto ?? true;
|
||||||
responseAutoCopy = settings.responseAutoCopy ?? false;
|
titleAutoGenerateModel = settings?.title?.model ?? '';
|
||||||
showUsername = settings.showUsername ?? false;
|
titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
|
||||||
fullScreenMode = settings.fullScreenMode ?? false;
|
|
||||||
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
|
||||||
titleGenerationPrompt =
|
titleGenerationPrompt =
|
||||||
settings.titleGenerationPrompt ??
|
settings?.title?.prompt ??
|
||||||
$i18n.t(
|
$i18n.t(
|
||||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
||||||
) + ' {{prompt}}';
|
) + ' {{prompt}}';
|
||||||
|
|
||||||
|
responseAutoCopy = settings.responseAutoCopy ?? false;
|
||||||
|
showUsername = settings.showUsername ?? false;
|
||||||
|
fullScreenMode = settings.fullScreenMode ?? false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -190,8 +203,9 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-2 pr-2">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1">
|
||||||
|
<div class=" text-xs mb-1">Local Models</div>
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={titleAutoGenerateModel}
|
bind:value={titleAutoGenerateModel}
|
||||||
@@ -207,6 +221,24 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class=" text-xs mb-1">External Models</div>
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={titleAutoGenerateModelExternal}
|
||||||
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
>
|
||||||
|
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||||
|
{#each $models as model}
|
||||||
|
{#if model.name !== 'hr'}
|
||||||
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 mr-2">
|
<div class="mt-3 mr-2">
|
||||||
|
|||||||
@@ -5,9 +5,12 @@
|
|||||||
import {
|
import {
|
||||||
createModel,
|
createModel,
|
||||||
deleteModel,
|
deleteModel,
|
||||||
|
downloadModel,
|
||||||
getOllamaUrls,
|
getOllamaUrls,
|
||||||
getOllamaVersion,
|
getOllamaVersion,
|
||||||
pullModel
|
pullModel,
|
||||||
|
cancelOllamaRequest,
|
||||||
|
uploadModel
|
||||||
} from '$lib/apis/ollama';
|
} from '$lib/apis/ollama';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { WEBUI_NAME, models, user } from '$lib/stores';
|
import { WEBUI_NAME, models, user } from '$lib/stores';
|
||||||
@@ -60,11 +63,13 @@
|
|||||||
let pullProgress = null;
|
let pullProgress = null;
|
||||||
|
|
||||||
let modelUploadMode = 'file';
|
let modelUploadMode = 'file';
|
||||||
let modelInputFile = '';
|
let modelInputFile: File[] | null = null;
|
||||||
let modelFileUrl = '';
|
let modelFileUrl = '';
|
||||||
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
|
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
|
||||||
let modelFileDigest = '';
|
let modelFileDigest = '';
|
||||||
|
|
||||||
let uploadProgress = null;
|
let uploadProgress = null;
|
||||||
|
let uploadMessage = '';
|
||||||
|
|
||||||
let deleteModelTag = '';
|
let deleteModelTag = '';
|
||||||
|
|
||||||
@@ -159,7 +164,7 @@
|
|||||||
// Remove the downloaded model
|
// Remove the downloaded model
|
||||||
delete modelDownloadStatus[modelName];
|
delete modelDownloadStatus[modelName];
|
||||||
|
|
||||||
console.log(data);
|
modelDownloadStatus = { ...modelDownloadStatus };
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
toast.error(data.error);
|
toast.error(data.error);
|
||||||
@@ -184,35 +189,32 @@
|
|||||||
|
|
||||||
const uploadModelHandler = async () => {
|
const uploadModelHandler = async () => {
|
||||||
modelTransferring = true;
|
modelTransferring = true;
|
||||||
uploadProgress = 0;
|
|
||||||
|
|
||||||
let uploaded = false;
|
let uploaded = false;
|
||||||
let fileResponse = null;
|
let fileResponse = null;
|
||||||
let name = '';
|
let name = '';
|
||||||
|
|
||||||
if (modelUploadMode === 'file') {
|
if (modelUploadMode === 'file') {
|
||||||
const file = modelInputFile[0];
|
const file = modelInputFile ? modelInputFile[0] : null;
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, {
|
if (file) {
|
||||||
method: 'POST',
|
uploadMessage = 'Uploading...';
|
||||||
headers: {
|
|
||||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
|
||||||
},
|
(error) => {
|
||||||
body: formData
|
toast.error(error);
|
||||||
}).catch((error) => {
|
return null;
|
||||||
console.log(error);
|
}
|
||||||
return null;
|
);
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, {
|
uploadProgress = 0;
|
||||||
method: 'GET',
|
fileResponse = await downloadModel(
|
||||||
headers: {
|
localStorage.token,
|
||||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
modelFileUrl,
|
||||||
}
|
selectedOllamaUrlIdx
|
||||||
}).catch((error) => {
|
).catch((error) => {
|
||||||
console.log(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -235,6 +237,9 @@
|
|||||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||||
|
|
||||||
if (data.progress) {
|
if (data.progress) {
|
||||||
|
if (uploadMessage) {
|
||||||
|
uploadMessage = '';
|
||||||
|
}
|
||||||
uploadProgress = data.progress;
|
uploadProgress = data.progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +258,9 @@
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const error = await fileResponse?.json();
|
||||||
|
toast.error(error?.detail ?? error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploaded) {
|
if (uploaded) {
|
||||||
@@ -318,7 +326,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelFileUrl = '';
|
modelFileUrl = '';
|
||||||
modelInputFile = '';
|
|
||||||
|
if (modelUploadInputElement) {
|
||||||
|
modelUploadInputElement.value = '';
|
||||||
|
}
|
||||||
|
modelInputFile = null;
|
||||||
modelTransferring = false;
|
modelTransferring = false;
|
||||||
uploadProgress = null;
|
uploadProgress = null;
|
||||||
|
|
||||||
@@ -364,12 +376,24 @@
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line !== '') {
|
if (line !== '') {
|
||||||
let data = JSON.parse(line);
|
let data = JSON.parse(line);
|
||||||
|
console.log(data);
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw data.error;
|
throw data.error;
|
||||||
}
|
}
|
||||||
if (data.detail) {
|
if (data.detail) {
|
||||||
throw data.detail;
|
throw data.detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
modelDownloadStatus[opts.modelName] = {
|
||||||
|
...modelDownloadStatus[opts.modelName],
|
||||||
|
requestId: data.id,
|
||||||
|
reader,
|
||||||
|
done: false
|
||||||
|
};
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
if (data.digest) {
|
if (data.digest) {
|
||||||
let downloadProgress = 0;
|
let downloadProgress = 0;
|
||||||
@@ -379,11 +403,17 @@
|
|||||||
downloadProgress = 100;
|
downloadProgress = 100;
|
||||||
}
|
}
|
||||||
modelDownloadStatus[opts.modelName] = {
|
modelDownloadStatus[opts.modelName] = {
|
||||||
|
...modelDownloadStatus[opts.modelName],
|
||||||
pullProgress: downloadProgress,
|
pullProgress: downloadProgress,
|
||||||
digest: data.digest
|
digest: data.digest
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
toast.success(data.status);
|
toast.success(data.status);
|
||||||
|
|
||||||
|
modelDownloadStatus[opts.modelName] = {
|
||||||
|
...modelDownloadStatus[opts.modelName],
|
||||||
|
done: data.status === 'success'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,7 +426,14 @@
|
|||||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opts.callback({ success: true, modelName: opts.modelName });
|
|
||||||
|
console.log(modelDownloadStatus[opts.modelName]);
|
||||||
|
|
||||||
|
if (modelDownloadStatus[opts.modelName].done) {
|
||||||
|
opts.callback({ success: true, modelName: opts.modelName });
|
||||||
|
} else {
|
||||||
|
opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -466,6 +503,18 @@
|
|||||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||||
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cancelModelPullHandler = async (model: string) => {
|
||||||
|
const { reader, requestId } = modelDownloadStatus[model];
|
||||||
|
if (reader) {
|
||||||
|
await reader.cancel();
|
||||||
|
|
||||||
|
await cancelOllamaRequest(localStorage.token, requestId);
|
||||||
|
delete modelDownloadStatus[model];
|
||||||
|
await deleteModel(localStorage.token, model);
|
||||||
|
toast.success(`${model} download has been canceled`);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
@@ -596,20 +645,58 @@
|
|||||||
|
|
||||||
{#if Object.keys(modelDownloadStatus).length > 0}
|
{#if Object.keys(modelDownloadStatus).length > 0}
|
||||||
{#each Object.keys(modelDownloadStatus) as model}
|
{#each Object.keys(modelDownloadStatus) as model}
|
||||||
<div class="flex flex-col">
|
{#if 'pullProgress' in modelDownloadStatus[model]}
|
||||||
<div class="font-medium mb-1">{model}</div>
|
<div class="flex flex-col">
|
||||||
<div class="">
|
<div class="font-medium mb-1">{model}</div>
|
||||||
<div
|
<div class="">
|
||||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
<div class="flex flex-row justify-between space-x-4 pr-2">
|
||||||
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
|
<div class=" flex-1">
|
||||||
>
|
<div
|
||||||
{modelDownloadStatus[model].pullProgress ?? 0}%
|
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||||
</div>
|
style="width: {Math.max(
|
||||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
15,
|
||||||
{modelDownloadStatus[model].digest}
|
modelDownloadStatus[model].pullProgress ?? 0
|
||||||
|
)}%"
|
||||||
|
>
|
||||||
|
{modelDownloadStatus[model].pullProgress ?? 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tooltip content="Cancel">
|
||||||
|
<button
|
||||||
|
class="text-gray-800 dark:text-gray-100"
|
||||||
|
on:click={() => {
|
||||||
|
cancelModelPullHandler(model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-800 dark:text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18 17.94 6M18 18 6.06 6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{#if 'digest' in modelDownloadStatus[model]}
|
||||||
|
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||||
|
{modelDownloadStatus[model].digest}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -715,7 +802,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
|
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
modelUploadInputElement.click();
|
modelUploadInputElement.click();
|
||||||
}}
|
}}
|
||||||
@@ -730,7 +817,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||||
''
|
''
|
||||||
? 'mr-2'
|
? 'mr-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
@@ -745,7 +832,7 @@
|
|||||||
|
|
||||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||||
<button
|
<button
|
||||||
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
|
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 disabled:cursor-not-allowed transition"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={modelTransferring}
|
disabled={modelTransferring}
|
||||||
>
|
>
|
||||||
@@ -800,7 +887,7 @@
|
|||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={modelFileContent}
|
bind:value={modelFileContent}
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="6"
|
rows="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -815,7 +902,23 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if uploadProgress !== null}
|
{#if uploadMessage}
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
||||||
|
|
||||||
|
<div class="w-full rounded-full dark:bg-gray-800">
|
||||||
|
<div
|
||||||
|
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
{uploadMessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||||
|
{modelFileDigest}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if uploadProgress !== null}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,153 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
|
||||||
|
import { chatId, modelfiles } from '$lib/stores';
|
||||||
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import Link from '../icons/Link.svelte';
|
||||||
|
|
||||||
|
let chat = null;
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let downloadChat: Function;
|
const shareLocalChat = async () => {
|
||||||
export let shareChat: Function;
|
const _chat = chat;
|
||||||
|
|
||||||
|
const sharedChat = await shareChatById(localStorage.token, $chatId);
|
||||||
|
const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`;
|
||||||
|
|
||||||
|
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
|
||||||
|
copyToClipboard(chatShareUrl);
|
||||||
|
chat = await getChatById(localStorage.token, $chatId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareChat = async () => {
|
||||||
|
const _chat = chat.chat;
|
||||||
|
console.log('share', _chat);
|
||||||
|
|
||||||
|
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||||
|
const url = 'https://openwebui.com';
|
||||||
|
// const url = 'http://localhost:5173';
|
||||||
|
|
||||||
|
const tab = await window.open(`${url}/chats/upload`, '_blank');
|
||||||
|
window.addEventListener(
|
||||||
|
'message',
|
||||||
|
(event) => {
|
||||||
|
if (event.origin !== url) return;
|
||||||
|
if (event.data === 'loaded') {
|
||||||
|
tab.postMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
chat: _chat,
|
||||||
|
modelfiles: $modelfiles.filter((modelfile) =>
|
||||||
|
_chat.models.includes(modelfile.tagName)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
(async () => {
|
||||||
|
if ($chatId) {
|
||||||
|
chat = await getChatById(localStorage.token, $chatId);
|
||||||
|
} else {
|
||||||
|
chat = null;
|
||||||
|
console.log(chat);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show size="xs">
|
<Modal bind:show size="sm">
|
||||||
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
<div>
|
||||||
<button
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white"
|
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
shareChat();
|
|
||||||
show = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$i18n.t('Share to OpenWebUI Community')}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex justify-center space-x-1 mt-1.5">
|
|
||||||
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
class="self-center"
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
downloadChat();
|
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Download as a File')}
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<hr class=" dark:border-gray-800" />
|
||||||
|
|
||||||
|
{#if chat}
|
||||||
|
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||||
|
<div class=" text-sm dark:text-gray-300 mb-1">
|
||||||
|
{#if chat.share_id}
|
||||||
|
<a href="/s/{chat.share_id}" target="_blank"
|
||||||
|
>You have shared this chat <span class=" underline">before</span>.</a
|
||||||
|
>
|
||||||
|
Click here to
|
||||||
|
<button
|
||||||
|
class="underline"
|
||||||
|
on:click={async () => {
|
||||||
|
const res = await deleteSharedChatById(localStorage.token, $chatId);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
chat = await getChatById(localStorage.token, $chatId);
|
||||||
|
}
|
||||||
|
}}>delete this link</button
|
||||||
|
> and create a new shared link.
|
||||||
|
{:else}
|
||||||
|
Messages you send after creating your link won't be shared. Users with the URL will be
|
||||||
|
able to view the shared chat.
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="flex flex-col items-end space-x-1 mt-1.5">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button
|
||||||
|
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
shareChat();
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Share to OpenWebUI Community')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
shareLocalChat();
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link />
|
||||||
|
|
||||||
|
{#if chat.share_id}
|
||||||
|
{$i18n.t('Update and Copy Link')}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Copy Link')}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
|
import Tags from '../common/Tags.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let tags;
|
||||||
|
export let deleteTag: Function;
|
||||||
|
export let addTag: Function;
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:show size="xs">
|
||||||
|
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||||
|
<Tags {tags} {deleteTag} {addTag} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import { DropdownMenu } from 'bits-ui';
|
import { DropdownMenu } from 'bits-ui';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
transition={flyAndScale}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||||
<div class="flex items-center">Profile</div>
|
<div class="flex items-center">Profile</div>
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
export let show = false;
|
export let show = false;
|
||||||
export let src = '';
|
export let src = '';
|
||||||
export let alt = '';
|
export let alt = '';
|
||||||
|
|
||||||
|
const downloadImage = (url, filename) => {
|
||||||
|
fetch(url)
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = objectUrl;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error downloading image:', error));
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
@@ -35,10 +51,7 @@
|
|||||||
<button
|
<button
|
||||||
class=" p-5"
|
class=" p-5"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const a = document.createElement('a');
|
downloadImage(src, 'Image.png');
|
||||||
a.href = src;
|
|
||||||
a.download = 'Image.png';
|
|
||||||
a.click();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
export let show = true;
|
export let show = true;
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
|
|
||||||
@@ -41,10 +43,10 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=" modal-content m-auto rounded-2xl max-w-full {sizeToWidth(
|
class=" m-auto rounded-2xl max-w-full {sizeToWidth(
|
||||||
size
|
size
|
||||||
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
||||||
in:fade={{ duration: 10 }}
|
in:flyAndScale
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select } from 'bits-ui';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import Check from '../icons/Check.svelte';
|
||||||
|
import Search from '../icons/Search.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
export let placeholder = 'Select a model';
|
||||||
|
export let searchEnabled = true;
|
||||||
|
export let searchPlaceholder = 'Search a model';
|
||||||
|
|
||||||
|
export let items = [
|
||||||
|
{ value: 'mango', label: 'Mango' },
|
||||||
|
{ value: 'watermelon', label: 'Watermelon' },
|
||||||
|
{ value: 'apple', label: 'Apple' },
|
||||||
|
{ value: 'pineapple', label: 'Pineapple' },
|
||||||
|
{ value: 'orange', label: 'Orange' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let searchValue = '';
|
||||||
|
|
||||||
|
$: filteredItems = searchValue
|
||||||
|
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
|
||||||
|
: items;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select.Root
|
||||||
|
{items}
|
||||||
|
onOpenChange={() => {
|
||||||
|
searchValue = '';
|
||||||
|
}}
|
||||||
|
selected={items.find((item) => item.value === value)}
|
||||||
|
onSelectedChange={(selectedItem) => {
|
||||||
|
value = selectedItem.value;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="relative w-full" aria-label={placeholder}>
|
||||||
|
<Select.Value
|
||||||
|
class="inline-flex h-input px-0.5 w-full outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
||||||
|
{placeholder}
|
||||||
|
/>
|
||||||
|
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content
|
||||||
|
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
||||||
|
transition={flyAndScale}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
{#if searchEnabled}
|
||||||
|
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
|
||||||
|
<Search className="size-4" strokeWidth="2.5" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:value={searchValue}
|
||||||
|
class="w-full text-sm bg-transparent outline-none"
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="px-3 my-2 max-h-80 overflow-y-auto">
|
||||||
|
{#each filteredItems as item}
|
||||||
|
<Select.Item
|
||||||
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
value={item.value}
|
||||||
|
label={item.label}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
|
||||||
|
{#if value === item.value}
|
||||||
|
<div class="ml-auto">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Select.Item>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<div class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let className: string = 'text-white';
|
export let className: string = '';
|
||||||
export let theme: 'blue' | 'white' | 'black' = 'white';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-center text-center {className}">
|
<div class="flex justify-center text-center {className}">
|
||||||
<svg
|
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
|
||||||
class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue'
|
><style>
|
||||||
? 'text-sky-600'
|
.spinner_ajPY {
|
||||||
: theme === 'white'
|
transform-origin: center;
|
||||||
? 'text-white'
|
animation: spinner_AtaB 0.75s infinite linear;
|
||||||
: 'text-gray-600'} "
|
}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@keyframes spinner_AtaB {
|
||||||
fill="none"
|
100% {
|
||||||
viewBox="0 0 24 24"
|
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
|
||||||
>
|
>
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
||||||
<path
|
|
||||||
class="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
export let addTag: Function;
|
export let addTag: Function;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row space-x-0.5 line-clamp-1">
|
<div class="flex flex-row flex-wrap gap-0.5 line-clamp-1">
|
||||||
<TagList
|
<TagList
|
||||||
{tags}
|
{tags}
|
||||||
on:delete={(e) => {
|
on:delete={(e) => {
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TagInput
|
<TagInput
|
||||||
|
label={tags.length == 0 ? 'Add Tags' : ''}
|
||||||
on:add={(e) => {
|
on:add={(e) => {
|
||||||
addTag(e.detail);
|
addTag(e.detail);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, getContext } from 'svelte';
|
import { createEventDispatcher, getContext } from 'svelte';
|
||||||
|
import { tags } from '$lib/stores';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let label = '';
|
||||||
let showTagInput = false;
|
let showTagInput = false;
|
||||||
let tagName = '';
|
let tagName = '';
|
||||||
|
|
||||||
|
const addTagHandler = async () => {
|
||||||
|
tagName = tagName.trim();
|
||||||
|
if (tagName !== '') {
|
||||||
|
dispatch('add', tagName);
|
||||||
|
tagName = '';
|
||||||
|
showTagInput = false;
|
||||||
|
} else {
|
||||||
|
toast.error('Invalid Tag');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 pl-1.5">
|
<div class="flex space-x-1 pl-1.5">
|
||||||
{#if showTagInput}
|
{#if showTagInput}
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button
|
<button type="button" on:click={addTagHandler}>
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
dispatch('add', tagName);
|
|
||||||
tagName = '';
|
|
||||||
showTagInput = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -34,12 +41,21 @@
|
|||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
bind:value={tagName}
|
bind:value={tagName}
|
||||||
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[8rem]"
|
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]"
|
||||||
placeholder={$i18n.t('Add a tag')}
|
placeholder={$i18n.t('Add a tag')}
|
||||||
|
list="tagOptions"
|
||||||
|
on:keydown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
addTagHandler();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<datalist id="tagOptions">
|
||||||
|
{#each $tags as tag}
|
||||||
|
<option value={tag.name} />
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Tag Suggestions -->
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -62,4 +78,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if label && !showTagInput}
|
||||||
|
<span class="text-xs pl-1.5 self-center">{label}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
{#each tags as tag}
|
{#each tags as tag}
|
||||||
<div
|
<div
|
||||||
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-600 dark:text-white"
|
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white"
|
||||||
>
|
>
|
||||||
<div class=" text-[0.7rem] font-medium self-center line-clamp-1">
|
<div class=" text-[0.7rem] font-medium self-center line-clamp-1">
|
||||||
{tag.name}
|
{tag.name}
|
||||||
|
|||||||
@@ -29,6 +29,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={tooltipElement} aria-label={content}>
|
<div bind:this={tooltipElement} aria-label={content} class="flex">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
|
class="w-full text-sm font-medium py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadDocInputElement.click();
|
uploadDocInputElement.click();
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
updateRAGConfig,
|
updateRAGConfig,
|
||||||
getQuerySettings,
|
getQuerySettings,
|
||||||
scanDocs,
|
scanDocs,
|
||||||
updateQuerySettings
|
updateQuerySettings,
|
||||||
|
resetVectorDB
|
||||||
} from '$lib/apis/rag';
|
} from '$lib/apis/rag';
|
||||||
|
|
||||||
import { documents } from '$lib/stores';
|
import { documents } from '$lib/stores';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@@ -17,6 +19,8 @@
|
|||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
|
let showResetConfirm = false;
|
||||||
|
|
||||||
let chunkSize = 0;
|
let chunkSize = 0;
|
||||||
let chunkOverlap = 0;
|
let chunkOverlap = 0;
|
||||||
let pdfExtractImages = true;
|
let pdfExtractImages = true;
|
||||||
@@ -231,6 +235,100 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
|
{#if showResetConfirm}
|
||||||
|
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</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;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
|
on:click={() => {
|
||||||
|
showResetConfirm = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="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>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import fileSaver from 'file-saver';
|
|
||||||
const { saveAs } = fileSaver;
|
|
||||||
|
|
||||||
import { getChatById } from '$lib/apis/chats';
|
import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
|
||||||
import { WEBUI_NAME, chatId, modelfiles, settings } from '$lib/stores';
|
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
import ShareChatModal from '../chat/ShareChatModal.svelte';
|
import ShareChatModal from '../chat/ShareChatModal.svelte';
|
||||||
import TagInput from '../common/Tags/TagInput.svelte';
|
import ModelSelector from '../chat/ModelSelector.svelte';
|
||||||
import Tags from '../common/Tags.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
import Menu from './Navbar/Menu.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -16,126 +16,126 @@
|
|||||||
export let title: string = $WEBUI_NAME;
|
export let title: string = $WEBUI_NAME;
|
||||||
export let shareEnabled: boolean = false;
|
export let shareEnabled: boolean = false;
|
||||||
|
|
||||||
|
export let chat;
|
||||||
|
export let selectedModels;
|
||||||
|
|
||||||
export let tags = [];
|
export let tags = [];
|
||||||
export let addTag: Function;
|
export let addTag: Function;
|
||||||
export let deleteTag: Function;
|
export let deleteTag: Function;
|
||||||
|
|
||||||
|
export let showModelSelector = true;
|
||||||
|
|
||||||
let showShareChatModal = false;
|
let showShareChatModal = false;
|
||||||
|
let showDownloadChatModal = false;
|
||||||
let tagName = '';
|
|
||||||
let showTagInput = false;
|
|
||||||
|
|
||||||
const shareChat = async () => {
|
|
||||||
const chat = (await getChatById(localStorage.token, $chatId)).chat;
|
|
||||||
console.log('share', chat);
|
|
||||||
|
|
||||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
|
||||||
const url = 'https://openwebui.com';
|
|
||||||
// const url = 'http://localhost:5173';
|
|
||||||
|
|
||||||
const tab = await window.open(`${url}/chats/upload`, '_blank');
|
|
||||||
window.addEventListener(
|
|
||||||
'message',
|
|
||||||
(event) => {
|
|
||||||
if (event.origin !== url) return;
|
|
||||||
if (event.data === 'loaded') {
|
|
||||||
tab.postMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
chat: chat,
|
|
||||||
modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName))
|
|
||||||
}),
|
|
||||||
'*'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadChat = async () => {
|
|
||||||
const chat = (await getChatById(localStorage.token, $chatId)).chat;
|
|
||||||
console.log('download', chat);
|
|
||||||
|
|
||||||
const chatText = chat.messages.reduce((a, message, i, arr) => {
|
|
||||||
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
let blob = new Blob([chatText], {
|
|
||||||
type: 'text/plain'
|
|
||||||
});
|
|
||||||
|
|
||||||
saveAs(blob, `chat-${chat.title}.txt`);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} />
|
<ShareChatModal bind:show={showShareChatModal} />
|
||||||
<nav
|
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
|
||||||
id="nav"
|
|
||||||
class=" sticky py-2.5 top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-900/90 dark:text-gray-200 backdrop-blur-xl z-30"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class=" flex {$settings?.fullScreenMode ?? null
|
class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
|
||||||
? 'max-w-full'
|
w-full mx-auto px-3"
|
||||||
: 'max-w-3xl'} w-full mx-auto px-3"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center w-full max-w-full">
|
<div class="flex items-center w-full max-w-full">
|
||||||
<div class="pr-2 self-start">
|
<div class="flex-1 overflow-hidden max-w-full">
|
||||||
<button
|
{#if showModelSelector}
|
||||||
id="new-chat-button"
|
<ModelSelector bind:selectedModels />
|
||||||
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition"
|
{/if}
|
||||||
on:click={initNewChat}
|
</div>
|
||||||
>
|
|
||||||
<div class=" m-auto self-center">
|
<div class="self-start flex flex-none items-center">
|
||||||
<svg
|
<div class="flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
{#if !shareEnabled}
|
||||||
fill="currentColor"
|
<Tooltip content="Settings">
|
||||||
class="w-5 h-5"
|
<button
|
||||||
|
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
|
||||||
|
id="open-settings-button"
|
||||||
|
on:click={async () => {
|
||||||
|
await showSettings.set(!$showSettings);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
/>
|
fill="none"
|
||||||
<path
|
viewBox="0 0 24 24"
|
||||||
d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
|
stroke-width="1.5"
|
||||||
/>
|
stroke="currentColor"
|
||||||
</svg>
|
class="w-5 h-5"
|
||||||
</div>
|
>
|
||||||
</button>
|
<path
|
||||||
</div>
|
stroke-linecap="round"
|
||||||
<div class=" flex-1 self-center font-medium line-clamp-1">
|
stroke-linejoin="round"
|
||||||
<div>
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||||
{title != '' ? title : $WEBUI_NAME}
|
/>
|
||||||
</div>
|
<path
|
||||||
</div>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
<div class="pl-2 self-center flex items-center space-x-2">
|
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
{#if shareEnabled}
|
/>
|
||||||
<Tags {tags} {deleteTag} {addTag} />
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
</Tooltip>
|
||||||
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition border dark:border-gray-600"
|
{:else}
|
||||||
on:click={async () => {
|
<Menu
|
||||||
|
{chat}
|
||||||
|
{shareEnabled}
|
||||||
|
shareHandler={() => {
|
||||||
showShareChatModal = !showShareChatModal;
|
showShareChatModal = !showShareChatModal;
|
||||||
|
}}
|
||||||
// console.log(showShareChatModal);
|
downloadHandler={() => {
|
||||||
|
showDownloadChatModal = !showDownloadChatModal;
|
||||||
|
}}
|
||||||
|
{tags}
|
||||||
|
{deleteTag}
|
||||||
|
{addTag}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
|
||||||
|
>
|
||||||
|
<div class=" m-auto self-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Menu>
|
||||||
|
{/if}
|
||||||
|
<Tooltip content="New Chat">
|
||||||
|
<button
|
||||||
|
id="new-chat-button"
|
||||||
|
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
|
||||||
|
on:click={() => {
|
||||||
|
initNewChat();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" m-auto self-center">
|
<div class=" m-auto self-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-5 h-5"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
|
||||||
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
|
/>
|
||||||
clip-rule="evenodd"
|
<path
|
||||||
|
d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
|
||||||
|
import fileSaver from 'file-saver';
|
||||||
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
|
import { jsPDF } from 'jspdf';
|
||||||
|
|
||||||
|
import { showSettings } from '$lib/stores';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
|
import Tags from '$lib/components/common/Tags.svelte';
|
||||||
|
|
||||||
|
export let shareEnabled: boolean = false;
|
||||||
|
export let shareHandler: Function;
|
||||||
|
export let downloadHandler: Function;
|
||||||
|
|
||||||
|
// export let tagHandler: Function;
|
||||||
|
|
||||||
|
export let chat;
|
||||||
|
export let tags;
|
||||||
|
export let deleteTag: Function;
|
||||||
|
export let addTag: Function;
|
||||||
|
|
||||||
|
export let onClose: Function = () => {};
|
||||||
|
|
||||||
|
const downloadChatAsTxt = async () => {
|
||||||
|
const _chat = chat.chat;
|
||||||
|
console.log('download', chat);
|
||||||
|
|
||||||
|
const chatText = _chat.messages.reduce((a, message, i, arr) => {
|
||||||
|
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
let blob = new Blob([chatText], {
|
||||||
|
type: 'text/plain'
|
||||||
|
});
|
||||||
|
|
||||||
|
saveAs(blob, `chat-${_chat.title}.txt`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadChatAsPdf = async () => {
|
||||||
|
const _chat = chat.chat;
|
||||||
|
console.log('download', chat);
|
||||||
|
|
||||||
|
const doc = new jsPDF();
|
||||||
|
|
||||||
|
// Initialize y-coordinate for text placement
|
||||||
|
let yPos = 10;
|
||||||
|
const pageHeight = doc.internal.pageSize.height;
|
||||||
|
|
||||||
|
// Function to check if new text exceeds the current page height
|
||||||
|
function checkAndAddNewPage() {
|
||||||
|
if (yPos > pageHeight - 10) {
|
||||||
|
doc.addPage();
|
||||||
|
yPos = 10; // Reset yPos for the new page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to add text with specific style
|
||||||
|
function addStyledText(text, isTitle = false) {
|
||||||
|
// Set font style and size based on the parameters
|
||||||
|
doc.setFont('helvetica', isTitle ? 'bold' : 'normal');
|
||||||
|
doc.setFontSize(isTitle ? 12 : 10);
|
||||||
|
|
||||||
|
const textMargin = 7;
|
||||||
|
|
||||||
|
// Split text into lines to ensure it fits within the page width
|
||||||
|
const lines = doc.splitTextToSize(text, 180); // Adjust the width as needed
|
||||||
|
|
||||||
|
lines.forEach((line) => {
|
||||||
|
checkAndAddNewPage(); // Check if we need a new page before adding more text
|
||||||
|
doc.text(line, 10, yPos);
|
||||||
|
yPos += textMargin; // Increment yPos for the next line
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add extra space after a block of text
|
||||||
|
yPos += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
_chat.messages.forEach((message, i) => {
|
||||||
|
// Add user text in bold
|
||||||
|
doc.setFont('helvetica', 'normal', 'bold');
|
||||||
|
|
||||||
|
addStyledText(message.role.toUpperCase(), { isTitle: true });
|
||||||
|
addStyledText(message.content);
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.save(`chat-${_chat.title}.pdf`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
on:change={(e) => {
|
||||||
|
if (e.detail === false) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
|
||||||
|
sideOffset={8}
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||||
|
on:click={async () => {
|
||||||
|
await showSettings.set(!$showSettings);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="flex items-center">Settings</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
{#if shareEnabled}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||||
|
on:click={() => {
|
||||||
|
shareHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="flex items-center">Share</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<!-- <DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||||
|
on:click={() => {
|
||||||
|
downloadHandler();
|
||||||
|
}}
|
||||||
|
/> -->
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="flex items-center">Download</div>
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent
|
||||||
|
class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
|
||||||
|
transition={flyAndScale}
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||||
|
on:click={() => {
|
||||||
|
downloadChatAsTxt();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center line-clamp-1">Plain text (.txt)</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||||
|
on:click={() => {
|
||||||
|
downloadChatAsPdf();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center line-clamp-1">PDF document (.pdf)</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||||
|
|
||||||
|
<div class="flex p-1">
|
||||||
|
<Tags {tags} {deleteTag} {addTag} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||||
|
on:click={() => {
|
||||||
|
tagHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="flex items-center">Tag</div>
|
||||||
|
</DropdownMenu.Item> -->
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
@@ -20,10 +20,9 @@
|
|||||||
getAllChatTags
|
getAllChatTags
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { fade, slide } from 'svelte/transition';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import Dropdown from '../common/Dropdown.svelte';
|
|
||||||
import ChatMenu from './Sidebar/ChatMenu.svelte';
|
import ChatMenu from './Sidebar/ChatMenu.svelte';
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
@@ -108,7 +107,7 @@
|
|||||||
bind:this={navElement}
|
bind:this={navElement}
|
||||||
class="h-screen max-h-[100dvh] min-h-screen {show
|
class="h-screen max-h-[100dvh] min-h-screen {show
|
||||||
? 'lg:relative w-[260px]'
|
? 'lg:relative w-[260px]'
|
||||||
: '-translate-x-[260px] w-[0px]'} bg-black text-gray-200 shadow-2xl text-sm transition z-40 fixed top-0 left-0
|
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
<div class="px-2 flex justify-center space-x-2">
|
<div class="px-2 flex justify-center space-x-2">
|
||||||
<a
|
<a
|
||||||
id="sidebar-new-chat-button"
|
id="sidebar-new-chat-button"
|
||||||
class="flex-grow flex justify-between rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
|
class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
|
||||||
href="/"
|
href="/"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
selectedChatId = null;
|
selectedChatId = null;
|
||||||
@@ -135,7 +134,7 @@
|
|||||||
<div class="self-center mr-1.5">
|
<div class="self-center mr-1.5">
|
||||||
<img
|
<img
|
||||||
src="{WEBUI_BASE_URL}/static/favicon.png"
|
src="{WEBUI_BASE_URL}/static/favicon.png"
|
||||||
class=" w-7 -translate-x-1.5 rounded-full"
|
class=" size-6 -translate-x-1.5 rounded-full"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,7 +163,7 @@
|
|||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin'}
|
||||||
<div class="px-2 flex justify-center mt-0.5">
|
<div class="px-2 flex justify-center mt-0.5">
|
||||||
<a
|
<a
|
||||||
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
|
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
|
||||||
href="/modelfiles"
|
href="/modelfiles"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedChatId = null;
|
selectedChatId = null;
|
||||||
@@ -176,7 +175,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
@@ -196,7 +195,7 @@
|
|||||||
|
|
||||||
<div class="px-2 flex justify-center">
|
<div class="px-2 flex justify-center">
|
||||||
<a
|
<a
|
||||||
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
|
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
|
||||||
href="/prompts"
|
href="/prompts"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedChatId = null;
|
selectedChatId = null;
|
||||||
@@ -208,7 +207,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
@@ -228,7 +227,7 @@
|
|||||||
|
|
||||||
<div class="px-2 flex justify-center mb-1">
|
<div class="px-2 flex justify-center mb-1">
|
||||||
<a
|
<a
|
||||||
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
|
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
|
||||||
href="/documents"
|
href="/documents"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedChatId = null;
|
selectedChatId = null;
|
||||||
@@ -240,7 +239,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
@@ -261,7 +260,7 @@
|
|||||||
|
|
||||||
<div class="relative flex flex-col flex-1 overflow-y-auto">
|
<div class="relative flex flex-col flex-1 overflow-y-auto">
|
||||||
{#if !($settings.saveChatHistory ?? true)}
|
{#if !($settings.saveChatHistory ?? true)}
|
||||||
<div class="absolute z-40 w-full h-full bg-black/90 flex justify-center">
|
<div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center">
|
||||||
<div class=" text-left px-5 py-2">
|
<div class=" text-left px-5 py-2">
|
||||||
<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
|
<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
|
||||||
<div class="text-xs mt-2">
|
<div class="text-xs mt-2">
|
||||||
@@ -305,7 +304,7 @@
|
|||||||
|
|
||||||
<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
|
<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
|
||||||
<div class="flex w-full" id="chat-search">
|
<div class="flex w-full" id="chat-search">
|
||||||
<div class="self-center pl-3 py-2 rounded-l-xl bg-gray-950">
|
<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -321,7 +320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
|
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm dark:text-gray-300 dark:bg-gray-950 outline-none"
|
||||||
placeholder={$i18n.t('Search')}
|
placeholder={$i18n.t('Search')}
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
on:focus={() => {
|
on:focus={() => {
|
||||||
@@ -334,7 +333,7 @@
|
|||||||
{#if $tags.length > 0}
|
{#if $tags.length > 0}
|
||||||
<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
|
<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
|
||||||
<button
|
<button
|
||||||
class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full"
|
class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
}}
|
}}
|
||||||
@@ -343,7 +342,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{#each $tags as tag}
|
{#each $tags as tag}
|
||||||
<button
|
<button
|
||||||
class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full"
|
class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
|
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
|
||||||
if (chatIds.length === 0) {
|
if (chatIds.length === 0) {
|
||||||
@@ -385,10 +384,10 @@
|
|||||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
|
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
|
||||||
chat.id === chatTitleEditId ||
|
chat.id === chatTitleEditId ||
|
||||||
chat.id === chatDeleteId
|
chat.id === chatDeleteId
|
||||||
? 'bg-gray-900'
|
? 'bg-gray-300 dark:bg-gray-900'
|
||||||
: chat.id === selectedChatId
|
: chat.id === selectedChatId
|
||||||
? 'bg-gray-950'
|
? 'bg-gray-100 dark:bg-gray-950'
|
||||||
: 'group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
|
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
|
||||||
</div>
|
</div>
|
||||||
@@ -397,10 +396,10 @@
|
|||||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
|
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
|
||||||
chat.id === chatTitleEditId ||
|
chat.id === chatTitleEditId ||
|
||||||
chat.id === chatDeleteId
|
chat.id === chatDeleteId
|
||||||
? 'bg-gray-900'
|
? 'bg-gray-300 dark:bg-gray-900'
|
||||||
: chat.id === selectedChatId
|
: chat.id === selectedChatId
|
||||||
? 'bg-gray-950'
|
? 'bg-gray-100 dark:bg-gray-950'
|
||||||
: 'group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||||
href="/c/{chat.id}"
|
href="/c/{chat.id}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedChatId = chat.id;
|
selectedChatId = chat.id;
|
||||||
@@ -422,10 +421,10 @@
|
|||||||
class="
|
class="
|
||||||
|
|
||||||
{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
|
{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
|
||||||
? ' from-gray-900'
|
? 'from-gray-300 dark:from-gray-900'
|
||||||
: chat.id === selectedChatId
|
: chat.id === selectedChatId
|
||||||
? 'from-gray-950'
|
? 'from-gray-100 dark:from-gray-950'
|
||||||
: 'invisible group-hover:visible 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%
|
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
|
||||||
|
|
||||||
to-transparent"
|
to-transparent"
|
||||||
@@ -433,7 +432,7 @@
|
|||||||
{#if chatTitleEditId === chat.id}
|
{#if chatTitleEditId === chat.id}
|
||||||
<div class="flex self-center space-x-1.5 z-10">
|
<div class="flex self-center space-x-1.5 z-10">
|
||||||
<button
|
<button
|
||||||
class=" self-center hover:text-white transition"
|
class=" self-center dark:hover:text-white transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editChatTitle(chat.id, chatTitle);
|
editChatTitle(chat.id, chatTitle);
|
||||||
chatTitleEditId = null;
|
chatTitleEditId = null;
|
||||||
@@ -454,7 +453,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class=" self-center hover:text-white transition"
|
class=" self-center dark:hover:text-white transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
chatTitleEditId = null;
|
chatTitleEditId = null;
|
||||||
chatTitle = '';
|
chatTitle = '';
|
||||||
@@ -475,7 +474,7 @@
|
|||||||
{:else if chatDeleteId === chat.id}
|
{:else if chatDeleteId === chat.id}
|
||||||
<div class="flex self-center space-x-1.5 z-10">
|
<div class="flex self-center space-x-1.5 z-10">
|
||||||
<button
|
<button
|
||||||
class=" self-center hover:text-white transition"
|
class=" self-center dark:hover:text-white transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
deleteChat(chat.id);
|
deleteChat(chat.id);
|
||||||
}}
|
}}
|
||||||
@@ -494,7 +493,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class=" self-center hover:text-white transition"
|
class=" self-center dark:hover:text-white transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
chatDeleteId = null;
|
chatDeleteId = null;
|
||||||
}}
|
}}
|
||||||
@@ -527,7 +526,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Chat Menu"
|
aria-label="Chat Menu"
|
||||||
class=" self-center hover:text-white transition"
|
class=" self-center dark:hover:text-white transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedChatId = chat.id;
|
selectedChatId = chat.id;
|
||||||
}}
|
}}
|
||||||
@@ -558,7 +557,7 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if $user !== undefined}
|
{#if $user !== undefined}
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-900 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showDropdown = !showDropdown;
|
showDropdown = !showDropdown;
|
||||||
}}
|
}}
|
||||||
@@ -576,13 +575,13 @@
|
|||||||
{#if showDropdown}
|
{#if showDropdown}
|
||||||
<div
|
<div
|
||||||
id="dropdownDots"
|
id="dropdownDots"
|
||||||
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-gray-900"
|
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-white dark:bg-gray-900"
|
||||||
in:slide={{ duration: 150 }}
|
transition:fade|slide={{ duration: 100 }}
|
||||||
>
|
>
|
||||||
<div class="py-2 w-full">
|
<div class="py-2 w-full">
|
||||||
{#if $user.role === 'admin'}
|
{#if $user.role === 'admin'}
|
||||||
<button
|
<button
|
||||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
goto('/admin');
|
goto('/admin');
|
||||||
showDropdown = false;
|
showDropdown = false;
|
||||||
@@ -608,7 +607,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
goto('/playground');
|
goto('/playground');
|
||||||
showDropdown = false;
|
showDropdown = false;
|
||||||
@@ -635,7 +634,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await showSettings.set(true);
|
await showSettings.set(true);
|
||||||
showDropdown = false;
|
showDropdown = false;
|
||||||
@@ -666,11 +665,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-700 m-0 p-0" />
|
<hr class=" dark:border-gray-700 m-0 p-0" />
|
||||||
|
|
||||||
<div class="py-2 w-full">
|
<div class="py-2 w-full">
|
||||||
<button
|
<button
|
||||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
location.href = '/auth';
|
location.href = '/auth';
|
||||||
@@ -707,7 +706,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
|
class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -722,7 +721,7 @@
|
|||||||
}}
|
}}
|
||||||
><span class="" data-state="closed"
|
><span class="" data-state="closed"
|
||||||
><div
|
><div
|
||||||
class="flex h-[72px] w-8 items-center justify-center opacity-20 group-hover:opacity-100 transition"
|
class="flex h-[72px] w-8 items-center justify-center opacity-50 group-hover:opacity-100 transition"
|
||||||
>
|
>
|
||||||
<div class="flex h-6 w-6 flex-col items-center">
|
<div class="flex h-6 w-6 flex-col items-center">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu } from 'bits-ui';
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||||
@@ -25,10 +26,11 @@
|
|||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700/50 z-50 bg-gray-850 text-white"
|
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
transition={flyAndScale}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const SUPPORTED_FILE_TYPE = [
|
|||||||
'text/plain',
|
'text/plain',
|
||||||
'text/csv',
|
'text/csv',
|
||||||
'text/xml',
|
'text/xml',
|
||||||
|
'text/html',
|
||||||
'text/x-python',
|
'text/x-python',
|
||||||
'text/css',
|
'text/css',
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
@@ -50,6 +51,8 @@ export const SUPPORTED_FILE_EXTENSIONS = [
|
|||||||
'h',
|
'h',
|
||||||
'c',
|
'c',
|
||||||
'cs',
|
'cs',
|
||||||
|
'htm',
|
||||||
|
'html',
|
||||||
'sql',
|
'sql',
|
||||||
'log',
|
'log',
|
||||||
'ini',
|
'ini',
|
||||||
|
|||||||
+30
-24
@@ -37,30 +37,36 @@ const createIsLoadingStore = (i18n: i18nType) => {
|
|||||||
return isLoading;
|
return isLoading;
|
||||||
};
|
};
|
||||||
|
|
||||||
i18next
|
export const initI18n = (defaultLocale: string) => {
|
||||||
.use(
|
let detectionOrder = defaultLocale
|
||||||
resourcesToBackend(
|
? ['querystring', 'localStorage']
|
||||||
(language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)
|
: ['querystring', 'localStorage', 'navigator'];
|
||||||
)
|
let fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US'];
|
||||||
)
|
|
||||||
.use(LanguageDetector)
|
const loadResource = (language: string, namespace: string) =>
|
||||||
.init({
|
import(`./locales/${language}/${namespace}.json`);
|
||||||
debug: false,
|
|
||||||
detection: {
|
i18next
|
||||||
order: ['querystring', 'localStorage', 'navigator'],
|
.use(resourcesToBackend(loadResource))
|
||||||
caches: ['localStorage'],
|
.use(LanguageDetector)
|
||||||
lookupQuerystring: 'lang',
|
.init({
|
||||||
lookupLocalStorage: 'locale'
|
debug: false,
|
||||||
},
|
detection: {
|
||||||
fallbackLng: {
|
order: detectionOrder,
|
||||||
default: ['en-US']
|
caches: ['localStorage'],
|
||||||
},
|
lookupQuerystring: 'lang',
|
||||||
ns: 'translation',
|
lookupLocalStorage: 'locale'
|
||||||
returnEmptyString: false,
|
},
|
||||||
interpolation: {
|
fallbackLng: {
|
||||||
escapeValue: false // not needed for svelte as it escapes by default
|
default: fallbackDefaultLocale
|
||||||
}
|
},
|
||||||
});
|
ns: 'translation',
|
||||||
|
returnEmptyString: false,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false // not needed for svelte as it escapes by default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const i18n = createI18nStore(i18next);
|
const i18n = createI18nStore(i18next);
|
||||||
const isLoadingStore = createIsLoadingStore(i18next);
|
const isLoadingStore = createIsLoadingStore(i18next);
|
||||||
|
|||||||
@@ -0,0 +1,363 @@
|
|||||||
|
{
|
||||||
|
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'с', 'м', 'ч', 'д', 'с' или '-1' за неограничен срок.",
|
||||||
|
"(Beta)": "(Бета)",
|
||||||
|
"(e.g. `sh webui.sh --api`)": "(например `sh webui.sh --api`)",
|
||||||
|
"(latest)": "(последна)",
|
||||||
|
"{{modelName}} is thinking...": "{{modelName}} мисли ...",
|
||||||
|
"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
|
||||||
|
"a user": "потребител",
|
||||||
|
"About": "Относно",
|
||||||
|
"Account": "Акаунт",
|
||||||
|
"Action": "Действие",
|
||||||
|
"Add a model": "Добавяне на модел",
|
||||||
|
"Add a model tag name": "Добавяне на име на таг за модел",
|
||||||
|
"Add a short description about what this modelfile does": "Добавяне на кратко описание за това какво прави този модфайл",
|
||||||
|
"Add a short title for this prompt": "Добавяне на кратко заглавие за този промпт",
|
||||||
|
"Add a tag": "Добавяне на таг",
|
||||||
|
"Add Docs": "Добавяне на Документи",
|
||||||
|
"Add Files": "Добавяне на Файлове",
|
||||||
|
"Add message": "Добавяне на съобщение",
|
||||||
|
"add tags": "добавяне на тагове",
|
||||||
|
"Adjusting these settings will apply changes universally to all users.": "При промяна на тези настройки промените се прилагат за всички потребители.",
|
||||||
|
"admin": "админ",
|
||||||
|
"Admin Panel": "Панел на Администратор",
|
||||||
|
"Admin Settings": "Настройки на Администратор",
|
||||||
|
"Advanced Parameters": "Разширени Параметри",
|
||||||
|
"all": "всички",
|
||||||
|
"All Users": "Всички Потребители",
|
||||||
|
"Allow": "Позволи",
|
||||||
|
"Allow Chat Deletion": "Позволи Изтриване на Чат",
|
||||||
|
"alphanumeric characters and hyphens": "алфанумерични знаци и тире",
|
||||||
|
"Already have an account?": "Вече имате акаунт? ",
|
||||||
|
"an assistant": "асистент",
|
||||||
|
"and": "и",
|
||||||
|
"API Base URL": "API Базов URL",
|
||||||
|
"API Key": "API Ключ",
|
||||||
|
"API RPM": "API RPM",
|
||||||
|
"are allowed - Activate this command by typing": "са разрешени - Активирайте тази команда чрез въвеждане",
|
||||||
|
"Are you sure?": "Сигурни ли сте?",
|
||||||
|
"Audio": "Аудио",
|
||||||
|
"Auto-playback response": "Аувтоматично възпроизвеждане на Отговора",
|
||||||
|
"Auto-send input after 3 sec.": "Аувтоматично изпращане на входа след 3 сек.",
|
||||||
|
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Базов URL",
|
||||||
|
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Базов URL е задължителен.",
|
||||||
|
"available!": "наличен!",
|
||||||
|
"Back": "Назад",
|
||||||
|
"Builder Mode": "Режим на Създаване",
|
||||||
|
"Cancel": "Отказ",
|
||||||
|
"Categories": "Категории",
|
||||||
|
"Change Password": "Промяна на Парола",
|
||||||
|
"Chat": "Чат",
|
||||||
|
"Chat History": "Чат История",
|
||||||
|
"Chat History is off for this browser.": "Чат История е изключен за този браузър.",
|
||||||
|
"Chats": "Чатове",
|
||||||
|
"Check Again": "Проверете Още Веднъж",
|
||||||
|
"Check for updates": "Проверка за актуализации",
|
||||||
|
"Checking for updates...": "Проверка за актуализации...",
|
||||||
|
"Choose a model before saving...": "Изберете модел преди запазване...",
|
||||||
|
"Chunk Overlap": "Chunk Overlap",
|
||||||
|
"Chunk Params": "Chunk Params",
|
||||||
|
"Chunk Size": "Chunk Size",
|
||||||
|
"Click here for help.": "Натиснете тук за помощ.",
|
||||||
|
"Click here to check other modelfiles.": "Натиснете тук за проверка на други моделфайлове.",
|
||||||
|
"Click here to select": "Натиснете тук, за да изберете",
|
||||||
|
"Click here to select documents.": "Натиснете тук, за да изберете документи.",
|
||||||
|
"click here.": "натиснете тук.",
|
||||||
|
"Click on the user role button to change a user's role.": "Натиснете върху бутона за промяна на ролята на потребителя.",
|
||||||
|
"Close": "Затвори",
|
||||||
|
"Collection": "Колекция",
|
||||||
|
"Command": "Команда",
|
||||||
|
"Confirm Password": "Потвърди Парола",
|
||||||
|
"Connections": "Връзки",
|
||||||
|
"Content": "Съдържание",
|
||||||
|
"Context Length": "Дължина на Контекста",
|
||||||
|
"Conversation Mode": "Режим на Чат",
|
||||||
|
"Copy last code block": "Копиране на последен код блок",
|
||||||
|
"Copy last response": "Копиране на последен отговор",
|
||||||
|
"Copying to clipboard was successful!": "Копирането в клипборда беше успешно!",
|
||||||
|
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Създайте кратка фраза от 3-5 думи като заглавие за следващото запитване, като стриктно спазвате ограничението от 3-5 думи и избягвате използването на думата 'заглавие':",
|
||||||
|
"Create a modelfile": "Създаване на модфайл",
|
||||||
|
"Create Account": "Създаване на Акаунт",
|
||||||
|
"Created at": "Създадено на",
|
||||||
|
"Created by": "Създадено от",
|
||||||
|
"Current Model": "Текущ модел",
|
||||||
|
"Current Password": "Текуща Парола",
|
||||||
|
"Custom": "Персонализиран",
|
||||||
|
"Customize Ollama models for a specific purpose": "Персонализиране на Ollama моделите за конкретна цел",
|
||||||
|
"Dark": "Тъмен",
|
||||||
|
"Database": "База данни",
|
||||||
|
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
|
||||||
|
"Default": "По подразбиране",
|
||||||
|
"Default (Automatic1111)": "По подразбиране (Automatic1111)",
|
||||||
|
"Default (Web API)": "По подразбиране (Web API)",
|
||||||
|
"Default model updated": "Моделът по подразбиране е обновен",
|
||||||
|
"Default Prompt Suggestions": "Промпт Предложения по подразбиране",
|
||||||
|
"Default User Role": "Роля на потребителя по подразбиране",
|
||||||
|
"delete": "изтриване",
|
||||||
|
"Delete a model": "Изтриване на модел",
|
||||||
|
"Delete chat": "Изтриване на чат",
|
||||||
|
"Delete Chats": "Изтриване на Чатове",
|
||||||
|
"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}",
|
||||||
|
"Deleted {tagName}": "Изтрито {tagName}",
|
||||||
|
"Description": "Описание",
|
||||||
|
"Desktop Notifications": "Десктоп Известия",
|
||||||
|
"Disabled": "Деактивиран",
|
||||||
|
"Discover a modelfile": "Откриване на модфайл",
|
||||||
|
"Discover a prompt": "Откриване на промпт",
|
||||||
|
"Discover, download, and explore custom prompts": "Откриване, сваляне и преглед на персонализирани промптове",
|
||||||
|
"Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели",
|
||||||
|
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
|
||||||
|
"Document": "Документ",
|
||||||
|
"Document Settings": "Документ Настройки",
|
||||||
|
"Documents": "Документи",
|
||||||
|
"does not make any external connections, and your data stays securely on your locally hosted server.": "няма външни връзки, и вашите данни остават сигурни на локално назначен сървър.",
|
||||||
|
"Don't Allow": "Не Позволявай",
|
||||||
|
"Don't have an account?": "Нямате акаунт?",
|
||||||
|
"Download as a File": "Сваляне като Файл",
|
||||||
|
"Download Database": "Сваляне на база данни",
|
||||||
|
"Drop any files here to add to the conversation": "Пускане на файлове тук, за да ги добавите в чата",
|
||||||
|
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.",
|
||||||
|
"Edit Doc": "Редактиране на документ",
|
||||||
|
"Edit User": "Редактиране на потребител",
|
||||||
|
"Email": "Имейл",
|
||||||
|
"Enable Chat History": "Вклюване на Чат История",
|
||||||
|
"Enable New Sign Ups": "Вклюване на Нови Потребители",
|
||||||
|
"Enabled": "Включено",
|
||||||
|
"Enter {{role}} message here": "Въведете съобщение за {{role}} тук",
|
||||||
|
"Enter API Key": "Въведете API ключ",
|
||||||
|
"Enter Chunk Overlap": "Въведете Chunk Overlap",
|
||||||
|
"Enter Chunk Size": "Въведете Chunk Size",
|
||||||
|
"Enter Image Size (e.g. 512x512)": "Въведете размер на изображението (напр. 512x512)",
|
||||||
|
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Въведете LiteLLM API Base URL (litellm_params.api_base)",
|
||||||
|
"Enter LiteLLM API Key (litellm_params.api_key)": "Въведете LiteLLM API Key (litellm_params.api_key)",
|
||||||
|
"Enter LiteLLM API RPM (litellm_params.rpm)": "Въведете LiteLLM API RPM (litellm_params.rpm)",
|
||||||
|
"Enter LiteLLM Model (litellm_params.model)": "Въведете LiteLLM Model (litellm_params.model)",
|
||||||
|
"Enter Max Tokens (litellm_params.max_tokens)": "Въведете Max Tokens (litellm_params.max_tokens)",
|
||||||
|
"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
|
||||||
|
"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
|
||||||
|
"Enter stop sequence": "Въведете стоп последователност",
|
||||||
|
"Enter Top K": "Въведете Top K",
|
||||||
|
"Enter URL (e.g. http://127.0.0.1:7860/)": "Въведете URL (напр. http://127.0.0.1:7860/)",
|
||||||
|
"Enter Your Email": "Въведете имейл",
|
||||||
|
"Enter Your Full Name": "Въведете вашето пълно име",
|
||||||
|
"Enter Your Password": "Въведете вашата парола",
|
||||||
|
"Experimental": "Експериментално",
|
||||||
|
"Export All Chats (All Users)": "Експортване на всички чатове (За всички потребители)",
|
||||||
|
"Export Chats": "Експортване на чатове",
|
||||||
|
"Export Documents Mapping": "Експортване на документен мапинг",
|
||||||
|
"Export Modelfiles": "Експортване на модфайлове",
|
||||||
|
"Export Prompts": "Експортване на промптове",
|
||||||
|
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
|
||||||
|
"File Mode": "Файл Мод",
|
||||||
|
"File not found.": "Файл не е намерен.",
|
||||||
|
"Focus chat input": "Фокусиране на чат вход",
|
||||||
|
"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
|
||||||
|
"From (Base Model)": "От (Базов модел)",
|
||||||
|
"Full Screen Mode": "На Цял екран",
|
||||||
|
"General": "Основни",
|
||||||
|
"General Settings": "Основни Настройки",
|
||||||
|
"Hello, {{name}}": "Здравей, {{name}}",
|
||||||
|
"Hide": "Скрий",
|
||||||
|
"Hide Additional Params": "Скрий допълнителни параметри",
|
||||||
|
"How can I help you today?": "Как мога да ви помогна днес?",
|
||||||
|
"Image Generation (Experimental)": "Генерация на изображения (Експериментално)",
|
||||||
|
"Image Generation Engine": "Двигател за генериране на изображения",
|
||||||
|
"Image Settings": "Настройки на изображения",
|
||||||
|
"Images": "Изображения",
|
||||||
|
"Import Chats": "Импортване на чатове",
|
||||||
|
"Import Documents Mapping": "Импортване на документен мапинг",
|
||||||
|
"Import Modelfiles": "Импортване на модфайлове",
|
||||||
|
"Import Prompts": "Импортване на промптове",
|
||||||
|
"Include `--api` flag when running stable-diffusion-webui": "Включете флага `--api`, когато стартирате stable-diffusion-webui",
|
||||||
|
"Interface": "Интерфейс",
|
||||||
|
"join our Discord for help.": "свържете се с нашия Discord за помощ.",
|
||||||
|
"JSON": "JSON",
|
||||||
|
"JWT Expiration": "JWT Expiration",
|
||||||
|
"JWT Token": "JWT Token",
|
||||||
|
"Keep Alive": "Keep Alive",
|
||||||
|
"Keyboard shortcuts": "Клавиши за бърз достъп",
|
||||||
|
"Language": "Език",
|
||||||
|
"Light": "Светъл",
|
||||||
|
"Listening...": "Слушам...",
|
||||||
|
"LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.",
|
||||||
|
"Made by OpenWebUI Community": "Направено от OpenWebUI общността",
|
||||||
|
"Make sure to enclose them with": "Уверете се, че са заключени с",
|
||||||
|
"Manage LiteLLM Models": "Управление на LiteLLM Моделите",
|
||||||
|
"Manage Models": "Управление на Моделите",
|
||||||
|
"Manage Ollama Models": "Управление на Ollama Моделите",
|
||||||
|
"Max Tokens": "Max Tokens",
|
||||||
|
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модели могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.",
|
||||||
|
"Mirostat": "Mirostat",
|
||||||
|
"Mirostat Eta": "Mirostat Eta",
|
||||||
|
"Mirostat Tau": "Mirostat Tau",
|
||||||
|
"MMMM DD, YYYY": "MMMM DD, YYYY",
|
||||||
|
"Model '{{modelName}}' has been successfully downloaded.": "Моделът '{{modelName}}' беше успешно свален.",
|
||||||
|
"Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.",
|
||||||
|
"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
|
||||||
|
"Model {{modelName}} already exists.": "Моделът {{modelName}} вече съществува.",
|
||||||
|
"Model Name": "Име на модел",
|
||||||
|
"Model not selected": "Не е избран модел",
|
||||||
|
"Model Tag Name": "Име на таг на модел",
|
||||||
|
"Model Whitelisting": "Модел Whitelisting",
|
||||||
|
"Model(s) Whitelisted": "Модели Whitelisted",
|
||||||
|
"Modelfile": "Модфайл",
|
||||||
|
"Modelfile Advanced Settings": "Разширени настройки на модфайл",
|
||||||
|
"Modelfile Content": "Съдържание на модфайл",
|
||||||
|
"Modelfiles": "Модфайлове",
|
||||||
|
"Models": "Модели",
|
||||||
|
"My Documents": "Мои документи",
|
||||||
|
"My Modelfiles": "Мои модфайлове",
|
||||||
|
"My Prompts": "Мои промптове",
|
||||||
|
"Name": "Име",
|
||||||
|
"Name Tag": "Име Таг",
|
||||||
|
"Name your modelfile": "Име на модфайла",
|
||||||
|
"New Chat": "Нов чат",
|
||||||
|
"New Password": "Нова парола",
|
||||||
|
"Not sure what to add?": "Не сте сигурни, какво да добавите?",
|
||||||
|
"Not sure what to write? Switch to": "Не сте сигурни, какво да напишете? Превключете към",
|
||||||
|
"Off": "Изкл.",
|
||||||
|
"Okay, Let's Go!": "ОК, Нека започваме!",
|
||||||
|
"Ollama Base URL": "Ollama Базов URL",
|
||||||
|
"Ollama Version": "Ollama Версия",
|
||||||
|
"On": "Вкл.",
|
||||||
|
"Only": "Само",
|
||||||
|
"Only alphanumeric characters and hyphens are allowed in the command string.": "Само алфанумерични знаци и тире са разрешени в командния низ.",
|
||||||
|
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Упс! Задръжте! Файловете ви все още са в пещта за обработка. Готвим ги до съвършенство. Моля, бъдете търпеливи и ще ви уведомим, когато са готови.",
|
||||||
|
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
|
||||||
|
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
|
||||||
|
"Open": "Отвори",
|
||||||
|
"Open AI": "Open AI",
|
||||||
|
"Open AI (Dall-E)": "Open AI (Dall-E)",
|
||||||
|
"Open new chat": "Отвори нов чат",
|
||||||
|
"OpenAI API": "OpenAI API",
|
||||||
|
"OpenAI API Key": "OpenAI API ключ",
|
||||||
|
"OpenAI API Key is required.": "OpenAI API ключ е задължителен.",
|
||||||
|
"or": "или",
|
||||||
|
"Parameters": "Параметри",
|
||||||
|
"Password": "Парола",
|
||||||
|
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
|
||||||
|
"pending": "в очакване",
|
||||||
|
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
|
||||||
|
"Playground": "Плейграунд",
|
||||||
|
"Profile": "Профил",
|
||||||
|
"Prompt Content": "Съдържание на промпта",
|
||||||
|
"Prompt suggestions": "Промпт предложения",
|
||||||
|
"Prompts": "Промптове",
|
||||||
|
"Pull a model from Ollama.com": "Издърпайте модел от Ollama.com",
|
||||||
|
"Pull Progress": "Прогрес на издърпването",
|
||||||
|
"Query Params": "Query Параметри",
|
||||||
|
"RAG Template": "RAG Шаблон",
|
||||||
|
"Raw Format": "Raw Формат",
|
||||||
|
"Record voice": "Записване на глас",
|
||||||
|
"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
|
||||||
|
"Release Notes": "Бележки по изданието",
|
||||||
|
"Repeat Last N": "Repeat Last N",
|
||||||
|
"Repeat Penalty": "Repeat Penalty",
|
||||||
|
"Request Mode": "Request Mode",
|
||||||
|
"Reset Vector Storage": "Ресет Vector Storage",
|
||||||
|
"Response AutoCopy to Clipboard": "Аувтоматично копиране на отговор в клипборда",
|
||||||
|
"Role": "Роля",
|
||||||
|
"Rosé Pine": "Rosé Pine",
|
||||||
|
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
||||||
|
"Save": "Запис",
|
||||||
|
"Save & Create": "Запис & Създаване",
|
||||||
|
"Save & Submit": "Запис & Изпращане",
|
||||||
|
"Save & Update": "Запис & Актуализиране",
|
||||||
|
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
|
||||||
|
"Scan": "Сканиране",
|
||||||
|
"Scan complete!": "Сканиране завършено!",
|
||||||
|
"Scan for documents from {{path}}": "Сканиране за документи в {{path}}",
|
||||||
|
"Search": "Търси",
|
||||||
|
"Search Documents": "Търси Документи",
|
||||||
|
"Search Prompts": "Търси Промптове",
|
||||||
|
"See readme.md for instructions": "Виж readme.md за инструкции",
|
||||||
|
"See what's new": "Виж какво е новото",
|
||||||
|
"Seed": "Seed",
|
||||||
|
"Select a mode": "Изберете режим",
|
||||||
|
"Select a model": "Изберете модел",
|
||||||
|
"Select an Ollama instance": "Изберете Ollama инстанция",
|
||||||
|
"Send a Message": "Изпращане на Съобщение",
|
||||||
|
"Send message": "Изпращане на съобщение",
|
||||||
|
"Server connection verified": "Server connection verified",
|
||||||
|
"Set as default": "Задай по подразбиране",
|
||||||
|
"Set Default Model": "Задай Модел По Подразбиране",
|
||||||
|
"Set Image Size": "Задай Размер на Изображението",
|
||||||
|
"Set Steps": "Задай Стъпки",
|
||||||
|
"Set Title Auto-Generation Model": "Задай Модел за Автоматично Генериране на Заглавие",
|
||||||
|
"Set Voice": "Задай Глас",
|
||||||
|
"Settings": "Настройки",
|
||||||
|
"Settings saved successfully!": "Настройките са запазени успешно!",
|
||||||
|
"Share to OpenWebUI Community": "Споделите с OpenWebUI Общността",
|
||||||
|
"short-summary": "short-summary",
|
||||||
|
"Show": "Покажи",
|
||||||
|
"Show Additional Params": "Покажи допълнителни параметри",
|
||||||
|
"Show shortcuts": "Покажи",
|
||||||
|
"sidebar": "sidebar",
|
||||||
|
"Sign in": "Вписване",
|
||||||
|
"Sign Out": "Изход",
|
||||||
|
"Sign up": "Регистрация",
|
||||||
|
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
|
||||||
|
"Speech-to-Text Engine": "Speech-to-Text Engine",
|
||||||
|
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.",
|
||||||
|
"Stop Sequence": "Stop Sequence",
|
||||||
|
"STT Settings": "STT Настройки",
|
||||||
|
"Submit": "Изпращане",
|
||||||
|
"Success": "Успех",
|
||||||
|
"Successfully updated.": "Успешно обновено.",
|
||||||
|
"Sync All": "Синхронизиране на всички",
|
||||||
|
"System": "Система",
|
||||||
|
"System Prompt": "Системен Промпт",
|
||||||
|
"Tags": "Тагове",
|
||||||
|
"Temperature": "Температура",
|
||||||
|
"Template": "Шаблон",
|
||||||
|
"Text Completion": "Text Completion",
|
||||||
|
"Text-to-Speech Engine": "Text-to-Speech Engine",
|
||||||
|
"Tfs Z": "Tfs Z",
|
||||||
|
"Theme": "Тема",
|
||||||
|
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Това гарантира, че ценните ви разговори се запазват сигурно във вашата бекенд база данни. Благодарим ви!",
|
||||||
|
"This setting does not sync across browsers or devices.": "Тази настройка не се синхронизира между браузъри или устройства.",
|
||||||
|
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Съвет: Актуализирайте няколко слота за променливи последователно, като натискате клавиша Tab в чат входа след всяка подмяна.",
|
||||||
|
"Title": "Заглавие",
|
||||||
|
"Title Auto-Generation": "Автоматично Генериране на Заглавие",
|
||||||
|
"Title Generation Prompt": "Промпт за Генериране на Заглавие",
|
||||||
|
"to": "в",
|
||||||
|
"To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,",
|
||||||
|
"To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,",
|
||||||
|
"to chat input.": "към чат входа.",
|
||||||
|
"Toggle settings": "Toggle settings",
|
||||||
|
"Toggle sidebar": "Toggle sidebar",
|
||||||
|
"Top K": "Top K",
|
||||||
|
"Top P": "Top P",
|
||||||
|
"Trouble accessing Ollama?": "Проблеми с достъпът до Ollama?",
|
||||||
|
"TTS Settings": "TTS Настройки",
|
||||||
|
"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 password": "Обновяване на парола",
|
||||||
|
"Upload a GGUF model": "Качване на GGUF модел",
|
||||||
|
"Upload files": "Качване на файлове",
|
||||||
|
"Upload Progress": "Прогрес на качването",
|
||||||
|
"URL Mode": "URL Mode",
|
||||||
|
"Use '#' in the prompt input to load and select your documents.": "Използвайте '#' във промпта за да заредите и изберете вашите документи.",
|
||||||
|
"Use Gravatar": "Използвайте Gravatar",
|
||||||
|
"user": "потребител",
|
||||||
|
"User Permissions": "Права на потребителя",
|
||||||
|
"Users": "Потребители",
|
||||||
|
"Utilize": "Използване",
|
||||||
|
"Valid time units:": "Валидни единици за време:",
|
||||||
|
"variable": "променлива",
|
||||||
|
"variable to have them replaced with clipboard content.": "променливи да се заменят съдържанието от клипборд.",
|
||||||
|
"Version": "Версия",
|
||||||
|
"Web": "Уеб",
|
||||||
|
"WebUI Add-ons": "WebUI Добавки",
|
||||||
|
"WebUI Settings": "WebUI Настройки",
|
||||||
|
"WebUI will make requests to": "WebUI ще направи заявки към",
|
||||||
|
"What’s New in": "Какво е новото в",
|
||||||
|
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Когато историята е изключена, нови чатове в този браузър ще не се показват в историята на никои от вашия профил.",
|
||||||
|
"Whisper (Local)": "Whisper (Локален)",
|
||||||
|
"Write a prompt suggestion (e.g. Who are you?)": "Напиши предложение за промпт (напр. Кой сте вие?)",
|
||||||
|
"Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 знака, което описва [тема или ключова дума].",
|
||||||
|
"You": "Вие",
|
||||||
|
"You're a helpful assistant.": "Вие сте полезен асистент.",
|
||||||
|
"You're now logged in.": "Сега, вие влязохте в системата."
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
{
|
||||||
|
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' per no caduca mai.",
|
||||||
|
"(Beta)": "(Beta)",
|
||||||
|
"(e.g. `sh webui.sh --api`)": "(p. ex. `sh webui.sh --api`)",
|
||||||
|
"(latest)": "(últim)",
|
||||||
|
"{{modelName}} is thinking...": "{{modelName}} està pensant...",
|
||||||
|
"{{webUIName}} Backend Required": "Es requereix Backend de {{webUIName}}",
|
||||||
|
"a user": "un usuari",
|
||||||
|
"About": "Sobre",
|
||||||
|
"Account": "Compte",
|
||||||
|
"Action": "Acció",
|
||||||
|
"Add a model": "Afegeix un model",
|
||||||
|
"Add a model tag name": "Afegeix un nom d'etiqueta de model",
|
||||||
|
"Add a short description about what this modelfile does": "Afegeix una descripció curta del que fa aquest arxiu de model",
|
||||||
|
"Add a short title for this prompt": "Afegeix un títol curt per aquest prompt",
|
||||||
|
"Add a tag": "Afegeix una etiqueta",
|
||||||
|
"Add Docs": "Afegeix Documents",
|
||||||
|
"Add Files": "Afegeix Arxius",
|
||||||
|
"Add message": "Afegeix missatge",
|
||||||
|
"add tags": "afegeix etiquetes",
|
||||||
|
"Adjusting these settings will apply changes universally to all users.": "Ajustar aquests paràmetres aplicarà canvis de manera universal a tots els usuaris.",
|
||||||
|
"admin": "administrador",
|
||||||
|
"Admin Panel": "Panell d'Administració",
|
||||||
|
"Admin Settings": "Configuració d'Administració",
|
||||||
|
"Advanced Parameters": "Paràmetres Avançats",
|
||||||
|
"all": "tots",
|
||||||
|
"All Users": "Tots els Usuaris",
|
||||||
|
"Allow": "Permet",
|
||||||
|
"Allow Chat Deletion": "Permet la Supressió del Xat",
|
||||||
|
"alphanumeric characters and hyphens": "caràcters alfanumèrics i guions",
|
||||||
|
"Already have an account?": "Ja tens un compte?",
|
||||||
|
"an assistant": "un assistent",
|
||||||
|
"and": "i",
|
||||||
|
"API Base URL": "URL Base de l'API",
|
||||||
|
"API Key": "Clau de l'API",
|
||||||
|
"API RPM": "RPM de l'API",
|
||||||
|
"are allowed - Activate this command by typing": "estan permesos - Activa aquesta comanda escrivint",
|
||||||
|
"Are you sure?": "Estàs segur?",
|
||||||
|
"Audio": "Àudio",
|
||||||
|
"Auto-playback response": "Resposta de reproducció automàtica",
|
||||||
|
"Auto-send input after 3 sec.": "Enviar entrada automàticament després de 3 segons",
|
||||||
|
"AUTOMATIC1111 Base URL": "URL Base AUTOMATIC1111",
|
||||||
|
"AUTOMATIC1111 Base URL is required.": "Es requereix l'URL Base AUTOMATIC1111.",
|
||||||
|
"available!": "disponible!",
|
||||||
|
"Back": "Enrere",
|
||||||
|
"Builder Mode": "Mode Constructor",
|
||||||
|
"Cancel": "Cancel·la",
|
||||||
|
"Categories": "Categories",
|
||||||
|
"Change Password": "Canvia la Contrasenya",
|
||||||
|
"Chat": "Xat",
|
||||||
|
"Chat History": "Històric del Xat",
|
||||||
|
"Chat History is off for this browser.": "L'historial de xat està desactivat per a aquest navegador.",
|
||||||
|
"Chats": "Xats",
|
||||||
|
"Check Again": "Comprova-ho de Nou",
|
||||||
|
"Check for updates": "Comprova si hi ha actualitzacions",
|
||||||
|
"Checking for updates...": "Comprovant actualitzacions...",
|
||||||
|
"Choose a model before saving...": "Tria un model abans de guardar...",
|
||||||
|
"Chunk Overlap": "Solapament de Blocs",
|
||||||
|
"Chunk Params": "Paràmetres de Blocs",
|
||||||
|
"Chunk Size": "Mida del Bloc",
|
||||||
|
"Click here for help.": "Fes clic aquí per ajuda.",
|
||||||
|
"Click here to check other modelfiles.": "Fes clic aquí per comprovar altres fitxers de model.",
|
||||||
|
"Click here to select": "Fes clic aquí per seleccionar",
|
||||||
|
"Click here to select documents.": "Fes clic aquí per seleccionar documents.",
|
||||||
|
"click here.": "fes clic aquí.",
|
||||||
|
"Click on the user role button to change a user's role.": "Fes clic al botó de rol d'usuari per canviar el rol d'un usuari.",
|
||||||
|
"Close": "Tanca",
|
||||||
|
"Collection": "Col·lecció",
|
||||||
|
"Command": "Comanda",
|
||||||
|
"Confirm Password": "Confirma la Contrasenya",
|
||||||
|
"Connections": "Connexions",
|
||||||
|
"Content": "Contingut",
|
||||||
|
"Context Length": "Longitud del Context",
|
||||||
|
"Conversation Mode": "Mode de Conversa",
|
||||||
|
"Copy last code block": "Copia l'últim bloc de codi",
|
||||||
|
"Copy last response": "Copia l'última resposta",
|
||||||
|
"Copying to clipboard was successful!": "La còpia al porta-retalls ha estat exitosa!",
|
||||||
|
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Crea una frase concisa de 3-5 paraules com a capçalera per a la següent consulta, seguint estrictament el límit de 3-5 paraules i evitant l'ús de la paraula 'títol':",
|
||||||
|
"Create a modelfile": "Crea un fitxer de model",
|
||||||
|
"Create Account": "Crea un Compte",
|
||||||
|
"Created at": "Creat el",
|
||||||
|
"Created by": "Creat per",
|
||||||
|
"Current Model": "Model Actual",
|
||||||
|
"Current Password": "Contrasenya Actual",
|
||||||
|
"Custom": "Personalitzat",
|
||||||
|
"Customize Ollama models for a specific purpose": "Personalitza els models Ollama per a un propòsit específic",
|
||||||
|
"Dark": "Fosc",
|
||||||
|
"Database": "Base de Dades",
|
||||||
|
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
|
||||||
|
"Default": "Per defecte",
|
||||||
|
"Default (Automatic1111)": "Per defecte (Automatic1111)",
|
||||||
|
"Default (Web API)": "Per defecte (Web API)",
|
||||||
|
"Default model updated": "Model per defecte actualitzat",
|
||||||
|
"Default Prompt Suggestions": "Suggeriments de Prompt Per Defecte",
|
||||||
|
"Default User Role": "Rol d'Usuari Per Defecte",
|
||||||
|
"delete": "esborra",
|
||||||
|
"Delete a model": "Esborra un model",
|
||||||
|
"Delete chat": "Esborra xat",
|
||||||
|
"Delete Chats": "Esborra Xats",
|
||||||
|
"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}",
|
||||||
|
"Deleted {tagName}": "Esborrat {tagName}",
|
||||||
|
"Description": "Descripció",
|
||||||
|
"Desktop Notifications": "Notificacions d'Escriptori",
|
||||||
|
"Disabled": "Desactivat",
|
||||||
|
"Discover a modelfile": "Descobreix un fitxer de model",
|
||||||
|
"Discover a prompt": "Descobreix un prompt",
|
||||||
|
"Discover, download, and explore custom prompts": "Descobreix, descarrega i explora prompts personalitzats",
|
||||||
|
"Discover, download, and explore model presets": "Descobreix, descarrega i explora presets de models",
|
||||||
|
"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",
|
||||||
|
"Documents": "Documents",
|
||||||
|
"does not make any external connections, and your data stays securely on your locally hosted server.": "no realitza connexions externes, i les teves dades romanen segures al teu servidor allotjat localment.",
|
||||||
|
"Don't Allow": "No Permetre",
|
||||||
|
"Don't have an account?": "No tens un compte?",
|
||||||
|
"Download as a File": "Descarrega com a Arxiu",
|
||||||
|
"Download Database": "Descarrega Base de Dades",
|
||||||
|
"Drop any files here to add to the conversation": "Deixa qualsevol arxiu aquí per afegir-lo a la conversa",
|
||||||
|
"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 Doc": "Edita Document",
|
||||||
|
"Edit User": "Edita Usuari",
|
||||||
|
"Email": "Correu electrònic",
|
||||||
|
"Enable Chat History": "Activa Historial de Xat",
|
||||||
|
"Enable New Sign Ups": "Permet Noves Inscripcions",
|
||||||
|
"Enabled": "Activat",
|
||||||
|
"Enter {{role}} message here": "Introdueix aquí el missatge de {{role}}",
|
||||||
|
"Enter API Key": "Introdueix la Clau API",
|
||||||
|
"Enter Chunk Overlap": "Introdueix el Solapament de Blocs",
|
||||||
|
"Enter Chunk Size": "Introdueix la Mida del Bloc",
|
||||||
|
"Enter Image Size (e.g. 512x512)": "Introdueix la Mida de la Imatge (p. ex. 512x512)",
|
||||||
|
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Introdueix l'URL Base de LiteLLM API (litellm_params.api_base)",
|
||||||
|
"Enter LiteLLM API Key (litellm_params.api_key)": "Introdueix la Clau de LiteLLM API (litellm_params.api_key)",
|
||||||
|
"Enter LiteLLM API RPM (litellm_params.rpm)": "Introdueix RPM de LiteLLM API (litellm_params.rpm)",
|
||||||
|
"Enter LiteLLM Model (litellm_params.model)": "Introdueix el Model de LiteLLM (litellm_params.model)",
|
||||||
|
"Enter Max Tokens (litellm_params.max_tokens)": "Introdueix el Màxim de Tokens (litellm_params.max_tokens)",
|
||||||
|
"Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
|
||||||
|
"Enter Number of Steps (e.g. 50)": "Introdueix el Nombre de Passos (p. ex. 50)",
|
||||||
|
"Enter stop sequence": "Introdueix la seqüència de parada",
|
||||||
|
"Enter Top K": "Introdueix Top K",
|
||||||
|
"Enter URL (e.g. http://127.0.0.1:7860/)": "Introdueix l'URL (p. ex. http://127.0.0.1:7860/)",
|
||||||
|
"Enter Your Email": "Introdueix el Teu Correu Electrònic",
|
||||||
|
"Enter Your Full Name": "Introdueix el Teu Nom Complet",
|
||||||
|
"Enter Your Password": "Introdueix la Teva Contrasenya",
|
||||||
|
"Experimental": "Experimental",
|
||||||
|
"Export All Chats (All Users)": "Exporta Tots els Xats (Tots els Usuaris)",
|
||||||
|
"Export Chats": "Exporta Xats",
|
||||||
|
"Export Documents Mapping": "Exporta el Mapatge de Documents",
|
||||||
|
"Export Modelfiles": "Exporta Fitxers de Model",
|
||||||
|
"Export Prompts": "Exporta Prompts",
|
||||||
|
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
|
||||||
|
"File Mode": "Mode Arxiu",
|
||||||
|
"File not found.": "Arxiu no trobat.",
|
||||||
|
"Focus chat input": "Enfoca l'entrada del xat",
|
||||||
|
"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
|
||||||
|
"From (Base Model)": "Des de (Model Base)",
|
||||||
|
"Full Screen Mode": "Mode de Pantalla Completa",
|
||||||
|
"General": "General",
|
||||||
|
"General Settings": "Configuració General",
|
||||||
|
"Hello, {{name}}": "Hola, {{name}}",
|
||||||
|
"Hide": "Amaga",
|
||||||
|
"Hide Additional Params": "Amaga Paràmetres Addicionals",
|
||||||
|
"How can I help you today?": "Com et puc ajudar avui?",
|
||||||
|
"Image Generation (Experimental)": "Generació d'Imatges (Experimental)",
|
||||||
|
"Image Generation Engine": "Motor de Generació d'Imatges",
|
||||||
|
"Image Settings": "Configuració d'Imatges",
|
||||||
|
"Images": "Imatges",
|
||||||
|
"Import Chats": "Importa Xats",
|
||||||
|
"Import Documents Mapping": "Importa el Mapa de Documents",
|
||||||
|
"Import Modelfiles": "Importa Fitxers de Model",
|
||||||
|
"Import Prompts": "Importa Prompts",
|
||||||
|
"Include `--api` flag when running stable-diffusion-webui": "Inclou la bandera `--api` quan executis stable-diffusion-webui",
|
||||||
|
"Interface": "Interfície",
|
||||||
|
"join our Discord for help.": "uneix-te al nostre Discord per ajuda.",
|
||||||
|
"JSON": "JSON",
|
||||||
|
"JWT Expiration": "Expiració de JWT",
|
||||||
|
"JWT Token": "Token JWT",
|
||||||
|
"Keep Alive": "Mantén Actiu",
|
||||||
|
"Keyboard shortcuts": "Dreceres de Teclat",
|
||||||
|
"Language": "Idioma",
|
||||||
|
"Light": "Clar",
|
||||||
|
"Listening...": "Escoltant...",
|
||||||
|
"LLMs can make mistakes. Verify important information.": "Els LLMs poden cometre errors. Verifica la informació important.",
|
||||||
|
"Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI",
|
||||||
|
"Make sure to enclose them with": "Assegura't d'envoltar-los amb",
|
||||||
|
"Manage LiteLLM Models": "Gestiona Models LiteLLM",
|
||||||
|
"Manage Models": "Gestiona Models",
|
||||||
|
"Manage Ollama Models": "Gestiona Models Ollama",
|
||||||
|
"Max Tokens": "Màxim de Tokens",
|
||||||
|
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es poden descarregar un màxim de 3 models simultàniament. Si us plau, prova-ho més tard.",
|
||||||
|
"Mirostat": "Mirostat",
|
||||||
|
"Mirostat Eta": "Eta de Mirostat",
|
||||||
|
"Mirostat Tau": "Tau de Mirostat",
|
||||||
|
"MMMM DD, YYYY": "DD de MMMM, YYYY",
|
||||||
|
"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat amb èxit.",
|
||||||
|
"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
|
||||||
|
"Model {{modelId}} not found": "Model {{modelId}} no trobat",
|
||||||
|
"Model {{modelName}} already exists.": "El model {{modelName}} ja existeix.",
|
||||||
|
"Model Name": "Nom del Model",
|
||||||
|
"Model not selected": "Model no seleccionat",
|
||||||
|
"Model Tag Name": "Nom de l'Etiqueta del Model",
|
||||||
|
"Model Whitelisting": "Llista Blanca de Models",
|
||||||
|
"Model(s) Whitelisted": "Model(s) a la Llista Blanca",
|
||||||
|
"Modelfile": "Fitxer de Model",
|
||||||
|
"Modelfile Advanced Settings": "Configuració Avançada de Fitxers de Model",
|
||||||
|
"Modelfile Content": "Contingut del Fitxer de Model",
|
||||||
|
"Modelfiles": "Fitxers de Model",
|
||||||
|
"Models": "Models",
|
||||||
|
"My Documents": "Els Meus Documents",
|
||||||
|
"My Modelfiles": "Els Meus Fitxers de Model",
|
||||||
|
"My Prompts": "Els Meus Prompts",
|
||||||
|
"Name": "Nom",
|
||||||
|
"Name Tag": "Etiqueta de Nom",
|
||||||
|
"Name your modelfile": "Nomena el teu fitxer de model",
|
||||||
|
"New Chat": "Xat Nou",
|
||||||
|
"New Password": "Nova Contrasenya",
|
||||||
|
"Not sure what to add?": "No estàs segur del que afegir?",
|
||||||
|
"Not sure what to write? Switch to": "No estàs segur del que escriure? Canvia a",
|
||||||
|
"Off": "Desactivat",
|
||||||
|
"Okay, Let's Go!": "D'acord, Anem!",
|
||||||
|
"Ollama Base URL": "URL Base d'Ollama",
|
||||||
|
"Ollama Version": "Versió d'Ollama",
|
||||||
|
"On": "Activat",
|
||||||
|
"Only": "Només",
|
||||||
|
"Only alphanumeric characters and hyphens are allowed in the command string.": "Només es permeten caràcters alfanumèrics i guions en la cadena de comandes.",
|
||||||
|
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Ui! Aguanta! Els teus fitxers encara estan en el forn de processament. Els estem cuinant a la perfecció. Si us plau, tingues paciència i t'avisarem quan estiguin llestos.",
|
||||||
|
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que l'URL és invàlida. Si us plau, revisa-ho i prova de nou.",
|
||||||
|
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
|
||||||
|
"Open": "Obre",
|
||||||
|
"Open AI": "Open AI",
|
||||||
|
"Open AI (Dall-E)": "Open AI (Dall-E)",
|
||||||
|
"Open new chat": "Obre un nou xat",
|
||||||
|
"OpenAI API": "API d'OpenAI",
|
||||||
|
"OpenAI API Key": "Clau API d'OpenAI",
|
||||||
|
"OpenAI API Key is required.": "Es requereix la Clau API d'OpenAI.",
|
||||||
|
"or": "o",
|
||||||
|
"Parameters": "Paràmetres",
|
||||||
|
"Password": "Contrasenya",
|
||||||
|
"PDF Extract Images (OCR)": "Extreu Imatges de PDF (OCR)",
|
||||||
|
"pending": "pendent",
|
||||||
|
"Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}",
|
||||||
|
"Playground": "Zona de Jocs",
|
||||||
|
"Profile": "Perfil",
|
||||||
|
"Prompt Content": "Contingut del Prompt",
|
||||||
|
"Prompt suggestions": "Suggeriments de Prompt",
|
||||||
|
"Prompts": "Prompts",
|
||||||
|
"Pull a model from Ollama.com": "Treu un model d'Ollama.com",
|
||||||
|
"Pull Progress": "Progrés de Tracció",
|
||||||
|
"Query Params": "Paràmetres de Consulta",
|
||||||
|
"RAG Template": "Plantilla RAG",
|
||||||
|
"Raw Format": "Format Brut",
|
||||||
|
"Record voice": "Enregistra veu",
|
||||||
|
"Redirecting you to OpenWebUI Community": "Redirigint-te a la Comunitat OpenWebUI",
|
||||||
|
"Release Notes": "Notes de la Versió",
|
||||||
|
"Repeat Last N": "Repeteix Últim N",
|
||||||
|
"Repeat Penalty": "Penalització de Repetició",
|
||||||
|
"Request Mode": "Mode de Sol·licitud",
|
||||||
|
"Reset Vector Storage": "Reinicia l'Emmagatzematge de Vectors",
|
||||||
|
"Response AutoCopy to Clipboard": "Resposta AutoCopiar al Portapapers",
|
||||||
|
"Role": "Rol",
|
||||||
|
"Rosé Pine": "Rosé Pine",
|
||||||
|
"Rosé Pine Dawn": "Albada Rosé Pine",
|
||||||
|
"Save": "Guarda",
|
||||||
|
"Save & Create": "Guarda i Crea",
|
||||||
|
"Save & Submit": "Guarda i Envia",
|
||||||
|
"Save & Update": "Guarda i Actualitza",
|
||||||
|
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Guardar registres de xat directament a l'emmagatzematge del teu navegador ja no és suportat. Si us plau, pren un moment per descarregar i eliminar els teus registres de xat fent clic al botó de sota. No et preocupis, pots reimportar fàcilment els teus registres de xat al backend a través de",
|
||||||
|
"Scan": "Escaneja",
|
||||||
|
"Scan complete!": "Escaneig completat!",
|
||||||
|
"Scan for documents from {{path}}": "Escaneja documents des de {{path}}",
|
||||||
|
"Search": "Cerca",
|
||||||
|
"Search Documents": "Cerca Documents",
|
||||||
|
"Search Prompts": "Cerca Prompts",
|
||||||
|
"See readme.md for instructions": "Consulta el readme.md per a instruccions",
|
||||||
|
"See what's new": "Veure novetats",
|
||||||
|
"Seed": "Llavor",
|
||||||
|
"Select a mode": "Selecciona un mode",
|
||||||
|
"Select a model": "Selecciona un model",
|
||||||
|
"Select an Ollama instance": "Selecciona una instància d'Ollama",
|
||||||
|
"Send a Message": "Envia un Missatge",
|
||||||
|
"Send message": "Envia missatge",
|
||||||
|
"Server connection verified": "Connexió al servidor verificada",
|
||||||
|
"Set as default": "Estableix com a predeterminat",
|
||||||
|
"Set Default Model": "Estableix Model Predeterminat",
|
||||||
|
"Set Image Size": "Estableix Mida de la Imatge",
|
||||||
|
"Set Steps": "Estableix Passos",
|
||||||
|
"Set Title Auto-Generation Model": "Estableix Model d'Auto-Generació de Títol",
|
||||||
|
"Set Voice": "Estableix Veu",
|
||||||
|
"Settings": "Configuracions",
|
||||||
|
"Settings saved successfully!": "Configuracions guardades amb èxit!",
|
||||||
|
"Share to OpenWebUI Community": "Comparteix amb la Comunitat OpenWebUI",
|
||||||
|
"short-summary": "resum curt",
|
||||||
|
"Show": "Mostra",
|
||||||
|
"Show Additional Params": "Mostra Paràmetres Addicionals",
|
||||||
|
"Show shortcuts": "Mostra dreceres",
|
||||||
|
"sidebar": "barra lateral",
|
||||||
|
"Sign in": "Inicia sessió",
|
||||||
|
"Sign Out": "Tanca sessió",
|
||||||
|
"Sign up": "Registra't",
|
||||||
|
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
|
||||||
|
"Speech-to-Text Engine": "Motor de Veu a Text",
|
||||||
|
"SpeechRecognition API is not supported in this browser.": "L'API de Reconèixer Veu no és compatible amb aquest navegador.",
|
||||||
|
"Stop Sequence": "Atura Seqüència",
|
||||||
|
"STT Settings": "Configuracions STT",
|
||||||
|
"Submit": "Envia",
|
||||||
|
"Success": "Èxit",
|
||||||
|
"Successfully updated.": "Actualitzat amb èxit.",
|
||||||
|
"Sync All": "Sincronitza Tot",
|
||||||
|
"System": "Sistema",
|
||||||
|
"System Prompt": "Prompt del Sistema",
|
||||||
|
"Tags": "Etiquetes",
|
||||||
|
"Temperature": "Temperatura",
|
||||||
|
"Template": "Plantilla",
|
||||||
|
"Text Completion": "Completació de Text",
|
||||||
|
"Text-to-Speech Engine": "Motor de Text a Veu",
|
||||||
|
"Tfs Z": "Tfs Z",
|
||||||
|
"Theme": "Tema",
|
||||||
|
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden segurament guardades a la teva base de dades backend. Gràcies!",
|
||||||
|
"This setting does not sync across browsers or devices.": "Aquesta configuració no es sincronitza entre navegadors ni dispositius.",
|
||||||
|
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza diversos espais de variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
|
||||||
|
"Title": "Títol",
|
||||||
|
"Title Auto-Generation": "Auto-Generació de Títol",
|
||||||
|
"Title Generation Prompt": "Prompt de Generació de Títol",
|
||||||
|
"to": "a",
|
||||||
|
"To access the available model names for downloading,": "Per accedir als noms dels models disponibles per descarregar,",
|
||||||
|
"To access the GGUF models available for downloading,": "Per accedir als models GGUF disponibles per descarregar,",
|
||||||
|
"to chat input.": "a l'entrada del xat.",
|
||||||
|
"Toggle settings": "Commuta configuracions",
|
||||||
|
"Toggle sidebar": "Commuta barra lateral",
|
||||||
|
"Top K": "Top K",
|
||||||
|
"Top P": "Top P",
|
||||||
|
"Trouble accessing Ollama?": "Problemes accedint a Ollama?",
|
||||||
|
"TTS Settings": "Configuracions TTS",
|
||||||
|
"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 password": "Actualitza contrasenya",
|
||||||
|
"Upload a GGUF model": "Puja un model GGUF",
|
||||||
|
"Upload files": "Puja arxius",
|
||||||
|
"Upload Progress": "Progrés de Càrrega",
|
||||||
|
"URL Mode": "Mode URL",
|
||||||
|
"Use '#' in the prompt input to load and select your documents.": "Utilitza '#' a l'entrada del prompt per carregar i seleccionar els teus documents.",
|
||||||
|
"Use Gravatar": "Utilitza Gravatar",
|
||||||
|
"user": "usuari",
|
||||||
|
"User Permissions": "Permisos d'Usuari",
|
||||||
|
"Users": "Usuaris",
|
||||||
|
"Utilize": "Utilitza",
|
||||||
|
"Valid time units:": "Unitats de temps vàlides:",
|
||||||
|
"variable": "variable",
|
||||||
|
"variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.",
|
||||||
|
"Version": "Versió",
|
||||||
|
"Web": "Web",
|
||||||
|
"WebUI Add-ons": "Complements de WebUI",
|
||||||
|
"WebUI Settings": "Configuració de WebUI",
|
||||||
|
"WebUI will make requests to": "WebUI farà peticions a",
|
||||||
|
"What’s New in": "Què hi ha de Nou en",
|
||||||
|
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quan l'historial està desactivat, els nous xats en aquest navegador no apareixeran en el teu historial en cap dels teus dispositius.",
|
||||||
|
"Whisper (Local)": "Whisper (Local)",
|
||||||
|
"Write a prompt suggestion (e.g. Who are you?)": "Escriu una suggerència de prompt (p. ex. Qui ets tu?)",
|
||||||
|
"Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].",
|
||||||
|
"You": "Tu",
|
||||||
|
"You're a helpful assistant.": "Ets un assistent útil.",
|
||||||
|
"You're now logged in.": "Ara estàs connectat."
|
||||||
|
}
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
"Select a mode": "",
|
"Select a mode": "",
|
||||||
"Select a model": "Ein Modell auswählen",
|
"Select a model": "Ein Modell auswählen",
|
||||||
"Select an Ollama instance": "",
|
"Select an Ollama instance": "",
|
||||||
"Send a Messsage": "Eine Nachricht senden",
|
"Send a Message": "Eine Nachricht senden",
|
||||||
"Send message": "Nachricht senden",
|
"Send message": "Nachricht senden",
|
||||||
"Server connection verified": "Serververbindung überprüft",
|
"Server connection verified": "Serververbindung überprüft",
|
||||||
"Set as default": "Als Standard festlegen",
|
"Set as default": "Als Standard festlegen",
|
||||||
|
|||||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user