diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index ebc19589f..cea095ea1 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -4,6 +4,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
+ 'plugin:cypress/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 4c4cfa3b2..819483066 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -2,14 +2,16 @@
- [ ] **Description:** Briefly describe the changes in this pull request.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
-- [ ] **Documentation:** Have you updated relevant documentation?
+- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
+- [ ] **Testing:** Have you written and run sufficient tests for the changes?
+- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues?
---
## Description
-[Insert a brief description of the changes made in this pull request]
+[Insert a brief description of the changes made in this pull request, including any relevant motivation and impact.]
---
@@ -17,16 +19,32 @@
### Added
-- [List any new features or additions]
+- [List any new features, functionalities, or additions]
### Fixed
-- [List any fixes or corrections]
+- [List any fixes, corrections, or bug fixes]
### Changed
-- [List any changes or updates]
+- [List any changes, updates, refactorings, or optimizations]
### Removed
-- [List any removed features or files]
+- [List any removed features, files, or deprecated functionalities]
+
+### Security
+
+- [List any new or updated security-related changes, including vulnerability fixes]
+
+### Breaking Changes
+
+- [List any breaking changes affecting compatibility or functionality]
+
+---
+
+### Additional Information
+
+- [Insert any additional context, notes, or explanations for the changes]
+
+- [Reference any related issues, commits, or other relevant information]
diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml
new file mode 100644
index 000000000..dfceaacc1
--- /dev/null
+++ b/.github/workflows/integration-test.yml
@@ -0,0 +1,55 @@
+name: Integration Test
+
+on:
+ push:
+ branches:
+ - main
+ - dev
+ pull_request:
+ branches:
+ - main
+ - dev
+
+jobs:
+ cypress-run:
+ name: Run Cypress Integration Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Build and run Compose Stack
+ run: |
+ docker compose up --detach --build
+
+ - name: Preload Ollama model
+ run: |
+ docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
+
+ - name: Cypress run
+ uses: cypress-io/github-action@v6
+ with:
+ browser: chrome
+ wait-on: 'http://localhost:3000'
+ config: baseUrl=http://localhost:3000
+
+ - uses: actions/upload-artifact@v4
+ if: always()
+ name: Upload Cypress videos
+ with:
+ name: cypress-videos
+ path: cypress/videos
+ if-no-files-found: ignore
+
+ - name: Extract Compose logs
+ if: always()
+ run: |
+ docker compose logs > compose-logs.txt
+
+ - uses: actions/upload-artifact@v4
+ if: always()
+ name: Upload Compose logs
+ with:
+ name: compose-logs
+ path: compose-logs.txt
+ if-no-files-found: ignore
diff --git a/.gitignore b/.gitignore
index 2ccac4d50..55209604a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -297,4 +297,8 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
-.pnp.*
\ No newline at end of file
+.pnp.*
+
+# cypress artifacts
+cypress/videos
+cypress/screenshots
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 505ded309..62e960ab6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.1.122] - 2024-04-27
+
+### Added
+
+- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
+- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
+- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
+- **👨💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
+- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
+- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
+- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
+- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
+
+### Fixed
+
+- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
+- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
+- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
+- **🔤 Font Fallback**: Corrected font fallback issue.
+
+### Changed
+
+- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
+
## [0.1.121] - 2024-04-24
### Fixed
diff --git a/Dockerfile b/Dockerfile
index a8f664ada..c43cd8cb3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,8 +8,9 @@ ARG USE_CUDA_VER=cu121
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# 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)
-# IMPORTANT: If you change the default model (sentence-transformers/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 embedding model (sentence-transformers/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.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
+ARG USE_RERANKING_MODEL=""
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
@@ -30,6 +31,7 @@ ARG USE_CUDA
ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
+ARG USE_RERANKING_MODEL
## Basis ##
ENV ENV=prod \
@@ -38,7 +40,8 @@ ENV ENV=prod \
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
USE_CUDA_DOCKER=${USE_CUDA} \
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
- USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
+ USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
+ USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
## Basis URL Config ##
ENV OLLAMA_BASE_URL="/ollama" \
@@ -62,8 +65,11 @@ ENV WHISPER_MODEL="base" \
## RAG Embedding model settings ##
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
- RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
+ RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
+
+## Hugging Face download cache ##
+ENV HF_HOME="/app/backend/data/cache/embedding/models"
#### Other models ##########################################################
WORKDIR /app/backend
diff --git a/Makefile b/Makefile
index 1ec170a25..4b60b0496 100644
--- a/Makefile
+++ b/Makefile
@@ -1,27 +1,33 @@
+
+ifneq ($(shell which docker-compose 2>/dev/null),)
+ DOCKER_COMPOSE := docker-compose
+else
+ DOCKER_COMPOSE := docker compose
+endif
+
install:
- @docker-compose up -d
+ $(DOCKER_COMPOSE) up -d
remove:
@chmod +x confirm_remove.sh
@./confirm_remove.sh
-
start:
- @docker-compose start
+ $(DOCKER_COMPOSE) start
startAndBuild:
- docker-compose up -d --build
+ $(DOCKER_COMPOSE) up -d --build
stop:
- @docker-compose stop
+ $(DOCKER_COMPOSE) stop
update:
# Calls the LLM update script
chmod +x update_ollama_models.sh
@./update_ollama_models.sh
@git pull
- @docker-compose down
+ $(DOCKER_COMPOSE) down
# Make sure the ollama-webui container is stopped before rebuilding
@docker stop open-webui || true
- @docker-compose up --build -d
- @docker-compose start
+ $(DOCKER_COMPOSE) up --build -d
+ $(DOCKER_COMPOSE) start
diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py
index 2059ac3c0..fd72b203c 100644
--- a/backend/apps/images/main.py
+++ b/backend/apps/images/main.py
@@ -32,11 +32,15 @@ import logging
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
+ IMAGE_GENERATION_ENGINE,
ENABLE_IMAGE_GENERATION,
AUTOMATIC1111_BASE_URL,
COMFYUI_BASE_URL,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
+ IMAGE_GENERATION_MODEL,
+ IMAGE_SIZE,
+ IMAGE_STEPS,
)
@@ -55,21 +59,21 @@ app.add_middleware(
allow_headers=["*"],
)
-app.state.ENGINE = ""
+app.state.ENGINE = IMAGE_GENERATION_ENGINE
app.state.ENABLED = ENABLE_IMAGE_GENERATION
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
-app.state.MODEL = ""
+app.state.MODEL = IMAGE_GENERATION_MODEL
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
-app.state.IMAGE_SIZE = "512x512"
-app.state.IMAGE_STEPS = 50
+app.state.IMAGE_SIZE = IMAGE_SIZE
+app.state.IMAGE_STEPS = IMAGE_STEPS
@app.get("/config")
diff --git a/backend/apps/litellm/main.py b/backend/apps/litellm/main.py
index 119e9107e..2a37622f1 100644
--- a/backend/apps/litellm/main.py
+++ b/backend/apps/litellm/main.py
@@ -21,12 +21,15 @@ from utils.utils import get_verified_user, get_current_user, get_admin_user
from config import SRC_LOG_LEVELS, ENV
from constants import MESSAGES
+import os
+
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
from config import (
- MODEL_FILTER_ENABLED,
+ ENABLE_LITELLM,
+ ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
DATA_DIR,
LITELLM_PROXY_PORT,
@@ -57,11 +60,20 @@ LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
with open(LITELLM_CONFIG_DIR, "r") as file:
litellm_config = yaml.safe_load(file)
+
+app.state.ENABLE = ENABLE_LITELLM
app.state.CONFIG = litellm_config
# Global variable to store the subprocess reference
background_process = None
+CONFLICT_ENV_VARS = [
+ # Uvicorn uses PORT, so LiteLLM might use it as well
+ "PORT",
+ # LiteLLM uses DATABASE_URL for Prisma connections
+ "DATABASE_URL",
+]
+
async def run_background_process(command):
global background_process
@@ -70,9 +82,11 @@ async def run_background_process(command):
try:
# Log the command to be executed
log.info(f"Executing command: {command}")
+ # Filter environment variables known to conflict with litellm
+ env = {k: v for k, v in os.environ.items() if k not in CONFLICT_ENV_VARS}
# Execute the command and create a subprocess
process = await asyncio.create_subprocess_exec(
- *command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ *command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)
background_process = process
log.info("Subprocess started successfully.")
@@ -130,7 +144,7 @@ async def startup_event():
asyncio.create_task(start_litellm_background())
-app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
+app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
@@ -198,49 +212,56 @@ async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_use
@app.get("/models")
@app.get("/v1/models")
async def get_models(user=Depends(get_current_user)):
- while not background_process:
- await asyncio.sleep(0.1)
- url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
- r = None
- try:
- r = requests.request(method="GET", url=f"{url}/models")
- r.raise_for_status()
+ if app.state.ENABLE:
+ while not background_process:
+ await asyncio.sleep(0.1)
- data = r.json()
+ url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
+ r = None
+ try:
+ r = requests.request(method="GET", url=f"{url}/models")
+ r.raise_for_status()
- 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"],
+ data = r.json()
+
+ if app.state.ENABLE_MODEL_FILTER:
+ if user and user.role == "user":
+ data["data"] = list(
+ filter(
+ lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
+ data["data"],
+ )
)
- )
- return data
- except Exception as e:
+ return data
+ except Exception as e:
- log.exception(e)
- error_detail = "Open WebUI: Server Connection Error"
- if r is not None:
- try:
- res = r.json()
- if "error" in res:
- error_detail = f"External: {res['error']}"
- except:
- error_detail = f"External: {e}"
+ log.exception(e)
+ error_detail = "Open WebUI: Server Connection Error"
+ if r is not None:
+ try:
+ res = r.json()
+ if "error" in res:
+ error_detail = f"External: {res['error']}"
+ except:
+ error_detail = f"External: {e}"
+ return {
+ "data": [
+ {
+ "id": model["model_name"],
+ "object": "model",
+ "created": int(time.time()),
+ "owned_by": "openai",
+ }
+ for model in app.state.CONFIG["model_list"]
+ ],
+ "object": "list",
+ }
+ else:
return {
- "data": [
- {
- "id": model["model_name"],
- "object": "model",
- "created": int(time.time()),
- "owned_by": "openai",
- }
- for model in app.state.CONFIG["model_list"]
- ],
+ "data": [],
"object": "list",
}
diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py
index 9258efa66..f1c836faa 100644
--- a/backend/apps/ollama/main.py
+++ b/backend/apps/ollama/main.py
@@ -16,6 +16,7 @@ from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, ConfigDict
import os
+import re
import copy
import random
import requests
@@ -36,7 +37,7 @@ from utils.utils import decode_token, get_current_user, get_admin_user
from config import (
SRC_LOG_LEVELS,
OLLAMA_BASE_URLS,
- MODEL_FILTER_ENABLED,
+ ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
UPLOAD_DIR,
)
@@ -55,7 +56,7 @@ app.add_middleware(
)
-app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
+app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
@@ -168,7 +169,7 @@ async def get_ollama_tags(
if url_idx == None:
models = await get_all_models()
- if app.state.MODEL_FILTER_ENABLED:
+ if app.state.ENABLE_MODEL_FILTER:
if user.role == "user":
models["models"] = list(
filter(
@@ -216,7 +217,9 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
if len(responses) > 0:
lowest_version = min(
responses,
- key=lambda x: tuple(map(int, x["version"].split("-")[0].split("."))),
+ key=lambda x: tuple(
+ map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
+ ),
)
return {"version": lowest_version["version"]}
diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py
index 0fbbd365e..d9e378303 100644
--- a/backend/apps/openai/main.py
+++ b/backend/apps/openai/main.py
@@ -24,7 +24,7 @@ from config import (
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
- MODEL_FILTER_ENABLED,
+ ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
)
from typing import List, Optional
@@ -45,7 +45,7 @@ app.add_middleware(
allow_headers=["*"],
)
-app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
+app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
@@ -225,7 +225,7 @@ async def get_all_models():
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
if url_idx == None:
models = await get_all_models()
- if app.state.MODEL_FILTER_ENABLED:
+ if app.state.ENABLE_MODEL_FILTER:
if user.role == "user":
models["data"] = list(
filter(
diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py
index 5da7489f1..a33a29659 100644
--- a/backend/apps/rag/main.py
+++ b/backend/apps/rag/main.py
@@ -39,8 +39,6 @@ import json
import sentence_transformers
-from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
-
from apps.web.models.documents import (
Documents,
DocumentForm,
@@ -48,9 +46,12 @@ from apps.web.models.documents import (
)
from apps.rag.utils import (
- query_embeddings_doc,
- query_embeddings_collection,
- generate_openai_embeddings,
+ get_model_path,
+ get_embedding_function,
+ query_doc,
+ query_doc_with_hybrid_search,
+ query_collection,
+ query_collection_with_hybrid_search,
)
from utils.misc import (
@@ -60,13 +61,22 @@ from utils.misc import (
extract_folders_after_data_docs,
)
from utils.utils import get_current_user, get_admin_user
+
from config import (
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
+ RAG_TOP_K,
+ RAG_RELEVANCE_THRESHOLD,
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL,
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
+ ENABLE_RAG_HYBRID_SEARCH,
+ RAG_RERANKING_MODEL,
+ PDF_EXTRACT_IMAGES,
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
+ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
RAG_OPENAI_API_BASE_URL,
RAG_OPENAI_API_KEY,
DEVICE_TYPE,
@@ -83,31 +93,75 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
app = FastAPI()
+app.state.TOP_K = RAG_TOP_K
+app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
+
+app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
-app.state.TOP_K = 4
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
-
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
+app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
-app.state.PDF_EXTRACT_IMAGES = False
+app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
-if app.state.RAG_EMBEDDING_ENGINE == "":
- app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
- app.state.RAG_EMBEDDING_MODEL,
- device=DEVICE_TYPE,
- trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
- )
+def update_embedding_model(
+ embedding_model: str,
+ update_model: bool = False,
+):
+ if embedding_model and app.state.RAG_EMBEDDING_ENGINE == "":
+ app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
+ get_model_path(embedding_model, update_model),
+ device=DEVICE_TYPE,
+ trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
+ )
+ else:
+ app.state.sentence_transformer_ef = None
+
+
+def update_reranking_model(
+ reranking_model: str,
+ update_model: bool = False,
+):
+ if reranking_model:
+ app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
+ get_model_path(reranking_model, update_model),
+ device=DEVICE_TYPE,
+ trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
+ )
+ else:
+ app.state.sentence_transformer_rf = None
+
+
+update_embedding_model(
+ app.state.RAG_EMBEDDING_MODEL,
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
+)
+
+update_reranking_model(
+ app.state.RAG_RERANKING_MODEL,
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
+)
+
+
+app.state.EMBEDDING_FUNCTION = get_embedding_function(
+ app.state.RAG_EMBEDDING_ENGINE,
+ app.state.RAG_EMBEDDING_MODEL,
+ app.state.sentence_transformer_ef,
+ app.state.OPENAI_API_KEY,
+ app.state.OPENAI_API_BASE_URL,
+)
origins = ["*"]
+
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
@@ -134,6 +188,7 @@ async def get_status():
"template": app.state.RAG_TEMPLATE,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
+ "reranking_model": app.state.RAG_RERANKING_MODEL,
}
@@ -150,6 +205,11 @@ async def get_embedding_config(user=Depends(get_admin_user)):
}
+@app.get("/reranking")
+async def get_reraanking_config(user=Depends(get_admin_user)):
+ return {"status": True, "reranking_model": app.state.RAG_RERANKING_MODEL}
+
+
class OpenAIConfigForm(BaseModel):
url: str
key: str
@@ -170,22 +230,22 @@ async def update_embedding_config(
)
try:
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
+ app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
- app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
- app.state.sentence_transformer_ef = None
-
if form_data.openai_config != None:
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.OPENAI_API_KEY = form_data.openai_config.key
- else:
- sentence_transformer_ef = sentence_transformers.SentenceTransformer(
- app.state.RAG_EMBEDDING_MODEL,
- device=DEVICE_TYPE,
- trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
- )
- app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
- app.state.sentence_transformer_ef = sentence_transformer_ef
+
+ update_embedding_model(app.state.RAG_EMBEDDING_MODEL, True)
+
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
+ app.state.RAG_EMBEDDING_ENGINE,
+ app.state.RAG_EMBEDDING_MODEL,
+ app.state.sentence_transformer_ef,
+ app.state.OPENAI_API_KEY,
+ app.state.OPENAI_API_BASE_URL,
+ )
return {
"status": True,
@@ -196,7 +256,6 @@ async def update_embedding_config(
"key": app.state.OPENAI_API_KEY,
},
}
-
except Exception as e:
log.exception(f"Problem updating embedding model: {e}")
raise HTTPException(
@@ -205,6 +264,34 @@ async def update_embedding_config(
)
+class RerankingModelUpdateForm(BaseModel):
+ reranking_model: str
+
+
+@app.post("/reranking/update")
+async def update_reranking_config(
+ form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
+):
+ log.info(
+ f"Updating reranking model: {app.state.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
+ )
+ try:
+ app.state.RAG_RERANKING_MODEL = form_data.reranking_model
+
+ update_reranking_model(app.state.RAG_RERANKING_MODEL, True)
+
+ return {
+ "status": True,
+ "reranking_model": app.state.RAG_RERANKING_MODEL,
+ }
+ except Exception as e:
+ log.exception(f"Problem updating reranking model: {e}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=ERROR_MESSAGES.DEFAULT(e),
+ )
+
+
@app.get("/config")
async def get_rag_config(user=Depends(get_admin_user)):
return {
@@ -257,12 +344,16 @@ async def get_query_settings(user=Depends(get_admin_user)):
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
+ "r": app.state.RELEVANCE_THRESHOLD,
+ "hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
}
class QuerySettingsForm(BaseModel):
k: Optional[int] = None
+ r: Optional[float] = None
template: Optional[str] = None
+ hybrid: Optional[bool] = None
@app.post("/query/settings/update")
@@ -271,13 +362,23 @@ async def update_query_settings(
):
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
app.state.TOP_K = form_data.k if form_data.k else 4
- return {"status": True, "template": app.state.RAG_TEMPLATE}
+ app.state.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
+ app.state.ENABLE_RAG_HYBRID_SEARCH = form_data.hybrid if form_data.hybrid else False
+ return {
+ "status": True,
+ "template": app.state.RAG_TEMPLATE,
+ "k": app.state.TOP_K,
+ "r": app.state.RELEVANCE_THRESHOLD,
+ "hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
+ }
class QueryDocForm(BaseModel):
collection_name: str
query: str
k: Optional[int] = None
+ r: Optional[float] = None
+ hybrid: Optional[bool] = None
@app.post("/query/doc")
@@ -286,34 +387,22 @@ def query_doc_handler(
user=Depends(get_current_user),
):
try:
- if app.state.RAG_EMBEDDING_ENGINE == "":
- query_embeddings = app.state.sentence_transformer_ef.encode(
- form_data.query
- ).tolist()
- elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
- query_embeddings = generate_ollama_embeddings(
- GenerateEmbeddingsForm(
- **{
- "model": app.state.RAG_EMBEDDING_MODEL,
- "prompt": form_data.query,
- }
- )
+ if app.state.ENABLE_RAG_HYBRID_SEARCH:
+ return query_doc_with_hybrid_search(
+ collection_name=form_data.collection_name,
+ query=form_data.query,
+ embeddings_function=app.state.EMBEDDING_FUNCTION,
+ reranking_function=app.state.sentence_transformer_rf,
+ k=form_data.k if form_data.k else app.state.TOP_K,
+ r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
)
- elif app.state.RAG_EMBEDDING_ENGINE == "openai":
- query_embeddings = generate_openai_embeddings(
- model=app.state.RAG_EMBEDDING_MODEL,
- text=form_data.query,
- key=app.state.OPENAI_API_KEY,
- url=app.state.OPENAI_API_BASE_URL,
+ else:
+ return query_doc(
+ collection_name=form_data.collection_name,
+ query=form_data.query,
+ embeddings_function=app.state.EMBEDDING_FUNCTION,
+ k=form_data.k if form_data.k else app.state.TOP_K,
)
-
- return query_embeddings_doc(
- collection_name=form_data.collection_name,
- query=form_data.query,
- query_embeddings=query_embeddings,
- k=form_data.k if form_data.k else app.state.TOP_K,
- )
-
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -326,6 +415,8 @@ class QueryCollectionsForm(BaseModel):
collection_names: List[str]
query: str
k: Optional[int] = None
+ r: Optional[float] = None
+ hybrid: Optional[bool] = None
@app.post("/query/collection")
@@ -334,33 +425,23 @@ def query_collection_handler(
user=Depends(get_current_user),
):
try:
- if app.state.RAG_EMBEDDING_ENGINE == "":
- query_embeddings = app.state.sentence_transformer_ef.encode(
- form_data.query
- ).tolist()
- elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
- query_embeddings = generate_ollama_embeddings(
- GenerateEmbeddingsForm(
- **{
- "model": app.state.RAG_EMBEDDING_MODEL,
- "prompt": form_data.query,
- }
- )
+ if app.state.ENABLE_RAG_HYBRID_SEARCH:
+ return query_collection_with_hybrid_search(
+ collection_names=form_data.collection_names,
+ query=form_data.query,
+ embeddings_function=app.state.EMBEDDING_FUNCTION,
+ reranking_function=app.state.sentence_transformer_rf,
+ k=form_data.k if form_data.k else app.state.TOP_K,
+ r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
)
- elif app.state.RAG_EMBEDDING_ENGINE == "openai":
- query_embeddings = generate_openai_embeddings(
- model=app.state.RAG_EMBEDDING_MODEL,
- text=form_data.query,
- key=app.state.OPENAI_API_KEY,
- url=app.state.OPENAI_API_BASE_URL,
+ else:
+ return query_collection(
+ collection_names=form_data.collection_names,
+ query=form_data.query,
+ embeddings_function=app.state.EMBEDDING_FUNCTION,
+ k=form_data.k if form_data.k else app.state.TOP_K,
)
- return query_embeddings_collection(
- collection_names=form_data.collection_names,
- query_embeddings=query_embeddings,
- k=form_data.k if form_data.k else app.state.TOP_K,
- )
-
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -427,8 +508,6 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
texts = [doc.page_content for doc in docs]
- texts = list(map(lambda x: x.replace("\n", " "), texts))
-
metadatas = [doc.metadata for doc in docs]
try:
@@ -440,27 +519,16 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
collection = CHROMA_CLIENT.create_collection(name=collection_name)
- if app.state.RAG_EMBEDDING_ENGINE == "":
- embeddings = app.state.sentence_transformer_ef.encode(texts).tolist()
- elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
- embeddings = [
- generate_ollama_embeddings(
- GenerateEmbeddingsForm(
- **{"model": app.state.RAG_EMBEDDING_MODEL, "prompt": text}
- )
- )
- for text in texts
- ]
- elif app.state.RAG_EMBEDDING_ENGINE == "openai":
- embeddings = [
- generate_openai_embeddings(
- model=app.state.RAG_EMBEDDING_MODEL,
- text=text,
- key=app.state.OPENAI_API_KEY,
- url=app.state.OPENAI_API_BASE_URL,
- )
- for text in texts
- ]
+ embedding_func = get_embedding_function(
+ app.state.RAG_EMBEDDING_ENGINE,
+ app.state.RAG_EMBEDDING_MODEL,
+ app.state.sentence_transformer_ef,
+ app.state.OPENAI_API_KEY,
+ app.state.OPENAI_API_BASE_URL,
+ )
+
+ embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
+ embeddings = embedding_func(embedding_texts)
for batch in create_batches(
api=CHROMA_CLIENT,
diff --git a/backend/apps/rag/utils.py b/backend/apps/rag/utils.py
index 0ce299279..eb9d5c84b 100644
--- a/backend/apps/rag/utils.py
+++ b/backend/apps/rag/utils.py
@@ -1,3 +1,4 @@
+import os
import logging
import requests
@@ -8,6 +9,16 @@ from apps.ollama.main import (
GenerateEmbeddingsForm,
)
+from huggingface_hub import snapshot_download
+
+from langchain_core.documents import Document
+from langchain_community.retrievers import BM25Retriever
+from langchain.retrievers import (
+ ContextualCompressionRetriever,
+ EnsembleRetriever,
+)
+
+from typing import Optional
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
@@ -15,88 +26,164 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
-def query_embeddings_doc(collection_name: str, query: str, query_embeddings, k: int):
+def query_doc(
+ collection_name: str,
+ query: str,
+ embedding_function,
+ k: int,
+):
try:
- # if you use docker use the model from the environment variable
- log.info(f"query_embeddings_doc {query_embeddings}")
collection = CHROMA_CLIENT.get_collection(name=collection_name)
-
+ query_embeddings = embedding_function(query)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
)
- log.info(f"query_embeddings_doc:result {result}")
+ log.info(f"query_doc:result {result}")
return result
except Exception as e:
raise e
-def merge_and_sort_query_results(query_results, k):
+def query_doc_with_hybrid_search(
+ collection_name: str,
+ query: str,
+ embedding_function,
+ k: int,
+ reranking_function,
+ r: int,
+):
+ try:
+ collection = CHROMA_CLIENT.get_collection(name=collection_name)
+ documents = collection.get() # get all documents
+
+ bm25_retriever = BM25Retriever.from_texts(
+ texts=documents.get("documents"),
+ metadatas=documents.get("metadatas"),
+ )
+ bm25_retriever.k = k
+
+ chroma_retriever = ChromaRetriever(
+ collection=collection,
+ embedding_function=embedding_function,
+ top_n=k,
+ )
+
+ ensemble_retriever = EnsembleRetriever(
+ retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
+ )
+
+ compressor = RerankCompressor(
+ embedding_function=embedding_function,
+ reranking_function=reranking_function,
+ r_score=r,
+ top_n=k,
+ )
+
+ compression_retriever = ContextualCompressionRetriever(
+ base_compressor=compressor, base_retriever=ensemble_retriever
+ )
+
+ result = compression_retriever.invoke(query)
+ result = {
+ "distances": [[d.metadata.get("score") for d in result]],
+ "documents": [[d.page_content for d in result]],
+ "metadatas": [[d.metadata for d in result]],
+ }
+ log.info(f"query_doc_with_hybrid_search:result {result}")
+ return result
+ except Exception as e:
+ raise e
+
+
+def merge_and_sort_query_results(query_results, k, reverse=False):
# Initialize lists to store combined data
- combined_ids = []
combined_distances = []
- combined_metadatas = []
combined_documents = []
+ combined_metadatas = []
- # Combine data from each dictionary
for data in query_results:
- combined_ids.extend(data["ids"][0])
combined_distances.extend(data["distances"][0])
- combined_metadatas.extend(data["metadatas"][0])
combined_documents.extend(data["documents"][0])
+ combined_metadatas.extend(data["metadatas"][0])
- # Create a list of tuples (distance, id, metadata, document)
- combined = list(
- zip(combined_distances, combined_ids, combined_metadatas, combined_documents)
- )
+ # Create a list of tuples (distance, document, metadata)
+ combined = list(zip(combined_distances, combined_documents, combined_metadatas))
# Sort the list based on distances
- combined.sort(key=lambda x: x[0])
+ combined.sort(key=lambda x: x[0], reverse=reverse)
- # Unzip the sorted list
- sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = zip(*combined)
+ # We don't have anything :-(
+ if not combined:
+ sorted_distances = []
+ sorted_documents = []
+ sorted_metadatas = []
+ else:
+ # Unzip the sorted list
+ sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
- # Slicing the lists to include only k elements
- sorted_distances = list(sorted_distances)[:k]
- sorted_ids = list(sorted_ids)[:k]
- sorted_metadatas = list(sorted_metadatas)[:k]
- sorted_documents = list(sorted_documents)[:k]
+ # Slicing the lists to include only k elements
+ sorted_distances = list(sorted_distances)[:k]
+ sorted_documents = list(sorted_documents)[:k]
+ sorted_metadatas = list(sorted_metadatas)[:k]
# Create the output dictionary
- merged_query_results = {
- "ids": [sorted_ids],
+ result = {
"distances": [sorted_distances],
- "metadatas": [sorted_metadatas],
"documents": [sorted_documents],
- "embeddings": None,
- "uris": None,
- "data": None,
+ "metadatas": [sorted_metadatas],
}
- return merged_query_results
+ return result
-def query_embeddings_collection(
- collection_names: List[str], query: str, query_embeddings, k: int
+def query_collection(
+ collection_names: List[str],
+ query: str,
+ embedding_function,
+ k: int,
+):
+ results = []
+ for collection_name in collection_names:
+ try:
+ result = query_doc(
+ collection_name=collection_name,
+ query=query,
+ k=k,
+ embedding_function=embedding_function,
+ )
+ results.append(result)
+ except:
+ pass
+ return merge_and_sort_query_results(results, k=k)
+
+
+def query_collection_with_hybrid_search(
+ collection_names: List[str],
+ query: str,
+ embedding_function,
+ k: int,
+ reranking_function,
+ r: float,
):
results = []
- log.info(f"query_embeddings_collection {query_embeddings}")
-
for collection_name in collection_names:
try:
- result = query_embeddings_doc(
+ result = query_doc_with_hybrid_search(
collection_name=collection_name,
query=query,
- query_embeddings=query_embeddings,
+ embedding_function=embedding_function,
k=k,
+ reranking_function=reranking_function,
+ r=r,
)
results.append(result)
except:
pass
- return merge_and_sort_query_results(results, k)
+ return merge_and_sort_query_results(results, k=k, reverse=True)
def rag_template(template: str, context: str, query: str):
@@ -105,20 +192,53 @@ def rag_template(template: str, context: str, query: str):
return template
-def rag_messages(
- docs,
- messages,
- template,
- k,
+def get_embedding_function(
embedding_engine,
embedding_model,
embedding_function,
openai_key,
openai_url,
):
- log.debug(
- f"docs: {docs} {messages} {embedding_engine} {embedding_model} {embedding_function} {openai_key} {openai_url}"
- )
+ if embedding_engine == "":
+ return lambda query: embedding_function.encode(query).tolist()
+ elif embedding_engine in ["ollama", "openai"]:
+ if embedding_engine == "ollama":
+ func = lambda query: generate_ollama_embeddings(
+ GenerateEmbeddingsForm(
+ **{
+ "model": embedding_model,
+ "prompt": query,
+ }
+ )
+ )
+ elif embedding_engine == "openai":
+ func = lambda query: generate_openai_embeddings(
+ model=embedding_model,
+ text=query,
+ key=openai_key,
+ url=openai_url,
+ )
+
+ def generate_multiple(query, f):
+ if isinstance(query, list):
+ return [f(q) for q in query]
+ else:
+ return f(query)
+
+ return lambda query: generate_multiple(query, func)
+
+
+def rag_messages(
+ docs,
+ messages,
+ template,
+ embedding_function,
+ k,
+ reranking_function,
+ r,
+ hybrid_search,
+):
+ log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
@@ -145,62 +265,65 @@ def rag_messages(
content_type = None
query = ""
+ extracted_collections = []
relevant_contexts = []
for doc in docs:
context = None
- try:
+ collection = doc.get("collection_name")
+ if collection:
+ collection = [collection]
+ else:
+ collection = doc.get("collection_names", [])
+ collection = set(collection).difference(extracted_collections)
+ if not collection:
+ log.debug(f"skipping {doc} as it has already been extracted")
+ continue
+
+ try:
if doc["type"] == "text":
context = doc["content"]
else:
- if embedding_engine == "":
- query_embeddings = embedding_function.encode(query).tolist()
- elif embedding_engine == "ollama":
- query_embeddings = generate_ollama_embeddings(
- GenerateEmbeddingsForm(
- **{
- "model": embedding_model,
- "prompt": query,
- }
- )
- )
- elif embedding_engine == "openai":
- query_embeddings = generate_openai_embeddings(
- model=embedding_model,
- text=query,
- key=openai_key,
- url=openai_url,
- )
-
- if doc["type"] == "collection":
- context = query_embeddings_collection(
- collection_names=doc["collection_names"],
+ if hybrid_search:
+ context = query_collection_with_hybrid_search(
+ collection_names=(
+ doc["collection_names"]
+ if doc["type"] == "collection"
+ else [doc["collection_name"]]
+ ),
query=query,
- query_embeddings=query_embeddings,
+ embedding_function=embedding_function,
k=k,
+ reranking_function=reranking_function,
+ r=r,
)
else:
- context = query_embeddings_doc(
- collection_name=doc["collection_name"],
+ context = query_collection(
+ collection_names=(
+ doc["collection_names"]
+ if doc["type"] == "collection"
+ else [doc["collection_name"]]
+ ),
query=query,
- query_embeddings=query_embeddings,
+ embedding_function=embedding_function,
k=k,
)
-
except Exception as e:
log.exception(e)
context = None
- relevant_contexts.append(context)
+ if context:
+ relevant_contexts.append(context)
- log.debug(f"relevant_contexts: {relevant_contexts}")
+ extracted_collections.extend(collection)
context_string = ""
for context in relevant_contexts:
- if context:
- context_string += " ".join(context["documents"][0]) + "\n"
+ items = context["documents"][0]
+ context_string += "\n\n".join(items)
+ context_string = context_string.strip()
ra_content = rag_template(
template=template,
@@ -208,6 +331,8 @@ def rag_messages(
query=query,
)
+ log.debug(f"ra_content: {ra_content}")
+
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
@@ -229,6 +354,44 @@ def rag_messages(
return messages
+def get_model_path(model: str, update_model: bool = False):
+ # Construct huggingface_hub kwargs with local_files_only to return the snapshot path
+ cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME")
+
+ local_files_only = not update_model
+
+ snapshot_kwargs = {
+ "cache_dir": cache_dir,
+ "local_files_only": local_files_only,
+ }
+
+ log.debug(f"model: {model}")
+ log.debug(f"snapshot_kwargs: {snapshot_kwargs}")
+
+ # Inspiration from upstream sentence_transformers
+ if (
+ os.path.exists(model)
+ or ("\\" in model or model.count("/") > 1)
+ and local_files_only
+ ):
+ # If fully qualified path exists, return input, else set repo_id
+ return model
+ elif "/" not in model:
+ # Set valid repo_id for model short-name
+ model = "sentence-transformers" + "/" + model
+
+ snapshot_kwargs["repo_id"] = model
+
+ # Attempt to query the huggingface_hub library to determine the local path and/or to update
+ try:
+ model_repo_path = snapshot_download(**snapshot_kwargs)
+ log.debug(f"model_repo_path: {model_repo_path}")
+ return model_repo_path
+ except Exception as e:
+ log.exception(f"Cannot determine model snapshot path: {e}")
+ return model
+
+
def generate_openai_embeddings(
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
):
@@ -250,3 +413,99 @@ def generate_openai_embeddings(
except Exception as e:
print(e)
return None
+
+
+from typing import Any
+
+from langchain_core.retrievers import BaseRetriever
+from langchain_core.callbacks import CallbackManagerForRetrieverRun
+
+
+class ChromaRetriever(BaseRetriever):
+ collection: Any
+ embedding_function: Any
+ top_n: int
+
+ def _get_relevant_documents(
+ self,
+ query: str,
+ *,
+ run_manager: CallbackManagerForRetrieverRun,
+ ) -> List[Document]:
+ query_embeddings = self.embedding_function(query)
+
+ results = self.collection.query(
+ query_embeddings=[query_embeddings],
+ n_results=self.top_n,
+ )
+
+ ids = results["ids"][0]
+ metadatas = results["metadatas"][0]
+ documents = results["documents"][0]
+
+ return [
+ Document(
+ metadata=metadatas[idx],
+ page_content=documents[idx],
+ )
+ for idx in range(len(ids))
+ ]
+
+
+import operator
+
+from typing import Optional, Sequence
+
+from langchain_core.documents import BaseDocumentCompressor, Document
+from langchain_core.callbacks import Callbacks
+from langchain_core.pydantic_v1 import Extra
+
+from sentence_transformers import util
+
+
+class RerankCompressor(BaseDocumentCompressor):
+ embedding_function: Any
+ reranking_function: Any
+ r_score: float
+ top_n: int
+
+ class Config:
+ extra = Extra.forbid
+ arbitrary_types_allowed = True
+
+ def compress_documents(
+ self,
+ documents: Sequence[Document],
+ query: str,
+ callbacks: Optional[Callbacks] = None,
+ ) -> Sequence[Document]:
+ if self.reranking_function:
+ scores = self.reranking_function.predict(
+ [(query, doc.page_content) for doc in documents]
+ )
+ else:
+ query_embedding = self.embedding_function(query)
+ document_embedding = self.embedding_function(
+ [doc.page_content for doc in documents]
+ )
+ scores = util.cos_sim(query_embedding, document_embedding)[0]
+
+ docs_with_scores = list(zip(documents, scores.tolist()))
+ if self.r_score:
+ docs_with_scores = [
+ (d, s) for d, s in docs_with_scores if s >= self.r_score
+ ]
+
+ reverse = self.reranking_function is not None
+ result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=reverse)
+
+ final_results = []
+ for doc, doc_score in result[: self.top_n]:
+ metadata = doc.metadata
+ metadata["score"] = doc_score
+ doc = Document(
+ page_content=doc.page_content,
+ metadata=metadata,
+ )
+ final_results.append(doc)
+ return final_results
diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py
index fad566ce9..136e3fafc 100644
--- a/backend/apps/web/internal/db.py
+++ b/backend/apps/web/internal/db.py
@@ -1,6 +1,7 @@
from peewee import *
from peewee_migrate import Router
-from config import SRC_LOG_LEVELS, DATA_DIR
+from playhouse.db_url import connect
+from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL
import os
import logging
@@ -11,12 +12,12 @@ log.setLevel(SRC_LOG_LEVELS["DB"])
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
- log.info("File renamed successfully.")
+ log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
-
-DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
+DB = connect(DATABASE_URL)
+log.info(f"Connected to a {DB.__class__.__name__} database.")
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
router.run()
DB.connect(reuse_if_open=True)
diff --git a/backend/apps/web/internal/migrations/001_initial_schema.py b/backend/apps/web/internal/migrations/001_initial_schema.py
index 24ea6d39f..77788fff9 100644
--- a/backend/apps/web/internal/migrations/001_initial_schema.py
+++ b/backend/apps/web/internal/migrations/001_initial_schema.py
@@ -37,6 +37,18 @@ with suppress(ImportError):
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
+ # We perform different migrations for SQLite and other databases
+ # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
+ # will require per-database SQL queries.
+ # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
+ # schema instead of trying to migrate from an older schema.
+ if isinstance(database, pw.SqliteDatabase):
+ migrate_sqlite(migrator, database, fake=fake)
+ else:
+ migrate_external(migrator, database, fake=fake)
+
+
+def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
@@ -129,6 +141,99 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
table_name = "user"
+def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
+ @migrator.create_model
+ class Auth(pw.Model):
+ id = pw.CharField(max_length=255, unique=True)
+ email = pw.CharField(max_length=255)
+ password = pw.TextField()
+ active = pw.BooleanField()
+
+ class Meta:
+ table_name = "auth"
+
+ @migrator.create_model
+ class Chat(pw.Model):
+ id = pw.CharField(max_length=255, unique=True)
+ user_id = pw.CharField(max_length=255)
+ title = pw.TextField()
+ chat = pw.TextField()
+ timestamp = pw.BigIntegerField()
+
+ class Meta:
+ table_name = "chat"
+
+ @migrator.create_model
+ class ChatIdTag(pw.Model):
+ id = pw.CharField(max_length=255, unique=True)
+ tag_name = pw.CharField(max_length=255)
+ chat_id = pw.CharField(max_length=255)
+ user_id = pw.CharField(max_length=255)
+ timestamp = pw.BigIntegerField()
+
+ class Meta:
+ table_name = "chatidtag"
+
+ @migrator.create_model
+ class Document(pw.Model):
+ id = pw.AutoField()
+ collection_name = pw.CharField(max_length=255, unique=True)
+ name = pw.CharField(max_length=255, unique=True)
+ title = pw.TextField()
+ filename = pw.TextField()
+ content = pw.TextField(null=True)
+ user_id = pw.CharField(max_length=255)
+ timestamp = pw.BigIntegerField()
+
+ class Meta:
+ table_name = "document"
+
+ @migrator.create_model
+ class Modelfile(pw.Model):
+ id = pw.AutoField()
+ tag_name = pw.CharField(max_length=255, unique=True)
+ user_id = pw.CharField(max_length=255)
+ modelfile = pw.TextField()
+ timestamp = pw.BigIntegerField()
+
+ class Meta:
+ table_name = "modelfile"
+
+ @migrator.create_model
+ class Prompt(pw.Model):
+ id = pw.AutoField()
+ command = pw.CharField(max_length=255, unique=True)
+ user_id = pw.CharField(max_length=255)
+ title = pw.TextField()
+ content = pw.TextField()
+ timestamp = pw.BigIntegerField()
+
+ class Meta:
+ table_name = "prompt"
+
+ @migrator.create_model
+ class Tag(pw.Model):
+ id = pw.CharField(max_length=255, unique=True)
+ name = pw.CharField(max_length=255)
+ user_id = pw.CharField(max_length=255)
+ data = pw.TextField(null=True)
+
+ class Meta:
+ table_name = "tag"
+
+ @migrator.create_model
+ class User(pw.Model):
+ id = pw.CharField(max_length=255, unique=True)
+ name = pw.CharField(max_length=255)
+ email = pw.CharField(max_length=255)
+ role = pw.CharField(max_length=255)
+ profile_image_url = pw.TextField()
+ timestamp = pw.BigIntegerField()
+
+ class Meta:
+ table_name = "user"
+
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
diff --git a/backend/apps/web/internal/migrations/005_add_updated_at.py b/backend/apps/web/internal/migrations/005_add_updated_at.py
index 63a023cdb..950866ef0 100644
--- a/backend/apps/web/internal/migrations/005_add_updated_at.py
+++ b/backend/apps/web/internal/migrations/005_add_updated_at.py
@@ -37,6 +37,13 @@ with suppress(ImportError):
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
+ if isinstance(database, pw.SqliteDatabase):
+ migrate_sqlite(migrator, database, fake=fake)
+ else:
+ migrate_external(migrator, database, fake=fake)
+
+
+def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
# Adding fields created_at and updated_at to the 'chat' table
migrator.add_fields(
"chat",
@@ -60,9 +67,40 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
)
+def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
+ # Adding fields created_at and updated_at to the 'chat' table
+ migrator.add_fields(
+ "chat",
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
+ )
+
+ # Populate the new fields from an existing 'timestamp' field
+ migrator.sql(
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
+ )
+
+ # Now that the data has been copied, remove the original 'timestamp' field
+ migrator.remove_fields("chat", "timestamp")
+
+ # Update the fields to be not null now that they are populated
+ migrator.change_fields(
+ "chat",
+ created_at=pw.BigIntegerField(null=False),
+ updated_at=pw.BigIntegerField(null=False),
+ )
+
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
+ if isinstance(database, pw.SqliteDatabase):
+ rollback_sqlite(migrator, database, fake=fake)
+ else:
+ rollback_external(migrator, database, fake=fake)
+
+
+def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
@@ -75,3 +113,18 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
+
+
+def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
+ # Recreate the timestamp field initially allowing null values for safe transition
+ migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
+
+ # Copy the earliest created_at date back into the new timestamp field
+ # This assumes created_at was originally a copy of timestamp
+ migrator.sql("UPDATE chat SET timestamp = created_at")
+
+ # Remove the created_at and updated_at fields
+ migrator.remove_fields("chat", "created_at", "updated_at")
+
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
+ migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))
diff --git a/backend/apps/web/internal/migrations/006_migrate_timestamps_and_charfields.py b/backend/apps/web/internal/migrations/006_migrate_timestamps_and_charfields.py
new file mode 100644
index 000000000..caca14d32
--- /dev/null
+++ b/backend/apps/web/internal/migrations/006_migrate_timestamps_and_charfields.py
@@ -0,0 +1,130 @@
+"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
+
+Some examples (model - class or model name)::
+
+ > Model = migrator.orm['table_name'] # Return model in current state by name
+ > Model = migrator.ModelClass # Return model in current state by name
+
+ > migrator.sql(sql) # Run custom SQL
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
+ > migrator.remove_model(model, cascade=True) # Remove a model
+ > migrator.add_fields(model, **fields) # Add fields to a model
+ > migrator.change_fields(model, **fields) # Change fields
+ > migrator.remove_fields(model, *field_names, cascade=True)
+ > migrator.rename_field(model, old_field_name, new_field_name)
+ > migrator.rename_table(model, new_table_name)
+ > migrator.add_index(model, *col_names, unique=False)
+ > migrator.add_not_null(model, *field_names)
+ > migrator.add_default(model, field_name, default)
+ > migrator.add_constraint(model, name, sql)
+ > migrator.drop_index(model, *col_names)
+ > migrator.drop_not_null(model, *field_names)
+ > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+ import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+ """Write your migrations here."""
+
+ # Alter the tables with timestamps
+ migrator.change_fields(
+ "chatidtag",
+ timestamp=pw.BigIntegerField(),
+ )
+ migrator.change_fields(
+ "document",
+ timestamp=pw.BigIntegerField(),
+ )
+ migrator.change_fields(
+ "modelfile",
+ timestamp=pw.BigIntegerField(),
+ )
+ migrator.change_fields(
+ "prompt",
+ timestamp=pw.BigIntegerField(),
+ )
+ migrator.change_fields(
+ "user",
+ timestamp=pw.BigIntegerField(),
+ )
+ # Alter the tables with varchar to text where necessary
+ migrator.change_fields(
+ "auth",
+ password=pw.TextField(),
+ )
+ migrator.change_fields(
+ "chat",
+ title=pw.TextField(),
+ )
+ migrator.change_fields(
+ "document",
+ title=pw.TextField(),
+ filename=pw.TextField(),
+ )
+ migrator.change_fields(
+ "prompt",
+ title=pw.TextField(),
+ )
+ migrator.change_fields(
+ "user",
+ profile_image_url=pw.TextField(),
+ )
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+ """Write your rollback migrations here."""
+
+ if isinstance(database, pw.SqliteDatabase):
+ # Alter the tables with timestamps
+ migrator.change_fields(
+ "chatidtag",
+ timestamp=pw.DateField(),
+ )
+ migrator.change_fields(
+ "document",
+ timestamp=pw.DateField(),
+ )
+ migrator.change_fields(
+ "modelfile",
+ timestamp=pw.DateField(),
+ )
+ migrator.change_fields(
+ "prompt",
+ timestamp=pw.DateField(),
+ )
+ migrator.change_fields(
+ "user",
+ timestamp=pw.DateField(),
+ )
+ migrator.change_fields(
+ "auth",
+ password=pw.CharField(max_length=255),
+ )
+ migrator.change_fields(
+ "chat",
+ title=pw.CharField(),
+ )
+ migrator.change_fields(
+ "document",
+ title=pw.CharField(),
+ filename=pw.CharField(),
+ )
+ migrator.change_fields(
+ "prompt",
+ title=pw.CharField(),
+ )
+ migrator.change_fields(
+ "user",
+ profile_image_url=pw.CharField(),
+ )
diff --git a/backend/apps/web/internal/migrations/007_add_user_last_active_at.py b/backend/apps/web/internal/migrations/007_add_user_last_active_at.py
new file mode 100644
index 000000000..2d45fa7ea
--- /dev/null
+++ b/backend/apps/web/internal/migrations/007_add_user_last_active_at.py
@@ -0,0 +1,79 @@
+"""Peewee migrations -- 002_add_local_sharing.py.
+
+Some examples (model - class or model name)::
+
+ > Model = migrator.orm['table_name'] # Return model in current state by name
+ > Model = migrator.ModelClass # Return model in current state by name
+
+ > migrator.sql(sql) # Run custom SQL
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
+ > migrator.remove_model(model, cascade=True) # Remove a model
+ > migrator.add_fields(model, **fields) # Add fields to a model
+ > migrator.change_fields(model, **fields) # Change fields
+ > migrator.remove_fields(model, *field_names, cascade=True)
+ > migrator.rename_field(model, old_field_name, new_field_name)
+ > migrator.rename_table(model, new_table_name)
+ > migrator.add_index(model, *col_names, unique=False)
+ > migrator.add_not_null(model, *field_names)
+ > migrator.add_default(model, field_name, default)
+ > migrator.add_constraint(model, name, sql)
+ > migrator.drop_index(model, *col_names)
+ > migrator.drop_not_null(model, *field_names)
+ > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+ import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+ """Write your migrations here."""
+
+ # Adding fields created_at and updated_at to the 'user' table
+ migrator.add_fields(
+ "user",
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
+ last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
+ )
+
+ # Populate the new fields from an existing 'timestamp' field
+ migrator.sql(
+ "UPDATE user SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL"
+ )
+
+ # Now that the data has been copied, remove the original 'timestamp' field
+ migrator.remove_fields("user", "timestamp")
+
+ # Update the fields to be not null now that they are populated
+ migrator.change_fields(
+ "user",
+ created_at=pw.BigIntegerField(null=False),
+ updated_at=pw.BigIntegerField(null=False),
+ last_active_at=pw.BigIntegerField(null=False),
+ )
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+ """Write your rollback migrations here."""
+
+ # Recreate the timestamp field initially allowing null values for safe transition
+ migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
+
+ # Copy the earliest created_at date back into the new timestamp field
+ # This assumes created_at was originally a copy of timestamp
+ migrator.sql("UPDATE user SET timestamp = created_at")
+
+ # Remove the created_at and updated_at fields
+ migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
+
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
+ migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))
diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py
index a97312ff9..9c4e5ffed 100644
--- a/backend/apps/web/models/auths.py
+++ b/backend/apps/web/models/auths.py
@@ -23,7 +23,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Auth(Model):
id = CharField(unique=True)
email = CharField()
- password = CharField()
+ password = TextField()
active = BooleanField()
class Meta:
diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py
index ea7fb355d..891151b94 100644
--- a/backend/apps/web/models/chats.py
+++ b/backend/apps/web/models/chats.py
@@ -17,11 +17,11 @@ from apps.web.internal.db import DB
class Chat(Model):
id = CharField(unique=True)
user_id = CharField()
- title = CharField()
+ title = TextField()
chat = TextField() # Save Chat JSON as Text
- created_at = DateTimeField()
- updated_at = DateTimeField()
+ created_at = BigIntegerField()
+ updated_at = BigIntegerField()
share_id = CharField(null=True, unique=True)
archived = BooleanField(default=False)
@@ -191,7 +191,7 @@ class ChatTable:
except:
return None
- def get_archived_chat_lists_by_user_id(
+ def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -204,7 +204,7 @@ class ChatTable:
# .offset(skip)
]
- def get_chat_lists_by_user_id(
+ def get_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -217,7 +217,7 @@ class ChatTable:
# .offset(skip)
]
- def get_chat_lists_by_chat_ids(
+ def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -228,20 +228,6 @@ class ChatTable:
.order_by(Chat.updated_at.desc())
]
- def get_all_chats(self) -> List[ChatModel]:
- return [
- ChatModel(**model_to_dict(chat))
- for chat in Chat.select().order_by(Chat.updated_at.desc())
- ]
-
- def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
- return [
- ChatModel(**model_to_dict(chat))
- for chat in Chat.select()
- .where(Chat.user_id == user_id)
- .order_by(Chat.updated_at.desc())
- ]
-
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
@@ -271,9 +257,28 @@ class ChatTable:
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
- for chat in Chat.select().limit(limit).offset(skip)
+ for chat in Chat.select().order_by(Chat.updated_at.desc())
+ # .limit(limit).offset(skip)
]
+ def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
+ return [
+ ChatModel(**model_to_dict(chat))
+ for chat in Chat.select()
+ .where(Chat.user_id == user_id)
+ .order_by(Chat.updated_at.desc())
+ # .limit(limit).offset(skip)
+ ]
+
+ def delete_chat_by_id(self, id: str) -> bool:
+ try:
+ query = Chat.delete().where((Chat.id == id))
+ query.execute() # Remove the rows, return number of rows removed.
+
+ return True and self.delete_shared_chat_by_chat_id(id)
+ except:
+ return False
+
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
diff --git a/backend/apps/web/models/documents.py b/backend/apps/web/models/documents.py
index 91e721a48..42b99596c 100644
--- a/backend/apps/web/models/documents.py
+++ b/backend/apps/web/models/documents.py
@@ -25,11 +25,11 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Document(Model):
collection_name = CharField(unique=True)
name = CharField(unique=True)
- title = CharField()
- filename = CharField()
+ title = TextField()
+ filename = TextField()
content = TextField(null=True)
user_id = CharField()
- timestamp = DateField()
+ timestamp = BigIntegerField()
class Meta:
database = DB
diff --git a/backend/apps/web/models/modelfiles.py b/backend/apps/web/models/modelfiles.py
index 50439a808..1d60d7c55 100644
--- a/backend/apps/web/models/modelfiles.py
+++ b/backend/apps/web/models/modelfiles.py
@@ -20,7 +20,7 @@ class Modelfile(Model):
tag_name = CharField(unique=True)
user_id = CharField()
modelfile = TextField()
- timestamp = DateField()
+ timestamp = BigIntegerField()
class Meta:
database = DB
diff --git a/backend/apps/web/models/prompts.py b/backend/apps/web/models/prompts.py
index e6b663c04..bc4e3e58b 100644
--- a/backend/apps/web/models/prompts.py
+++ b/backend/apps/web/models/prompts.py
@@ -19,9 +19,9 @@ import json
class Prompt(Model):
command = CharField(unique=True)
user_id = CharField()
- title = CharField()
+ title = TextField()
content = TextField()
- timestamp = DateField()
+ timestamp = BigIntegerField()
class Meta:
database = DB
diff --git a/backend/apps/web/models/tags.py b/backend/apps/web/models/tags.py
index 02de5b9d7..d9a967ff7 100644
--- a/backend/apps/web/models/tags.py
+++ b/backend/apps/web/models/tags.py
@@ -35,7 +35,7 @@ class ChatIdTag(Model):
tag_name = CharField()
chat_id = CharField()
user_id = CharField()
- timestamp = DateField()
+ timestamp = BigIntegerField()
class Meta:
database = DB
diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py
index 7d1e182da..1a127a778 100644
--- a/backend/apps/web/models/users.py
+++ b/backend/apps/web/models/users.py
@@ -18,8 +18,12 @@ class User(Model):
name = CharField()
email = CharField()
role = CharField()
- profile_image_url = CharField()
- timestamp = DateField()
+ profile_image_url = TextField()
+
+ last_active_at = BigIntegerField()
+ updated_at = BigIntegerField()
+ created_at = BigIntegerField()
+
api_key = CharField(null=True, unique=True)
class Meta:
@@ -32,7 +36,11 @@ class UserModel(BaseModel):
email: str
role: str = "pending"
profile_image_url: str
- timestamp: int # timestamp in epoch
+
+ last_active_at: int # timestamp in epoch
+ updated_at: int # timestamp in epoch
+ created_at: int # timestamp in epoch
+
api_key: Optional[str] = None
@@ -73,7 +81,9 @@ class UsersTable:
"email": email,
"role": role,
"profile_image_url": profile_image_url,
- "timestamp": int(time.time()),
+ "last_active_at": int(time.time()),
+ "created_at": int(time.time()),
+ "updated_at": int(time.time()),
}
)
result = User.create(**user.model_dump())
@@ -137,6 +147,16 @@ class UsersTable:
except:
return None
+ def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
+ try:
+ query = User.update(last_active_at=int(time.time())).where(User.id == id)
+ query.execute()
+
+ user = User.get(User.id == id)
+ return UserModel(**model_to_dict(user))
+ except:
+ return None
+
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try:
query = User.update(**updated).where(User.id == id)
diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py
index 89d8c1c8f..321b26034 100644
--- a/backend/apps/web/routers/auths.py
+++ b/backend/apps/web/routers/auths.py
@@ -1,3 +1,5 @@
+import logging
+
from fastapi import Request
from fastapi import Depends, HTTPException, status
diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py
index bbe3d84b9..73b15e04a 100644
--- a/backend/apps/web/routers/chats.py
+++ b/backend/apps/web/routers/chats.py
@@ -36,15 +36,49 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
-# GetChats
+# GetChatList
############################
@router.get("/", response_model=List[ChatTitleIdResponse])
-async def get_user_chats(
+@router.get("/list", response_model=List[ChatTitleIdResponse])
+async def get_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
- return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
+ return Chats.get_chat_list_by_user_id(user.id, skip, limit)
+
+
+############################
+# DeleteAllChats
+############################
+
+
+@router.delete("/", response_model=bool)
+async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
+
+ if (
+ user.role == "user"
+ and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+ )
+
+ result = Chats.delete_chats_by_user_id(user.id)
+ return result
+
+
+############################
+# GetUserChatList
+############################
+
+
+@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
+async def get_user_chat_list_by_user_id(
+ user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
+):
+ return Chats.get_chat_list_by_user_id(user_id, skip, limit)
############################
@@ -53,22 +87,22 @@ async def get_user_chats(
@router.get("/archived", response_model=List[ChatTitleIdResponse])
-async def get_archived_user_chats(
+async def get_archived_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
- return Chats.get_archived_chat_lists_by_user_id(user.id, skip, limit)
+ return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
############################
-# GetAllChats
+# GetChats
############################
@router.get("/all", response_model=List[ChatResponse])
-async def get_all_user_chats(user=Depends(get_current_user)):
+async def get_user_chats(user=Depends(get_current_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
- for chat in Chats.get_all_chats_by_user_id(user.id)
+ for chat in Chats.get_chats_by_user_id(user.id)
]
@@ -86,7 +120,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
)
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
- for chat in Chats.get_all_chats()
+ for chat in Chats.get_chats()
]
@@ -107,45 +141,6 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
)
-############################
-# GetAllTags
-############################
-
-
-@router.get("/tags/all", response_model=List[TagModel])
-async def get_all_tags(user=Depends(get_current_user)):
- try:
- tags = Tags.get_tags_by_user_id(user.id)
- return tags
- except Exception as e:
- log.exception(e)
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
- )
-
-
-############################
-# GetChatsByTags
-############################
-
-
-@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
-async def get_user_chats_by_tag_name(
- tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
-):
- chat_ids = [
- chat_id_tag.chat_id
- for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
- ]
-
- chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
-
- if len(chats) == 0:
- Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
-
- return chats
-
-
############################
# GetChatById
############################
@@ -193,17 +188,18 @@ async def update_chat_by_id(
@router.delete("/{id}", response_model=bool)
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
- if (
- user.role == "user"
- and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
- ):
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
- )
+ if user.role == "admin":
+ result = Chats.delete_chat_by_id(id)
+ return result
+ else:
+ if not request.app.state.USER_PERMISSIONS["chat"]["deletion"]:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+ )
- result = Chats.delete_chat_by_id_and_user_id(id, user.id)
- return result
+ result = Chats.delete_chat_by_id_and_user_id(id, user.id)
+ return result
############################
@@ -303,6 +299,45 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
)
+############################
+# GetAllTags
+############################
+
+
+@router.get("/tags/all", response_model=List[TagModel])
+async def get_all_tags(user=Depends(get_current_user)):
+ try:
+ tags = Tags.get_tags_by_user_id(user.id)
+ return tags
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+
+############################
+# GetChatsByTags
+############################
+
+
+@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
+async def get_user_chat_list_by_tag_name(
+ tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
+):
+ chat_ids = [
+ chat_id_tag.chat_id
+ for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
+ ]
+
+ chats = Chats.get_chat_list_by_chat_ids(chat_ids, skip, limit)
+
+ if len(chats) == 0:
+ Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
+
+ return chats
+
+
############################
# GetChatTagsById
############################
@@ -383,24 +418,3 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
-
-
-############################
-# DeleteAllChats
-############################
-
-
-@router.delete("/", response_model=bool)
-async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
-
- if (
- user.role == "user"
- and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
- ):
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
- )
-
- result = Chats.delete_chats_by_user_id(user.id)
- return result
diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py
index 953f5ce5c..12805873d 100644
--- a/backend/apps/web/routers/utils.py
+++ b/backend/apps/web/routers/utils.py
@@ -1,5 +1,6 @@
from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status
+from peewee import SqliteDatabase
from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
@@ -7,7 +8,7 @@ from pydantic import BaseModel
from fpdf import FPDF
import markdown
-
+from apps.web.internal.db import DB
from utils.utils import get_admin_user
from utils.misc import calculate_sha256, get_gravatar_url
@@ -96,8 +97,13 @@ async def download_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
+ if not isinstance(DB, SqliteDatabase):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=ERROR_MESSAGES.DB_NOT_SQLITE,
+ )
return FileResponse(
- f"{DATA_DIR}/webui.db",
+ DB.database,
media_type="application/octet-stream",
filename="webui.db",
)
diff --git a/backend/config.py b/backend/config.py
index f421c8aea..f864062d9 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -71,6 +71,9 @@ except ImportError:
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
+if WEBUI_NAME != "Open WebUI":
+ WEBUI_NAME += " (Open WebUI)"
+
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
####################################
@@ -195,9 +198,6 @@ if CUSTOM_NAME:
except Exception as e:
log.exception(e)
pass
-else:
- if WEBUI_NAME != "Open WebUI":
- WEBUI_NAME += " (Open WebUI)"
####################################
@@ -220,7 +220,7 @@ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
# Docs DIR
####################################
-DOCS_DIR = f"{DATA_DIR}/docs"
+DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
@@ -375,8 +375,7 @@ USER_PERMISSIONS_CHAT_DELETION = (
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
-
-MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true"
+ENABLE_MODEL_FILTER = os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true"
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
@@ -418,19 +417,57 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
####################################
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
+CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
+CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
+CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
+CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
+# Comma-separated list of header=value pairs
+CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
+if CHROMA_HTTP_HEADERS:
+ CHROMA_HTTP_HEADERS = dict(
+ [pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
+ )
+else:
+ CHROMA_HTTP_HEADERS = None
+CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
+RAG_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
+RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
+
+ENABLE_RAG_HYBRID_SEARCH = (
+ os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
+)
+
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
+PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
+
RAG_EMBEDDING_MODEL = os.environ.get(
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
+RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
+ os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
+)
+
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
+RAG_RERANKING_MODEL = os.environ.get("RAG_RERANKING_MODEL", "")
+if not RAG_RERANKING_MODEL == "":
+ log.info(f"Reranking model set: {RAG_RERANKING_MODEL}"),
+
+RAG_RERANKING_MODEL_AUTO_UPDATE = (
+ os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
+)
+
+RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
+ os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
+)
+
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
@@ -439,16 +476,28 @@ if USE_CUDA.lower() == "true":
else:
DEVICE_TYPE = "cpu"
+if CHROMA_HTTP_HOST != "":
+ CHROMA_CLIENT = chromadb.HttpClient(
+ host=CHROMA_HTTP_HOST,
+ port=CHROMA_HTTP_PORT,
+ headers=CHROMA_HTTP_HEADERS,
+ ssl=CHROMA_HTTP_SSL,
+ tenant=CHROMA_TENANT,
+ database=CHROMA_DATABASE,
+ settings=Settings(allow_reset=True, anonymized_telemetry=False),
+ )
+else:
+ CHROMA_CLIENT = chromadb.PersistentClient(
+ path=CHROMA_DATA_PATH,
+ settings=Settings(allow_reset=True, anonymized_telemetry=False),
+ tenant=CHROMA_TENANT,
+ database=CHROMA_DATABASE,
+ )
-CHROMA_CLIENT = chromadb.PersistentClient(
- path=CHROMA_DATA_PATH,
- settings=Settings(allow_reset=True, anonymized_telemetry=False),
-)
-CHUNK_SIZE = 1500
-CHUNK_OVERLAP = 100
+CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
+CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
-
-RAG_TEMPLATE = """Use the following context as your learned knowledge, inside XML tags.
+DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside XML tags.
[context]
@@ -458,10 +507,12 @@ When answer to user:
- If you don't know when you are not sure, ask for clarification.
Avoid mentioning that you obtained the information from the context.
And answer according to the language of the user's question.
-
+
Given the context information, answer the query.
Query: [query]"""
+RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
+
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
@@ -480,18 +531,25 @@ WHISPER_MODEL_AUTO_UPDATE = (
# Images
####################################
+IMAGE_GENERATION_ENGINE = os.getenv("IMAGE_GENERATION_ENGINE", "")
+
ENABLE_IMAGE_GENERATION = (
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
)
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
-COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
+COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
IMAGES_OPENAI_API_BASE_URL = os.getenv(
"IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
)
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
+IMAGE_SIZE = os.getenv("IMAGE_SIZE", "512x512")
+
+IMAGE_STEPS = int(os.getenv("IMAGE_STEPS", 50))
+
+IMAGE_GENERATION_MODEL = os.getenv("IMAGE_GENERATION_MODEL", "")
####################################
# Audio
@@ -504,7 +562,17 @@ AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
# LiteLLM
####################################
+
+ENABLE_LITELLM = os.environ.get("ENABLE_LITELLM", "True").lower() == "true"
+
LITELLM_PROXY_PORT = int(os.getenv("LITELLM_PROXY_PORT", "14365"))
if LITELLM_PROXY_PORT < 0 or LITELLM_PROXY_PORT > 65535:
raise ValueError("Invalid port number for LITELLM_PROXY_PORT")
LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
+
+
+####################################
+# Database
+####################################
+
+DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
diff --git a/backend/constants.py b/backend/constants.py
index 310c13311..a26945756 100644
--- a/backend/constants.py
+++ b/backend/constants.py
@@ -69,3 +69,5 @@ class ERROR_MESSAGES(str, Enum):
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
+
+ DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
diff --git a/backend/main.py b/backend/main.py
index c7c78e18d..1b2772627 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -47,7 +47,8 @@ from config import (
FRONTEND_BUILD_DIR,
CACHE_DIR,
STATIC_DIR,
- MODEL_FILTER_ENABLED,
+ ENABLE_LITELLM,
+ ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
@@ -89,7 +90,7 @@ https://github.com/open-webui/open-webui
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
-app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
+app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.WEBHOOK_URL = WEBHOOK_URL
@@ -116,15 +117,14 @@ class RAGMiddleware(BaseHTTPMiddleware):
if "docs" in data:
data = {**data}
data["messages"] = rag_messages(
- data["docs"],
- data["messages"],
- rag_app.state.RAG_TEMPLATE,
- rag_app.state.TOP_K,
- rag_app.state.RAG_EMBEDDING_ENGINE,
- rag_app.state.RAG_EMBEDDING_MODEL,
- rag_app.state.sentence_transformer_ef,
- rag_app.state.OPENAI_API_KEY,
- rag_app.state.OPENAI_API_BASE_URL,
+ docs=data["docs"],
+ messages=data["messages"],
+ template=rag_app.state.RAG_TEMPLATE,
+ embedding_function=rag_app.state.EMBEDDING_FUNCTION,
+ k=rag_app.state.TOP_K,
+ reranking_function=rag_app.state.sentence_transformer_rf,
+ r=rag_app.state.RELEVANCE_THRESHOLD,
+ hybrid_search=rag_app.state.ENABLE_RAG_HYBRID_SEARCH,
)
del data["docs"]
@@ -176,7 +176,8 @@ async def check_url(request: Request, call_next):
@app.on_event("startup")
async def on_startup():
- asyncio.create_task(start_litellm_background())
+ if ENABLE_LITELLM:
+ asyncio.create_task(start_litellm_background())
app.mount("/api/v1", webui_app)
@@ -215,7 +216,7 @@ async def get_app_config():
@app.get("/api/config/model/filter")
async def get_model_filter_config(user=Depends(get_admin_user)):
return {
- "enabled": app.state.MODEL_FILTER_ENABLED,
+ "enabled": app.state.ENABLE_MODEL_FILTER,
"models": app.state.MODEL_FILTER_LIST,
}
@@ -229,20 +230,20 @@ class ModelFilterConfigForm(BaseModel):
async def update_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
):
- app.state.MODEL_FILTER_ENABLED = form_data.enabled
+ app.state.ENABLE_MODEL_FILTER = form_data.enabled
app.state.MODEL_FILTER_LIST = form_data.models
- ollama_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
+ ollama_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
- openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
+ openai_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
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.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
return {
- "enabled": app.state.MODEL_FILTER_ENABLED,
+ "enabled": app.state.ENABLE_MODEL_FILTER,
"models": app.state.MODEL_FILTER_LIST,
}
@@ -326,4 +327,5 @@ app.mount(
@app.on_event("shutdown")
async def shutdown_event():
- await shutdown_litellm_background()
+ if ENABLE_LITELLM:
+ await shutdown_litellm_background()
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 10bcc3b69..336cae17a 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -15,6 +15,8 @@ requests
aiohttp
peewee
peewee-migrate
+psycopg2-binary
+pymysql
bcrypt
litellm==1.35.17
diff --git a/backend/start.sh b/backend/start.sh
index 06adf1ff8..9a8e91133 100755
--- a/backend/start.sh
+++ b/backend/start.sh
@@ -6,6 +6,7 @@ cd "$SCRIPT_DIR" || exit
KEY_FILE=.webui_secret_key
PORT="${PORT:-8080}"
+HOST="${HOST:-0.0.0.0}"
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
echo "No WEBUI_SECRET_KEY provided"
@@ -29,4 +30,4 @@ if [ "$USE_CUDA_DOCKER" = "true" ]; then
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 "$HOST" --port "$PORT" --forwarded-allow-ips '*'
diff --git a/backend/start_windows.bat b/backend/start_windows.bat
index b2c370179..e687de10d 100644
--- a/backend/start_windows.bat
+++ b/backend/start_windows.bat
@@ -7,7 +7,7 @@ SET "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%" || exit /b
SET "KEY_FILE=.webui_secret_key"
-SET "PORT=%PORT:8080%"
+IF "%PORT%"=="" SET PORT=8080
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
diff --git a/backend/utils/utils.py b/backend/utils/utils.py
index 49e15789f..af4fd85c0 100644
--- a/backend/utils/utils.py
+++ b/backend/utils/utils.py
@@ -89,6 +89,8 @@ def get_current_user(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
+ else:
+ Users.update_user_last_active_by_id(user.id)
return user
else:
raise HTTPException(
@@ -99,11 +101,15 @@ def get_current_user(
def get_current_user_by_api_key(api_key: str):
user = Users.get_user_by_api_key(api_key)
+
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
+ else:
+ Users.update_user_last_active_by_id(user.id)
+
return user
diff --git a/confirm_remove.sh b/confirm_remove.sh
index 729c25070..051908e6d 100755
--- a/confirm_remove.sh
+++ b/confirm_remove.sh
@@ -2,7 +2,12 @@
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
read ans
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
- docker-compose down -v
+ command docker-compose 2>/dev/null
+ if [ "$?" == "0" ]; then
+ docker-compose down -v
+ else
+ docker compose down -v
+ fi
else
echo "Operation cancelled."
fi
diff --git a/cypress.config.ts b/cypress.config.ts
new file mode 100644
index 000000000..dbb538233
--- /dev/null
+++ b/cypress.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+ e2e: {
+ baseUrl: 'http://localhost:8080'
+ },
+ video: true
+});
diff --git a/cypress/e2e/chat.cy.ts b/cypress/e2e/chat.cy.ts
new file mode 100644
index 000000000..fce786272
--- /dev/null
+++ b/cypress/e2e/chat.cy.ts
@@ -0,0 +1,46 @@
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
+
+// These tests run through the chat flow.
+describe('Settings', () => {
+ // Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
+ after(() => {
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+ });
+
+ beforeEach(() => {
+ // Login as the admin user
+ cy.loginAdmin();
+ // Visit the home page
+ cy.visit('/');
+ });
+
+ context('Ollama', () => {
+ it('user can select a model', () => {
+ // Click on the model selector
+ cy.get('button[aria-label="Select a model"]').click();
+ // Select the first model
+ cy.get('div[role="option"][data-value]').first().click();
+ });
+
+ it('user can perform text chat', () => {
+ // Click on the model selector
+ cy.get('button[aria-label="Select a model"]').click();
+ // Select the first model
+ cy.get('div[role="option"][data-value]').first().click();
+ // Type a message
+ cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
+ force: true
+ });
+ // Send the message
+ cy.get('button[type="submit"]').click();
+ // User's message should be visible
+ cy.get('.chat-user').should('exist');
+ // Wait for the response
+ cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
+ .find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
+ .should('exist');
+ });
+ });
+});
diff --git a/cypress/e2e/registration.cy.ts b/cypress/e2e/registration.cy.ts
new file mode 100644
index 000000000..232d75e88
--- /dev/null
+++ b/cypress/e2e/registration.cy.ts
@@ -0,0 +1,52 @@
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
+import { adminUser } from '../support/e2e';
+
+// These tests assume the following defaults:
+// 1. No users exist in the database or that the test admin user is an admin
+// 2. Language is set to English
+// 3. The default role for new users is 'pending'
+describe('Registration and Login', () => {
+ // Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
+ after(() => {
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+ });
+
+ beforeEach(() => {
+ cy.visit('/');
+ });
+
+ it('should register a new user as pending', () => {
+ const userName = `Test User - ${Date.now()}`;
+ const userEmail = `cypress-${Date.now()}@example.com`;
+ // Toggle from sign in to sign up
+ cy.contains('Sign up').click();
+ // Fill out the form
+ cy.get('input[autocomplete="name"]').type(userName);
+ cy.get('input[autocomplete="email"]').type(userEmail);
+ cy.get('input[type="password"]').type('password');
+ // Submit the form
+ cy.get('button[type="submit"]').click();
+ // Wait until the user is redirected to the home page
+ cy.contains(userName);
+ // Expect the user to be pending
+ cy.contains('Check Again');
+ });
+
+ it('can login with the admin user', () => {
+ // Fill out the form
+ cy.get('input[autocomplete="email"]').type(adminUser.email);
+ cy.get('input[type="password"]').type(adminUser.password);
+ // Submit the form
+ cy.get('button[type="submit"]').click();
+ // Wait until the user is redirected to the home page
+ cy.contains(adminUser.name);
+ // Dismiss the changelog dialog if it is visible
+ cy.getAllLocalStorage().then((ls) => {
+ if (!ls['version']) {
+ cy.get('button').contains("Okay, Let's Go!").click();
+ }
+ });
+ });
+});
diff --git a/cypress/e2e/settings.cy.ts b/cypress/e2e/settings.cy.ts
new file mode 100644
index 000000000..560ce9794
--- /dev/null
+++ b/cypress/e2e/settings.cy.ts
@@ -0,0 +1,88 @@
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
+import { adminUser } from '../support/e2e';
+
+// These tests run through the various settings pages, ensuring that the user can interact with them as expected
+describe('Settings', () => {
+ // Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
+ after(() => {
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+ });
+
+ beforeEach(() => {
+ // Login as the admin user
+ cy.loginAdmin();
+ // Visit the home page
+ cy.visit('/');
+ // Open the sidebar if it is not already open
+ cy.get('[aria-label="Open sidebar"]').then(() => {
+ cy.get('button[id="sidebar-toggle-button"]').click();
+ });
+ // Click on the profile link
+ cy.get('button').contains(adminUser.name).click();
+ // Click on the settings link
+ cy.get('button').contains('Settings').click();
+ });
+
+ context('General', () => {
+ it('user can open the General modal and hit save', () => {
+ cy.get('button').contains('General').click();
+ cy.get('button').contains('Save').click();
+ });
+ });
+
+ context('Connections', () => {
+ it('user can open the Connections modal and hit save', () => {
+ cy.get('button').contains('Connections').click();
+ cy.get('button').contains('Save').click();
+ });
+ });
+
+ context('Models', () => {
+ it('user can open the Models modal', () => {
+ cy.get('button').contains('Models').click();
+ });
+ });
+
+ context('Interface', () => {
+ it('user can open the Interface modal and hit save', () => {
+ cy.get('button').contains('Interface').click();
+ cy.get('button').contains('Save').click();
+ });
+ });
+
+ context('Audio', () => {
+ it('user can open the Audio modal and hit save', () => {
+ cy.get('button').contains('Audio').click();
+ cy.get('button').contains('Save').click();
+ });
+ });
+
+ context('Images', () => {
+ it('user can open the Images modal and hit save', () => {
+ cy.get('button').contains('Images').click();
+ // Currently fails because the backend requires a valid URL
+ // cy.get('button').contains('Save').click();
+ });
+ });
+
+ context('Chats', () => {
+ it('user can open the Chats modal', () => {
+ cy.get('button').contains('Chats').click();
+ });
+ });
+
+ context('Account', () => {
+ it('user can open the Account modal and hit save', () => {
+ cy.get('button').contains('Account').click();
+ cy.get('button').contains('Save').click();
+ });
+ });
+
+ context('About', () => {
+ it('user can open the About modal', () => {
+ cy.get('button').contains('About').click();
+ });
+ });
+});
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
new file mode 100644
index 000000000..1eedc98df
--- /dev/null
+++ b/cypress/support/e2e.ts
@@ -0,0 +1,73 @@
+///
+
+export const adminUser = {
+ name: 'Admin User',
+ email: 'admin@example.com',
+ password: 'password'
+};
+
+const login = (email: string, password: string) => {
+ return cy.session(
+ email,
+ () => {
+ // Visit auth page
+ cy.visit('/auth');
+ // Fill out the form
+ cy.get('input[autocomplete="email"]').type(email);
+ cy.get('input[type="password"]').type(password);
+ // Submit the form
+ cy.get('button[type="submit"]').click();
+ // Wait until the user is redirected to the home page
+ cy.get('#chat-search').should('exist');
+ // Get the current version to skip the changelog dialog
+ if (localStorage.getItem('version') === null) {
+ cy.get('button').contains("Okay, Let's Go!").click();
+ }
+ },
+ {
+ validate: () => {
+ cy.request({
+ method: 'GET',
+ url: '/api/v1/auths/',
+ headers: {
+ Authorization: 'Bearer ' + localStorage.getItem('token')
+ }
+ });
+ }
+ }
+ );
+};
+
+const register = (name: string, email: string, password: string) => {
+ return cy
+ .request({
+ method: 'POST',
+ url: '/api/v1/auths/signup',
+ body: {
+ name: name,
+ email: email,
+ password: password
+ },
+ failOnStatusCode: false
+ })
+ .then((response) => {
+ expect(response.status).to.be.oneOf([200, 400]);
+ });
+};
+
+const registerAdmin = () => {
+ return register(adminUser.name, adminUser.email, adminUser.password);
+};
+
+const loginAdmin = () => {
+ return login(adminUser.email, adminUser.password);
+};
+
+Cypress.Commands.add('login', (email, password) => login(email, password));
+Cypress.Commands.add('register', (name, email, password) => register(name, email, password));
+Cypress.Commands.add('registerAdmin', () => registerAdmin());
+Cypress.Commands.add('loginAdmin', () => loginAdmin());
+
+before(() => {
+ cy.registerAdmin();
+});
diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts
new file mode 100644
index 000000000..e6c69121a
--- /dev/null
+++ b/cypress/support/index.d.ts
@@ -0,0 +1,11 @@
+// load the global Cypress types
+///
+
+declare namespace Cypress {
+ interface Chainable {
+ login(email: string, password: string): Chainable;
+ register(name: string, email: string, password: string): Chainable;
+ registerAdmin(): Chainable;
+ loginAdmin(): Chainable;
+ }
+}
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
new file mode 100644
index 000000000..ff28d9464
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "inlineSourceMap": true,
+ "sourceMap": false
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 55b35dd58..913c55b78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "open-webui",
- "version": "0.1.121",
+ "version": "0.1.122",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
- "version": "0.1.121",
+ "version": "0.1.122",
"dependencies": {
"@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5",
@@ -19,7 +19,6 @@
"i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1",
"js-sha256": "^0.10.1",
- "jspdf": "^2.5.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"svelte-sonner": "^0.3.19",
@@ -35,8 +34,10 @@
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"autoprefixer": "^10.4.16",
+ "cypress": "^13.8.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-cypress": "^3.0.2",
"eslint-plugin-svelte": "^2.30.0",
"i18next-parser": "^8.13.0",
"postcss": "^8.4.31",
@@ -95,6 +96,73 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@colors/colors": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@cypress/request": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz",
+ "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==",
+ "dev": true,
+ "dependencies": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "http-signature": "~1.3.6",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "performance-now": "^2.1.0",
+ "qs": "6.10.4",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "^4.1.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@cypress/request/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@cypress/xvfb": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+ "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.1.0",
+ "lodash.once": "^4.1.1"
+ }
+ },
+ "node_modules/@cypress/xvfb/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -1068,12 +1136,6 @@
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true
},
- "node_modules/@types/raf": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
- "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
- "optional": true
- },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -1085,6 +1147,18 @@
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
+ "node_modules/@types/sinonjs__fake-timers": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
+ "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
+ "dev": true
+ },
+ "node_modules/@types/sizzle": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
+ "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
+ "dev": true
+ },
"node_modules/@types/symlink-or-copy": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz",
@@ -1100,6 +1174,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
@@ -1316,6 +1400,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1332,6 +1429,42 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1375,6 +1508,26 @@
"node": ">= 8"
}
},
+ "node_modules/arch": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+ "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -1404,20 +1557,51 @@
"node": ">=8"
}
},
+ "node_modules/asn1": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+ "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "node_modules/assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
- "node_modules/atob": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
- "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
- "bin": {
- "atob": "bin/atob.js"
- },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
"engines": {
- "node": ">= 4.5.0"
+ "node": ">= 4.0.0"
}
},
"node_modules/autoprefixer": {
@@ -1457,6 +1641,21 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/aws4": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
+ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
+ "dev": true
+ },
"node_modules/axobject-query": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
@@ -1477,15 +1676,6 @@
"dev": true,
"optional": true
},
- "node_modules/base64-arraybuffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
- "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
- "optional": true,
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1506,6 +1696,15 @@
}
]
},
+ "node_modules/bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+ "dev": true,
+ "dependencies": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1556,6 +1755,18 @@
"node": ">= 6"
}
},
+ "node_modules/blob-util": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
+ "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
+ "dev": true
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -1693,17 +1904,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/btoa": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
- "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
- "bin": {
- "btoa": "bin/btoa.js"
- },
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -1758,6 +1958,34 @@
"@types/ws": "~8.5.10"
}
},
+ "node_modules/cachedir": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
+ "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1796,30 +2024,11 @@
}
]
},
- "node_modules/canvg": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
- "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
- "optional": true,
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "@types/raf": "^3.4.0",
- "core-js": "^3.8.3",
- "raf": "^3.4.1",
- "regenerator-runtime": "^0.13.7",
- "rgbcolor": "^1.0.1",
- "stackblur-canvas": "^2.0.0",
- "svg-pathdata": "^6.0.3"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/canvg/node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
- "optional": true
+ "node_modules/caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
+ "dev": true
},
"node_modules/chalk": {
"version": "4.1.2",
@@ -1837,6 +2046,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/check-more-types": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+ "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -1911,6 +2129,113 @@
"node": ">= 6"
}
},
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-table3": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz",
+ "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": "10.* || >= 12.*"
+ },
+ "optionalDependencies": {
+ "@colors/colors": "1.5.0"
+ }
+ },
+ "node_modules/cli-table3/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/cli-table3/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -1964,6 +2289,12 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@@ -1973,6 +2304,18 @@
"node": ">=0.1.90"
}
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
@@ -1982,6 +2325,15 @@
"node": ">=16"
}
},
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -2007,17 +2359,6 @@
"node": ">= 0.6"
}
},
- "node_modules/core-js": {
- "version": "3.36.1",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
- "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
- "hasInstallScript": true,
- "optional": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -2038,15 +2379,6 @@
"node": ">= 8"
}
},
- "node_modules/css-line-break": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
- "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
- "optional": true,
- "dependencies": {
- "utrie": "^1.0.2"
- }
- },
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -2099,6 +2431,138 @@
"node": ">=4"
}
},
+ "node_modules/cypress": {
+ "version": "13.8.1",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.1.tgz",
+ "integrity": "sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@cypress/request": "^3.0.0",
+ "@cypress/xvfb": "^1.2.4",
+ "@types/sinonjs__fake-timers": "8.1.1",
+ "@types/sizzle": "^2.3.2",
+ "arch": "^2.2.0",
+ "blob-util": "^2.0.2",
+ "bluebird": "^3.7.2",
+ "buffer": "^5.7.1",
+ "cachedir": "^2.3.0",
+ "chalk": "^4.1.0",
+ "check-more-types": "^2.24.0",
+ "cli-cursor": "^3.1.0",
+ "cli-table3": "~0.6.1",
+ "commander": "^6.2.1",
+ "common-tags": "^1.8.0",
+ "dayjs": "^1.10.4",
+ "debug": "^4.3.4",
+ "enquirer": "^2.3.6",
+ "eventemitter2": "6.4.7",
+ "execa": "4.1.0",
+ "executable": "^4.1.1",
+ "extract-zip": "2.0.1",
+ "figures": "^3.2.0",
+ "fs-extra": "^9.1.0",
+ "getos": "^3.2.1",
+ "is-ci": "^3.0.1",
+ "is-installed-globally": "~0.4.0",
+ "lazy-ass": "^1.6.0",
+ "listr2": "^3.8.3",
+ "lodash": "^4.17.21",
+ "log-symbols": "^4.0.0",
+ "minimist": "^1.2.8",
+ "ospath": "^1.2.2",
+ "pretty-bytes": "^5.6.0",
+ "process": "^0.11.10",
+ "proxy-from-env": "1.0.0",
+ "request-progress": "^3.0.0",
+ "semver": "^7.5.3",
+ "supports-color": "^8.1.1",
+ "tmp": "~0.2.1",
+ "untildify": "^4.0.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "cypress": "bin/cypress"
+ },
+ "engines": {
+ "node": "^16.0.0 || ^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/cypress/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/cypress/node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cypress/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cypress/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
+ "dev": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
@@ -2140,6 +2604,32 @@
"node": ">=0.10.0"
}
},
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -2239,12 +2729,6 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
- "node_modules/dompurify": {
- "version": "2.4.9",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz",
- "integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==",
- "optional": true
- },
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
@@ -2265,6 +2749,16 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
+ "node_modules/ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
+ "dev": true,
+ "dependencies": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.4.715",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz",
@@ -2277,6 +2771,28 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enquirer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/ensure-posix-path": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz",
@@ -2301,6 +2817,27 @@
"integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==",
"dev": true
},
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es6-promise": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@@ -2445,6 +2982,18 @@
"eslint": ">=7.0.0"
}
},
+ "node_modules/eslint-plugin-cypress": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.0.2.tgz",
+ "integrity": "sha512-5hIWc3SqXSuR+Sd7gmNMzx8yJ3LWQQS0e+qLvEVF4C1JfFtu1s9imtEm1KxlCBCcKb7+6CyR9KQYs0GiI02AlA==",
+ "dev": true,
+ "dependencies": {
+ "globals": "^13.20.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=7 <9"
+ }
+ },
"node_modules/eslint-plugin-svelte": {
"version": "2.35.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.1.tgz",
@@ -2612,6 +3161,88 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter2": {
+ "version": "6.4.7",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
+ "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
+ "dev": true
+ },
+ "node_modules/execa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+ "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/executable": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+ "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ]
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2673,10 +3304,38 @@
"reusify": "^1.0.4"
}
},
- "node_modules/fflate": {
- "version": "0.4.8",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
- "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/figures/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
@@ -2767,6 +3426,29 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2894,6 +3576,58 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/getos": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
+ "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
+ "dev": true,
+ "dependencies": {
+ "async": "^3.2.0"
+ }
+ },
+ "node_modules/getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
+ "dev": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0"
+ }
+ },
"node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
@@ -2954,6 +3688,21 @@
"node": ">=10"
}
},
+ "node_modules/global-dirs": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz",
+ "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==",
+ "dev": true,
+ "dependencies": {
+ "ini": "2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -2999,6 +3748,18 @@
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
},
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -3029,6 +3790,42 @@
"node": ">=8"
}
},
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -3097,19 +3894,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/html2canvas": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
- "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
- "optional": true,
- "dependencies": {
- "css-line-break": "^2.1.0",
- "text-segmentation": "^1.0.3"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -3129,6 +3913,29 @@
"entities": "^4.4.0"
}
},
+ "node_modules/http-signature": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
+ "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
+ "dev": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^2.0.2",
+ "sshpk": "^1.14.1"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.12.0"
+ }
+ },
"node_modules/i18next": {
"version": "23.10.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.1.tgz",
@@ -3282,6 +4089,15 @@
"node": ">=0.8.19"
}
},
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -3296,6 +4112,15 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3322,6 +4147,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-ci": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+ "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+ "dev": true,
+ "dependencies": {
+ "ci-info": "^3.2.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@@ -3363,6 +4200,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-installed-globally": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+ "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+ "dev": true,
+ "dependencies": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@@ -3415,6 +4268,36 @@
"@types/estree": "*"
}
},
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "dev": true
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-valid-glob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
@@ -3436,6 +4319,12 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "node_modules/isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
+ "dev": true
+ },
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -3480,12 +4369,24 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
+ "dev": true
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "dev": true
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3498,6 +4399,12 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "dev": true
+ },
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -3510,21 +4417,19 @@
"graceful-fs": "^4.1.6"
}
},
- "node_modules/jspdf": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
- "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
+ "node_modules/jsprim": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+ "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
"dependencies": {
- "@babel/runtime": "^7.14.0",
- "atob": "^2.1.2",
- "btoa": "^1.2.1",
- "fflate": "^0.4.8"
- },
- "optionalDependencies": {
- "canvg": "^3.0.6",
- "core-js": "^3.6.0",
- "dompurify": "^2.2.0",
- "html2canvas": "^1.0.0-rc.5"
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.4.0",
+ "verror": "1.10.0"
}
},
"node_modules/katex": {
@@ -3573,6 +4478,15 @@
"integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
"dev": true
},
+ "node_modules/lazy-ass": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+ "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
+ "dev": true,
+ "engines": {
+ "node": "> 0.8"
+ }
+ },
"node_modules/lead": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz",
@@ -3613,6 +4527,70 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "node_modules/listr2": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
+ "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
+ "dev": true,
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "colorette": "^2.0.16",
+ "log-update": "^4.0.0",
+ "p-map": "^4.0.0",
+ "rfdc": "^1.3.0",
+ "rxjs": "^7.5.1",
+ "through": "^2.3.8",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "enquirer": ">= 2.3.0 < 3"
+ },
+ "peerDependenciesMeta": {
+ "enquirer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
@@ -3633,6 +4611,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
@@ -3651,6 +4635,97 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "dev": true
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -3725,6 +4800,12 @@
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
},
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3747,6 +4828,36 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -3901,6 +5012,18 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -3931,6 +5054,15 @@
"node": ">= 6"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3939,6 +5071,21 @@
"wrappy": "1"
}
},
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -3956,6 +5103,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/ospath": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+ "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==",
+ "dev": true
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -3986,6 +5139,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4095,11 +5263,17 @@
"node": ">=8"
}
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true
+ },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
- "optional": true
+ "dev": true
},
"node_modules/periscopic": {
"version": "3.1.0",
@@ -4406,6 +5580,27 @@
"svelte": "^3.2.0 || ^4.0.0-next.0"
}
},
+ "node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -4421,6 +5616,28 @@
"node": "10.* || >= 12.*"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+ "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
+ "dev": true
+ },
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
+ "dev": true
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4430,6 +5647,27 @@
"node": ">=6"
}
},
+ "node_modules/qs": {
+ "version": "6.10.4",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
+ "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
+ "dev": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -4521,15 +5759,6 @@
"rimraf": "bin.js"
}
},
- "node_modules/raf": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
- "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
- "optional": true,
- "dependencies": {
- "performance-now": "^2.1.0"
- }
- },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -4586,6 +5815,21 @@
"node": ">= 10"
}
},
+ "node_modules/request-progress": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+ "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==",
+ "dev": true,
+ "dependencies": {
+ "throttleit": "^1.0.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -4623,6 +5867,25 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -4633,14 +5896,11 @@
"node": ">=0.10.0"
}
},
- "node_modules/rgbcolor": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
- "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
- "optional": true,
- "engines": {
- "node": ">= 0.8.15"
- }
+ "node_modules/rfdc": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz",
+ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
+ "dev": true
},
"node_modules/rimraf": {
"version": "3.0.2",
@@ -4746,6 +6006,15 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -4855,6 +6124,23 @@
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
},
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4876,6 +6162,24 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -4918,6 +6222,20 @@
"node": ">=8"
}
},
+ "node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
@@ -4962,13 +6280,29 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true
},
- "node_modules/stackblur-canvas": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
- "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
- "optional": true,
+ "node_modules/sshpk": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
+ "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
+ "dev": true,
+ "dependencies": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ },
+ "bin": {
+ "sshpk-conv": "bin/sshpk-conv",
+ "sshpk-sign": "bin/sshpk-sign",
+ "sshpk-verify": "bin/sshpk-verify"
+ },
"engines": {
- "node": ">=0.1.14"
+ "node": ">=0.10.0"
}
},
"node_modules/stream-composer": {
@@ -5092,6 +6426,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -5372,15 +6715,6 @@
"@types/estree": "*"
}
},
- "node_modules/svg-pathdata": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
- "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
- "optional": true,
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/symlink-or-copy": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
@@ -5519,15 +6853,6 @@
"streamx": "^2.12.5"
}
},
- "node_modules/text-segmentation": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
- "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
- "optional": true,
- "dependencies": {
- "utrie": "^1.0.2"
- }
- },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -5555,6 +6880,21 @@
"node": ">=0.8"
}
},
+ "node_modules/throttleit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz",
+ "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true
+ },
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
@@ -5582,6 +6922,15 @@
"@popperjs/core": "^2.9.0"
}
},
+ "node_modules/tmp": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
+ "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -5614,6 +6963,30 @@
"node": ">=6"
}
},
+ "node_modules/tough-cookie": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+ "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
+ "dev": true,
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie/node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -5637,6 +7010,24 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
+ "dev": true
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -5713,6 +7104,15 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -5752,21 +7152,22 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
- "node_modules/utrie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
- "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
- "optional": true,
- "dependencies": {
- "base64-arraybuffer": "^1.0.2"
- }
- },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@@ -5788,6 +7189,26 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "node_modules/verror/node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
+ "dev": true
+ },
"node_modules/vinyl": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz",
@@ -6477,6 +7898,16 @@
"node": ">= 6"
}
},
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 777f0f07b..c38120727 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "open-webui",
- "version": "0.1.121",
+ "version": "0.1.122",
"private": true,
"scripts": {
"dev": "vite dev --host",
@@ -14,7 +14,8 @@
"lint:backend": "pylint backend/",
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
"format:backend": "black . --exclude \"/venv/\"",
- "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
+ "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'",
+ "cy:open": "cypress open"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
@@ -25,8 +26,10 @@
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"autoprefixer": "^10.4.16",
+ "cypress": "^13.8.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-cypress": "^3.0.2",
"eslint-plugin-svelte": "^2.30.0",
"i18next-parser": "^8.13.0",
"postcss": "^8.4.31",
@@ -53,7 +56,6 @@
"i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1",
"js-sha256": "^0.10.1",
- "jspdf": "^2.5.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"svelte-sonner": "^0.3.19",
diff --git a/run-compose.sh b/run-compose.sh
index 08fba272b..21574e959 100755
--- a/run-compose.sh
+++ b/run-compose.sh
@@ -82,6 +82,7 @@ usage() {
echo "Examples:"
echo " $0 --drop"
echo " $0 --enable-gpu[count=1]"
+ echo " $0 --enable-gpu[count=all]"
echo " $0 --enable-api[port=11435]"
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000]"
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data]"
@@ -160,7 +161,7 @@ else
if [[ $enable_gpu == true ]]; then
# Validate and process command-line arguments
if [[ -n $gpu_count ]]; then
- if ! [[ $gpu_count =~ ^[0-9]+$ ]]; then
+ if ! [[ $gpu_count =~ ^([0-9]+|all)$ ]]; then
echo "Invalid GPU count: $gpu_count"
exit 1
fi
diff --git a/src/app.html b/src/app.html
index 2d1ef0d12..1aa01e8b6 100644
--- a/src/app.html
+++ b/src/app.html
@@ -43,9 +43,46 @@
})();
+ Open WebUI
+
%sveltekit.head%
%sveltekit.body%
+
+
+
+
+

+
+
+