forked from open-webui/open-webui
Revert "Merge Updates & Dockerfile improvements" (#3)
This reverts commit 9763d885be
.
This commit is contained in:
parent
9763d885be
commit
099b1d066b
155 changed files with 4795 additions and 14501 deletions
34
.github/workflows/format-backend.yaml
vendored
34
.github/workflows/format-backend.yaml
vendored
|
@ -1,39 +1,27 @@
|
||||||
name: Python CI
|
name: Python CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ['main']
|
||||||
- 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:
|
||||||
python-version: [3.11]
|
node-version:
|
||||||
|
- latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Python
|
||||||
- name: Set up Python
|
uses: actions/setup-python@v4
|
||||||
uses: actions/setup-python@v2
|
- name: Use Bun
|
||||||
with:
|
uses: oven-sh/setup-bun@v1
|
||||||
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 black
|
pip install yapf
|
||||||
|
|
||||||
- name: Format backend
|
- name: Format backend
|
||||||
run: npm run format:backend
|
run: bun run format:backend
|
||||||
|
|
||||||
- name: Check for changes after format
|
|
||||||
run: git diff --exit-code
|
|
||||||
|
|
42
.github/workflows/format-build-frontend.yaml
vendored
42
.github/workflows/format-build-frontend.yaml
vendored
|
@ -1,36 +1,22 @@
|
||||||
name: Frontend Build
|
name: Bun CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ['main']
|
||||||
- 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:
|
||||||
- name: Checkout Repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- name: Use Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
- name: Setup Node.js
|
- run: bun --version
|
||||||
uses: actions/setup-node@v3
|
- name: Install frontend dependencies
|
||||||
with:
|
run: bun install
|
||||||
node-version: '20' # Or specify any other version you want to use
|
- name: Format frontend
|
||||||
|
run: bun run format
|
||||||
- name: Install Dependencies
|
- name: Build frontend
|
||||||
run: npm install
|
run: bun run build
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
|
||||||
|
|
80
CHANGELOG.md
80
CHANGELOG.md
|
@ -5,86 +5,6 @@ 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.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
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization).
|
|
||||||
- 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices.
|
|
||||||
- **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning.
|
|
||||||
- **RAG Encoding Issue**: Fixed encoding problem in RAG.
|
|
||||||
- **npm Audit Fix**: Addressed npm audit findings.
|
|
||||||
- **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation.
|
|
||||||
|
|
||||||
## [0.1.112] - 2024-03-15
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- 🗨️ Resolved chat malfunction after image generation.
|
|
||||||
- 🎨 Fixed various RAG issues.
|
|
||||||
- 🧪 Rectified experimental broken GGUF upload logic.
|
|
||||||
|
|
||||||
## [0.1.111] - 2024-03-10
|
## [0.1.111] - 2024-03-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
15
Dockerfile
15
Dockerfile
|
@ -2,8 +2,6 @@
|
||||||
# Initialize device type args
|
# Initialize device type args
|
||||||
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
|
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
|
||||||
ARG USE_CUDA=false
|
ARG USE_CUDA=false
|
||||||
ARG USE_CUDA_VER=cu121
|
|
||||||
ARG USE_EMBEDDING_MODEL=all-MiniLM-L6-v2
|
|
||||||
ARG USE_MPS=false
|
ARG USE_MPS=false
|
||||||
ARG INCLUDE_OLLAMA=false
|
ARG INCLUDE_OLLAMA=false
|
||||||
|
|
||||||
|
@ -30,9 +28,8 @@ RUN npm run build
|
||||||
######## WebUI backend ########
|
######## WebUI backend ########
|
||||||
FROM python:3.11-slim-bookworm as base
|
FROM python:3.11-slim-bookworm as base
|
||||||
|
|
||||||
|
# Use args
|
||||||
ARG USE_CUDA
|
ARG USE_CUDA
|
||||||
ARG USE_CUDA_VER
|
|
||||||
ARG USE_EMBEDDING_MODEL
|
|
||||||
ARG USE_MPS
|
ARG USE_MPS
|
||||||
ARG INCLUDE_OLLAMA
|
ARG INCLUDE_OLLAMA
|
||||||
|
|
||||||
|
@ -42,9 +39,7 @@ ENV ENV=prod \
|
||||||
# pass build args to the build
|
# pass build args to the build
|
||||||
INCLUDE_OLLAMA_DOCKER=${INCLUDE_OLLAMA} \
|
INCLUDE_OLLAMA_DOCKER=${INCLUDE_OLLAMA} \
|
||||||
USE_MPS_DOCKER=${USE_MPS} \
|
USE_MPS_DOCKER=${USE_MPS} \
|
||||||
USE_CUDA_DOCKER=${USE_CUDA} \
|
USE_CUDA_DOCKER=${USE_CUDA}
|
||||||
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
|
||||||
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
|
|
||||||
|
|
||||||
## Basis URL Config ##
|
## Basis URL Config ##
|
||||||
ENV OLLAMA_BASE_URL="/ollama" \
|
ENV OLLAMA_BASE_URL="/ollama" \
|
||||||
|
@ -66,7 +61,7 @@ ENV WHISPER_MODEL="base" \
|
||||||
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
||||||
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
||||||
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||||
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2" \
|
||||||
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
|
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
|
||||||
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" \
|
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" \
|
||||||
# device type for whisper tts and embbeding models - "cpu" (default) or "mps" (apple silicon) - choosing this right can lead to better performance
|
# device type for whisper tts and embbeding models - "cpu" (default) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||||
|
@ -83,10 +78,8 @@ WORKDIR /app/backend
|
||||||
COPY ./backend/requirements.txt ./requirements.txt
|
COPY ./backend/requirements.txt ./requirements.txt
|
||||||
|
|
||||||
RUN if [ "$USE_CUDA" = "true" ]; then \
|
RUN if [ "$USE_CUDA" = "true" ]; then \
|
||||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117 --no-cache-dir && \
|
||||||
pip3 install -r requirements.txt --no-cache-dir; \
|
pip3 install -r requirements.txt --no-cache-dir; \
|
||||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
|
|
||||||
python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \
|
|
||||||
elif [ "$USE_MPS" = "true" ]; then \
|
elif [ "$USE_MPS" = "true" ]; then \
|
||||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||||
pip3 install -r requirements.txt --no-cache-dir && \
|
pip3 install -r requirements.txt --no-cache-dir && \
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -8,8 +8,6 @@ remove:
|
||||||
|
|
||||||
start:
|
start:
|
||||||
@docker-compose start
|
@docker-compose start
|
||||||
startAndBuild:
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@docker-compose stop
|
@docker-compose stop
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
|
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
|
||||||
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
|
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
|
||||||
|
|
||||||
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||||
|
|
||||||
![Open WebUI Demo](./demo.gif)
|
![Open WebUI Demo](./demo.gif)
|
||||||
|
|
||||||
|
@ -79,8 +79,6 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
||||||
|
|
||||||
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
||||||
|
|
||||||
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
|
|
||||||
|
|
||||||
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
|
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
|
||||||
|
|
||||||
## 🔗 Also Check Out Open WebUI Community!
|
## 🔗 Also Check Out Open WebUI Community!
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
FastAPI,
|
FastAPI,
|
||||||
Request,
|
Request,
|
||||||
|
@ -22,24 +21,11 @@ from utils.utils import (
|
||||||
)
|
)
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
from config import (
|
from config import CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR, DEVICE_TYPE
|
||||||
SRC_LOG_LEVELS,
|
|
||||||
CACHE_DIR,
|
|
||||||
UPLOAD_DIR,
|
|
||||||
WHISPER_MODEL,
|
|
||||||
WHISPER_MODEL_DIR,
|
|
||||||
DEVICE_TYPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
if DEVICE_TYPE != "cuda":
|
||||||
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
|
||||||
|
|
||||||
whisper_device_type = DEVICE_TYPE
|
|
||||||
|
|
||||||
if whisper_device_type != "cuda":
|
|
||||||
whisper_device_type = "cpu"
|
whisper_device_type = "cpu"
|
||||||
|
|
||||||
log.info(f"whisper_device_type: {whisper_device_type}")
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
@ -56,7 +42,7 @@ def transcribe(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
log.info(f"file.content_type: {file.content_type}")
|
print(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(
|
||||||
|
@ -80,7 +66,7 @@ def transcribe(
|
||||||
)
|
)
|
||||||
|
|
||||||
segments, info = model.transcribe(file_path, beam_size=5)
|
segments, info = model.transcribe(file_path, beam_size=5)
|
||||||
log.info(
|
print(
|
||||||
"Detected language '%s' with probability %f"
|
"Detected language '%s' with probability %f"
|
||||||
% (info.language, info.language_probability)
|
% (info.language, info.language_probability)
|
||||||
)
|
)
|
||||||
|
@ -90,7 +76,7 @@ def transcribe(
|
||||||
return {"text": transcript.strip()}
|
return {"text": transcript.strip()}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(e)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
|
|
@ -18,8 +18,6 @@ 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
|
||||||
|
@ -27,14 +25,10 @@ from pathlib import Path
|
||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
|
from config import CACHE_DIR, AUTOMATIC1111_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)
|
||||||
|
|
||||||
|
@ -55,8 +49,6 @@ 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
|
||||||
|
@ -79,48 +71,32 @@ 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 EngineUrlUpdateForm(BaseModel):
|
class UrlUpdateForm(BaseModel):
|
||||||
AUTOMATIC1111_BASE_URL: Optional[str] = None
|
url: str
|
||||||
COMFYUI_BASE_URL: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/url")
|
@app.get("/url")
|
||||||
async def get_engine_url(user=Depends(get_admin_user)):
|
async def get_automatic1111_url(user=Depends(get_admin_user)):
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/url/update")
|
@app.post("/url/update")
|
||||||
async def update_engine_url(
|
async def update_automatic1111_url(
|
||||||
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
form_data: UrlUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
if form_data.AUTOMATIC1111_BASE_URL == None:
|
if form_data.url == "":
|
||||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
else:
|
else:
|
||||||
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
url = form_data.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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,18 +186,6 @@ 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"
|
||||||
|
@ -243,8 +207,6 @@ 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()
|
||||||
|
@ -259,12 +221,10 @@ 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()
|
||||||
|
@ -308,24 +268,7 @@ def save_b64_image(b64_str):
|
||||||
|
|
||||||
return image_id
|
return image_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error saving image: {e}")
|
print(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
|
||||||
|
|
||||||
|
|
||||||
|
@ -335,8 +278,6 @@ 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":
|
||||||
|
@ -352,7 +293,6 @@ def generate_image(
|
||||||
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
|
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
|
||||||
"response_format": "b64_json",
|
"response_format": "b64_json",
|
||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"https://api.openai.com/v1/images/generations",
|
url=f"https://api.openai.com/v1/images/generations",
|
||||||
json=data,
|
json=data,
|
||||||
|
@ -360,6 +300,7 @@ def generate_image(
|
||||||
)
|
)
|
||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
res = r.json()
|
res = r.json()
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
|
@ -374,47 +315,12 @@ 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,
|
||||||
|
@ -435,7 +341,7 @@ def generate_image(
|
||||||
|
|
||||||
res = r.json()
|
res = r.json()
|
||||||
|
|
||||||
log.debug(f"res: {res}")
|
print(res)
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
|
|
||||||
|
@ -450,10 +356,7 @@ def generate_image(
|
||||||
return images
|
return images
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = e
|
print(e)
|
||||||
|
if r:
|
||||||
if r != None:
|
print(r.json())
|
||||||
data = r.json()
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
if "error" in data:
|
|
||||||
error = data["error"]["message"]
|
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))
|
|
||||||
|
|
|
@ -1,234 +0,0 @@
|
||||||
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,27 +1,10 @@
|
||||||
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, Response
|
from fastapi import FastAPI, Request, Depends, status
|
||||||
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 SRC_LOG_LEVELS, ENV
|
from config import 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()
|
||||||
|
|
||||||
|
@ -43,58 +26,16 @@ 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)
|
|
||||||
|
|
|
@ -1,49 +1,24 @@
|
||||||
from fastapi import (
|
from fastapi import FastAPI, Request, Response, HTTPException, Depends, status
|
||||||
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(
|
||||||
|
@ -94,7 +69,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
|
||||||
|
|
||||||
log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}")
|
print(app.state.OLLAMA_BASE_URLS)
|
||||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,7 +90,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
|
||||||
log.error(f"Connection error: {e}")
|
print(f"Connection error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,7 +98,6 @@ def merge_models_lists(model_lists):
|
||||||
merged_models = {}
|
merged_models = {}
|
||||||
|
|
||||||
for idx, model_list in enumerate(model_lists):
|
for idx, model_list in enumerate(model_lists):
|
||||||
if model_list is not None:
|
|
||||||
for model in model_list:
|
for model in model_list:
|
||||||
digest = model["digest"]
|
digest = model["digest"]
|
||||||
if digest not in merged_models:
|
if digest not in merged_models:
|
||||||
|
@ -139,16 +113,16 @@ def merge_models_lists(model_lists):
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models():
|
async def get_all_models():
|
||||||
log.info("get_all_models()")
|
print("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)
|
||||||
|
responses = list(filter(lambda x: x is not None, responses))
|
||||||
|
|
||||||
models = {
|
models = {
|
||||||
"models": merge_models_lists(
|
"models": merge_models_lists(
|
||||||
map(lambda response: response["models"] if response else None, responses)
|
map(lambda response: response["models"], responses)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
||||||
|
|
||||||
return models
|
return models
|
||||||
|
@ -180,7 +154,7 @@ async def get_ollama_tags(
|
||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -207,17 +181,11 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
responses = list(filter(lambda x: x is not None, responses))
|
responses = list(filter(lambda x: x is not None, responses))
|
||||||
|
|
||||||
if len(responses) > 0:
|
|
||||||
lowest_version = min(
|
lowest_version = min(
|
||||||
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
|
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"version": lowest_version["version"]}
|
return {"version": lowest_version["version"]}
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
try:
|
try:
|
||||||
|
@ -226,7 +194,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -252,33 +220,18 @@ 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]
|
||||||
log.info(f"url: {url}")
|
print(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():
|
||||||
try:
|
|
||||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
|
||||||
|
|
||||||
for chunk in r.iter_content(chunk_size=8192):
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
if request_id in REQUEST_POOL:
|
|
||||||
yield chunk
|
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",
|
||||||
|
@ -299,9 +252,8 @@ 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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -340,7 +292,7 @@ async def push_model(
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.debug(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
|
@ -372,7 +324,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -400,9 +352,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)
|
||||||
):
|
):
|
||||||
log.debug(f"form_data: {form_data}")
|
print(form_data)
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
|
@ -424,7 +376,7 @@ async def create_model(
|
||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
log.debug(f"r: {r}")
|
print(r)
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream_content(),
|
stream_content(),
|
||||||
|
@ -437,7 +389,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -475,7 +427,7 @@ async def copy_model(
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
|
@ -485,11 +437,11 @@ async def copy_model(
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
log.debug(f"r.text: {r.text}")
|
print(r.text)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -522,7 +474,7 @@ async def delete_model(
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
|
@ -532,11 +484,11 @@ async def delete_model(
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
log.debug(f"r.text: {r.text}")
|
print(r.text)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -562,7 +514,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]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
|
@ -574,7 +526,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -614,7 +566,7 @@ async def generate_embeddings(
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
|
@ -626,7 +578,7 @@ async def generate_embeddings(
|
||||||
|
|
||||||
return r.json()
|
return r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -670,11 +622,11 @@ async def generate_completion(
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
detail="error_detail",
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
|
@ -695,7 +647,7 @@ async def generate_completion(
|
||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
log.warning("User: canceled request")
|
print("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
|
@ -750,7 +702,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] = None
|
stream: Optional[bool] = True
|
||||||
keep_alive: Optional[Union[int, str]] = None
|
keep_alive: Optional[Union[int, str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -772,15 +724,11 @@ async def generate_chat_completion(
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
log.debug(
|
print(form_data.model_dump_json(exclude_none=True).encode())
|
||||||
"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
|
||||||
|
@ -799,7 +747,7 @@ async def generate_chat_completion(
|
||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
log.warning("User: canceled request")
|
print("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
|
@ -822,7 +770,7 @@ async def generate_chat_completion(
|
||||||
headers=dict(r.headers),
|
headers=dict(r.headers),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -876,7 +824,7 @@ async def generate_openai_chat_completion(
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
log.info(f"url: {url}")
|
print(url)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
|
@ -899,7 +847,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:
|
||||||
log.warning("User: canceled request")
|
print("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
|
@ -942,220 +890,6 @@ 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]
|
||||||
|
@ -1206,7 +940,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:
|
||||||
log.warning("User: canceled request")
|
print("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
|
|
|
@ -6,7 +6,6 @@ import requests
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
@ -20,7 +19,6 @@ 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,
|
||||||
|
@ -33,9 +31,6 @@ 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,
|
||||||
|
@ -116,7 +111,6 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
|
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
r = None
|
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
|
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
|
||||||
|
@ -139,7 +133,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -149,9 +143,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
except:
|
except:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=r.status_code, detail=error_detail)
|
||||||
status_code=r.status_code if r else 500, detail=error_detail
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
||||||
|
@ -165,7 +157,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
|
||||||
log.error(f"Connection error: {e}")
|
print(f"Connection error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -173,7 +165,6 @@ def merge_models_lists(model_lists):
|
||||||
merged_list = []
|
merged_list = []
|
||||||
|
|
||||||
for idx, models in enumerate(model_lists):
|
for idx, models in enumerate(model_lists):
|
||||||
if models is not None and "error" not in models:
|
|
||||||
merged_list.extend(
|
merged_list.extend(
|
||||||
[
|
[
|
||||||
{**model, "urlIdx": idx}
|
{**model, "urlIdx": idx}
|
||||||
|
@ -187,7 +178,7 @@ def merge_models_lists(model_lists):
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models():
|
async def get_all_models():
|
||||||
log.info("get_all_models()")
|
print("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": []}
|
||||||
|
@ -196,24 +187,15 @@ async def get_all_models():
|
||||||
fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
|
fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
|
||||||
for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
|
for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
|
||||||
]
|
]
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
|
responses = list(
|
||||||
|
filter(lambda x: x is not None and "error" not in x, responses)
|
||||||
|
)
|
||||||
models = {
|
models = {
|
||||||
"data": merge_models_lists(
|
"data": merge_models_lists(
|
||||||
list(
|
list(map(lambda response: response["data"], responses))
|
||||||
map(
|
|
||||||
lambda response: (
|
|
||||||
response["data"]
|
|
||||||
if response and "data" in response
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
responses,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -236,9 +218,6 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
|
||||||
return models
|
return models
|
||||||
else:
|
else:
|
||||||
url = app.state.OPENAI_API_BASE_URLS[url_idx]
|
url = app.state.OPENAI_API_BASE_URLS[url_idx]
|
||||||
|
|
||||||
r = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(method="GET", url=f"{url}/models")
|
r = requests.request(method="GET", url=f"{url}/models")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@ -251,7 +230,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -285,7 +264,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
|
||||||
log.debug("Modified body_dict:", body)
|
print("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:
|
||||||
|
@ -297,7 +276,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:
|
||||||
log.error("Error loading request body into a dictionary:", e)
|
print("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]
|
||||||
|
@ -311,8 +290,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
r = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method=request.method,
|
method=request.method,
|
||||||
|
@ -335,7 +312,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:
|
||||||
log.exception(e)
|
print(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:
|
||||||
|
@ -345,6 +322,4 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
except:
|
except:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=r.status_code, detail=error_detail)
|
||||||
status_code=r.status_code if r else 500, detail=error_detail
|
|
||||||
)
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from fastapi import (
|
||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os, shutil, logging
|
import os, shutil
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -21,7 +21,6 @@ from langchain_community.document_loaders import (
|
||||||
TextLoader,
|
TextLoader,
|
||||||
PyPDFLoader,
|
PyPDFLoader,
|
||||||
CSVLoader,
|
CSVLoader,
|
||||||
BSHTMLLoader,
|
|
||||||
Docx2txtLoader,
|
Docx2txtLoader,
|
||||||
UnstructuredEPubLoader,
|
UnstructuredEPubLoader,
|
||||||
UnstructuredWordDocumentLoader,
|
UnstructuredWordDocumentLoader,
|
||||||
|
@ -55,7 +54,6 @@ 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,
|
||||||
|
@ -68,9 +66,6 @@ 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(
|
||||||
|
@ -116,6 +111,39 @@ 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 {
|
||||||
|
@ -245,7 +273,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:
|
||||||
log.exception(e)
|
print(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),
|
||||||
|
@ -289,69 +317,13 @@ 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:
|
||||||
log.exception(e)
|
print(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
|
||||||
|
@ -409,8 +381,6 @@ 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":
|
||||||
|
@ -429,9 +399,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||||
elif file_ext in known_source_ext or (
|
elif file_ext in known_source_ext or (
|
||||||
file_content_type and file_content_type.find("text/") >= 0
|
file_content_type and file_content_type.find("text/") >= 0
|
||||||
):
|
):
|
||||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
loader = TextLoader(file_path)
|
||||||
else:
|
else:
|
||||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
loader = TextLoader(file_path)
|
||||||
known_type = False
|
known_type = False
|
||||||
|
|
||||||
return loader, known_type
|
return loader, known_type
|
||||||
|
@ -445,7 +415,7 @@ 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"
|
||||||
|
|
||||||
log.info(f"file.content_type: {file.content_type}")
|
print(file.content_type)
|
||||||
try:
|
try:
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
|
@ -461,8 +431,6 @@ 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()
|
||||||
|
|
||||||
try:
|
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
@ -472,13 +440,13 @@ def store_doc(
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"known_type": known_type,
|
"known_type": known_type,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=e,
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(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,
|
||||||
|
@ -491,37 +459,6 @@ 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("./**/*"):
|
||||||
|
@ -540,7 +477,6 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
||||||
)
|
)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
|
|
||||||
try:
|
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
@ -573,12 +509,9 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
log.exception(e)
|
|
||||||
pass
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -599,11 +532,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:
|
||||||
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
|
print("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:
|
||||||
log.exception(e)
|
print(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import logging
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
from config import 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):
|
||||||
|
@ -95,13 +91,14 @@ def query_collection(
|
||||||
|
|
||||||
|
|
||||||
def rag_template(template: str, context: str, query: str):
|
def rag_template(template: str, context: str, query: str):
|
||||||
template = template.replace("[context]", context)
|
template = re.sub(r"\[context\]", context, template)
|
||||||
template = template.replace("[query]", query)
|
template = re.sub(r"\[query\]", query, template)
|
||||||
|
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def rag_messages(docs, messages, template, k, embedding_function):
|
def rag_messages(docs, messages, template, k, embedding_function):
|
||||||
log.debug(f"docs: {docs}")
|
print(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):
|
||||||
|
@ -141,8 +138,6 @@ 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"],
|
||||||
|
@ -151,13 +146,11 @@ 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:
|
||||||
log.exception(e)
|
print(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,13 @@
|
||||||
from peewee import *
|
from peewee import *
|
||||||
from config import SRC_LOG_LEVELS, DATA_DIR
|
from config import 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")
|
||||||
log.info("File renamed successfully.")
|
print("File renamed successfully.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ from config import (
|
||||||
DEFAULT_USER_ROLE,
|
DEFAULT_USER_ROLE,
|
||||||
ENABLE_SIGNUP,
|
ENABLE_SIGNUP,
|
||||||
USER_PERMISSIONS,
|
USER_PERMISSIONS,
|
||||||
WEBHOOK_URL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
@ -33,7 +32,6 @@ 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.add_middleware(
|
app.add_middleware(
|
||||||
|
|
|
@ -2,7 +2,6 @@ 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
|
||||||
|
@ -10,11 +9,6 @@ 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
|
||||||
####################
|
####################
|
||||||
|
@ -92,7 +86,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]:
|
||||||
log.info("insert_new_auth")
|
print("insert_new_auth")
|
||||||
|
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
@ -109,7 +103,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]:
|
||||||
log.info(f"authenticate_user: {email}")
|
print("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:
|
||||||
|
|
|
@ -95,6 +95,20 @@ class ChatTable:
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
query = Chat.update(
|
||||||
|
chat=json.dumps(chat),
|
||||||
|
title=chat["title"] if "title" in chat else "New Chat",
|
||||||
|
timestamp=int(time.time()),
|
||||||
|
).where(Chat.id == id)
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
chat = Chat.get(Chat.id == id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_chat_lists_by_user_id(
|
def get_chat_lists_by_user_id(
|
||||||
self, user_id: str, skip: int = 0, limit: int = 50
|
self, user_id: str, skip: int = 0, limit: int = 50
|
||||||
) -> List[ChatModel]:
|
) -> List[ChatModel]:
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -12,11 +11,6 @@ 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
|
||||||
####################
|
####################
|
||||||
|
@ -124,7 +118,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:
|
||||||
log.exception(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_doc_content_by_name(
|
def update_doc_content_by_name(
|
||||||
|
@ -144,7 +138,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:
|
||||||
log.exception(e)
|
print(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, form_data: ModelfileForm
|
self, user_id: str,
|
||||||
) -> Optional[ModelfileModel]:
|
form_data: ModelfileForm) -> Optional[ModelfileModel]:
|
||||||
if "tagName" in form_data.modelfile:
|
if "tagName" in form_data.modelfile:
|
||||||
modelfile = ModelfileModel(
|
modelfile = ModelfileModel(
|
||||||
**{
|
**{
|
||||||
|
@ -73,8 +73,7 @@ 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())
|
||||||
|
@ -88,28 +87,29 @@ class ModelfilesTable:
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
|
def get_modelfile_by_tag_name(self,
|
||||||
|
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, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
|
def get_modelfiles(self,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 50) -> List[ModelfileResponse]:
|
||||||
return [
|
return [
|
||||||
ModelfileResponse(
|
ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**model_to_dict(modelfile),
|
**model_to_dict(modelfile),
|
||||||
"modelfile": json.loads(modelfile.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
|
self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]:
|
||||||
) -> Optional[ModelfileModel]:
|
|
||||||
try:
|
try:
|
||||||
query = Modelfile.update(
|
query = Modelfile.update(
|
||||||
modelfile=json.dumps(modelfile),
|
modelfile=json.dumps(modelfile),
|
||||||
|
|
|
@ -52,9 +52,8 @@ class PromptsTable:
|
||||||
self.db = db
|
self.db = db
|
||||||
self.db.create_tables([Prompt])
|
self.db.create_tables([Prompt])
|
||||||
|
|
||||||
def insert_new_prompt(
|
def insert_new_prompt(self, user_id: str,
|
||||||
self, user_id: str, form_data: PromptForm
|
form_data: PromptForm) -> Optional[PromptModel]:
|
||||||
) -> Optional[PromptModel]:
|
|
||||||
prompt = PromptModel(
|
prompt = PromptModel(
|
||||||
**{
|
**{
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
|
@ -62,8 +61,7 @@ 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())
|
||||||
|
@ -83,14 +81,13 @@ class PromptsTable:
|
||||||
|
|
||||||
def get_prompts(self) -> List[PromptModel]:
|
def get_prompts(self) -> List[PromptModel]:
|
||||||
return [
|
return [
|
||||||
PromptModel(**model_to_dict(prompt))
|
PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select()
|
||||||
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, form_data: PromptForm
|
self, command: str,
|
||||||
) -> Optional[PromptModel]:
|
form_data: PromptForm) -> Optional[PromptModel]:
|
||||||
try:
|
try:
|
||||||
query = Prompt.update(
|
query = Prompt.update(
|
||||||
title=form_data.title,
|
title=form_data.title,
|
||||||
|
|
|
@ -6,15 +6,9 @@ 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
|
||||||
####################
|
####################
|
||||||
|
@ -179,7 +173,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.
|
||||||
log.debug(f"res: {res}")
|
print(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:
|
||||||
|
@ -191,7 +185,7 @@ class TagTable:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"delete_tag: {e}")
|
print("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(
|
||||||
|
@ -204,7 +198,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.
|
||||||
log.debug(f"res: {res}")
|
print(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:
|
||||||
|
@ -216,7 +210,7 @@ class TagTable:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"delete_tag: {e}")
|
print("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:
|
||||||
|
|
|
@ -27,8 +27,7 @@ from utils.utils import (
|
||||||
create_token,
|
create_token,
|
||||||
)
|
)
|
||||||
from utils.misc import parse_duration, validate_email_format
|
from utils.misc import parse_duration, validate_email_format
|
||||||
from utils.webhook import post_webhook
|
from constants import ERROR_MESSAGES
|
||||||
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@ -156,17 +155,6 @@ 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",
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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 (
|
||||||
|
@ -28,11 +27,6 @@ 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()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
@ -84,7 +78,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:
|
||||||
log.exception(e)
|
print(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()
|
||||||
)
|
)
|
||||||
|
@ -101,7 +95,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:
|
||||||
log.exception(e)
|
print(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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,12 +10,7 @@ import uuid
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
from utils.utils import (
|
from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token
|
||||||
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
|
||||||
|
|
||||||
|
@ -48,6 +43,7 @@ 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(
|
async def get_modelfiles(skip: int = 0,
|
||||||
skip: int = 0, limit: int = 50, user=Depends(get_current_user)
|
limit: int = 50,
|
||||||
):
|
user=Depends(get_current_user)):
|
||||||
return Modelfiles.get_modelfiles(skip, limit)
|
return Modelfiles.get_modelfiles(skip, limit)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,16 +36,17 @@ async def get_modelfiles(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[ModelfileResponse])
|
@router.post("/create", response_model=Optional[ModelfileResponse])
|
||||||
async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
|
async def create_new_modelfile(form_data: ModelfileForm,
|
||||||
|
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": json.loads(modelfile.modelfile),
|
"modelfile":
|
||||||
}
|
json.loads(modelfile.modelfile),
|
||||||
)
|
})
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
@ -59,18 +60,17 @@ async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Optional[ModelfileResponse])
|
@router.post("/", response_model=Optional[ModelfileResponse])
|
||||||
async def get_modelfile_by_tag_name(
|
async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
||||||
form_data: ModelfileTagNameForm, user=Depends(get_current_user)
|
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": json.loads(modelfile.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,9 +84,8 @@ async def get_modelfile_by_tag_name(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update", response_model=Optional[ModelfileResponse])
|
@router.post("/update", response_model=Optional[ModelfileResponse])
|
||||||
async def update_modelfile_by_tag_name(
|
async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
||||||
form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
|
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 = {
|
||||||
|
@ -95,15 +94,14 @@ async def update_modelfile_by_tag_name(
|
||||||
}
|
}
|
||||||
|
|
||||||
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": json.loads(modelfile.modelfile),
|
"modelfile":
|
||||||
}
|
json.loads(modelfile.modelfile),
|
||||||
)
|
})
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
@ -117,8 +115,7 @@ async def update_modelfile_by_tag_name(
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/delete", response_model=bool)
|
@router.delete("/delete", response_model=bool)
|
||||||
async def delete_modelfile_by_tag_name(
|
async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
||||||
form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
|
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,7 +7,6 @@ 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
|
||||||
|
@ -15,11 +14,6 @@ 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()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
@ -89,7 +83,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)
|
||||||
log.debug(f"hashed: {hashed}")
|
print(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())
|
||||||
|
|
|
@ -21,6 +21,155 @@ 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]}/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,
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
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
|
||||||
|
@ -23,10 +21,9 @@ try:
|
||||||
|
|
||||||
load_dotenv(find_dotenv("../.env"))
|
load_dotenv(find_dotenv("../.env"))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
log.warning("dotenv not installed, skipping...")
|
print("dotenv not installed, skipping...")
|
||||||
|
|
||||||
WEBUI_NAME = "Open WebUI"
|
WEBUI_NAME = "Open WebUI"
|
||||||
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
|
||||||
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
|
@ -103,47 +100,6 @@ 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
|
||||||
####################################
|
####################################
|
||||||
|
@ -155,7 +111,7 @@ if CUSTOM_NAME:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if r.ok:
|
if r.ok:
|
||||||
if "logo" in data:
|
if "logo" in data:
|
||||||
WEBUI_FAVICON_URL = 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"]
|
||||||
|
@ -169,7 +125,7 @@ if CUSTOM_NAME:
|
||||||
|
|
||||||
WEBUI_NAME = data["name"]
|
WEBUI_NAME = data["name"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
print(e)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,9 +194,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):
|
||||||
log.info("Config file doesn't exist. Creating...")
|
print("Config file doesn't exist. Creating...")
|
||||||
create_config_file(LITELLM_CONFIG_PATH)
|
create_config_file(LITELLM_CONFIG_PATH)
|
||||||
log.info("Config file created successfully.")
|
print("Config file created successfully.")
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
|
@ -253,7 +209,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", "")
|
||||||
INCLUDE_OLLAMA = os.environ.get("INCLUDE_OLLAMA_ENV", "false")
|
INCLUDE_OLLAMA = os.environ.get("INCLUDE_OLLAMA_ENV", "false")
|
||||||
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 = (
|
||||||
|
@ -271,9 +227,6 @@ if ENV == "prod":
|
||||||
else:
|
else:
|
||||||
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
|
||||||
|
@ -303,10 +256,8 @@ OPENAI_API_BASE_URLS = (
|
||||||
OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL
|
OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL
|
||||||
)
|
)
|
||||||
|
|
||||||
OPENAI_API_BASE_URLS = [
|
OPENAI_API_BASE_URLS = [url.strip() for url in OPENAI_API_BASE_URLS.split(";")]
|
||||||
url.strip() if url != "" else "https://api.openai.com/v1"
|
|
||||||
for url in OPENAI_API_BASE_URLS.split(";")
|
|
||||||
]
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI
|
# WEBUI
|
||||||
|
@ -343,19 +294,13 @@ 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").lower() == "true"
|
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", False)
|
||||||
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
|
||||||
|
@ -440,4 +385,3 @@ 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", "")
|
|
||||||
|
|
|
@ -5,13 +5,6 @@ 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__()
|
||||||
|
@ -53,12 +46,9 @@ 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}"
|
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
|
||||||
)
|
)
|
||||||
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="": f"OpenAI API was not found"
|
||||||
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
|
||||||
|
|
||||||
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
|
||||||
|
|
|
@ -1,22 +1,33 @@
|
||||||
{
|
{
|
||||||
"version": 0,
|
"version": "0.0.1",
|
||||||
"ui": {
|
"ui": {
|
||||||
"default_locale": "en-US",
|
|
||||||
"prompt_suggestions": [
|
"prompt_suggestions": [
|
||||||
{
|
{
|
||||||
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
"title": [
|
||||||
|
"Help me study",
|
||||||
|
"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"],
|
"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."
|
"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": ["Tell me a fun fact", "about the Roman Empire"],
|
"title": [
|
||||||
|
"Tell me a fun fact",
|
||||||
|
"about the Roman Empire"
|
||||||
|
],
|
||||||
"content": "Tell me a random 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"],
|
"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."
|
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,7 +4,6 @@ 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
|
||||||
|
@ -32,7 +31,6 @@ 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,
|
||||||
|
@ -40,16 +38,9 @@ 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):
|
||||||
|
@ -67,9 +58,6 @@ 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 = ["*"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +66,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
|
||||||
):
|
):
|
||||||
log.debug(f"request.url.path: {request.url.path}")
|
print(request.url.path)
|
||||||
|
|
||||||
# Read the original request body
|
# Read the original request body
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
|
@ -90,6 +78,7 @@ 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"],
|
||||||
|
@ -100,7 +89,7 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||||
)
|
)
|
||||||
del data["docs"]
|
del data["docs"]
|
||||||
|
|
||||||
log.debug(f"data['messages']: {data['messages']}")
|
print(data["messages"])
|
||||||
|
|
||||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||||
|
|
||||||
|
@ -164,18 +153,11 @@ 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,
|
||||||
|
@ -196,9 +178,10 @@ class ModelFilterConfigForm(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/config/model/filter")
|
@app.post("/api/config/model/filter")
|
||||||
async def update_model_filter_config(
|
async def get_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
|
||||||
|
|
||||||
|
@ -208,39 +191,15 @@ async def update_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,
|
||||||
}
|
}
|
||||||
|
@ -248,7 +207,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 {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
|
return CHANGELOG
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/version/updates")
|
@app.get("/api/version/updates")
|
||||||
|
|
|
@ -45,4 +45,3 @@ PyJWT
|
||||||
pyjwt[crypto]
|
pyjwt[crypto]
|
||||||
|
|
||||||
black
|
black
|
||||||
langfuse
|
|
||||||
|
|
|
@ -28,9 +28,4 @@ if [ "$INCLUDE_OLLAMA" = "true" ]; then
|
||||||
ollama serve &
|
ollama serve &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$USE_CUDA_DOCKER" = "true" ]; then
|
|
||||||
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
|
||||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
|
||||||
fi
|
|
||||||
|
|
||||||
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
|
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
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
|
|
BIN
demo.gif
BIN
demo.gif
Binary file not shown.
Before Width: | Height: | Size: 5 MiB After Width: | Height: | Size: 6.1 MiB |
|
@ -50,18 +50,6 @@ We welcome pull requests. Before submitting one, please:
|
||||||
|
|
||||||
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
|
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
|
||||||
|
|
||||||
### 🌐 Translations and Internationalization
|
|
||||||
|
|
||||||
Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project.
|
|
||||||
|
|
||||||
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language.
|
|
||||||
|
|
||||||
To add a new language:
|
|
||||||
|
|
||||||
- Create a new directory in the `src/lib/i18n/locales` path with the appropriate language code as its name. For instance, if you're adding translations for Spanish (Spain), create a new directory named `es-ES`.
|
|
||||||
- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object.
|
|
||||||
- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`.
|
|
||||||
|
|
||||||
### 🤔 Questions & Feedback
|
### 🤔 Questions & Feedback
|
||||||
|
|
||||||
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
|
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
// i18next-parser.config.ts
|
|
||||||
import { getLanguages } from './src/lib/i18n/index.ts';
|
|
||||||
|
|
||||||
const getLangCodes = async () => {
|
|
||||||
const languages = await getLanguages();
|
|
||||||
return languages.map((l) => l.code);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
contextSeparator: '_',
|
|
||||||
createOldCatalogs: false,
|
|
||||||
defaultNamespace: 'translation',
|
|
||||||
defaultValue: '',
|
|
||||||
indentation: 2,
|
|
||||||
keepRemoved: false,
|
|
||||||
keySeparator: false,
|
|
||||||
lexers: {
|
|
||||||
svelte: ['JavascriptLexer'],
|
|
||||||
js: ['JavascriptLexer'],
|
|
||||||
ts: ['JavascriptLexer'],
|
|
||||||
|
|
||||||
default: ['JavascriptLexer']
|
|
||||||
},
|
|
||||||
lineEnding: 'auto',
|
|
||||||
locales: await getLangCodes(),
|
|
||||||
namespaceSeparator: false,
|
|
||||||
output: 'src/lib/i18n/locales/$LOCALE/$NAMESPACE.json',
|
|
||||||
pluralSeparator: '_',
|
|
||||||
input: 'src/**/*.{js,svelte}',
|
|
||||||
sort: true,
|
|
||||||
verbose: true,
|
|
||||||
failOnWarnings: false,
|
|
||||||
failOnUpdate: false,
|
|
||||||
customValueTemplate: null,
|
|
||||||
resetDefaultValueLocale: null,
|
|
||||||
i18nextOptions: null,
|
|
||||||
yamlOptions: null
|
|
||||||
};
|
|
|
@ -88,7 +88,7 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.ollama.persistence.size | quote }}
|
storage: {{ .Values.ollama.persistence.size | quote }}
|
||||||
storageClassName: {{ .Values.ollama.persistence.storageClass }}
|
storageClass: {{ .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 }}
|
||||||
storageClassName: {{ .Values.webui.persistence.storageClass }}
|
storageClass: {{ .Values.webui.persistence.storageClass }}
|
||||||
{{- with .Values.webui.persistence.selector }}
|
{{- with .Values.webui.persistence.selector }}
|
||||||
selector:
|
selector:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
|
|
|
@ -4,9 +4,6 @@ metadata:
|
||||||
name: {{ include "open-webui.name" . }}
|
name: {{ include "open-webui.name" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "open-webui.labels" . | nindent 4 }}
|
{{- include "open-webui.labels" . | nindent 4 }}
|
||||||
{{- with .Values.webui.service.labels }}
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.webui.service.annotations }}
|
{{- with .Values.webui.service.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
|
@ -14,16 +11,14 @@ metadata:
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
{{- include "open-webui.selectorLabels" . | nindent 4 }}
|
{{- include "open-webui.selectorLabels" . | nindent 4 }}
|
||||||
type: {{ .Values.webui.service.type | default "ClusterIP" }}
|
{{- with .Values.webui.service }}
|
||||||
|
type: {{ .type }}
|
||||||
ports:
|
ports:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
port: {{ .Values.webui.service.port }}
|
port: {{ .port }}
|
||||||
targetPort: http
|
targetPort: http
|
||||||
{{- if .Values.webui.service.nodePort }}
|
{{- if .nodePort }}
|
||||||
nodePort: {{ .Values.webui.service.nodePort | int }}
|
nodePort: {{ .nodePort | int }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.webui.service.loadBalancerClass }}
|
{{- end }}
|
||||||
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
|
|
|
@ -70,5 +70,3 @@ webui:
|
||||||
port: 80
|
port: 80
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
nodePort: ""
|
nodePort: ""
|
||||||
labels: {}
|
|
||||||
loadBalancerClass: ""
|
|
||||||
|
|
|
@ -35,4 +35,4 @@ spec:
|
||||||
volumes:
|
volumes:
|
||||||
- name: webui-volume
|
- name: webui-volume
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: open-webui-pvc
|
claimName: ollama-webui-pvc
|
|
@ -2,8 +2,8 @@ apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: open-webui
|
app: ollama-webui
|
||||||
name: open-webui-pvc
|
name: ollama-webui-pvc
|
||||||
namespace: open-webui
|
namespace: open-webui
|
||||||
spec:
|
spec:
|
||||||
accessModes: ["ReadWriteOnce"]
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
|
6053
package-lock.json
generated
6053
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.116",
|
"version": "0.1.111",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
|
@ -13,8 +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": "black . --exclude \"/venv/\"",
|
"format:backend": "yapf --recursive backend -p -i"
|
||||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
|
@ -28,7 +27,6 @@
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
"i18next-parser": "^8.13.0",
|
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
|
@ -44,13 +42,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.19.7",
|
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"i18next": "^23.10.0",
|
|
||||||
"i18next-browser-languagedetector": "^7.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",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
|
|
|
@ -78,7 +78,3 @@ select {
|
||||||
/* for Chrome */
|
/* for Chrome */
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.katex-mathml {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
25
src/app.html
25
src/app.html
|
@ -8,39 +8,18 @@
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
<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)
|
||||||
) {
|
) {
|
||||||
document.documentElement.classList.add('light');
|
document.documentElement.classList.add('light');
|
||||||
} else if (localStorage.theme && localStorage.theme !== 'system') {
|
} else if (localStorage.theme) {
|
||||||
localStorage.theme.split(' ').forEach((e) => {
|
localStorage.theme.split(' ').forEach((e) => {
|
||||||
document.documentElement.classList.add(e);
|
document.documentElement.classList.add(e);
|
||||||
});
|
});
|
||||||
} else if (localStorage.theme && localStorage.theme === 'system') {
|
|
||||||
systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
document.documentElement.classList.add(systemTheme ? 'dark' : 'light');
|
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
|
|
||||||
if (localStorage.theme === 'system') {
|
|
||||||
if (e.matches) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.documentElement.classList.remove('light');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add('light');
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
|
|
@ -139,7 +139,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
||||||
return res.OPENAI_API_KEY;
|
return res.OPENAI_API_KEY;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
export const getAUTOMATIC1111Url = 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 getImageGenerationEngineUrls = async (token: string = '') => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res.AUTOMATIC1111_BASE_URL;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => {
|
export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => {
|
||||||
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 updateImageGenerationEngineUrls = async (token: string = '', urls:
|
||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...urls
|
url: url
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
@ -203,7 +203,7 @@ export const updateImageGenerationEngineUrls = async (token: string = '', urls:
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res.AUTOMATIC1111_BASE_URL;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageSize = async (token: string = '') => {
|
export const getImageSize = async (token: string = '') => {
|
||||||
|
|
|
@ -139,60 +139,3 @@ 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 cancelOllamaRequest = async (token: string = '', requestId: string) => {
|
export const cancelChatCompletion = 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,73 +390,6 @@ 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,53 +263,3 @@ 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';
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
<script>
|
|
||||||
import { getContext } from 'svelte';
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class=" text-center text-6xl mb-3">📄</div>
|
<div class=" text-center text-6xl mb-3">📄</div>
|
||||||
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
|
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div>
|
||||||
|
|
||||||
<slot
|
<slot
|
||||||
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
||||||
{$i18n.t('Drop any files here to add to the conversation')}
|
Drop any files here to add to the conversation
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Confetti } from 'svelte-confetti';
|
import { Confetti } from 'svelte-confetti';
|
||||||
|
|
||||||
import { WEBUI_NAME, config } from '$lib/stores';
|
import { WEBUI_NAME, config } from '$lib/stores';
|
||||||
|
@ -9,8 +9,6 @@
|
||||||
|
|
||||||
import Modal from './common/Modal.svelte';
|
import Modal from './common/Modal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
let changelog = null;
|
let changelog = null;
|
||||||
|
@ -25,8 +23,7 @@
|
||||||
<div class="px-5 py-4 dark:text-gray-300">
|
<div class="px-5 py-4 dark:text-gray-300">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="text-xl font-bold">
|
<div class="text-xl font-bold">
|
||||||
{$i18n.t('What’s New in')}
|
What’s New in {$WEBUI_NAME}
|
||||||
{$WEBUI_NAME}
|
|
||||||
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
@ -48,7 +45,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1">
|
<div class="flex items-center mt-1">
|
||||||
<div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
|
<div class="text-sm dark:text-gray-200">Release Notes</div>
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||||
<div class="text-sm dark:text-gray-200">
|
<div class="text-sm dark:text-gray-200">
|
||||||
v{WEBUI_VERSION}
|
v{WEBUI_VERSION}
|
||||||
|
@ -111,7 +108,7 @@
|
||||||
}}
|
}}
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
>
|
>
|
||||||
<span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
|
<span class="relative">Okay, Let's Go!</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { updateUserById } from '$lib/apis/users';
|
import { updateUserById } from '$lib/apis/users';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
@ -43,7 +42,7 @@
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
|
<div class=" text-lg font-medium self-center">Edit User</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -85,8 +84,7 @@
|
||||||
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
|
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
{$i18n.t('Created at')}
|
Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')}
|
||||||
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,7 +93,7 @@
|
||||||
|
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Email</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -110,7 +108,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Name</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -124,7 +122,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
<div class=" mb-1 text-xs text-gray-500">New Password</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -142,7 +140,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { downloadDatabase } from '$lib/apis/utils';
|
import { downloadDatabase } from '$lib/apis/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
@ -19,10 +17,10 @@
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
<div class=" mb-2 text-sm font-medium">Database</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
<!-- <div class=" self-center text-xs font-medium">Allow Chat Deletion</div> -->
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
|
@ -48,7 +46,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
<div class=" self-center text-sm font-medium">Download Database</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
|
||||||
import {
|
import {
|
||||||
getDefaultUserRole,
|
getDefaultUserRole,
|
||||||
getJWTExpiresDuration,
|
getJWTExpiresDuration,
|
||||||
|
@ -8,17 +7,13 @@
|
||||||
updateDefaultUserRole,
|
updateDefaultUserRole,
|
||||||
updateJWTExpiresDuration
|
updateJWTExpiresDuration
|
||||||
} from '$lib/apis/auths';
|
} from '$lib/apis/auths';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
let signUpEnabled = true;
|
let signUpEnabled = true;
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
@ -31,32 +26,27 @@
|
||||||
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();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
<div class=" mb-2 text-sm font-medium">General Settings</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
<div class=" self-center text-xs font-medium">Enable New Sign Ups</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -76,7 +66,7 @@
|
||||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
<span class="ml-2 self-center">Enabled</span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -91,25 +81,25 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
<span class="ml-2 self-center">Disabled</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
<div class=" self-center text-xs font-medium">Default User Role</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={defaultUserRole}
|
bind:value={defaultUserRole}
|
||||||
placeholder="Select a theme"
|
placeholder="Select a theme"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
updateDefaultUserRoleHandler(e.target.value);
|
updateDefaultUserRoleHandler(e.target.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="pending">{$i18n.t('pending')}</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="user">{$i18n.t('user')}</option>
|
<option value="user">User</option>
|
||||||
<option value="admin">{$i18n.t('admin')}</option>
|
<option value="admin">Admin</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,24 +108,7 @@
|
||||||
|
|
||||||
<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('Webhook URL')}</div>
|
<div class=" self-center text-xs font-medium">JWT Expiration</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="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
<div class="flex mt-2 space-x-2">
|
||||||
|
@ -148,9 +121,8 @@
|
||||||
</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('Valid time units:')}
|
Valid time units: <span class=" text-gray-300 font-medium"
|
||||||
<span class=" text-gray-300 font-medium"
|
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span
|
||||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -162,7 +134,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
|
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
|
||||||
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
|
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
|
||||||
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
|
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
|
||||||
|
|
||||||
import { onMount, getContext } from 'svelte';
|
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
@ -42,10 +39,10 @@
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">User Permissions</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
|
<div class=" self-center text-xs font-medium">Allow Chat Deletion</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -65,7 +62,7 @@
|
||||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
|
<span class="ml-2 self-center">Allow</span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -80,7 +77,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
|
<span class="ml-2 self-center">Don't Allow</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,21 +89,21 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
|
<div class=" text-sm font-medium">Manage Models</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" space-y-3">
|
<div class=" space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
|
<div class=" text-xs font-medium">Model Whitelisting</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
whitelistEnabled = !whitelistEnabled;
|
whitelistEnabled = !whitelistEnabled;
|
||||||
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
|
}}>{whitelistEnabled ? 'On' : 'Off'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,7 +119,7 @@
|
||||||
bind:value={modelId}
|
bind:value={modelId}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>Select a model</option>
|
||||||
{#each $models.filter((model) => model.id) as model}
|
{#each $models.filter((model) => model.id) as model}
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
|
||||||
>{model.name}</option
|
>{model.name}</option
|
||||||
|
@ -177,8 +174,7 @@
|
||||||
|
|
||||||
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
|
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
|
||||||
<div class=" text-xs font-medium">
|
<div class=" text-xs font-medium">
|
||||||
{whitelistModels.length}
|
{whitelistModels.length} Model(s) Whitelisted
|
||||||
{$i18n.t('Model(s) Whitelisted')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,7 +189,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Database from './Settings/Database.svelte';
|
import Database from './Settings/Database.svelte';
|
||||||
|
|
||||||
import General from './Settings/General.svelte';
|
import General from './Settings/General.svelte';
|
||||||
import Users from './Settings/Users.svelte';
|
import Users from './Settings/Users.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
let selectedTab = 'general';
|
let selectedTab = 'general';
|
||||||
|
@ -16,7 +13,7 @@
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
|
<div class=" text-lg font-medium self-center">Admin Settings</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -64,7 +61,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('General')}</div>
|
<div class=" self-center">General</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -88,7 +85,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Users')}</div>
|
<div class=" self-center">Users</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -116,7 +113,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Database')}</div>
|
<div class=" self-center">Database</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:min-h-[380px]">
|
<div class="flex-1 md:min-h-[380px]">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
import { settings } from '$lib/stores';
|
import { settings } from '$lib/stores';
|
||||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
|
@ -14,8 +14,6 @@
|
||||||
import { transcribeAudio } from '$lib/apis/audio';
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let submitPrompt: Function;
|
export let submitPrompt: Function;
|
||||||
export let stopResponse: Function;
|
export let stopResponse: Function;
|
||||||
|
|
||||||
|
@ -211,11 +209,11 @@
|
||||||
// Event triggered when an error occurs
|
// Event triggered when an error occurs
|
||||||
speechRecognition.onerror = function (event) {
|
speechRecognition.onerror = function (event) {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
|
toast.error(`Speech recognition error: ${event.error}`);
|
||||||
isRecording = false;
|
isRecording = false;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
|
toast.error('SpeechRecognition API is not supported in this browser.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,15 +333,12 @@
|
||||||
uploadDoc(file);
|
uploadDoc(file);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(
|
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
||||||
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
|
||||||
{ file_type: file['type'] }
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
uploadDoc(file);
|
uploadDoc(file);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`File not found.`));
|
toast.error(`File not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,16 +477,13 @@
|
||||||
filesInputElement.value = '';
|
filesInputElement.value = '';
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(
|
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
||||||
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
|
||||||
{ file_type: file['type'] }
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
uploadDoc(file);
|
uploadDoc(file);
|
||||||
filesInputElement.value = '';
|
filesInputElement.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`File not found.`));
|
toast.error(`File not found.`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -578,7 +570,7 @@
|
||||||
{file.name}
|
{file.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
<div class=" text-gray-500 text-sm">Document</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if file.type === 'collection'}
|
{:else if file.type === 'collection'}
|
||||||
|
@ -606,7 +598,7 @@
|
||||||
{file?.title ?? `#${file.name}`}
|
{file?.title ?? `#${file.name}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
<div class=" text-gray-500 text-sm">Collection</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -640,7 +632,7 @@
|
||||||
<div class=" flex">
|
<div class=" flex">
|
||||||
{#if fileUploadEnabled}
|
{#if fileUploadEnabled}
|
||||||
<div class=" self-end mb-2 ml-1">
|
<div class=" self-end mb-2 ml-1">
|
||||||
<Tooltip content={$i18n.t('Upload files')}>
|
<Tooltip content="Upload files">
|
||||||
<button
|
<button
|
||||||
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -672,8 +664,8 @@
|
||||||
placeholder={chatInputPlaceholder !== ''
|
placeholder={chatInputPlaceholder !== ''
|
||||||
? chatInputPlaceholder
|
? chatInputPlaceholder
|
||||||
: isRecording
|
: isRecording
|
||||||
? $i18n.t('Listening...')
|
? 'Listening...'
|
||||||
: $i18n.t('Send a Message')}
|
: '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) {
|
||||||
|
@ -812,7 +804,7 @@
|
||||||
|
|
||||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||||
<Tooltip content={$i18n.t('Record voice')}>
|
<Tooltip content="Record voice">
|
||||||
{#if speechRecognitionEnabled}
|
{#if speechRecognitionEnabled}
|
||||||
<button
|
<button
|
||||||
id="voice-input-button"
|
id="voice-input-button"
|
||||||
|
@ -881,7 +873,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Send message')}>
|
<Tooltip content="Send message">
|
||||||
<button
|
<button
|
||||||
class="{prompt !== ''
|
class="{prompt !== ''
|
||||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||||
|
@ -927,7 +919,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="mt-1.5 text-xs text-gray-500 text-center">
|
<div class="mt-1.5 text-xs text-gray-500 text-center">
|
||||||
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
LLMs can make mistakes. Verify important information.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,11 +3,9 @@
|
||||||
|
|
||||||
import { documents } from '$lib/stores';
|
import { documents } from '$lib/stores';
|
||||||
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
|
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
@ -119,7 +117,7 @@
|
||||||
{doc?.title ?? `#${doc.name}`}
|
{doc?.title ?? `#${doc.name}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div>
|
<div class=" text-xs text-gray-600 line-clamp-1">Collection</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" font-medium text-black line-clamp-1">
|
<div class=" font-medium text-black line-clamp-1">
|
||||||
#{doc.name} ({doc.filename})
|
#{doc.name} ({doc.filename})
|
||||||
|
@ -142,9 +140,7 @@
|
||||||
confirmSelectWeb(url);
|
confirmSelectWeb(url);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(
|
|
||||||
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -153,7 +149,7 @@
|
||||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
|
<div class=" text-xs text-gray-600 line-clamp-1">Web</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
import { generatePrompt } from '$lib/apis/ollama';
|
import { generatePrompt } from '$lib/apis/ollama';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let user = null;
|
export let user = null;
|
||||||
|
|
||||||
|
@ -43,7 +41,7 @@
|
||||||
user = JSON.parse(JSON.stringify(model.name));
|
user = JSON.parse(JSON.stringify(model.name));
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
|
chatInputPlaceholder = `'${model.name}' is thinking...`;
|
||||||
|
|
||||||
const chatInputElement = document.getElementById('chat-textarea');
|
const chatInputElement = document.getElementById('chat-textarea');
|
||||||
|
|
||||||
|
@ -115,9 +113,7 @@
|
||||||
toast.error(error.error);
|
toast.error(error.error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
|
||||||
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { prompts } from '$lib/stores';
|
import { prompts } from '$lib/stores';
|
||||||
import { findWordIndices } from '$lib/utils';
|
import { findWordIndices } from '$lib/utils';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
let selectedCommandIdx = 0;
|
let selectedCommandIdx = 0;
|
||||||
let filteredPromptCommands = [];
|
let filteredPromptCommands = [];
|
||||||
|
@ -31,7 +29,7 @@
|
||||||
|
|
||||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
if (command.content.includes('{{CLIPBOARD}}')) {
|
||||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
toast.error('Failed to read clipboard contents');
|
||||||
return '{{CLIPBOARD}}';
|
return '{{CLIPBOARD}}';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -115,9 +113,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
{$i18n.t(
|
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
|
||||||
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
|
input after each replacement.
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||||
|
@ -13,8 +13,6 @@
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
export let sendPrompt: Function;
|
export let sendPrompt: Function;
|
||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
|
@ -69,7 +67,7 @@
|
||||||
navigator.clipboard.writeText(text).then(
|
navigator.clipboard.writeText(text).then(
|
||||||
function () {
|
function () {
|
||||||
console.log('Async: Copying to clipboard was successful!');
|
console.log('Async: Copying to clipboard was successful!');
|
||||||
toast.success($i18n.t('Copying to clipboard was successful!'));
|
toast.success('Copying to clipboard was successful!');
|
||||||
},
|
},
|
||||||
function (err) {
|
function (err) {
|
||||||
console.error('Async: Could not copy text: ', err);
|
console.error('Async: Could not copy text: ', err);
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let models = [];
|
export let models = [];
|
||||||
export let modelfiles = [];
|
export let modelfiles = [];
|
||||||
|
@ -33,7 +31,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=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -41,7 +39,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=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
|
@ -66,9 +64,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
|
<div class=" line-clamp-1">Hello, {$user.name}</div>
|
||||||
|
|
||||||
<div>{$i18n.t('How can I help you today?')}</div>
|
<div>How can I help you today?</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,9 +8,7 @@
|
||||||
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -318,7 +316,7 @@
|
||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||||
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Name>
|
</Name>
|
||||||
|
@ -362,7 +360,7 @@
|
||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -371,7 +369,7 @@
|
||||||
cancelEditMessage();
|
cancelEditMessage();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Cancel')}
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -422,7 +420,7 @@
|
||||||
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<div class="flex self-center min-w-fit -mt-1">
|
<div class="flex self-center min-w-fit">
|
||||||
<button
|
<button
|
||||||
class="self-center dark:hover:text-white hover:text-black transition"
|
class="self-center dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { tick, createEventDispatcher, getContext } from 'svelte';
|
import { tick, createEventDispatcher } from 'svelte';
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import ProfileImage from './ProfileImage.svelte';
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
import { modelfiles, settings } from '$lib/stores';
|
import { modelfiles, settings } from '$lib/stores';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let user;
|
export let user;
|
||||||
|
@ -67,18 +65,17 @@
|
||||||
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
||||||
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('You')}
|
You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
||||||
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else if $settings.showUsername}
|
{:else if $settings.showUsername}
|
||||||
{user.name}
|
{user.name}
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('You')}
|
You
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||||
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Name>
|
</Name>
|
||||||
|
@ -126,7 +123,7 @@
|
||||||
{file.name}
|
{file.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
<div class=" text-gray-500 text-sm">Document</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{:else if file.type === 'collection'}
|
{:else if file.type === 'collection'}
|
||||||
|
@ -155,7 +152,7 @@
|
||||||
{file?.title ?? `#${file.name}`}
|
{file?.title ?? `#${file.name}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
<div class=" text-gray-500 text-sm">Collection</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -184,7 +181,7 @@
|
||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Save & Submit')}
|
Save & Submit
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -193,7 +190,7 @@
|
||||||
cancelEditMessage();
|
cancelEditMessage();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Cancel')}
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,7 +200,7 @@
|
||||||
|
|
||||||
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<div class="flex self-center -mt-1">
|
<div class="flex self-center">
|
||||||
<button
|
<button
|
||||||
class="self-center dark:hover:text-white hover:text-black transition"
|
class="self-center dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
<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 } 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');
|
|
||||||
|
|
||||||
export let selectedModels = [''];
|
export let selectedModels = [''];
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
@ -16,7 +10,7 @@
|
||||||
const saveDefaultModel = async () => {
|
const saveDefaultModel = async () => {
|
||||||
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
||||||
if (hasEmptyModel.length) {
|
if (hasEmptyModel.length) {
|
||||||
toast.error($i18n.t('Choose a model before saving...'));
|
toast.error('Choose a model before saving...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.set({ ...$settings, models: selectedModels });
|
settings.set({ ...$settings, models: selectedModels });
|
||||||
|
@ -26,7 +20,7 @@
|
||||||
console.log('setting default models globally');
|
console.log('setting default models globally');
|
||||||
await setDefaultModels(localStorage.token, selectedModels.join(','));
|
await setDefaultModels(localStorage.token, selectedModels.join(','));
|
||||||
}
|
}
|
||||||
toast.success($i18n.t('Default model updated'));
|
toast.success('Default model updated');
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (selectedModels.length > 0 && $models.length > 0) {
|
$: if (selectedModels.length > 0 && $models.length > 0) {
|
||||||
|
@ -36,30 +30,34 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col mt-0.5 w-full">
|
<div class="flex flex-col my-2">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex w-full">
|
<div class="flex">
|
||||||
<div class="overflow-hidden w-full">
|
<select
|
||||||
<div class="mr-0.5 max-w-full">
|
id="models"
|
||||||
<Selector
|
class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400"
|
||||||
placeholder={$i18n.t('Select a model')}
|
|
||||||
items={$models
|
|
||||||
.filter((model) => model.name !== 'hr')
|
|
||||||
.map((model) => ({
|
|
||||||
value: model.id,
|
|
||||||
label: model.name,
|
|
||||||
info: model
|
|
||||||
}))}
|
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
/>
|
{disabled}
|
||||||
</div>
|
>
|
||||||
</div>
|
<option class=" text-gray-700" value="" selected disabled>Select a model</option>
|
||||||
|
|
||||||
|
{#each $models as model}
|
||||||
|
{#if model.name === 'hr'}
|
||||||
|
<hr />
|
||||||
|
{:else}
|
||||||
|
<option value={model.id} class="text-gray-700 text-lg"
|
||||||
|
>{model.name +
|
||||||
|
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
{#if selectedModelIdx === 0}
|
{#if selectedModelIdx === 0}
|
||||||
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
|
|
||||||
<Tooltip content="Add Model">
|
|
||||||
<button
|
<button
|
||||||
class=" "
|
class=" self-center {selectedModelIdx === 0
|
||||||
|
? 'mr-3'
|
||||||
|
: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedModels = [...selectedModels, ''];
|
selectedModels = [...selectedModels, ''];
|
||||||
|
@ -76,12 +74,12 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
|
|
||||||
<Tooltip content="Remove Model">
|
|
||||||
<button
|
<button
|
||||||
|
class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx ===
|
||||||
|
0
|
||||||
|
? 'mr-3'
|
||||||
|
: 'mr-7'}"
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedModels.splice(selectedModelIdx, 1);
|
selectedModels.splice(selectedModelIdx, 1);
|
||||||
|
@ -99,13 +97,41 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
{/if}
|
||||||
</div>
|
|
||||||
|
{#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-0.5 ml-1 text-[0.7rem] text-gray-500">
|
<div class="text-left mt-1.5 text-xs text-gray-500">
|
||||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
<button on:click={saveDefaultModel}> Set as default</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,389 +0,0 @@
|
||||||
<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>
|
|
|
@ -4,9 +4,7 @@
|
||||||
import { WEBUI_VERSION } from '$lib/constants';
|
import { WEBUI_VERSION } from '$lib/constants';
|
||||||
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
|
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
|
||||||
import { compareVersion } from '$lib/utils';
|
import { compareVersion } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
let ollamaVersion = '';
|
let ollamaVersion = '';
|
||||||
|
|
||||||
|
@ -45,8 +43,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
||||||
<div>
|
<div>
|
||||||
{$WEBUI_NAME}
|
{$WEBUI_NAME} Version
|
||||||
{$i18n.t('Version')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
|
@ -59,10 +56,10 @@
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{updateAvailable === null
|
{updateAvailable === null
|
||||||
? $i18n.t('Checking for updates...')
|
? 'Checking for updates...'
|
||||||
: updateAvailable
|
: updateAvailable
|
||||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
? `(v${version.latest} available!)`
|
||||||
: $i18n.t('(latest)')}
|
: '(latest)'}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -72,7 +69,7 @@
|
||||||
showChangelog.set(true);
|
showChangelog.set(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>{$i18n.t("See what's new")}</div>
|
<div>See what's new</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -82,7 +79,7 @@
|
||||||
checkForVersionUpdates();
|
checkForVersionUpdates();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Check for updates')}
|
Check for updates
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,7 +88,7 @@
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||||
{ollamaVersion ?? 'N/A'}
|
{ollamaVersion ?? 'N/A'}
|
||||||
|
@ -126,8 +123,7 @@
|
||||||
</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')}
|
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"
|
||||||
target="_blank">Timothy J. Baek</a
|
target="_blank">Timothy J. Baek</a
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
import { updateUserProfile } from '$lib/apis/auths';
|
import { updateUserProfile } from '$lib/apis/auths';
|
||||||
|
@ -9,8 +9,6 @@
|
||||||
import { getGravatarUrl } from '$lib/apis/utils';
|
import { getGravatarUrl } from '$lib/apis/utils';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
let profileImageUrl = '';
|
let profileImageUrl = '';
|
||||||
|
@ -40,7 +38,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<input
|
<input
|
||||||
id="profile-image-input"
|
id="profile-image-input"
|
||||||
bind:this={profileImageInputElement}
|
bind:this={profileImageInputElement}
|
||||||
|
@ -103,7 +101,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Profile</div>
|
||||||
|
|
||||||
<div class="flex space-x-5">
|
<div class="flex space-x-5">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
@ -145,13 +143,13 @@
|
||||||
const url = await getGravatarUrl($user.email);
|
const url = await getGravatarUrl($user.email);
|
||||||
|
|
||||||
profileImageUrl = url;
|
profileImageUrl = url;
|
||||||
}}>{$i18n.t('Use Gravatar')}</button
|
}}>Use Gravatar</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Name</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -172,7 +170,7 @@
|
||||||
|
|
||||||
<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 Token')}</div>
|
<div class=" self-center text-xs font-medium">JWT Token</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2">
|
<div class="flex mt-2">
|
||||||
|
@ -282,7 +280,7 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { updateUserPassword } from '$lib/apis/auths';
|
import { updateUserPassword } from '$lib/apis/auths';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
let currentPassword = '';
|
let currentPassword = '';
|
||||||
let newPassword = '';
|
let newPassword = '';
|
||||||
|
@ -20,7 +17,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Successfully updated.'));
|
toast.success('Successfully updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPassword = '';
|
currentPassword = '';
|
||||||
|
@ -43,20 +40,20 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('Change Password')}</div>
|
<div class=" font-medium">Change Password</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
show = !show;
|
show = !show;
|
||||||
}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
}}>{show ? 'Hide' : 'Show'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class=" py-2.5 space-y-1.5">
|
<div class=" py-2.5 space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -70,7 +67,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
<div class=" mb-1 text-xs text-gray-500">New Password</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -84,7 +81,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -102,7 +99,7 @@
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
|
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
|
||||||
>
|
>
|
||||||
{$i18n.t('Update password')}
|
Update password
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
|
@ -57,14 +55,14 @@
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
|
<div class=" text-sm font-medium">Parameters</div>
|
||||||
|
|
||||||
<AdvancedParams bind:options />
|
<AdvancedParams bind:options />
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div class=" py-1 w-full justify-between">
|
<div class=" py-1 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('Keep Alive')}</div>
|
<div class=" self-center text-xs font-medium">Keep Alive</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -74,9 +72,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if keepAlive === null}
|
{#if keepAlive === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +84,7 @@
|
||||||
<input
|
<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"
|
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"
|
type="text"
|
||||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
|
||||||
bind:value={keepAlive}
|
bind:value={keepAlive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,7 +93,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-1 flex w-full justify-between">
|
<div class=" py-1 flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
<div class=" self-center text-sm font-medium">Request Mode</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -104,7 +102,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if requestFormat === ''}
|
{#if requestFormat === ''}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else if requestFormat === 'json'}
|
{:else if requestFormat === 'json'}
|
||||||
<!-- <svg
|
<!-- <svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -116,7 +114,7 @@
|
||||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||||
/>
|
/>
|
||||||
</svg> -->
|
</svg> -->
|
||||||
<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
|
<span class="ml-2 self-center"> JSON </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -149,7 +147,7 @@
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let options = {
|
export let options = {
|
||||||
// Advanced
|
// Advanced
|
||||||
seed: 0,
|
seed: 0,
|
||||||
|
@ -24,7 +20,7 @@
|
||||||
<div class=" space-y-3 text-xs">
|
<div class=" space-y-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
|
<div class=" w-20 text-xs font-medium self-center">Seed</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<input
|
<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"
|
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"
|
||||||
|
@ -40,12 +36,12 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
|
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<input
|
<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"
|
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"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter stop sequence')}
|
placeholder="Enter Stop Sequence"
|
||||||
bind:value={options.stop}
|
bind:value={options.stop}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -55,7 +51,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Temperature')}</div>
|
<div class=" self-center text-xs font-medium">Temperature</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -65,9 +61,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.temperature === ''}
|
{#if options.temperature === ''}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,7 +97,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Mirostat')}</div>
|
<div class=" self-center text-xs font-medium">Mirostat</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -111,9 +107,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.mirostat === ''}
|
{#if options.mirostat === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -147,7 +143,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Mirostat Eta')}</div>
|
<div class=" self-center text-xs font-medium">Mirostat Eta</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -157,9 +153,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.mirostat_eta === ''}
|
{#if options.mirostat_eta === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,7 +189,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Mirostat Tau')}</div>
|
<div class=" self-center text-xs font-medium">Mirostat Tau</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -203,9 +199,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.mirostat_tau === ''}
|
{#if options.mirostat_tau === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -239,7 +235,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Top K')}</div>
|
<div class=" self-center text-xs font-medium">Top K</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -249,9 +245,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.top_k === ''}
|
{#if options.top_k === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -285,7 +281,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Top P')}</div>
|
<div class=" self-center text-xs font-medium">Top P</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -295,9 +291,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.top_p === ''}
|
{#if options.top_p === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -331,7 +327,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Repeat Penalty')}</div>
|
<div class=" self-center text-xs font-medium">Repeat Penalty</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -341,9 +337,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.repeat_penalty === ''}
|
{#if options.repeat_penalty === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -377,7 +373,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Repeat Last N')}</div>
|
<div class=" self-center text-xs font-medium">Repeat Last N</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -387,9 +383,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.repeat_last_n === ''}
|
{#if options.repeat_last_n === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -423,7 +419,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Tfs Z')}</div>
|
<div class=" self-center text-xs font-medium">Tfs Z</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -433,9 +429,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.tfs_z === ''}
|
{#if options.tfs_z === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -469,7 +465,7 @@
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Context Length')}</div>
|
<div class=" self-center text-xs font-medium">Context Length</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -479,9 +475,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.num_ctx === ''}
|
{#if options.num_ctx === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -514,7 +510,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 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('Max Tokens')}</div>
|
<div class=" self-center text-xs font-medium">Max Tokens</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -524,9 +520,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.num_predict === ''}
|
{#if options.num_predict === ''}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
|
@ -103,36 +101,32 @@
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">STT Settings</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
<div class=" self-center text-xs font-medium">Speech-to-Text Engine</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={STTEngine}
|
bind:value={STTEngine}
|
||||||
placeholder="Select a mode"
|
placeholder="Select a mode"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
if (e.target.value !== '') {
|
if (e.target.value !== '') {
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
||||||
toast.error(
|
toast.error(`Permission denied when accessing microphone: ${err}`);
|
||||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
|
||||||
error: err
|
|
||||||
})
|
|
||||||
);
|
|
||||||
STTEngine = '';
|
STTEngine = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
<option value="">Default (Web API)</option>
|
||||||
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
|
<option value="whisper-local">Whisper (Local)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
|
<div class=" self-center text-xs font-medium">Conversation Mode</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -142,17 +136,15 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if conversationMode === true}
|
{#if conversationMode === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">Auto-send input after 3 sec.</div>
|
||||||
{$i18n.t('Auto-send input after 3 sec.')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -162,22 +154,22 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if speechAutoSend === true}
|
{#if speechAutoSend === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">TTS Settings</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
<div class=" self-center text-xs font-medium">Text-to-Speech Engine</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={TTSEngine}
|
bind:value={TTSEngine}
|
||||||
placeholder="Select a mode"
|
placeholder="Select a mode"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
|
@ -190,14 +182,14 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
<option value="">Default (Web API)</option>
|
||||||
<option value="openai">{$i18n.t('Open AI')}</option>
|
<option value="openai">Open AI</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
<div class=" self-center text-xs font-medium">Auto-playback response</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -207,9 +199,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if responseAutoPlayback === true}
|
{#if responseAutoPlayback === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -219,7 +211,7 @@
|
||||||
|
|
||||||
{#if TTSEngine === ''}
|
{#if TTSEngine === ''}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Set Voice</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
|
@ -227,7 +219,7 @@
|
||||||
bind:value={speaker}
|
bind:value={speaker}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
>
|
>
|
||||||
<option value="" selected>{$i18n.t('Default')}</option>
|
<option value="" selected>Default</option>
|
||||||
{#each voices.filter((v) => v.localService === true) as voice}
|
{#each voices.filter((v) => v.localService === true) as voice}
|
||||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
||||||
>
|
>
|
||||||
|
@ -238,7 +230,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if TTSEngine === 'openai'}
|
{:else if TTSEngine === 'openai'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Set Voice</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
|
@ -262,7 +254,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
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 {
|
||||||
|
@ -12,12 +13,10 @@
|
||||||
getChatList
|
getChatList
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
// Chats
|
// Chats
|
||||||
let saveChatHistory = true;
|
let saveChatHistory = true;
|
||||||
|
@ -100,13 +99,13 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]">
|
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||||
<div class=" space-y-2">
|
<div class=" space-y-2">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
|
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
|
||||||
>
|
>
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div>
|
<div class=" self-center text-sm font-medium">Chat History</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -130,7 +129,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center"> {$i18n.t('On')} </span>
|
<span class="ml-2 self-center"> On </span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -148,13 +147,13 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-left w-full font-medium mt-0.5">
|
<div class="text-xs text-left w-full font-medium mt-0.5">
|
||||||
{$i18n.t('This setting does not sync across browsers or devices.')}
|
This setting does not sync across browsers or devices.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -189,7 +188,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div>
|
<div class=" self-center text-sm font-medium">Import Chats</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
|
@ -211,7 +210,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
|
<div class=" self-center text-sm font-medium">Export Chats</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -233,7 +232,7 @@
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{$i18n.t('Are you sure?')}</span>
|
<span>Are you sure?</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-1.5 items-center">
|
<div class="flex space-x-1.5 items-center">
|
||||||
|
@ -297,7 +296,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div>
|
<div class=" self-center text-sm font-medium">Delete Chats</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -325,9 +324,39 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">
|
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div>
|
||||||
{$i18n.t('Export All Chats (All Users)')}
|
</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('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>
|
||||||
|
<div class=" self-center text-sm font-medium">Reset Vector Storage</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { models, user } from '$lib/stores';
|
import { models, user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
|
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
|
||||||
|
@ -12,8 +12,6 @@
|
||||||
} from '$lib/apis/openai';
|
} from '$lib/apis/openai';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
|
||||||
// External
|
// External
|
||||||
|
@ -44,7 +42,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ollamaVersion) {
|
if (ollamaVersion) {
|
||||||
toast.success($i18n.t('Server connection verified'));
|
toast.success('Server connection verified');
|
||||||
await models.set(await getModels());
|
await models.set(await getModels());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -65,17 +63,17 @@
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" pr-1.5 overflow-y-scroll max-h-[22rem] space-y-3">
|
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
|
||||||
<div class=" space-y-3">
|
<div class=" space-y-3">
|
||||||
<div class="mt-2 space-y-2 pr-1.5">
|
<div class="mt-2 space-y-2 pr-1.5">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
<div class=" font-medium">OpenAI API</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showOpenAI = !showOpenAI;
|
showOpenAI = !showOpenAI;
|
||||||
}}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
}}>{showOpenAI ? 'Hide' : 'Show'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -86,7 +84,7 @@
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder="API Base URL"
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -95,7 +93,7 @@
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder="API Key"
|
||||||
bind:value={OPENAI_API_KEYS[idx]}
|
bind:value={OPENAI_API_KEYS[idx]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -145,8 +143,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('WebUI will make requests to')}
|
WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span>
|
||||||
<span class=" text-gray-200">'{url}/models'</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,7 +154,7 @@
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
|
||||||
<div class="flex w-full gap-1.5">
|
<div class="flex w-full gap-1.5">
|
||||||
<div class="flex-1 flex flex-col gap-2">
|
<div class="flex-1 flex flex-col gap-2">
|
||||||
{#each OLLAMA_BASE_URLS as url, idx}
|
{#each OLLAMA_BASE_URLS as url, idx}
|
||||||
|
@ -236,13 +233,13 @@
|
||||||
</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('Trouble accessing Ollama?')}
|
Trouble accessing Ollama?
|
||||||
<a
|
<a
|
||||||
class=" text-gray-300 font-medium underline"
|
class=" text-gray-300 font-medium"
|
||||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{$i18n.t('Click here for help.')}
|
Click here for help.
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -253,7 +250,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { getLanguages } from '$lib/i18n';
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { models, user, theme } from '$lib/stores';
|
import { models, user } from '$lib/stores';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||||
|
|
||||||
|
@ -14,11 +11,8 @@
|
||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
|
||||||
// General
|
// General
|
||||||
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark'];
|
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
||||||
let selectedTheme = 'system';
|
let theme = 'dark';
|
||||||
|
|
||||||
let languages = [];
|
|
||||||
let lang = $i18n.language;
|
|
||||||
let notificationEnabled = false;
|
let notificationEnabled = false;
|
||||||
let system = '';
|
let system = '';
|
||||||
|
|
||||||
|
@ -69,11 +63,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
selectedTheme = localStorage.theme ?? 'system';
|
|
||||||
|
|
||||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||||
languages = await getLanguages();
|
|
||||||
|
|
||||||
|
theme = localStorage.theme ?? 'dark';
|
||||||
notificationEnabled = settings.notificationEnabled ?? false;
|
notificationEnabled = settings.notificationEnabled ?? false;
|
||||||
system = settings.system ?? '';
|
system = settings.system ?? '';
|
||||||
|
|
||||||
|
@ -89,103 +81,77 @@
|
||||||
options = { ...options, ...settings.options };
|
options = { ...options, ...settings.options };
|
||||||
options.stop = (settings?.options?.stop ?? []).join(',');
|
options.stop = (settings?.options?.stop ?? []).join(',');
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
const applyTheme = (_theme: string) => {
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme;
|
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem]">
|
||||||
|
<div class="">
|
||||||
|
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
|
||||||
|
|
||||||
if (_theme === 'system') {
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
<div class=" self-center text-xs font-medium">Theme</div>
|
||||||
}
|
<div class="flex items-center relative">
|
||||||
|
<div class=" absolute right-16">
|
||||||
|
{#if theme === 'dark'}
|
||||||
|
<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="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else if theme === 'light'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4 self-center"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
if (themeToApply === 'dark' && !_theme.includes('oled')) {
|
<select
|
||||||
document.documentElement.style.setProperty('--color-gray-900', '#171717');
|
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||||
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
|
bind:value={theme}
|
||||||
}
|
placeholder="Select a theme"
|
||||||
|
on:change={(e) => {
|
||||||
|
localStorage.theme = theme;
|
||||||
|
|
||||||
themes
|
themes
|
||||||
.filter((e) => e !== themeToApply)
|
.filter((e) => e !== theme)
|
||||||
.forEach((e) => {
|
.forEach((e) => {
|
||||||
e.split(' ').forEach((e) => {
|
e.split(' ').forEach((e) => {
|
||||||
document.documentElement.classList.remove(e);
|
document.documentElement.classList.remove(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
themeToApply.split(' ').forEach((e) => {
|
theme.split(' ').forEach((e) => {
|
||||||
document.documentElement.classList.add(e);
|
document.documentElement.classList.add(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(_theme);
|
console.log(theme);
|
||||||
};
|
|
||||||
|
|
||||||
const themeChangeHandler = (_theme: string) => {
|
|
||||||
theme.set(_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);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
|
||||||
<div class=" pr-1.5 overflow-y-scroll max-h-[22rem]">
|
|
||||||
<div class="">
|
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
|
|
||||||
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<select
|
|
||||||
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
|
||||||
bind:value={selectedTheme}
|
|
||||||
placeholder="Select a theme"
|
|
||||||
on:change={() => themeChangeHandler(selectedTheme)}
|
|
||||||
>
|
|
||||||
<option value="system">⚙️ {$i18n.t('System')}</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="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
|
||||||
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div>
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<select
|
|
||||||
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
|
||||||
bind:value={lang}
|
|
||||||
placeholder="Select a language"
|
|
||||||
on:change={(e) => {
|
|
||||||
$i18n.changeLanguage(lang);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each languages as language}
|
<option value="dark">Dark</option>
|
||||||
<option value={language['code']}>{language['title']}</option>
|
<option value="light">Light</option>
|
||||||
{/each}
|
<option value="rose-pine dark">Rosé Pine</option>
|
||||||
|
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $i18n.language === 'en-US'}
|
|
||||||
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
Couldn't find your language?
|
|
||||||
<a
|
|
||||||
class=" text-gray-300 font-medium underline"
|
|
||||||
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Help us translate Open WebUI!
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div>
|
<div class=" self-center text-xs font-medium">Notification</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -195,9 +161,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if notificationEnabled === true}
|
{#if notificationEnabled === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -207,7 +173,7 @@
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
<div class=" my-2.5 text-sm font-medium">System Prompt</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={system}
|
bind:value={system}
|
||||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
|
@ -217,13 +183,13 @@
|
||||||
|
|
||||||
<div class="mt-2 space-y-3 pr-1.5">
|
<div class="mt-2 space-y-3 pr-1.5">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
|
<div class=" font-medium">Advanced Parameters</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showAdvanced = !showAdvanced;
|
showAdvanced = !showAdvanced;
|
||||||
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
}}>{showAdvanced ? 'Hide' : 'Show'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -233,7 +199,7 @@
|
||||||
|
|
||||||
<div class=" py-1 w-full justify-between">
|
<div class=" py-1 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('Keep Alive')}</div>
|
<div class=" self-center text-xs font-medium">Keep Alive</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -243,9 +209,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if keepAlive === null}
|
{#if keepAlive === null}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
<span class="ml-2 self-center"> Custom </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -255,7 +221,7 @@
|
||||||
<input
|
<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"
|
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"
|
type="text"
|
||||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
|
||||||
bind:value={keepAlive}
|
bind:value={keepAlive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -264,7 +230,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-1 flex w-full justify-between">
|
<div class=" py-1 flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
<div class=" self-center text-sm font-medium">Request Mode</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -273,7 +239,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if requestFormat === ''}
|
{#if requestFormat === ''}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
<span class="ml-2 self-center"> Default </span>
|
||||||
{:else if requestFormat === 'json'}
|
{:else if requestFormat === 'json'}
|
||||||
<!-- <svg
|
<!-- <svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -285,7 +251,7 @@
|
||||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||||
/>
|
/>
|
||||||
</svg> -->
|
</svg> -->
|
||||||
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
|
<span class="ml-2 self-center"> JSON </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -320,7 +286,7 @@
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount } 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,
|
||||||
getImageGenerationEngineUrls,
|
updateAUTOMATIC1111Url,
|
||||||
updateImageGenerationEngineUrls,
|
|
||||||
updateImageSize,
|
updateImageSize,
|
||||||
getImageSteps,
|
getImageSteps,
|
||||||
updateImageSteps,
|
updateImageSteps,
|
||||||
|
@ -21,8 +21,6 @@
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
@ -31,8 +29,6 @@
|
||||||
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 = '';
|
||||||
|
@ -51,47 +47,24 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUrlHandler = async () => {
|
const updateAUTOMATIC1111UrlHandler = async () => {
|
||||||
if (imageGenerationEngine === 'comfyui') {
|
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
|
||||||
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
(error) => {
|
||||||
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) {
|
||||||
COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
|
AUTOMATIC1111_BASE_URL = res;
|
||||||
|
|
||||||
await getModels();
|
await getModels();
|
||||||
|
|
||||||
if (models) {
|
if (models) {
|
||||||
toast.success($i18n.t('Server connection verified'));
|
toast.success('Server connection verified');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
|
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 () => {
|
||||||
|
@ -126,11 +99,7 @@
|
||||||
imageGenerationEngine = res.engine;
|
imageGenerationEngine = res.engine;
|
||||||
enableImageGeneration = res.enabled;
|
enableImageGeneration = res.enabled;
|
||||||
}
|
}
|
||||||
const URLS = await getImageGenerationEngineUrls(localStorage.token);
|
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(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);
|
||||||
|
@ -147,13 +116,11 @@
|
||||||
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={async () => {
|
on:submit|preventDefault={async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
if (imageGenerationEngine === 'openai') {
|
|
||||||
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
||||||
}
|
|
||||||
|
|
||||||
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||||
|
|
||||||
|
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||||
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
|
@ -167,45 +134,39 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[20.5rem]">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">Image Settings</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
<div class=" self-center text-xs font-medium">Image Generation Engine</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={imageGenerationEngine}
|
bind:value={imageGenerationEngine}
|
||||||
placeholder={$i18n.t('Select a mode')}
|
placeholder="Select a mode"
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
await updateImageGeneration();
|
await updateImageGeneration();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
|
<option value="">Default (Automatic1111)</option>
|
||||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
<option value="openai">Open AI (Dall-E)</option>
|
||||||
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">Image Generation (Experimental)</div>
|
||||||
{$i18n.t('Image Generation (Experimental)')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
||||||
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
toast.error('AUTOMATIC1111 Base URL is required.');
|
||||||
enableImageGeneration = false;
|
|
||||||
} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
|
|
||||||
toast.error($i18n.t('ComfyUI Base URL is required.'));
|
|
||||||
enableImageGeneration = false;
|
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('OpenAI API Key is required.');
|
||||||
enableImageGeneration = false;
|
enableImageGeneration = false;
|
||||||
} else {
|
} else {
|
||||||
enableImageGeneration = !enableImageGeneration;
|
enableImageGeneration = !enableImageGeneration;
|
||||||
|
@ -216,9 +177,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if enableImageGeneration === true}
|
{#if enableImageGeneration === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -227,20 +188,22 @@
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
{#if imageGenerationEngine === ''}
|
{#if imageGenerationEngine === ''}
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
placeholder="Enter URL (e.g. http://127.0.0.1:7860/)"
|
||||||
bind:value={AUTOMATIC1111_BASE_URL}
|
bind:value={AUTOMATIC1111_BASE_URL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded-lg transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
updateUrlHandler();
|
// updateOllamaAPIUrlHandler();
|
||||||
|
|
||||||
|
updateAUTOMATIC1111UrlHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -259,53 +222,22 @@
|
||||||
</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('Include `--api` flag when running stable-diffusion-webui')}
|
Include `--api` flag when running stable-diffusion-webui
|
||||||
<a
|
<a
|
||||||
class=" text-gray-300 font-medium"
|
class=" text-gray-300 font-medium"
|
||||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
(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">OpenAI API Key</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter API Key')}
|
placeholder="Enter API Key"
|
||||||
bind:value={OPENAI_API_KEY}
|
bind:value={OPENAI_API_KEY}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -316,17 +248,16 @@
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Set Default Model</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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={selectedModel}
|
bind:value={selectedModel}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder="Select a model"
|
||||||
required
|
|
||||||
>
|
>
|
||||||
{#if !selectedModel}
|
{#if !selectedModel}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>Select a model</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each models ?? [] as model}
|
{#each models ?? [] as model}
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||||
|
@ -337,12 +268,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
placeholder="Enter Image Size (e.g. 512x512)"
|
||||||
bind:value={imageSize}
|
bind:value={imageSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -350,12 +281,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Set Steps</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
placeholder="Enter Number of Steps (e.g. 50)"
|
||||||
bind:value={steps}
|
bind:value={steps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -372,7 +303,7 @@
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
<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, settings, user } from '$lib/stores';
|
import { config, models, user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Addons
|
// Addons
|
||||||
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 = '';
|
||||||
|
|
||||||
|
@ -34,12 +31,7 @@
|
||||||
|
|
||||||
const toggleTitleAutoGenerate = async () => {
|
const toggleTitleAutoGenerate = async () => {
|
||||||
titleAutoGenerate = !titleAutoGenerate;
|
titleAutoGenerate = !titleAutoGenerate;
|
||||||
saveSettings({
|
saveSettings({ titleAutoGenerate: titleAutoGenerate });
|
||||||
title: {
|
|
||||||
...$settings.title,
|
|
||||||
auto: titleAutoGenerate
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleResponseAutoCopy = async () => {
|
const toggleResponseAutoCopy = async () => {
|
||||||
|
@ -71,13 +63,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings({
|
saveSettings({
|
||||||
title: {
|
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
||||||
...$settings.title,
|
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||||
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
|
||||||
modelExternal:
|
|
||||||
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
|
|
||||||
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,18 +75,14 @@
|
||||||
|
|
||||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||||
|
|
||||||
titleAutoGenerate = settings?.title?.auto ?? true;
|
titleAutoGenerate = settings.titleAutoGenerate ?? true;
|
||||||
titleAutoGenerateModel = settings?.title?.model ?? '';
|
|
||||||
titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
|
|
||||||
titleGenerationPrompt =
|
|
||||||
settings?.title?.prompt ??
|
|
||||||
$i18n.t(
|
|
||||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
|
||||||
) + ' {{prompt}}';
|
|
||||||
|
|
||||||
responseAutoCopy = settings.responseAutoCopy ?? false;
|
responseAutoCopy = settings.responseAutoCopy ?? false;
|
||||||
showUsername = settings.showUsername ?? false;
|
showUsername = settings.showUsername ?? false;
|
||||||
fullScreenMode = settings.fullScreenMode ?? false;
|
fullScreenMode = settings.fullScreenMode ?? false;
|
||||||
|
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
||||||
|
titleGenerationPrompt =
|
||||||
|
settings.titleGenerationPrompt ??
|
||||||
|
`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}}`;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -110,13 +93,13 @@
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
|
<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
|
<div class=" self-center text-xs font-medium">Title Auto-Generation</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -126,9 +109,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if titleAutoGenerate === true}
|
{#if titleAutoGenerate === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,9 +119,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div>
|
||||||
{$i18n.t('Response AutoCopy to Clipboard')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -148,9 +129,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if responseAutoCopy === true}
|
{#if responseAutoCopy === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,7 +139,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
|
<div class=" self-center text-xs font-medium">Full Screen Mode</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -168,9 +149,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if fullScreenMode === true}
|
{#if fullScreenMode === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,7 +160,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Display the username instead of You in the Chat')}
|
Display the username instead of "You" in the Chat
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -190,9 +171,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if showUsername === true}
|
{#if showUsername === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">On</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">Off</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,16 +183,15 @@
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<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">Set Title Auto-Generation Model</div>
|
||||||
<div class="flex w-full gap-2 pr-2">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1 mr-2">
|
||||||
<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}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
<option value="" selected>Current Model</option>
|
||||||
{#each $models as model}
|
{#each $models as model}
|
||||||
{#if model.size != null}
|
{#if model.size != null}
|
||||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
||||||
|
@ -221,28 +201,9 @@
|
||||||
{/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">
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Title Generation Prompt</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={titleGenerationPrompt}
|
bind:value={titleGenerationPrompt}
|
||||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
|
@ -256,9 +217,7 @@
|
||||||
|
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div class="flex w-full justify-between mb-2">
|
<div class="flex w-full justify-between mb-2">
|
||||||
<div class=" self-center text-sm font-semibold">
|
<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div>
|
||||||
{$i18n.t('Default Prompt Suggestions')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -331,19 +290,19 @@
|
||||||
|
|
||||||
{#if promptSuggestions.length > 0}
|
{#if promptSuggestions.length > 0}
|
||||||
<div class="text-xs text-left w-full mt-2">
|
<div class="text-xs text-left w-full mt-2">
|
||||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
Adjusting these settings will apply changes universally to all users.
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -5,22 +5,17 @@
|
||||||
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';
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
|
||||||
let showLiteLLM = false;
|
let showLiteLLM = false;
|
||||||
|
@ -63,13 +58,11 @@
|
||||||
let pullProgress = null;
|
let pullProgress = null;
|
||||||
|
|
||||||
let modelUploadMode = 'file';
|
let modelUploadMode = 'file';
|
||||||
let modelInputFile: File[] | null = null;
|
let modelInputFile = '';
|
||||||
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 = '';
|
||||||
|
|
||||||
|
@ -141,17 +134,11 @@
|
||||||
const pullModelHandler = async () => {
|
const pullModelHandler = async () => {
|
||||||
const sanitizedModelTag = modelTag.trim();
|
const sanitizedModelTag = modelTag.trim();
|
||||||
if (modelDownloadStatus[sanitizedModelTag]) {
|
if (modelDownloadStatus[sanitizedModelTag]) {
|
||||||
toast.error(
|
toast.error(`Model '${sanitizedModelTag}' is already in queue for downloading.`);
|
||||||
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
|
||||||
modelTag: sanitizedModelTag
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Object.keys(modelDownloadStatus).length === 3) {
|
if (Object.keys(modelDownloadStatus).length === 3) {
|
||||||
toast.error(
|
toast.error('Maximum of 3 models can be downloaded simultaneously. Please try again later.');
|
||||||
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,17 +151,15 @@
|
||||||
// Remove the downloaded model
|
// Remove the downloaded model
|
||||||
delete modelDownloadStatus[modelName];
|
delete modelDownloadStatus[modelName];
|
||||||
|
|
||||||
modelDownloadStatus = { ...modelDownloadStatus };
|
console.log(data);
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
toast.error(data.error);
|
toast.error(data.error);
|
||||||
} else {
|
} else {
|
||||||
toast.success(
|
toast.success(`Model '${modelName}' has been successfully downloaded.`);
|
||||||
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName })
|
|
||||||
);
|
|
||||||
|
|
||||||
const notification = new Notification($WEBUI_NAME, {
|
const notification = new Notification($WEBUI_NAME, {
|
||||||
body: $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }),
|
body: `Model '${modelName}' has been successfully downloaded.`,
|
||||||
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -189,32 +174,35 @@
|
||||||
|
|
||||||
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 ? modelInputFile[0] : null;
|
const file = modelInputFile[0];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
if (file) {
|
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, {
|
||||||
uploadMessage = 'Uploading...';
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
|
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||||
(error) => {
|
},
|
||||||
toast.error(error);
|
body: formData
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
uploadProgress = 0;
|
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, {
|
||||||
fileResponse = await downloadModel(
|
method: 'GET',
|
||||||
localStorage.token,
|
headers: {
|
||||||
modelFileUrl,
|
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||||
selectedOllamaUrlIdx
|
}
|
||||||
).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -237,9 +225,6 @@
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,9 +243,6 @@
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const error = await fileResponse?.json();
|
|
||||||
toast.error(error?.detail ?? error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploaded) {
|
if (uploaded) {
|
||||||
|
@ -326,11 +308,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
modelFileUrl = '';
|
modelFileUrl = '';
|
||||||
|
modelInputFile = '';
|
||||||
if (modelUploadInputElement) {
|
|
||||||
modelUploadInputElement.value = '';
|
|
||||||
}
|
|
||||||
modelInputFile = null;
|
|
||||||
modelTransferring = false;
|
modelTransferring = false;
|
||||||
uploadProgress = null;
|
uploadProgress = null;
|
||||||
|
|
||||||
|
@ -345,7 +323,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
|
toast.success(`Deleted ${deleteModelTag}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteModelTag = '';
|
deleteModelTag = '';
|
||||||
|
@ -376,24 +354,12 @@
|
||||||
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;
|
||||||
|
@ -403,17 +369,11 @@
|
||||||
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'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,14 +386,7 @@
|
||||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(modelDownloadStatus[opts.modelName]);
|
|
||||||
|
|
||||||
if (modelDownloadStatus[opts.modelName].done) {
|
|
||||||
opts.callback({ success: true, modelName: opts.modelName });
|
opts.callback({ success: true, modelName: opts.modelName });
|
||||||
} else {
|
|
||||||
opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -457,7 +410,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
|
toast.error(`Model ${liteLLMModelName} already exists.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
liteLLMModelName = '';
|
liteLLMModelName = '';
|
||||||
|
@ -503,25 +456,13 @@
|
||||||
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">
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]">
|
||||||
{#if ollamaVersion}
|
{#if ollamaVersion}
|
||||||
<div class="space-y-2 pr-1.5">
|
<div class="space-y-2 pr-1.5">
|
||||||
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
|
<div class="text-sm font-medium">Manage Ollama Models</div>
|
||||||
|
|
||||||
{#if OLLAMA_URLS.length > 0}
|
{#if OLLAMA_URLS.length > 0}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
@ -529,7 +470,7 @@
|
||||||
<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={selectedOllamaUrlIdx}
|
bind:value={selectedOllamaUrlIdx}
|
||||||
placeholder={$i18n.t('Select an Ollama instance')}
|
placeholder="Select an Ollama instance"
|
||||||
>
|
>
|
||||||
{#each OLLAMA_URLS as url, idx}
|
{#each OLLAMA_URLS as url, idx}
|
||||||
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
|
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
|
||||||
|
@ -572,14 +513,12 @@
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
|
<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
placeholder="Enter model tag (e.g. mistral:7b)"
|
||||||
modelTag: 'mistral:7b'
|
|
||||||
})}
|
|
||||||
bind:value={modelTag}
|
bind:value={modelTag}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -634,84 +573,47 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('To access the available model names for downloading,')}
|
To access the available model names for downloading, <a
|
||||||
<a
|
|
||||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||||
href="https://ollama.com/library"
|
href="https://ollama.com/library"
|
||||||
target="_blank">{$i18n.t('click here.')}</a
|
target="_blank">click here.</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if Object.keys(modelDownloadStatus).length > 0}
|
{#if Object.keys(modelDownloadStatus).length > 0}
|
||||||
{#each Object.keys(modelDownloadStatus) as model}
|
{#each Object.keys(modelDownloadStatus) as model}
|
||||||
{#if 'pullProgress' in modelDownloadStatus[model]}
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="font-medium mb-1">{model}</div>
|
<div class="font-medium mb-1">{model}</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex flex-row justify-between space-x-4 pr-2">
|
|
||||||
<div class=" flex-1">
|
|
||||||
<div
|
<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"
|
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||||
style="width: {Math.max(
|
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
|
||||||
15,
|
|
||||||
modelDownloadStatus[model].pullProgress ?? 0
|
|
||||||
)}%"
|
|
||||||
>
|
>
|
||||||
{modelDownloadStatus[model].pullProgress ?? 0}%
|
{modelDownloadStatus[model].pullProgress ?? 0}%
|
||||||
</div>
|
</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;">
|
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||||
{modelDownloadStatus[model].digest}
|
{modelDownloadStatus[model].digest}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
|
<div class=" mb-2 text-sm font-medium">Delete a model</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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={deleteModelTag}
|
bind:value={deleteModelTag}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
{#if !deleteModelTag}
|
{#if !deleteModelTag}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>Select a model</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
||||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||||
|
@ -744,13 +646,13 @@
|
||||||
|
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
|
<div class=" text-sm font-medium">Experimental</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showExperimentalOllama = !showExperimentalOllama;
|
showExperimentalOllama = !showExperimentalOllama;
|
||||||
}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
}}>{showExperimentalOllama ? 'Hide' : 'Show'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -762,7 +664,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" mb-2 flex w-full justify-between">
|
<div class=" mb-2 flex w-full justify-between">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
|
<div class=" text-sm font-medium">Upload a GGUF model</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
@ -776,9 +678,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if modelUploadMode === 'file'}
|
{#if modelUploadMode === 'file'}
|
||||||
<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
|
<span class="ml-2 self-center">File Mode</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
|
<span class="ml-2 self-center">URL Mode</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -802,7 +704,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
|
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
modelUploadInputElement.click();
|
modelUploadInputElement.click();
|
||||||
}}
|
}}
|
||||||
|
@ -810,21 +712,21 @@
|
||||||
{#if modelInputFile && modelInputFile.length > 0}
|
{#if modelInputFile && modelInputFile.length > 0}
|
||||||
{modelInputFile[0].name}
|
{modelInputFile[0].name}
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('Click here to select')}
|
Click here to select
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{: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 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||||
''
|
''
|
||||||
? 'mr-2'
|
? 'mr-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="url"
|
type="url"
|
||||||
required
|
required
|
||||||
bind:value={modelFileUrl}
|
bind:value={modelFileUrl}
|
||||||
placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
|
placeholder="Type Hugging Face Resolve (Download) URL"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -832,7 +734,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-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"
|
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={modelTransferring}
|
disabled={modelTransferring}
|
||||||
>
|
>
|
||||||
|
@ -884,43 +786,26 @@
|
||||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={modelFileContent}
|
bind:value={modelFileContent}
|
||||||
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"
|
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||||
rows="6"
|
rows="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
|
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('To access the GGUF models available for downloading,')}
|
To access the GGUF models available for downloading, <a
|
||||||
<a
|
|
||||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||||
href="https://huggingface.co/models?search=gguf"
|
href="https://huggingface.co/models?search=gguf"
|
||||||
target="_blank">{$i18n.t('click here.')}</a
|
target="_blank">click here.</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if uploadMessage}
|
{#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">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=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
|
||||||
|
|
||||||
<div class="w-full rounded-full dark:bg-gray-800">
|
<div class="w-full rounded-full dark:bg-gray-800">
|
||||||
<div
|
<div
|
||||||
|
@ -947,13 +832,13 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
|
<div class=" text-sm font-medium">Manage LiteLLM Models</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showLiteLLM = !showLiteLLM;
|
showLiteLLM = !showLiteLLM;
|
||||||
}}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
}}>{showLiteLLM ? 'Hide' : 'Show'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -961,16 +846,14 @@
|
||||||
{#if showLiteLLM}
|
{#if showLiteLLM}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
|
<div class=" text-sm font-medium">Add a model</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showLiteLLMParams = !showLiteLLMParams;
|
showLiteLLMParams = !showLiteLLMParams;
|
||||||
}}
|
}}
|
||||||
>{showLiteLLMParams
|
>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button
|
||||||
? $i18n.t('Hide Additional Params')
|
|
||||||
: $i18n.t('Show Additional Params')}</button
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -980,7 +863,7 @@
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
|
placeholder="Enter LiteLLM Model (litellm_params.model)"
|
||||||
bind:value={liteLLMModel}
|
bind:value={liteLLMModel}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -1007,7 +890,7 @@
|
||||||
|
|
||||||
{#if showLiteLLMParams}
|
{#if showLiteLLMParams}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
|
<div class=" mb-1.5 text-sm font-medium">Model Name</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -1021,14 +904,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
|
<div class=" mb-1.5 text-sm font-medium">API Base URL</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t(
|
placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)"
|
||||||
'Enter LiteLLM API Base URL (litellm_params.api_base)'
|
|
||||||
)}
|
|
||||||
bind:value={liteLLMAPIBase}
|
bind:value={liteLLMAPIBase}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -1037,12 +918,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
|
<div class=" mb-1.5 text-sm font-medium">API Key</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
|
placeholder="Enter LiteLLM API Key (litellm_params.api_key)"
|
||||||
bind:value={liteLLMAPIKey}
|
bind:value={liteLLMAPIKey}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -1051,12 +932,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
|
<div class="mb-1.5 text-sm font-medium">API RPM</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
|
placeholder="Enter LiteLLM API RPM (litellm_params.rpm)"
|
||||||
bind:value={liteLLMRPM}
|
bind:value={liteLLMRPM}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -1065,12 +946,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
|
<div class="mb-1.5 text-sm font-medium">Max Tokens</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
|
placeholder="Enter Max Tokens (litellm_params.max_tokens)"
|
||||||
bind:value={liteLLMMaxTokens}
|
bind:value={liteLLMMaxTokens}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
@ -1083,27 +964,27 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('Not sure what to add?')}
|
Not sure what to add?
|
||||||
<a
|
<a
|
||||||
class=" text-gray-300 font-medium underline"
|
class=" text-gray-300 font-medium underline"
|
||||||
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
|
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{$i18n.t('Click here for help.')}
|
Click here for help.
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">Delete a model</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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={deleteLiteLLMModelId}
|
bind:value={deleteLiteLLMModelId}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
{#if !deleteLiteLLMModelId}
|
{#if !deleteLiteLLMModelId}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>Select a model</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each liteLLMModelInfo as model}
|
{#each liteLLMModelInfo as model}
|
||||||
<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { models, settings, user } from '$lib/stores';
|
import { models, settings, user } from '$lib/stores';
|
||||||
|
|
||||||
|
@ -18,8 +17,6 @@
|
||||||
import Connections from './Settings/Connections.svelte';
|
import Connections from './Settings/Connections.svelte';
|
||||||
import Images from './Settings/Images.svelte';
|
import Images from './Settings/Images.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
const saveSettings = async (updated) => {
|
const saveSettings = async (updated) => {
|
||||||
|
@ -61,7 +58,7 @@
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
<div class=" text-lg font-medium self-center">Settings</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -109,7 +106,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('General')}</div>
|
<div class=" self-center">General</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin'}
|
||||||
|
@ -134,7 +131,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Connections')}</div>
|
<div class=" self-center">Connections</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -160,7 +157,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Models')}</div>
|
<div class=" self-center">Models</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -187,7 +184,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Interface')}</div>
|
<div class=" self-center">Interface</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -214,7 +211,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
<div class=" self-center">Audio</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $user.role === 'admin'}
|
{#if $user.role === 'admin'}
|
||||||
|
@ -241,7 +238,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Images')}</div>
|
<div class=" self-center">Images</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -268,7 +265,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Chats')}</div>
|
<div class=" self-center">Chats</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -294,7 +291,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Account')}</div>
|
<div class=" self-center">Account</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -320,16 +317,16 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('About')}</div>
|
<div class=" self-center">About</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:min-h-[25rem]">
|
<div class="flex-1 md:min-h-[380px]">
|
||||||
{#if selectedTab === 'general'}
|
{#if selectedTab === 'general'}
|
||||||
<General
|
<General
|
||||||
{getModels}
|
{getModels}
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success('Settings saved successfully!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'models'}
|
{:else if selectedTab === 'models'}
|
||||||
|
@ -338,28 +335,28 @@
|
||||||
<Connections
|
<Connections
|
||||||
{getModels}
|
{getModels}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success('Settings saved successfully!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'interface'}
|
{:else if selectedTab === 'interface'}
|
||||||
<Interface
|
<Interface
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success('Settings saved successfully!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'audio'}
|
{:else if selectedTab === 'audio'}
|
||||||
<Audio
|
<Audio
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success('Settings saved successfully!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'images'}
|
{:else if selectedTab === 'images'}
|
||||||
<Images
|
<Images
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success('Settings saved successfully!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'chats'}
|
{:else if selectedTab === 'chats'}
|
||||||
|
@ -367,7 +364,7 @@
|
||||||
{:else if selectedTab === 'account'}
|
{:else if selectedTab === 'account'}
|
||||||
<Account
|
<Account
|
||||||
saveHandler={() => {
|
saveHandler={() => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success('Settings saved successfully!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'about'}
|
{:else if selectedTab === 'about'}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let downloadChat: Function;
|
export let downloadChat: Function;
|
||||||
export let shareChat: Function;
|
export let shareChat: Function;
|
||||||
|
|
||||||
|
@ -20,11 +17,11 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Share to OpenWebUI Community')}
|
Share to OpenWebUI Community
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex justify-center space-x-1 mt-1.5">
|
<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>
|
<div class=" self-center text-gray-400 text-xs font-medium">or</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
||||||
|
@ -34,7 +31,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Download as a File')}
|
Download as a File
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
|
<div class=" text-lg font-medium self-center">Keyboard shortcuts</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -35,7 +32,7 @@
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
<div class="flex flex-col space-y-3 w-full self-start">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Open new chat')}</div>
|
<div class=" text-sm">Open new chat</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -59,7 +56,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Focus chat input')}</div>
|
<div class=" text-sm">Focus chat input</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -77,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Copy last code block')}</div>
|
<div class=" text-sm">Copy last code block</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -101,7 +98,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Copy last response')}</div>
|
<div class=" text-sm">Copy last response</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -127,7 +124,7 @@
|
||||||
|
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
<div class="flex flex-col space-y-3 w-full self-start">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Toggle settings')}</div>
|
<div class=" text-sm">Toggle settings</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -144,7 +141,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Toggle sidebar')}</div>
|
<div class=" text-sm">Toggle sidebar</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -168,7 +165,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Delete chat')}</div>
|
<div class=" text-sm">Delete chat</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
@ -191,7 +188,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">{$i18n.t('Show shortcuts')}</div>
|
<div class=" text-sm">Show shortcuts</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu } from 'bits-ui';
|
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
|
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenu.Root
|
|
||||||
onOpenChange={(state) => {
|
|
||||||
dispatch('change', state);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<slot />
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<slot name="content">
|
|
||||||
<DropdownMenu.Content
|
|
||||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
|
|
||||||
sideOffset={8}
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
transition={flyAndScale}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
|
||||||
<div class="flex items-center">Profile</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
|
|
||||||
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
|
||||||
<div class="flex items-center">Profile</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
|
|
||||||
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
|
||||||
<div class="flex items-center">Profile</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</slot>
|
|
||||||
</DropdownMenu.Root>
|
|
|
@ -2,22 +2,6 @@
|
||||||
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}
|
||||||
|
@ -51,7 +35,10 @@
|
||||||
<button
|
<button
|
||||||
class=" p-5"
|
class=" p-5"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
downloadImage(src, 'Image.png');
|
const a = document.createElement('a');
|
||||||
|
a.href = src;
|
||||||
|
a.download = 'Image.png';
|
||||||
|
a.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
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';
|
||||||
|
|
||||||
|
@ -43,10 +41,10 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=" m-auto rounded-2xl max-w-full {sizeToWidth(
|
class=" modal-content 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:flyAndScale
|
in:fade={{ duration: 10 }}
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
<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>
|
|
|
@ -8,7 +8,7 @@
|
||||||
export let addTag: Function;
|
export let addTag: Function;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row flex-wrap gap-0.5 line-clamp-1">
|
<div class="flex flex-row space-x-0.5 line-clamp-1">
|
||||||
<TagList
|
<TagList
|
||||||
{tags}
|
{tags}
|
||||||
on:delete={(e) => {
|
on:delete={(e) => {
|
||||||
|
@ -17,7 +17,6 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TagInput
|
<TagInput
|
||||||
label={tags.length == 0 ? 'Add Tags' : ''}
|
|
||||||
on:add={(e) => {
|
on:add={(e) => {
|
||||||
addTag(e.detail);
|
addTag(e.detail);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,31 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, getContext } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { tags } from '$lib/stores';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
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 type="button" on:click={addTagHandler}>
|
<input
|
||||||
|
bind:value={tagName}
|
||||||
|
class=" cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[4rem]"
|
||||||
|
placeholder="Add a tag"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
@ -39,23 +36,9 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<input
|
|
||||||
bind:value={tagName}
|
|
||||||
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')}
|
|
||||||
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
|
||||||
|
@ -78,8 +61,4 @@
|
||||||
</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-800 dark:text-white"
|
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"
|
||||||
>
|
>
|
||||||
<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} class="flex">
|
<div bind:this={tooltipElement} aria-label={content}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
@ -13,8 +13,6 @@
|
||||||
import { transformFileName } from '$lib/utils';
|
import { transformFileName } from '$lib/utils';
|
||||||
import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
|
import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let selectedDoc;
|
export let selectedDoc;
|
||||||
let uploadDocInputElement: HTMLInputElement;
|
let uploadDocInputElement: HTMLInputElement;
|
||||||
|
@ -73,7 +71,7 @@
|
||||||
inputFiles = null;
|
inputFiles = null;
|
||||||
uploadDocInputElement.value = '';
|
uploadDocInputElement.value = '';
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`File not found.`));
|
toast.error(`File not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
show = false;
|
show = false;
|
||||||
|
@ -98,7 +96,7 @@
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
|
<div class=" text-lg font-medium self-center">Add Docs</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -138,7 +136,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
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"
|
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadDocInputElement.click();
|
uploadDocInputElement.click();
|
||||||
|
@ -147,14 +145,14 @@
|
||||||
{#if inputFiles}
|
{#if inputFiles}
|
||||||
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('Click here to select documents.')}
|
Click here to select documents.
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
<div class=" mb-1.5 text-xs text-gray-500">Tags</div>
|
||||||
|
|
||||||
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -165,7 +163,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
@ -10,8 +10,6 @@
|
||||||
import Tags from '../common/Tags.svelte';
|
import Tags from '../common/Tags.svelte';
|
||||||
import { addTagById } from '$lib/apis/chats';
|
import { addTagById } from '$lib/apis/chats';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let selectedDoc;
|
export let selectedDoc;
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
|
<div class=" text-lg font-medium self-center">Edit Doc</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -107,7 +105,7 @@
|
||||||
>
|
>
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Name Tag</div>
|
||||||
|
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div
|
<div
|
||||||
|
@ -136,7 +134,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div>
|
<div class=" mb-1 text-xs text-gray-500">Title</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
|
@ -150,7 +148,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
<div class=" mb-1.5 text-xs text-gray-500">Tags</div>
|
||||||
|
|
||||||
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -161,7 +159,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -5,22 +5,16 @@
|
||||||
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 } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
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;
|
||||||
|
@ -37,7 +31,7 @@
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
await documents.set(await getDocs(localStorage.token));
|
await documents.set(await getDocs(localStorage.token));
|
||||||
toast.success($i18n.t('Scan complete!'));
|
toast.success('Scan complete!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,12 +69,10 @@
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
<div class=" mb-2 text-sm font-medium">General Settings</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">Scan for documents from '/data/docs'</div>
|
||||||
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center {loading
|
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center {loading
|
||||||
|
@ -93,7 +85,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
|
<div class="self-center font-medium">Scan</div>
|
||||||
|
|
||||||
<!-- <svg
|
<!-- <svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -141,18 +133,19 @@
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div class=" ">
|
<div class=" space-y-3">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
<div class=" space-y-3">
|
||||||
|
<div class=" text-sm font-medium">Chunk Params</div>
|
||||||
|
|
||||||
<div class=" flex">
|
<div class=" flex gap-2">
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between gap-2">
|
||||||
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
|
<div class="self-center text-xs font-medium min-w-fit">Chunk Size</div>
|
||||||
|
|
||||||
<div class="self-center p-3">
|
<div class="self-center">
|
||||||
<input
|
<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"
|
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="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Chunk Size')}
|
placeholder="Enter Chunk Size"
|
||||||
bind:value={chunkSize}
|
bind:value={chunkSize}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
min="0"
|
min="0"
|
||||||
|
@ -160,14 +153,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-2">
|
||||||
<div class=" self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Overlap')}</div>
|
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div>
|
||||||
|
|
||||||
<div class="self-center p-3">
|
<div class="self-center">
|
||||||
<input
|
<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"
|
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="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Chunk Overlap')}
|
placeholder="Enter Chunk Overlap"
|
||||||
bind:value={chunkOverlap}
|
bind:value={chunkOverlap}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
min="0"
|
min="0"
|
||||||
|
@ -178,31 +171,31 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
|
<div class=" text-xs font-medium">PDF Extract Images (OCR)</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
pdfExtractImages = !pdfExtractImages;
|
pdfExtractImages = !pdfExtractImages;
|
||||||
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
|
}}>{pdfExtractImages ? 'On' : 'Off'}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
<div class=" text-sm font-medium">Query Params</div>
|
||||||
|
|
||||||
<div class=" flex">
|
<div class=" flex py-2">
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between gap-2">
|
||||||
<div class="self-center text-xs font-medium flex-1">{$i18n.t('Top K')}</div>
|
<div class="self-center text-xs font-medium flex-1">Top K</div>
|
||||||
|
|
||||||
<div class="self-center p-3">
|
<div class="self-center">
|
||||||
<input
|
<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"
|
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="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Top K')}
|
placeholder="Enter Top K"
|
||||||
bind:value={querySettings.k}
|
bind:value={querySettings.k}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
min="0"
|
min="0"
|
||||||
|
@ -227,7 +220,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
|
<div class=" mb-2.5 text-sm font-medium">RAG Template</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={querySettings.template}
|
bind:value={querySettings.template}
|
||||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||||
|
@ -235,100 +228,7 @@
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
|
|
||||||
<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">
|
||||||
|
@ -336,7 +236,7 @@
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import General from './Settings/General.svelte';
|
import General from './Settings/General.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
let selectedTab = 'general';
|
let selectedTab = 'general';
|
||||||
|
@ -13,7 +10,7 @@
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Document Settings')}</div>
|
<div class=" text-lg font-medium self-center">Document Settings</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -61,7 +58,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('General')}</div>
|
<div class=" self-center">General</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:min-h-[380px]">
|
<div class="flex-1 md:min-h-[380px]">
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<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>
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue