diff --git a/.dockerignore b/.dockerignore index 58cf1f0f..e28863bf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ node_modules /package .env .env.* -!.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* __pycache__ diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..3d2aafc0 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Ollama URL for the backend to connect +# The path '/ollama' will be redirected to the specified backend URL +OLLAMA_BASE_URL='http://localhost:11434' + +OPENAI_API_BASE_URL='' +OPENAI_API_KEY='' + +# AUTOMATIC1111_BASE_URL="http://localhost:7860" + +# DO NOT TRACK +SCARF_NO_ANALYTICS=true +DO_NOT_TRACK=true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5a85d087..43866613 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,7 +32,7 @@ assignees: '' **Confirmation:** - [ ] I have read and followed all the instructions provided in the README.md. -- [ ] I have reviewed the troubleshooting.md document. +- [ ] I am on the latest version of both Open WebUI and Ollama. - [ ] I have included the browser console logs. - [ ] I have included the Docker container logs. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..4c4cfa3b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,32 @@ +## Pull Request Checklist + +- [ ] **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? +- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation? + +--- + +## Description + +[Insert a brief description of the changes made in this pull request] + +--- + +### Changelog Entry + +### Added + +- [List any new features or additions] + +### Fixed + +- [List any fixes or corrections] + +### Changed + +- [List any changes or updates] + +### Removed + +- [List any removed features or files] diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 00000000..259f0c5f --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + push: + branches: + - main # or whatever branch you want to use + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Check for changes in package.json + run: | + git diff --cached --diff-filter=d package.json || { + echo "No changes to package.json" + exit 1 + } + + - name: Get version number from package.json + id: get_version + run: | + VERSION=$(jq -r '.version' package.json) + echo "::set-output name=version::$VERSION" + + - name: Extract latest CHANGELOG entry + id: changelog + run: | + CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md) + CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g') + echo "Extracted latest release notes from CHANGELOG.md:" + echo -e "$CHANGELOG_CONTENT" + echo "::set-output name=content::$CHANGELOG_ESCAPED" + + - name: Create GitHub release + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const changelog = `${{ steps.changelog.outputs.content }}`; + const release = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: `v${{ steps.get_version.outputs.version }}`, + name: `v${{ steps.get_version.outputs.version }}`, + body: changelog, + }) + console.log(`Created release ${release.data.html_url}`) + + - name: Upload package to GitHub release + uses: actions/upload-artifact@v3 + with: + name: package + path: . + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index de32dbeb..bb71de8b 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -52,6 +52,7 @@ jobs: type=ref,event=tag type=sha,prefix=git- type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} flavor: | latest=${{ github.ref == 'refs/heads/main' }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3956e566 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,211 @@ +# Changelog + +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.114] - 2024-03-20 + +### Added + +- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL. +- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy. +- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others. + +### Fixed + +- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields. +- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding. + +### Changed + +- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization. + +## [0.1.113] - 2024-03-18 + +### Added + +- 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization). +- 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience. + +### Fixed + +- 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices. +- **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning. +- **RAG Encoding Issue**: Fixed encoding problem in RAG. +- **npm Audit Fix**: Addressed npm audit findings. +- **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px. + +### Changed + +- 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation. + +## [0.1.112] - 2024-03-15 + +### Fixed + +- 🗨️ Resolved chat malfunction after image generation. +- 🎨 Fixed various RAG issues. +- 🧪 Rectified experimental broken GGUF upload logic. + +## [0.1.111] - 2024-03-10 + +### Added + +- 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role. +- 🔄 **Update All Models**: Added a convenient button to update all models at once. +- 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance. +- 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111. +- 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow. + +### Fixed + +- 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094). +- 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104). +- 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105). +- 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098). +- 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue. + +## [0.1.110] - 2024-03-06 + +### Added + +- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively. + +### Fixed + +- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction. +- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly. +- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button. + +## [0.1.109] - 2024-03-06 + +### Added + +- **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278). +- **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064). +- **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050). + +### Fixed + +- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031). +- **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024). +- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006). +- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061). + +### Changed + +- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004). +- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details. + +## [0.1.108] - 2024-03-02 + +### Added + +- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter. +- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings. +- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes. +- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI. + +### Fixed + +- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use. +- Corrected numbered list display issue in Safari (#963). +- Restricted user ability to delete chats without proper permissions (#993). + +### Changed + +- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added. +- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`. + +## [0.1.107] - 2024-03-01 + +### Added + +- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository. + +### Fixed + +- Corrected issue where links in the settings modal didn't appear clickable (#960). +- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996). +- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992). + +### Changed + +- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI. +- Enhanced accessibility with the addition of dark mode on the authentication page. + +## [0.1.106] - 2024-02-27 + +### Added + +- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation. + +### Fixed + +- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924). +- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927). +- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940). +- Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930) + +## [0.1.105] - 2024-02-25 + +### Added + +- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management. + +### Changed + +- **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization. + +## [0.1.104] - 2024-02-25 + +### Added + +- **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About. +- **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click. + +### Changed + +- **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience. + +## [0.1.103] - 2024-02-25 + +### Added + +- **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality. + + - Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag. + - When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`. + +- **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature. + - Customize image generation by setting the number of steps; defaults to A1111 value. + +### Fixed + +- Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866). + +### Changed + +- Ollama is no longer required to run Open WebUI. +- Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/). + +## [0.1.102] - 2024-02-22 + +### Added + +- **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images. +- **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface. +- **🤖 Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co. +- **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes. + +## [0.1.101] - 2024-02-22 + +### Fixed + +- LaTex output formatting issue (#828) + +### Changed + +- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions. diff --git a/Dockerfile b/Dockerfile index 520c2964..de501838 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM python:3.11-slim-bookworm as base ENV ENV=prod ENV PORT "" -ENV OLLAMA_API_BASE_URL "/ollama/api" +ENV OLLAMA_BASE_URL "/ollama" ENV OPENAI_API_BASE_URL "" ENV OPENAI_API_KEY "" @@ -30,15 +30,31 @@ ENV WEBUI_SECRET_KEY "" ENV SCARF_NO_ANALYTICS true ENV DO_NOT_TRACK true -#Whisper TTS Settings +######## Preloaded models ######## +# whisper TTS Settings ENV WHISPER_MODEL="base" ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" +# RAG Embedding Model Settings +# 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 persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) +# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. +ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2" +# device type for whisper tts and embbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance +ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu" +ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" +ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR + +######## Preloaded models ######## + WORKDIR /app/backend # install python dependencies COPY ./backend/requirements.txt ./requirements.txt +RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y + RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir RUN pip3 install -r requirements.txt --no-cache-dir @@ -48,9 +64,10 @@ RUN apt-get update \ && apt-get install -y pandoc netcat-openbsd \ && rm -rf /var/lib/apt/lists/* -# RUN python -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('all-MiniLM-L6-v2')" -RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" - +# preload embedding model +RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])" +# preload tts model +RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" # copy embedding weight from build RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 @@ -58,8 +75,10 @@ COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onn # copy built frontend files COPY --from=build /app/build /app/build +COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md +COPY --from=build /app/package.json /app/package.json # copy backend files COPY ./backend . -CMD [ "bash", "start.sh"] \ No newline at end of file +CMD [ "bash", "start.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..cbcc41d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +install: + @docker-compose up -d + +remove: + @chmod +x confirm_remove.sh + @./confirm_remove.sh + + +start: + @docker-compose start + +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 + # Make sure the ollama-webui container is stopped before rebuilding + @docker stop open-webui || true + @docker-compose up --build -d + @docker-compose start + diff --git a/README.md b/README.md index a1d089e7..e2ee284e 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,20 @@ # Open WebUI (Formerly Ollama WebUI) 👋 -![GitHub stars](https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social) -![GitHub forks](https://img.shields.io/github/forks/ollama-webui/ollama-webui?style=social) -![GitHub watchers](https://img.shields.io/github/watchers/ollama-webui/ollama-webui?style=social) -![GitHub repo size](https://img.shields.io/github/repo-size/ollama-webui/ollama-webui) -![GitHub language count](https://img.shields.io/github/languages/count/ollama-webui/ollama-webui) -![GitHub top language](https://img.shields.io/github/languages/top/ollama-webui/ollama-webui) -![GitHub last commit](https://img.shields.io/github/last-commit/ollama-webui/ollama-webui?color=red) +![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social) +![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social) +![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social) +![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui) +![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui) +![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui) +![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red) ![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false) [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s) [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck) -ChatGPT-Style Web Interface for Ollama 🦙 +Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). ![Open WebUI Demo](./demo.gif) -Also check our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles for Ollama! 🦙🔍 - ## Features ⭐ - 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience. @@ -55,8 +53,6 @@ Also check our sibling project, [Open WebUI Community](https://openwebui.com/), - 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment. -- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**. - - 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history. - 📜 **Chat History**: Effortlessly access and manage your conversation history. @@ -67,66 +63,39 @@ Also check our sibling project, [Open WebUI Community](https://openwebui.com/), - ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs. +- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using AUTOMATIC1111 API (local) and DALL-E, enriching your chat experience with dynamic visual content. + +- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**. + +- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions. + - 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable. +- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability. + +- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes. + - 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators. - 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security. +- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors! + - 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features. ## 🔗 Also Check Out Open WebUI Community! -Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Ollama! 🚀 +Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Open WebUI! 🚀 ## How to Install 🚀 -🌟 **Important Note on User Roles and Privacy:** +> [!NOTE] +> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you. -- **Admin Creation:** The very first account to sign up on the Open WebUI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings. +### Quick Start with Docker 🐳 -- **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities. - -- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into the Open WebUI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control. - -### Steps to Install Open WebUI - -#### Before You Begin - -1. **Installing Docker:** - - - **For Windows and Mac Users:** - - - Download Docker Desktop from [Docker's official website](https://www.docker.com/products/docker-desktop). - - Follow the installation instructions provided on the website. After installation, open Docker Desktop to ensure it's running properly. - - - **For Ubuntu and Other Linux Users:** - - Open your terminal. - - Set up your Docker apt repository according to the [Docker documentation](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) - - Update your package index: - ```bash - sudo apt-get update - ``` - - Install Docker using the following command: - ```bash - sudo apt-get install docker-ce docker-ce-cli containerd.io - ``` - - Verify the Docker installation with: - ```bash - sudo docker run hello-world - ``` - This command downloads a test image and runs it in a container, which prints an informational message. - -2. **Ensure You Have the Latest Version of Ollama:** - - - Download the latest version from [https://ollama.com/](https://ollama.com/). - -3. **Verify Ollama Installation:** - - After installing Ollama, check if it's working by visiting [http://127.0.0.1:11434/](http://127.0.0.1:11434/) in your web browser. Remember, the port number might be different for you. - -#### Installing with Docker 🐳 - -- **Important:** When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data. +> [!IMPORTANT] +> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data. - **If Ollama is on your computer**, use this command: @@ -134,158 +103,51 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` -- **To build the container yourself**, follow these steps: +- **If Ollama is on a Different Server**, use this command: + +- To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL: ```bash - docker build -t open-webui . - docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always open-webui + docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` -- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). +- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 -#### Using Ollama on a Different Server +#### Open WebUI: Server Connection Error -- To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL: +If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`. - ```bash - docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main - ``` - - Or for a self-built container: - - ```bash - docker build -t open-webui . - docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always open-webui - ``` - -### Installing Ollama and Open WebUI Together - -#### Using Docker Compose - -- If you don't have Ollama yet, use Docker Compose for easy installation. Run this command: - - ```bash - docker compose up -d --build - ``` - -- **For GPU Support:** Use an additional Docker Compose file: - - ```bash - docker compose -f docker-compose.yaml -f docker-compose.gpu.yaml up -d --build - ``` - -- **To Expose Ollama API:** Use another Docker Compose file: - - ```bash - docker compose -f docker-compose.yaml -f docker-compose.api.yaml up -d --build - ``` - -#### Using `run-compose.sh` Script (Linux or Docker-Enabled WSL2 on Windows) - -- Give execute permission to the script: - - ```bash - chmod +x run-compose.sh - ``` - -- For CPU-only container: - - ```bash - ./run-compose.sh - ``` - -- For GPU support (read the note about GPU compatibility): - - ```bash - ./run-compose.sh --enable-gpu - ``` - -- To build the latest local version, add `--build`: - - ```bash - ./run-compose.sh --enable-gpu --build - ``` - -### Alternative Installation Methods - -For other ways to install, like using Kustomize or Helm, check out [INSTALLATION.md](/INSTALLATION.md). Join our [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) for more help and information. - -### Updating your Docker Installation - -In case you want to update your local Docker installation to the latest version, you can do it performing the following actions: +**Example Docker Command**: ```bash -docker rm -f open-webui -docker pull ghcr.io/open-webui/open-webui:main -[insert command you used to install] +docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` -In the last line, you need to use the very same command you used to install (local install, remote server, etc.) +### Other Installation Methods -## How to Install Without Docker +We offer various installation alternatives, including non-Docker methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance. -While we strongly recommend using our convenient Docker container installation for optimal support, we understand that some situations may require a non-Docker setup, especially for development purposes. Please note that non-Docker installations are not officially supported, and you might need to troubleshoot on your own. +### Troubleshooting -### Project Components +Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s). -The Open WebUI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment. +### Keeping Your Docker Installation Up-to-Date -> [!IMPORTANT] -> The backend is required for proper functionality +In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/): -### Requirements 📦 - -- 🐰 [Bun](https://bun.sh) >= 1.0.21 or 🐢 [Node.js](https://nodejs.org/en) >= 20.10 -- 🐍 [Python](https://python.org) >= 3.11 - -### Build and Install 🛠️ - -Run the following commands to install: - -```sh -git clone https://github.com/open-webui/open-webui.git -cd open-webui/ - -# Copying required .env file -cp -RPp example.env .env - -# Building Frontend Using Node -npm i -npm run build - -# or Building Frontend Using Bun -# bun install -# bun run build - -# Serving Frontend with the Backend -cd ./backend -pip install -r requirements.txt -U -sh start.sh +```bash +docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui ``` -You should have the Open WebUI up and running at http://localhost:8080/. Enjoy! 😄 +In the last part of the command, replace `open-webui` with your container name if it is different. -## Troubleshooting +### Moving from Ollama WebUI to Open WebUI -See [TROUBLESHOOTING.md](/TROUBLESHOOTING.md) for information on how to troubleshoot and/or join our [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s). +Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/). -## What's Next? 🚀 +## What's Next? 🌟 -### Roadmap 📝 - -Here are some exciting tasks on our roadmap: - -- 🔊 **Local Text-to-Speech Integration**: Seamlessly incorporate text-to-speech functionality directly within the platform, allowing for a smoother and more immersive user experience. -- 🛡️ **Granular Permissions and User Groups**: Empower administrators to finely control access levels and group users according to their roles and responsibilities. This feature ensures robust security measures and streamlined management of user privileges, enhancing overall platform functionality. -- 🔄 **Function Calling**: Empower your interactions by running code directly within the chat. Execute functions and commands effortlessly, enhancing the functionality of your conversations. -- ⚙️ **Custom Python Backend Actions**: Empower your Open WebUI by creating or downloading custom Python backend actions. Unleash the full potential of your web interface with tailored actions that suit your specific needs, enhancing functionality and versatility. -- 🔧 **Fine-tune Model (LoRA)**: Fine-tune your model directly from the user interface. This feature allows for precise customization and optimization of the chat experience to better suit your needs and preferences. -- 🧠 **Long-Term Memory**: Witness the power of persistent memory in our agents. Enjoy conversations that feel continuous as agents remember and reference past interactions, creating a more cohesive and personalized user experience. -- 🧪 **Research-Centric Features**: Empower researchers in the fields of LLM and HCI with a comprehensive web UI for conducting user studies. Stay tuned for ongoing feature enhancements (e.g., surveys, analytics, and participant tracking) to facilitate their research. -- 📈 **User Study Tools**: Providing specialized tools, like heat maps and behavior tracking modules, to empower researchers in capturing and analyzing user behavior patterns with precision and accuracy. -- 📚 **Enhanced Documentation**: Elevate your setup and customization experience with improved, comprehensive documentation. - -Feel free to contribute and help us make Open WebUI even better! 🙌 +Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/). ## Supporters ✨ @@ -308,6 +170,16 @@ This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LI If you have any questions, suggestions, or need assistance, please open an issue or join our [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝 +## Star History + + + + + + Star History Chart + + + --- Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open Web UI even more amazing together! 💪 diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index d3163501..8e8f89da 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -4,7 +4,7 @@ The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues. -- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama/api` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_API_BASE_URL` environment variable. Therefore, a request made to `/ollama/api` in the WebUI is effectively the same as making a request to `OLLAMA_API_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend. +- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend. - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer. @@ -15,7 +15,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c **Example Docker Command**: ```bash -docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main +docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` ### General Connection Errors @@ -25,8 +25,8 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_ **Troubleshooting Steps**: 1. **Verify Ollama URL Format**: - - When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups). + - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups). - In the Open WebUI, navigate to "Settings" > "General". - - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix. + - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`). By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord. diff --git a/backend/.dockerignore b/backend/.dockerignore index 11f9256f..97ab3283 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -4,4 +4,11 @@ _old uploads .ipynb_checkpoints *.db -_test \ No newline at end of file +_test +!/data +/data/* +!/data/litellm +/data/litellm/* +!data/litellm/config.yaml + +!data/config.json \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 4dd0b849..ea83b34f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -6,5 +6,11 @@ uploads *.db _test Pipfile -data/* +!/data +/data/* +!/data/litellm +/data/litellm/* +!data/litellm/config.yaml + +!data/config.json .webui_secret_key \ No newline at end of file diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py index 86e79c47..d8cb415f 100644 --- a/backend/apps/audio/main.py +++ b/backend/apps/audio/main.py @@ -56,7 +56,7 @@ def transcribe( model = WhisperModel( WHISPER_MODEL, - device="cpu", + device="auto", compute_type="int8", download_root=WHISPER_MODEL_DIR, ) diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py new file mode 100644 index 00000000..e14b0f6a --- /dev/null +++ b/backend/apps/images/main.py @@ -0,0 +1,365 @@ +import re +import requests +from fastapi import ( + FastAPI, + Request, + Depends, + HTTPException, + status, + UploadFile, + File, + Form, +) +from fastapi.middleware.cors import CORSMiddleware +from faster_whisper import WhisperModel + +from constants import ERROR_MESSAGES +from utils.utils import ( + get_current_user, + get_admin_user, +) +from utils.misc import calculate_sha256 +from typing import Optional +from pydantic import BaseModel +from pathlib import Path +import uuid +import base64 +import json + +from config import CACHE_DIR, AUTOMATIC1111_BASE_URL + + +IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/") +IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.ENGINE = "" +app.state.ENABLED = False + +app.state.OPENAI_API_KEY = "" +app.state.MODEL = "" + + +app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL + +app.state.IMAGE_SIZE = "512x512" +app.state.IMAGE_STEPS = 50 + + +@app.get("/config") +async def get_config(request: Request, user=Depends(get_admin_user)): + return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED} + + +class ConfigUpdateForm(BaseModel): + engine: str + enabled: bool + + +@app.post("/config/update") +async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)): + app.state.ENGINE = form_data.engine + app.state.ENABLED = form_data.enabled + return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED} + + +class UrlUpdateForm(BaseModel): + url: str + + +@app.get("/url") +async def get_automatic1111_url(user=Depends(get_admin_user)): + return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL} + + +@app.post("/url/update") +async def update_automatic1111_url( + form_data: UrlUpdateForm, user=Depends(get_admin_user) +): + + if form_data.url == "": + app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL + else: + url = form_data.url.strip("/") + try: + r = requests.head(url) + app.state.AUTOMATIC1111_BASE_URL = url + except Exception as e: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + return { + "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL, + "status": True, + } + + +class OpenAIKeyUpdateForm(BaseModel): + key: str + + +@app.get("/key") +async def get_openai_key(user=Depends(get_admin_user)): + return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} + + +@app.post("/key/update") +async def update_openai_key( + form_data: OpenAIKeyUpdateForm, user=Depends(get_admin_user) +): + + if form_data.key == "": + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + + app.state.OPENAI_API_KEY = form_data.key + return { + "OPENAI_API_KEY": app.state.OPENAI_API_KEY, + "status": True, + } + + +class ImageSizeUpdateForm(BaseModel): + size: str + + +@app.get("/size") +async def get_image_size(user=Depends(get_admin_user)): + return {"IMAGE_SIZE": app.state.IMAGE_SIZE} + + +@app.post("/size/update") +async def update_image_size( + form_data: ImageSizeUpdateForm, user=Depends(get_admin_user) +): + pattern = r"^\d+x\d+$" # Regular expression pattern + if re.match(pattern, form_data.size): + app.state.IMAGE_SIZE = form_data.size + return { + "IMAGE_SIZE": app.state.IMAGE_SIZE, + "status": True, + } + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."), + ) + + +class ImageStepsUpdateForm(BaseModel): + steps: int + + +@app.get("/steps") +async def get_image_size(user=Depends(get_admin_user)): + return {"IMAGE_STEPS": app.state.IMAGE_STEPS} + + +@app.post("/steps/update") +async def update_image_size( + form_data: ImageStepsUpdateForm, user=Depends(get_admin_user) +): + if form_data.steps >= 0: + app.state.IMAGE_STEPS = form_data.steps + return { + "IMAGE_STEPS": app.state.IMAGE_STEPS, + "status": True, + } + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."), + ) + + +@app.get("/models") +def get_models(user=Depends(get_current_user)): + try: + if app.state.ENGINE == "openai": + return [ + {"id": "dall-e-2", "name": "DALL·E 2"}, + {"id": "dall-e-3", "name": "DALL·E 3"}, + ] + else: + r = requests.get( + url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models" + ) + models = r.json() + return list( + map( + lambda model: {"id": model["title"], "name": model["model_name"]}, + models, + ) + ) + except Exception as e: + app.state.ENABLED = False + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + +@app.get("/models/default") +async def get_default_model(user=Depends(get_admin_user)): + try: + if app.state.ENGINE == "openai": + return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"} + else: + r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options") + options = r.json() + return {"model": options["sd_model_checkpoint"]} + except Exception as e: + app.state.ENABLED = False + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + +class UpdateModelForm(BaseModel): + model: str + + +def set_model_handler(model: str): + + if app.state.ENGINE == "openai": + app.state.MODEL = model + return app.state.MODEL + else: + r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options") + options = r.json() + + if model != options["sd_model_checkpoint"]: + options["sd_model_checkpoint"] = model + r = requests.post( + url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options + ) + + return options + + +@app.post("/models/default/update") +def update_default_model( + form_data: UpdateModelForm, + user=Depends(get_current_user), +): + return set_model_handler(form_data.model) + + +class GenerateImageForm(BaseModel): + model: Optional[str] = None + prompt: str + n: int = 1 + size: Optional[str] = None + negative_prompt: Optional[str] = None + + +def save_b64_image(b64_str): + image_id = str(uuid.uuid4()) + file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png") + + try: + # Split the base64 string to get the actual image data + img_data = base64.b64decode(b64_str) + + # Write the image data to a file + with open(file_path, "wb") as f: + f.write(img_data) + + return image_id + except Exception as e: + print(f"Error saving image: {e}") + return None + + +@app.post("/generations") +def generate_image( + form_data: GenerateImageForm, + user=Depends(get_current_user), +): + + r = None + try: + if app.state.ENGINE == "openai": + + headers = {} + headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + data = { + "model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2", + "prompt": form_data.prompt, + "n": form_data.n, + "size": form_data.size if form_data.size else app.state.IMAGE_SIZE, + "response_format": "b64_json", + } + + r = requests.post( + url=f"https://api.openai.com/v1/images/generations", + json=data, + headers=headers, + ) + + r.raise_for_status() + res = r.json() + + images = [] + + for image in res["data"]: + image_id = save_b64_image(image["b64_json"]) + images.append({"url": f"/cache/image/generations/{image_id}.png"}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json") + + with open(file_body_path, "w") as f: + json.dump(data, f) + + return images + + else: + if form_data.model: + set_model_handler(form_data.model) + + width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x"))) + + data = { + "prompt": form_data.prompt, + "batch_size": form_data.n, + "width": width, + "height": height, + } + + if app.state.IMAGE_STEPS != None: + data["steps"] = app.state.IMAGE_STEPS + + if form_data.negative_prompt != None: + data["negative_prompt"] = form_data.negative_prompt + + r = requests.post( + url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img", + json=data, + ) + + res = r.json() + + print(res) + + images = [] + + for image in res["images"]: + image_id = save_b64_image(image) + images.append({"url": f"/cache/image/generations/{image_id}.png"}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json") + + with open(file_body_path, "w") as f: + json.dump({**data, "info": res["info"]}, f) + + return images + + except Exception as e: + error = e + + if r != None: + data = r.json() + if "error" in data: + error = data["error"]["message"] + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) diff --git a/backend/apps/litellm/main.py b/backend/apps/litellm/main.py new file mode 100644 index 00000000..838b4707 --- /dev/null +++ b/backend/apps/litellm/main.py @@ -0,0 +1,95 @@ +from litellm.proxy.proxy_server import ProxyConfig, initialize +from litellm.proxy.proxy_server import app + +from fastapi import FastAPI, Request, Depends, status, Response +from fastapi.responses import JSONResponse + +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import StreamingResponse +import json + +from utils.utils import get_http_authorization_cred, get_current_user +from config import ENV + + +from config import ( + MODEL_FILTER_ENABLED, + MODEL_FILTER_LIST, +) + + +proxy_config = ProxyConfig() + + +async def config(): + router, model_list, general_settings = await proxy_config.load_config( + router=None, config_file_path="./data/litellm/config.yaml" + ) + + await initialize(config="./data/litellm/config.yaml", telemetry=False) + + +async def startup(): + await config() + + +@app.on_event("startup") +async def on_startup(): + await startup() + + +app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST + + +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + auth_header = request.headers.get("Authorization", "") + request.state.user = None + + try: + user = get_current_user(get_http_authorization_cred(auth_header)) + print(user) + request.state.user = user + except Exception as e: + return JSONResponse(status_code=400, content={"detail": str(e)}) + + response = await call_next(request) + return response + + +class ModifyModelsResponseMiddleware(BaseHTTPMiddleware): + async def dispatch( + self, request: Request, call_next: RequestResponseEndpoint + ) -> Response: + + response = await call_next(request) + user = request.state.user + + if "/models" in request.url.path: + if isinstance(response, StreamingResponse): + # Read the content of the streaming response + body = b"" + async for chunk in response.body_iterator: + body += chunk + + data = json.loads(body.decode("utf-8")) + + if app.state.MODEL_FILTER_ENABLED: + if user and user.role == "user": + data["data"] = list( + filter( + lambda model: model["id"] + in app.state.MODEL_FILTER_LIST, + data["data"], + ) + ) + + # Modified Flag + data["modified"] = True + return JSONResponse(content=data) + + return response + + +app.add_middleware(ModifyModelsResponseMiddleware) diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index bc797f08..6f56f3cf 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -3,15 +3,22 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from fastapi.concurrency import run_in_threadpool +from pydantic import BaseModel, ConfigDict + +import random import requests import json import uuid -from pydantic import BaseModel +import aiohttp +import asyncio from apps.web.models.users import Users from constants import ERROR_MESSAGES from utils.utils import decode_token, get_current_user, get_admin_user -from config import OLLAMA_API_BASE_URL, WEBUI_AUTH +from config import OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST + +from typing import Optional, List, Union + app = FastAPI() app.add_middleware( @@ -22,27 +29,48 @@ app.add_middleware( allow_headers=["*"], ) -app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL -# TARGET_SERVER_URL = OLLAMA_API_BASE_URL +app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS +app.state.MODELS = {} REQUEST_POOL = [] -@app.get("/url") -async def get_ollama_api_url(user=Depends(get_admin_user)): - return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} +# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances. +# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin, +# least connections, or least response time for better resource utilization and performance optimization. + + +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + response = await call_next(request) + return response + + +@app.get("/urls") +async def get_ollama_api_urls(user=Depends(get_admin_user)): + return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} class UrlUpdateForm(BaseModel): - url: str + urls: List[str] -@app.post("/url/update") +@app.post("/urls/update") async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): - app.state.OLLAMA_API_BASE_URL = form_data.url - return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + app.state.OLLAMA_BASE_URLS = form_data.urls + + print(app.state.OLLAMA_BASE_URLS) + return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} @app.get("/cancel/{request_id}") @@ -55,9 +83,824 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user)) raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) +async def fetch_url(url): + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + return await response.json() + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + return None + + +def merge_models_lists(model_lists): + merged_models = {} + + for idx, model_list in enumerate(model_lists): + if model_list is not None: + for model in model_list: + digest = model["digest"] + if digest not in merged_models: + model["urls"] = [idx] + merged_models[digest] = model + else: + merged_models[digest]["urls"].append(idx) + + return list(merged_models.values()) + + +# user=Depends(get_current_user) + + +async def get_all_models(): + print("get_all_models") + tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS] + responses = await asyncio.gather(*tasks) + + models = { + "models": merge_models_lists( + map(lambda response: response["models"] if response else None, responses) + ) + } + + app.state.MODELS = {model["model"]: model for model in models["models"]} + + return models + + +@app.get("/api/tags") +@app.get("/api/tags/{url_idx}") +async def get_ollama_tags( + 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 user.role == "user": + models["models"] = list( + filter( + lambda model: model["name"] in app.state.MODEL_FILTER_LIST, + models["models"], + ) + ) + return models + return models + else: + url = app.state.OLLAMA_BASE_URLS[url_idx] + try: + r = requests.request(method="GET", url=f"{url}/api/tags") + r.raise_for_status() + + return r.json() + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.get("/api/version") +@app.get("/api/version/{url_idx}") +async def get_ollama_versions(url_idx: Optional[int] = None): + + if url_idx == None: + + # returns lowest version + tasks = [fetch_url(f"{url}/api/version") for url in app.state.OLLAMA_BASE_URLS] + responses = await asyncio.gather(*tasks) + responses = list(filter(lambda x: x is not None, responses)) + + if len(responses) > 0: + lowest_version = min( + responses, key=lambda x: tuple(map(int, x["version"].split("."))) + ) + + return {"version": lowest_version["version"]} + else: + raise HTTPException( + status_code=500, + detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND, + ) + else: + url = app.state.OLLAMA_BASE_URLS[url_idx] + try: + r = requests.request(method="GET", url=f"{url}/api/version") + r.raise_for_status() + + return r.json() + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class ModelNameForm(BaseModel): + name: str + + +@app.post("/api/pull") +@app.post("/api/pull/{url_idx}") +async def pull_model( + form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user) +): + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + r = None + + def get_request(): + nonlocal url + nonlocal r + try: + + def stream_content(): + for chunk in r.iter_content(chunk_size=8192): + yield chunk + + r = requests.request( + method="POST", + url=f"{url}/api/pull", + data=form_data.model_dump_json(exclude_none=True).encode(), + stream=True, + ) + + r.raise_for_status() + + return StreamingResponse( + stream_content(), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + raise e + + try: + return await run_in_threadpool(get_request) + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class PushModelForm(BaseModel): + name: str + insecure: Optional[bool] = None + stream: Optional[bool] = None + + +@app.delete("/api/push") +@app.delete("/api/push/{url_idx}") +async def push_model( + form_data: PushModelForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + if form_data.name in app.state.MODELS: + url_idx = app.state.MODELS[form_data.name]["urls"][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name), + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + r = None + + def get_request(): + nonlocal url + nonlocal r + try: + + def stream_content(): + for chunk in r.iter_content(chunk_size=8192): + yield chunk + + r = requests.request( + method="POST", + url=f"{url}/api/push", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + + r.raise_for_status() + + return StreamingResponse( + stream_content(), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + raise e + + try: + return await run_in_threadpool(get_request) + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class CreateModelForm(BaseModel): + name: str + modelfile: Optional[str] = None + stream: Optional[bool] = None + path: Optional[str] = None + + +@app.post("/api/create") +@app.post("/api/create/{url_idx}") +async def create_model( + form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user) +): + print(form_data) + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + r = None + + def get_request(): + nonlocal url + nonlocal r + try: + + def stream_content(): + for chunk in r.iter_content(chunk_size=8192): + yield chunk + + r = requests.request( + method="POST", + url=f"{url}/api/create", + data=form_data.model_dump_json(exclude_none=True).encode(), + stream=True, + ) + + r.raise_for_status() + + print(r) + + return StreamingResponse( + stream_content(), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + raise e + + try: + return await run_in_threadpool(get_request) + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class CopyModelForm(BaseModel): + source: str + destination: str + + +@app.post("/api/copy") +@app.post("/api/copy/{url_idx}") +async def copy_model( + form_data: CopyModelForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + if form_data.source in app.state.MODELS: + url_idx = app.state.MODELS[form_data.source]["urls"][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source), + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + try: + r = requests.request( + method="POST", + url=f"{url}/api/copy", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + print(r.text) + + return True + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.delete("/api/delete") +@app.delete("/api/delete/{url_idx}") +async def delete_model( + form_data: ModelNameForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + if form_data.name in app.state.MODELS: + url_idx = app.state.MODELS[form_data.name]["urls"][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name), + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + try: + r = requests.request( + method="DELETE", + url=f"{url}/api/delete", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + print(r.text) + + return True + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.post("/api/show") +async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_user)): + if form_data.name not in app.state.MODELS: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name), + ) + + url_idx = random.choice(app.state.MODELS[form_data.name]["urls"]) + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + try: + r = requests.request( + method="POST", + url=f"{url}/api/show", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + return r.json() + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class GenerateEmbeddingsForm(BaseModel): + model: str + prompt: str + options: Optional[dict] = None + keep_alive: Optional[Union[int, str]] = None + + +@app.post("/api/embeddings") +@app.post("/api/embeddings/{url_idx}") +async def generate_embeddings( + form_data: GenerateEmbeddingsForm, + url_idx: Optional[int] = None, + user=Depends(get_current_user), +): + if url_idx == None: + if form_data.model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[form_data.model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + try: + r = requests.request( + method="POST", + url=f"{url}/api/embeddings", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + return r.json() + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class GenerateCompletionForm(BaseModel): + model: str + prompt: str + images: Optional[List[str]] = None + format: Optional[str] = None + options: Optional[dict] = None + system: Optional[str] = None + template: Optional[str] = None + context: Optional[str] = None + stream: Optional[bool] = True + raw: Optional[bool] = None + keep_alive: Optional[Union[int, str]] = None + + +@app.post("/api/generate") +@app.post("/api/generate/{url_idx}") +async def generate_completion( + form_data: GenerateCompletionForm, + url_idx: Optional[int] = None, + user=Depends(get_current_user), +): + + if url_idx == None: + if form_data.model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[form_data.model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail="error_detail", + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + r = None + + def get_request(): + nonlocal form_data + nonlocal r + + request_id = str(uuid.uuid4()) + try: + REQUEST_POOL.append(request_id) + + def stream_content(): + try: + if form_data.stream: + yield json.dumps({"id": request_id, "done": False}) + "\n" + + for chunk in r.iter_content(chunk_size=8192): + if request_id in REQUEST_POOL: + yield chunk + else: + print("User: canceled request") + break + finally: + if hasattr(r, "close"): + r.close() + if request_id in REQUEST_POOL: + REQUEST_POOL.remove(request_id) + + r = requests.request( + method="POST", + url=f"{url}/api/generate", + data=form_data.model_dump_json(exclude_none=True).encode(), + stream=True, + ) + + r.raise_for_status() + + return StreamingResponse( + stream_content(), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + raise e + + try: + return await run_in_threadpool(get_request) + except Exception as e: + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class ChatMessage(BaseModel): + role: str + content: str + images: Optional[List[str]] = None + + +class GenerateChatCompletionForm(BaseModel): + model: str + messages: List[ChatMessage] + format: Optional[str] = None + options: Optional[dict] = None + template: Optional[str] = None + stream: Optional[bool] = None + keep_alive: Optional[Union[int, str]] = None + + +@app.post("/api/chat") +@app.post("/api/chat/{url_idx}") +async def generate_chat_completion( + form_data: GenerateChatCompletionForm, + url_idx: Optional[int] = None, + user=Depends(get_current_user), +): + + if url_idx == None: + if form_data.model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[form_data.model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + r = None + + print(form_data.model_dump_json(exclude_none=True).encode()) + + def get_request(): + nonlocal form_data + nonlocal r + + request_id = str(uuid.uuid4()) + try: + REQUEST_POOL.append(request_id) + + def stream_content(): + try: + if form_data.stream: + yield json.dumps({"id": request_id, "done": False}) + "\n" + + for chunk in r.iter_content(chunk_size=8192): + if request_id in REQUEST_POOL: + yield chunk + else: + print("User: canceled request") + break + finally: + if hasattr(r, "close"): + r.close() + if request_id in REQUEST_POOL: + REQUEST_POOL.remove(request_id) + + r = requests.request( + method="POST", + url=f"{url}/api/chat", + data=form_data.model_dump_json(exclude_none=True).encode(), + stream=True, + ) + + r.raise_for_status() + + return StreamingResponse( + stream_content(), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + print(e) + raise e + + try: + return await run_in_threadpool(get_request) + except Exception as e: + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +# TODO: we should update this part once Ollama supports other types +class OpenAIChatMessage(BaseModel): + role: str + content: str + + model_config = ConfigDict(extra="allow") + + +class OpenAIChatCompletionForm(BaseModel): + model: str + messages: List[OpenAIChatMessage] + + model_config = ConfigDict(extra="allow") + + +@app.post("/v1/chat/completions") +@app.post("/v1/chat/completions/{url_idx}") +async def generate_openai_chat_completion( + form_data: OpenAIChatCompletionForm, + url_idx: Optional[int] = None, + user=Depends(get_current_user), +): + + if url_idx == None: + if form_data.model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[form_data.model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.OLLAMA_BASE_URLS[url_idx] + print(url) + + r = None + + def get_request(): + nonlocal form_data + nonlocal r + + request_id = str(uuid.uuid4()) + try: + REQUEST_POOL.append(request_id) + + def stream_content(): + try: + if form_data.stream: + yield json.dumps( + {"request_id": request_id, "done": False} + ) + "\n" + + for chunk in r.iter_content(chunk_size=8192): + if request_id in REQUEST_POOL: + yield chunk + else: + print("User: canceled request") + break + finally: + if hasattr(r, "close"): + r.close() + if request_id in REQUEST_POOL: + REQUEST_POOL.remove(request_id) + + r = requests.request( + method="POST", + url=f"{url}/v1/chat/completions", + data=form_data.model_dump_json(exclude_none=True).encode(), + stream=True, + ) + + r.raise_for_status() + + return StreamingResponse( + stream_content(), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + raise e + + try: + return await run_in_threadpool(get_request) + except Exception as e: + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) -async def proxy(path: str, request: Request, user=Depends(get_current_user)): - target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}" +async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)): + url = app.state.OLLAMA_BASE_URLS[0] + target_url = f"{url}/{path}" body = await request.body() headers = dict(request.headers) @@ -91,7 +934,13 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): def stream_content(): try: - if path in ["chat"]: + if path == "generate": + data = json.loads(body.decode("utf-8")) + + if not ("stream" in data and data["stream"] == False): + yield json.dumps({"id": request_id, "done": False}) + "\n" + + elif path == "chat": yield json.dumps({"id": request_id, "done": False}) + "\n" for chunk in r.iter_content(chunk_size=8192): @@ -103,7 +952,8 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): finally: if hasattr(r, "close"): r.close() - REQUEST_POOL.remove(request_id) + if request_id in REQUEST_POOL: + REQUEST_POOL.remove(request_id) r = requests.request( method=request.method, diff --git a/backend/apps/ollama/old_main.py b/backend/apps/ollama/old_main.py deleted file mode 100644 index 5e5b8811..00000000 --- a/backend/apps/ollama/old_main.py +++ /dev/null @@ -1,127 +0,0 @@ -from fastapi import FastAPI, Request, Response, HTTPException, Depends -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse - -import requests -import json -from pydantic import BaseModel - -from apps.web.models.users import Users -from constants import ERROR_MESSAGES -from utils.utils import decode_token, get_current_user -from config import OLLAMA_API_BASE_URL, WEBUI_AUTH - -import aiohttp - -app = FastAPI() -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL - -# TARGET_SERVER_URL = OLLAMA_API_BASE_URL - - -@app.get("/url") -async def get_ollama_api_url(user=Depends(get_current_user)): - if user and user.role == "admin": - return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} - else: - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - - -class UrlUpdateForm(BaseModel): - url: str - - -@app.post("/url/update") -async def update_ollama_api_url( - form_data: UrlUpdateForm, user=Depends(get_current_user) -): - if user and user.role == "admin": - app.state.OLLAMA_API_BASE_URL = form_data.url - return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} - else: - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - - -# async def fetch_sse(method, target_url, body, headers): -# async with aiohttp.ClientSession() as session: -# try: -# async with session.request( -# method, target_url, data=body, headers=headers -# ) as response: -# print(response.status) -# async for line in response.content: -# yield line -# except Exception as e: -# print(e) -# error_detail = "Open WebUI: Server Connection Error" -# yield json.dumps({"error": error_detail, "message": str(e)}).encode() - - -@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) -async def proxy(path: str, request: Request, user=Depends(get_current_user)): - target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}" - print(target_url) - - body = await request.body() - headers = dict(request.headers) - - if user.role in ["user", "admin"]: - if path in ["pull", "delete", "push", "copy", "create"]: - if user.role != "admin": - raise HTTPException( - status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED - ) - else: - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - - headers.pop("Host", None) - headers.pop("Authorization", None) - headers.pop("Origin", None) - headers.pop("Referer", None) - - session = aiohttp.ClientSession() - response = None - try: - response = await session.request( - request.method, target_url, data=body, headers=headers - ) - - print(response) - if not response.ok: - data = await response.json() - print(data) - response.raise_for_status() - - async def generate(): - async for line in response.content: - print(line) - yield line - await session.close() - - return StreamingResponse(generate(), response.status) - - except Exception as e: - print(e) - error_detail = "Open WebUI: Server Connection Error" - - if response is not None: - try: - res = await response.json() - if "error" in res: - error_detail = f"Ollama: {res['error']}" - except: - error_detail = f"Ollama: {e}" - - await session.close() - raise HTTPException( - status_code=response.status if response else 500, - detail=error_detail, - ) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index 36326430..67a99794 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -3,7 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, JSONResponse, FileResponse import requests +import aiohttp +import asyncio import json + from pydantic import BaseModel @@ -15,7 +18,15 @@ from utils.utils import ( get_verified_user, get_admin_user, ) -from config import OPENAI_API_BASE_URL, OPENAI_API_KEY, CACHE_DIR +from config import ( + OPENAI_API_BASE_URLS, + OPENAI_API_KEYS, + CACHE_DIR, + MODEL_FILTER_ENABLED, + MODEL_FILTER_LIST, +) +from typing import List, Optional + import hashlib from pathlib import Path @@ -29,116 +40,241 @@ app.add_middleware( allow_headers=["*"], ) -app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL -app.state.OPENAI_API_KEY = OPENAI_API_KEY +app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS +app.state.OPENAI_API_KEYS = OPENAI_API_KEYS + +app.state.MODELS = {} -class UrlUpdateForm(BaseModel): - url: str +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + response = await call_next(request) + return response -class KeyUpdateForm(BaseModel): - key: str +class UrlsUpdateForm(BaseModel): + urls: List[str] -@app.get("/url") -async def get_openai_url(user=Depends(get_admin_user)): - return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} +class KeysUpdateForm(BaseModel): + keys: List[str] -@app.post("/url/update") -async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): - app.state.OPENAI_API_BASE_URL = form_data.url - return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} +@app.get("/urls") +async def get_openai_urls(user=Depends(get_admin_user)): + return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} -@app.get("/key") -async def get_openai_key(user=Depends(get_admin_user)): - return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} +@app.post("/urls/update") +async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)): + app.state.OPENAI_API_BASE_URLS = form_data.urls + return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} -@app.post("/key/update") -async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)): - app.state.OPENAI_API_KEY = form_data.key - return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} +@app.get("/keys") +async def get_openai_keys(user=Depends(get_admin_user)): + return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} + + +@app.post("/keys/update") +async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)): + app.state.OPENAI_API_KEYS = form_data.keys + return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} @app.post("/audio/speech") async def speech(request: Request, user=Depends(get_verified_user)): - target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech" - - if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) - - body = await request.body() - - name = hashlib.sha256(body).hexdigest() - - SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") - SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) - file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") - file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") - - # Check if the file already exists in the cache - if file_path.is_file(): - return FileResponse(file_path) - - headers = {} - headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" - headers["Content-Type"] = "application/json" - + idx = None try: - print("openai") - r = requests.post( - url=target_url, - data=body, - headers=headers, - stream=True, - ) + idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1") + body = await request.body() + name = hashlib.sha256(body).hexdigest() - r.raise_for_status() + SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") + SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") + file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") - # Save the streaming content to a file - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) - with open(file_body_path, "w") as f: - json.dump(json.loads(body.decode("utf-8")), f) + headers = {} + headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}" + headers["Content-Type"] = "application/json" - # Return the saved file - return FileResponse(file_path) + r = None + try: + r = requests.post( + url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech", + data=body, + headers=headers, + stream=True, + ) + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(file_body_path, "w") as f: + json.dump(json.loads(body.decode("utf-8")), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + print(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}" + + raise HTTPException( + status_code=r.status_code if r else 500, detail=error_detail + ) + + except ValueError: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) + + +async def fetch_url(url, key): + try: + headers = {"Authorization": f"Bearer {key}"} + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + return await response.json() except Exception as e: - print(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}" + # Handle connection error here + print(f"Connection error: {e}") + return None - raise HTTPException(status_code=r.status_code, detail=error_detail) + +def merge_models_lists(model_lists): + merged_list = [] + + for idx, models in enumerate(model_lists): + if models is not None and "error" not in models: + merged_list.extend( + [ + {**model, "urlIdx": idx} + for model in models + if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx] + or "gpt" in model["id"] + ] + ) + + return merged_list + + +async def get_all_models(): + print("get_all_models") + + if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "": + models = {"data": []} + else: + tasks = [ + fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) + for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) + ] + + responses = await asyncio.gather(*tasks) + models = { + "data": merge_models_lists( + list( + map( + lambda response: ( + response["data"] + if response and "data" in response + else None + ), + responses, + ) + ) + ) + } + + print(models) + app.state.MODELS = {model["id"]: model for model in models["data"]} + + return models + + +@app.get("/models") +@app.get("/models/{url_idx}") +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 user.role == "user": + models["data"] = list( + filter( + lambda model: model["id"] in app.state.MODEL_FILTER_LIST, + models["data"], + ) + ) + return models + return models + else: + url = app.state.OPENAI_API_BASE_URLS[url_idx] + + r = None + + try: + r = requests.request(method="GET", url=f"{url}/models") + r.raise_for_status() + + response_data = r.json() + if "api.openai.com" in url: + response_data["data"] = list( + filter(lambda model: "gpt" in model["id"], response_data["data"]) + ) + + return response_data + except Exception as e: + print(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}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) async def proxy(path: str, request: Request, user=Depends(get_verified_user)): - target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}" - print(target_url, app.state.OPENAI_API_KEY) - - if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + idx = 0 body = await request.body() - # TODO: Remove below after gpt-4-vision fix from Open AI # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision) try: body = body.decode("utf-8") body = json.loads(body) + idx = app.state.MODELS[body.get("model")]["urlIdx"] + # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 # This is a workaround until OpenAI fixes the issue with this model if body.get("model") == "gpt-4-vision-preview": @@ -146,15 +282,32 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): body["max_tokens"] = 4000 print("Modified body_dict:", body) + # Fix for ChatGPT calls failing because the num_ctx key is in body + if "num_ctx" in body: + # If 'num_ctx' is in the dictionary, delete it + # Leaving it there generates an error with the + # OpenAI API (Feb 2024) + del body["num_ctx"] + # Convert the modified body back to JSON body = json.dumps(body) except json.JSONDecodeError as e: print("Error loading request body into a dictionary:", e) + url = app.state.OPENAI_API_BASE_URLS[idx] + key = app.state.OPENAI_API_KEYS[idx] + + target_url = f"{url}/{path}" + + if key == "": + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + headers = {} - headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" + headers["Authorization"] = f"Bearer {key}" headers["Content-Type"] = "application/json" + r = None + try: r = requests.request( method=request.method, @@ -174,21 +327,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): headers=dict(r.headers), ) else: - # For non-SSE, read the response and return it - # response_data = ( - # r.json() - # if r.headers.get("Content-Type", "") - # == "application/json" - # else r.text - # ) - response_data = r.json() - - if "openai" in app.state.OPENAI_API_BASE_URL and path == "models": - response_data["data"] = list( - filter(lambda model: "gpt" in model["id"], response_data["data"]) - ) - return response_data except Exception as e: print(e) @@ -201,4 +340,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): except: error_detail = f"External: {e}" - raise HTTPException(status_code=r.status_code, detail=error_detail) + raise HTTPException( + status_code=r.status_code if r else 500, detail=error_detail + ) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 07a30ade..5fc38b4a 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -1,6 +1,5 @@ from fastapi import ( FastAPI, - Request, Depends, HTTPException, status, @@ -10,9 +9,12 @@ from fastapi import ( ) from fastapi.middleware.cors import CORSMiddleware import os, shutil + +from pathlib import Path from typing import List -# from chromadb.utils import embedding_functions +from sentence_transformers import SentenceTransformer +from chromadb.utils import embedding_functions from langchain_community.document_loaders import ( WebBaseLoader, @@ -28,27 +30,68 @@ from langchain_community.document_loaders import ( UnstructuredExcelLoader, ) from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain_community.vectorstores import Chroma -from langchain.chains import RetrievalQA - from pydantic import BaseModel from typing import Optional - +import mimetypes import uuid -import time +import json -from utils.misc import calculate_sha256, calculate_sha256_string + +from apps.web.models.documents import ( + Documents, + DocumentForm, + DocumentResponse, +) + +from apps.rag.utils import query_doc, query_collection + +from utils.misc import ( + calculate_sha256, + calculate_sha256_string, + sanitize_filename, + extract_folders_after_data_docs, +) from utils.utils import get_current_user, get_admin_user -from config import UPLOAD_DIR, EMBED_MODEL, CHROMA_CLIENT, CHUNK_SIZE, CHUNK_OVERLAP +from config import ( + UPLOAD_DIR, + DOCS_DIR, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_DEVICE_TYPE, + CHROMA_CLIENT, + CHUNK_SIZE, + CHUNK_OVERLAP, + RAG_TEMPLATE, +) + from constants import ERROR_MESSAGES -# EMBEDDING_FUNC = embedding_functions.SentenceTransformerEmbeddingFunction( -# model_name=EMBED_MODEL -# ) +# +# if RAG_EMBEDDING_MODEL: +# sentence_transformer_ef = SentenceTransformer( +# model_name_or_path=RAG_EMBEDDING_MODEL, +# cache_folder=RAG_EMBEDDING_MODEL_DIR, +# device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, +# ) + app = FastAPI() +app.state.PDF_EXTRACT_IMAGES = False +app.state.CHUNK_SIZE = CHUNK_SIZE +app.state.CHUNK_OVERLAP = CHUNK_OVERLAP +app.state.RAG_TEMPLATE = RAG_TEMPLATE +app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL +app.state.TOP_K = 4 + +app.state.sentence_transformer_ef = ( + embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=app.state.RAG_EMBEDDING_MODEL, + device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, + ) +) + + origins = ["*"] app.add_middleware( @@ -68,9 +111,9 @@ class StoreWebForm(CollectionNameForm): url: str -def store_data_in_vector_db(data, collection_name) -> bool: +def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool: text_splitter = RecursiveCharacterTextSplitter( - chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP + chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP ) docs = text_splitter.split_documents(data) @@ -78,7 +121,16 @@ def store_data_in_vector_db(data, collection_name) -> bool: metadatas = [doc.metadata for doc in docs] try: - collection = CHROMA_CLIENT.create_collection(name=collection_name) + if overwrite: + for collection in CHROMA_CLIENT.list_collections(): + if collection_name == collection.name: + print(f"deleting existing collection {collection_name}") + CHROMA_CLIENT.delete_collection(name=collection_name) + + collection = CHROMA_CLIENT.create_collection( + name=collection_name, + embedding_function=app.state.sentence_transformer_ef, + ) collection.add( documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts] @@ -94,26 +146,133 @@ def store_data_in_vector_db(data, collection_name) -> bool: @app.get("/") async def get_status(): - return {"status": True} + return { + "status": True, + "chunk_size": app.state.CHUNK_SIZE, + "chunk_overlap": app.state.CHUNK_OVERLAP, + "template": app.state.RAG_TEMPLATE, + "embedding_model": app.state.RAG_EMBEDDING_MODEL, + } + + +@app.get("/embedding/model") +async def get_embedding_model(user=Depends(get_admin_user)): + return { + "status": True, + "embedding_model": app.state.RAG_EMBEDDING_MODEL, + } + + +class EmbeddingModelUpdateForm(BaseModel): + embedding_model: str + + +@app.post("/embedding/model/update") +async def update_embedding_model( + form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user) +): + app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model + app.state.sentence_transformer_ef = ( + embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=app.state.RAG_EMBEDDING_MODEL, + device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, + ) + ) + + return { + "status": True, + "embedding_model": app.state.RAG_EMBEDDING_MODEL, + } + + +@app.get("/config") +async def get_rag_config(user=Depends(get_admin_user)): + return { + "status": True, + "pdf_extract_images": app.state.PDF_EXTRACT_IMAGES, + "chunk": { + "chunk_size": app.state.CHUNK_SIZE, + "chunk_overlap": app.state.CHUNK_OVERLAP, + }, + } + + +class ChunkParamUpdateForm(BaseModel): + chunk_size: int + chunk_overlap: int + + +class ConfigUpdateForm(BaseModel): + pdf_extract_images: bool + chunk: ChunkParamUpdateForm + + +@app.post("/config/update") +async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)): + app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images + app.state.CHUNK_SIZE = form_data.chunk.chunk_size + app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap + + return { + "status": True, + "pdf_extract_images": app.state.PDF_EXTRACT_IMAGES, + "chunk": { + "chunk_size": app.state.CHUNK_SIZE, + "chunk_overlap": app.state.CHUNK_OVERLAP, + }, + } + + +@app.get("/template") +async def get_rag_template(user=Depends(get_current_user)): + return { + "status": True, + "template": app.state.RAG_TEMPLATE, + } + + +@app.get("/query/settings") +async def get_query_settings(user=Depends(get_admin_user)): + return { + "status": True, + "template": app.state.RAG_TEMPLATE, + "k": app.state.TOP_K, + } + + +class QuerySettingsForm(BaseModel): + k: Optional[int] = None + template: Optional[str] = None + + +@app.post("/query/settings/update") +async def update_query_settings( + form_data: QuerySettingsForm, user=Depends(get_admin_user) +): + 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} class QueryDocForm(BaseModel): collection_name: str query: str - k: Optional[int] = 4 + k: Optional[int] = None @app.post("/query/doc") -def query_doc( +def query_doc_handler( form_data: QueryDocForm, user=Depends(get_current_user), ): + try: - collection = CHROMA_CLIENT.get_collection( - name=form_data.collection_name, + return query_doc( + collection_name=form_data.collection_name, + query=form_data.query, + k=form_data.k if form_data.k else app.state.TOP_K, + embedding_function=app.state.sentence_transformer_ef, ) - result = collection.query(query_texts=[form_data.query], n_results=form_data.k) - return result except Exception as e: print(e) raise HTTPException( @@ -125,74 +284,20 @@ def query_doc( class QueryCollectionsForm(BaseModel): collection_names: List[str] query: str - k: Optional[int] = 4 - - -def merge_and_sort_query_results(query_results, k): - # Initialize lists to store combined data - combined_ids = [] - combined_distances = [] - combined_metadatas = [] - combined_documents = [] - - # 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]) - - # Create a list of tuples (distance, id, metadata, document) - combined = list( - zip(combined_distances, combined_ids, combined_metadatas, combined_documents) - ) - - # Sort the list based on distances - combined.sort(key=lambda x: x[0]) - - # Unzip the sorted list - sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = 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] - - # Create the output dictionary - merged_query_results = { - "ids": [sorted_ids], - "distances": [sorted_distances], - "metadatas": [sorted_metadatas], - "documents": [sorted_documents], - "embeddings": None, - "uris": None, - "data": None, - } - - return merged_query_results + k: Optional[int] = None @app.post("/query/collection") -def query_collection( +def query_collection_handler( form_data: QueryCollectionsForm, user=Depends(get_current_user), ): - results = [] - - for collection_name in form_data.collection_names: - try: - collection = CHROMA_CLIENT.get_collection( - name=collection_name, - ) - result = collection.query( - query_texts=[form_data.query], n_results=form_data.k - ) - results.append(result) - except: - pass - - return merge_and_sort_query_results(results, form_data.k) + return query_collection( + collection_names=form_data.collection_names, + query=form_data.query, + k=form_data.k if form_data.k else app.state.TOP_K, + embedding_function=app.state.sentence_transformer_ef, + ) @app.post("/web") @@ -206,7 +311,7 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): if collection_name == "": collection_name = calculate_sha256_string(form_data.url)[:63] - store_data_in_vector_db(data, collection_name) + store_data_in_vector_db(data, collection_name, overwrite=True) return { "status": True, "collection_name": collection_name, @@ -220,8 +325,8 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): ) -def get_loader(file, file_path): - file_ext = file.filename.split(".")[-1].lower() +def get_loader(filename: str, file_content_type: str, file_path: str): + file_ext = filename.split(".")[-1].lower() known_type = True known_source_ext = [ @@ -270,7 +375,7 @@ def get_loader(file, file_path): ] if file_ext == "pdf": - loader = PyPDFLoader(file_path) + loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES) elif file_ext == "csv": loader = CSVLoader(file_path) elif file_ext == "rst": @@ -279,23 +384,25 @@ def get_loader(file, file_path): loader = UnstructuredXMLLoader(file_path) elif file_ext == "md": loader = UnstructuredMarkdownLoader(file_path) - elif file.content_type == "application/epub+zip": + elif file_content_type == "application/epub+zip": loader = UnstructuredEPubLoader(file_path) elif ( - file.content_type + file_content_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" or file_ext in ["doc", "docx"] ): loader = Docx2txtLoader(file_path) - elif file.content_type in [ + elif file_content_type in [ "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ] or file_ext in ["xls", "xlsx"]: loader = UnstructuredExcelLoader(file_path) - elif file_ext in known_source_ext or file.content_type.find("text/") >= 0: - loader = TextLoader(file_path) + elif file_ext in known_source_ext or ( + file_content_type and file_content_type.find("text/") >= 0 + ): + loader = TextLoader(file_path, autodetect_encoding=True) else: - loader = TextLoader(file_path) + loader = TextLoader(file_path, autodetect_encoding=True) known_type = False return loader, known_type @@ -323,7 +430,7 @@ def store_doc( collection_name = calculate_sha256(f)[:63] f.close() - loader, known_type = get_loader(file, file_path) + loader, known_type = get_loader(file.filename, file.content_type, file_path) data = loader.load() result = store_data_in_vector_db(data, collection_name) @@ -353,6 +460,63 @@ def store_doc( ) +@app.get("/scan") +def scan_docs_dir(user=Depends(get_admin_user)): + for path in Path(DOCS_DIR).rglob("./**/*"): + try: + if path.is_file() and not path.name.startswith("."): + tags = extract_folders_after_data_docs(path) + filename = path.name + file_content_type = mimetypes.guess_type(path) + + f = open(path, "rb") + collection_name = calculate_sha256(f)[:63] + f.close() + + loader, known_type = get_loader( + filename, file_content_type[0], str(path) + ) + data = loader.load() + + result = store_data_in_vector_db(data, collection_name) + + if result: + sanitized_filename = sanitize_filename(filename) + doc = Documents.get_doc_by_name(sanitized_filename) + + if doc == None: + doc = Documents.insert_new_doc( + user.id, + DocumentForm( + **{ + "name": sanitized_filename, + "title": filename, + "collection_name": collection_name, + "filename": filename, + "content": ( + json.dumps( + { + "tags": list( + map( + lambda name: {"name": name}, + tags, + ) + ) + } + ) + if len(tags) + else "{}" + ), + } + ), + ) + + except Exception as e: + print(e) + + return True + + @app.get("/reset/db") def reset_vector_db(user=Depends(get_admin_user)): CHROMA_CLIENT.reset() diff --git a/backend/apps/rag/utils.py b/backend/apps/rag/utils.py new file mode 100644 index 00000000..a3537d4d --- /dev/null +++ b/backend/apps/rag/utils.py @@ -0,0 +1,182 @@ +import re +from typing import List + +from config import CHROMA_CLIENT + + +def query_doc(collection_name: str, query: str, k: int, embedding_function): + try: + # if you use docker use the model from the environment variable + collection = CHROMA_CLIENT.get_collection( + name=collection_name, + embedding_function=embedding_function, + ) + result = collection.query( + query_texts=[query], + n_results=k, + ) + return result + except Exception as e: + raise e + + +def merge_and_sort_query_results(query_results, k): + # Initialize lists to store combined data + combined_ids = [] + combined_distances = [] + combined_metadatas = [] + combined_documents = [] + + # 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]) + + # Create a list of tuples (distance, id, metadata, document) + combined = list( + zip(combined_distances, combined_ids, combined_metadatas, combined_documents) + ) + + # Sort the list based on distances + combined.sort(key=lambda x: x[0]) + + # Unzip the sorted list + sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = 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] + + # Create the output dictionary + merged_query_results = { + "ids": [sorted_ids], + "distances": [sorted_distances], + "metadatas": [sorted_metadatas], + "documents": [sorted_documents], + "embeddings": None, + "uris": None, + "data": None, + } + + return merged_query_results + + +def query_collection( + collection_names: List[str], query: str, k: int, embedding_function +): + + results = [] + + for collection_name in collection_names: + try: + # if you use docker use the model from the environment variable + collection = CHROMA_CLIENT.get_collection( + name=collection_name, + embedding_function=embedding_function, + ) + + result = collection.query( + query_texts=[query], + n_results=k, + ) + results.append(result) + except: + pass + + return merge_and_sort_query_results(results, k) + + +def rag_template(template: str, context: str, query: str): + template = template.replace("[context]", context) + template = template.replace("[query]", query) + return template + + +def rag_messages(docs, messages, template, k, embedding_function): + print(docs) + + last_user_message_idx = None + for i in range(len(messages) - 1, -1, -1): + if messages[i]["role"] == "user": + last_user_message_idx = i + break + + user_message = messages[last_user_message_idx] + + if isinstance(user_message["content"], list): + # Handle list content input + content_type = "list" + query = "" + for content_item in user_message["content"]: + if content_item["type"] == "text": + query = content_item["text"] + break + elif isinstance(user_message["content"], str): + # Handle text content input + content_type = "text" + query = user_message["content"] + else: + # Fallback in case the input does not match expected types + content_type = None + query = "" + + relevant_contexts = [] + + for doc in docs: + context = None + + try: + if doc["type"] == "collection": + context = query_collection( + collection_names=doc["collection_names"], + query=query, + k=k, + embedding_function=embedding_function, + ) + else: + context = query_doc( + collection_name=doc["collection_name"], + query=query, + k=k, + embedding_function=embedding_function, + ) + except Exception as e: + print(e) + context = None + + relevant_contexts.append(context) + + context_string = "" + for context in relevant_contexts: + if context: + context_string += " ".join(context["documents"][0]) + "\n" + + ra_content = rag_template( + template=template, + context=context_string, + query=query, + ) + + if content_type == "list": + new_content = [] + for content_item in user_message["content"]: + if content_item["type"] == "text": + # Update the text item's content with ra_content + new_content.append({"type": "text", "text": ra_content}) + else: + # Keep other types of content as they are + new_content.append(content_item) + new_user_message = {**user_message, "content": new_content} + else: + new_user_message = { + **user_message, + "content": ra_content, + } + + messages[last_user_message_idx] = new_user_message + + return messages diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py index 1f8c3bf7..d0aa9969 100644 --- a/backend/apps/web/internal/db.py +++ b/backend/apps/web/internal/db.py @@ -1,6 +1,16 @@ from peewee import * from config import DATA_DIR +import os -DB = SqliteDatabase(f"{DATA_DIR}/ollama.db") +# Check if the file exists +if os.path.exists(f"{DATA_DIR}/ollama.db"): + # Rename the file + os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db") + print("File renamed successfully.") +else: + pass + + +DB = SqliteDatabase(f"{DATA_DIR}/webui.db") DB.connect() diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 400ddac0..dd5c0c70 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -19,6 +19,7 @@ from config import ( DEFAULT_USER_ROLE, ENABLE_SIGNUP, USER_PERMISSIONS, + WEBHOOK_URL, ) app = FastAPI() @@ -26,10 +27,13 @@ app = FastAPI() origins = ["*"] app.state.ENABLE_SIGNUP = ENABLE_SIGNUP +app.state.JWT_EXPIRES_IN = "-1" + app.state.DEFAULT_MODELS = DEFAULT_MODELS app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.USER_PERMISSIONS = USER_PERMISSIONS +app.state.WEBHOOK_URL = WEBHOOK_URL app.add_middleware( @@ -55,7 +59,6 @@ app.include_router(utils.router, prefix="/utils", tags=["utils"]) async def get_status(): return { "status": True, - "version": WEBUI_VERSION, "auth": WEBUI_AUTH, "default_models": app.state.DEFAULT_MODELS, "default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS, diff --git a/backend/apps/web/models/tags.py b/backend/apps/web/models/tags.py index c14658cf..d4264501 100644 --- a/backend/apps/web/models/tags.py +++ b/backend/apps/web/models/tags.py @@ -167,6 +167,27 @@ class TagTable: .count() ) + def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool: + try: + query = ChatIdTag.delete().where( + (ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id) + ) + res = query.execute() # Remove the rows, return number of rows removed. + print(res) + + tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) + if tag_count == 0: + # Remove tag item from Tag col as well + query = Tag.delete().where( + (Tag.name == tag_name) & (Tag.user_id == user_id) + ) + query.execute() # Remove the rows, return number of rows removed. + + return True + except Exception as e: + print("delete_tag", e) + return False + def delete_tag_by_tag_name_and_chat_id_and_user_id( self, tag_name: str, chat_id: str, user_id: str ) -> bool: diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 7ccef630..d881ec74 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, status from pydantic import BaseModel import time import uuid +import re from apps.web.models.auths import ( SigninForm, @@ -25,8 +26,9 @@ from utils.utils import ( get_admin_user, create_token, ) -from utils.misc import get_gravatar_url, validate_email_format -from constants import ERROR_MESSAGES +from utils.misc import parse_duration, validate_email_format +from utils.webhook import post_webhook +from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES router = APIRouter() @@ -95,10 +97,13 @@ async def update_password( @router.post("/signin", response_model=SigninResponse) -async def signin(form_data: SigninForm): +async def signin(request: Request, form_data: SigninForm): user = Auths.authenticate_user(form_data.email.lower(), form_data.password) if user: - token = create_token(data={"id": user.id}) + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN), + ) return { "token": token, @@ -145,9 +150,23 @@ async def signup(request: Request, form_data: SignupForm): ) if user: - token = create_token(data={"id": user.id}) + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN), + ) # response.set_cookie(key='token', value=token, httponly=True) + if request.app.state.WEBHOOK_URL: + post_webhook( + request.app.state.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + "action": "signup", + "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + "user": user.model_dump_json(exclude_none=True), + }, + ) + return { "token": token, "token_type": "Bearer", @@ -200,3 +219,33 @@ async def update_default_user_role( if form_data.role in ["pending", "user", "admin"]: request.app.state.DEFAULT_USER_ROLE = form_data.role return request.app.state.DEFAULT_USER_ROLE + + +############################ +# JWT Expiration +############################ + + +@router.get("/token/expires") +async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)): + return request.app.state.JWT_EXPIRES_IN + + +class UpdateJWTExpiresDurationForm(BaseModel): + duration: str + + +@router.post("/token/expires/update") +async def update_token_expires_duration( + request: Request, + form_data: UpdateJWTExpiresDurationForm, + user=Depends(get_admin_user), +): + pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$" + + # Check if the input string matches the pattern + if re.match(pattern, form_data.duration): + request.app.state.JWT_EXPIRES_IN = form_data.duration + return request.app.state.JWT_EXPIRES_IN + else: + return request.app.state.JWT_EXPIRES_IN diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 00dcfb6e..0c0ac1ce 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -115,9 +115,12 @@ async def get_user_chats_by_tag_name( for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id) ] - print(chat_ids) + chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit) - return 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 ############################ @@ -268,6 +271,16 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)): @router.delete("/", response_model=bool) -async def delete_all_user_chats(user=Depends(get_current_user)): +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/documents.py b/backend/apps/web/routers/documents.py index 5bc473fa..7c69514f 100644 --- a/backend/apps/web/routers/documents.py +++ b/backend/apps/web/routers/documents.py @@ -96,6 +96,10 @@ async def get_doc_by_name(name: str, user=Depends(get_current_user)): ############################ +class TagItem(BaseModel): + name: str + + class TagDocumentForm(BaseModel): name: str tags: List[dict] diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index 86e1a9e5..0d34b040 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, UploadFile, File, BackgroundTasks from fastapi import Depends, HTTPException, status -from starlette.responses import StreamingResponse +from starlette.responses import StreamingResponse, FileResponse + from pydantic import BaseModel @@ -9,9 +10,11 @@ import os import aiohttp import json + +from utils.utils import get_admin_user from utils.misc import calculate_sha256, get_gravatar_url -from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR +from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR from constants import ERROR_MESSAGES @@ -72,7 +75,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024 hashed = calculate_sha256(file) file.seek(0) - url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}" + url = f"{OLLAMA_BASE_URLS[0]}/api/blobs/sha256:{hashed}" response = requests.post(url, data=file) if response.ok: @@ -144,7 +147,7 @@ def upload(file: UploadFile = File(...)): hashed = calculate_sha256(f) f.seek(0) - url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}" + url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" response = requests.post(url, data=f) if response.ok: @@ -172,3 +175,13 @@ async def get_gravatar( email: str, ): return get_gravatar_url(email) + + +@router.get("/db/download") +async def download_db(user=Depends(get_admin_user)): + + return FileResponse( + f"{DATA_DIR}/webui.db", + media_type="application/octet-stream", + filename="webui.db", + ) diff --git a/backend/config.py b/backend/config.py index d7c89b3b..9236e8a8 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,10 +1,20 @@ import os import chromadb from chromadb import Settings -from secrets import token_bytes from base64 import b64encode -from constants import ERROR_MESSAGES +from bs4 import BeautifulSoup + from pathlib import Path +import json +import yaml + +import markdown +import requests +import shutil + +from secrets import token_bytes +from constants import ERROR_MESSAGES + try: from dotenv import load_dotenv, find_dotenv @@ -13,6 +23,8 @@ try: except ImportError: print("dotenv not installed, skipping...") +WEBUI_NAME = "Open WebUI" +shutil.copyfile("../build/favicon.png", "./static/favicon.png") #################################### # ENV (dev,test,prod) @@ -20,6 +32,102 @@ except ImportError: ENV = os.environ.get("ENV", "dev") +try: + with open(f"../package.json", "r") as f: + PACKAGE_DATA = json.load(f) +except: + PACKAGE_DATA = {"version": "0.0.0"} + +VERSION = PACKAGE_DATA["version"] + + +# Function to parse each section +def parse_section(section): + items = [] + for li in section.find_all("li"): + # Extract raw HTML string + raw_html = str(li) + + # Extract text without HTML tags + text = li.get_text(separator=" ", strip=True) + + # Split into title and content + parts = text.split(": ", 1) + title = parts[0].strip() if len(parts) > 1 else "" + content = parts[1].strip() if len(parts) > 1 else text + + items.append({"title": title, "content": content, "raw": raw_html}) + return items + + +try: + with open("../CHANGELOG.md", "r") as file: + changelog_content = file.read() +except: + changelog_content = "" + +# Convert markdown content to HTML +html_content = markdown.markdown(changelog_content) + +# Parse the HTML content +soup = BeautifulSoup(html_content, "html.parser") + +# Initialize JSON structure +changelog_json = {} + +# Iterate over each version +for version in soup.find_all("h2"): + version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets + date = version.get_text().strip().split(" - ")[1] + + version_data = {"date": date} + + # Find the next sibling that is a h3 tag (section title) + current = version.find_next_sibling() + + while current and current.name != "h2": + if current.name == "h3": + section_title = current.get_text().lower() # e.g., "added", "fixed" + section_items = parse_section(current.find_next_sibling("ul")) + version_data[section_title] = section_items + + # Move to the next element + current = current.find_next_sibling() + + changelog_json[version_number] = version_data + + +CHANGELOG = changelog_json + + +#################################### +# CUSTOM_NAME +#################################### + +CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") +if CUSTOM_NAME: + try: + r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") + data = r.json() + if r.ok: + if "logo" in data: + url = ( + f"https://api.openwebui.com{data['logo']}" + if data["logo"][0] == "/" + else data["logo"] + ) + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open("./static/favicon.png", "wb") as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + WEBUI_NAME = data["name"] + except Exception as e: + print(e) + pass + #################################### # DATA/FRONTEND BUILD DIR @@ -28,6 +136,12 @@ ENV = os.environ.get("ENV", "dev") DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve()) FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build"))) +try: + with open(f"{DATA_DIR}/config.json", "r") as f: + CONFIG_DATA = json.load(f) +except: + CONFIG_DATA = {} + #################################### # File Upload DIR #################################### @@ -43,17 +157,76 @@ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) CACHE_DIR = f"{DATA_DIR}/cache" Path(CACHE_DIR).mkdir(parents=True, exist_ok=True) + #################################### -# OLLAMA_API_BASE_URL +# Docs DIR +#################################### + +DOCS_DIR = f"{DATA_DIR}/docs" +Path(DOCS_DIR).mkdir(parents=True, exist_ok=True) + + +#################################### +# LITELLM_CONFIG +#################################### + + +def create_config_file(file_path): + directory = os.path.dirname(file_path) + + # Check if directory exists, if not, create it + if not os.path.exists(directory): + os.makedirs(directory) + + # Data to write into the YAML file + config_data = { + "general_settings": {}, + "litellm_settings": {}, + "model_list": [], + "router_settings": {}, + } + + # Write data to YAML file + with open(file_path, "w") as file: + yaml.dump(config_data, file) + + +LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml" + +if not os.path.exists(LITELLM_CONFIG_PATH): + print("Config file doesn't exist. Creating...") + create_config_file(LITELLM_CONFIG_PATH) + print("Config file created successfully.") + + +#################################### +# OLLAMA_BASE_URL #################################### OLLAMA_API_BASE_URL = os.environ.get( "OLLAMA_API_BASE_URL", "http://localhost:11434/api" ) +OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") + + +if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": + OLLAMA_BASE_URL = ( + OLLAMA_API_BASE_URL[:-4] + if OLLAMA_API_BASE_URL.endswith("/api") + else OLLAMA_API_BASE_URL + ) + if ENV == "prod": - if OLLAMA_API_BASE_URL == "/ollama/api": - OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" + if OLLAMA_BASE_URL == "/ollama": + OLLAMA_BASE_URL = "http://host.docker.internal:11434" + + +OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") +OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL + +OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] + #################################### # OPENAI_API @@ -62,19 +235,40 @@ if ENV == "prod": OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") + if OPENAI_API_BASE_URL == "": OPENAI_API_BASE_URL = "https://api.openai.com/v1" +OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") +OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY + +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")] + + +OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") +OPENAI_API_BASE_URLS = ( + OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL +) + +OPENAI_API_BASE_URLS = [ + url.strip() if url != "" else "https://api.openai.com/v1" + for url in OPENAI_API_BASE_URLS.split(";") +] #################################### # WEBUI #################################### -ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", True) +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true" DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None) -DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( - "DEFAULT_PROMPT_SUGGESTIONS", - [ + + +DEFAULT_PROMPT_SUGGESTIONS = ( + CONFIG_DATA["ui"]["prompt_suggestions"] + if "ui" in CONFIG_DATA + and "prompt_suggestions" in CONFIG_DATA["ui"] + and type(CONFIG_DATA["ui"]["prompt_suggestions"]) is list + else [ { "title": ["Help me study", "vocabulary for a college entrance exam"], "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.", @@ -91,12 +285,25 @@ DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( "title": ["Show me a code snippet", "of a website's sticky header"], "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.", }, - ], + ] ) -DEFAULT_USER_ROLE = "pending" -USER_PERMISSIONS = {"chat": {"deletion": True}} +DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending") + +USER_PERMISSIONS_CHAT_DELETION = ( + os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true" +) + +USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}} + + +MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true" +MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "") +MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")] + +WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "") + #################################### # WEBUI_VERSION #################################### @@ -128,7 +335,12 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "": #################################### CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" -EMBED_MODEL = "all-MiniLM-L6-v2" +# 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 (all-MiniLM-L6-v2) +RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") +# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance +RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get( + "RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu" +) CHROMA_CLIENT = chromadb.PersistentClient( path=CHROMA_DATA_PATH, settings=Settings(allow_reset=True, anonymized_telemetry=False), @@ -136,9 +348,31 @@ CHROMA_CLIENT = chromadb.PersistentClient( CHUNK_SIZE = 1500 CHUNK_OVERLAP = 100 + +RAG_TEMPLATE = """Use the following context as your learned knowledge, inside XML tags. + + [context] + + +When answer to user: +- If you don't know, just say that you don't know. +- If you don't know when you are not sure, ask for clarification. +Avoid mentioning that you obtained the information from the context. +And answer according to the language of the user's question. + +Given the context information, answer the query. +Query: [query]""" + #################################### # Transcribe #################################### WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base") WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models") + + +#################################### +# Images +#################################### + +AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "") diff --git a/backend/constants.py b/backend/constants.py index 580db9c5..42c5c85e 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -5,6 +5,13 @@ class MESSAGES(str, Enum): DEFAULT = lambda msg="": f"{msg if msg else ''}" +class WEBHOOK_MESSAGES(str, Enum): + DEFAULT = lambda msg="": f"{msg if msg else ''}" + USER_SIGNUP = lambda username="": ( + f"New user signed up: {username}" if username else "New user signed up" + ) + + class ERROR_MESSAGES(str, Enum): def __str__(self) -> str: return super().__str__() @@ -41,6 +48,15 @@ class ERROR_MESSAGES(str, Enum): NOT_FOUND = "We could not find what you're looking for :/" USER_NOT_FOUND = "We could not find what you're looking for :/" API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." + MALICIOUS = "Unusual activities detected, please try again in a few minutes." PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." + INCORRECT_FORMAT = ( + lambda err="": f"Invalid format. Please use the correct format{err}" + ) + RATE_LIMIT_EXCEEDED = "API rate limit exceeded" + + MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" + OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" + OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" diff --git a/backend/data/config.json b/backend/data/config.json new file mode 100644 index 00000000..cd6687d7 --- /dev/null +++ b/backend/data/config.json @@ -0,0 +1,35 @@ +{ + "version": 0, + "ui": { + "prompt_suggestions": [ + { + "title": [ + "Help me study", + "vocabulary for a college entrance exam" + ], + "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option." + }, + { + "title": [ + "Give me ideas", + "for what to do with my kids' art" + ], + "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter." + }, + { + "title": [ + "Tell me a fun fact", + "about the Roman Empire" + ], + "content": "Tell me a random fun fact about the Roman Empire" + }, + { + "title": [ + "Show me a code snippet", + "of a website's sticky header" + ], + "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript." + } + ] + } +} \ No newline at end of file diff --git a/backend/data/litellm/config.yaml b/backend/data/litellm/config.yaml new file mode 100644 index 00000000..7d9d2b72 --- /dev/null +++ b/backend/data/litellm/config.yaml @@ -0,0 +1,4 @@ +general_settings: {} +litellm_settings: {} +model_list: [] +router_settings: {} diff --git a/backend/main.py b/backend/main.py index 3a28670e..35f405e6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,22 +1,46 @@ +from bs4 import BeautifulSoup +import json +import markdown import time +import os +import sys +import requests -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Depends, status from fastapi.staticfiles import StaticFiles from fastapi import HTTPException from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.middleware.cors import CORSMiddleware from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.middleware.base import BaseHTTPMiddleware from apps.ollama.main import app as ollama_app from apps.openai.main import app as openai_app +from apps.litellm.main import app as litellm_app, startup as litellm_app_startup from apps.audio.main import app as audio_app - - -from apps.web.main import app as webui_app +from apps.images.main import app as images_app from apps.rag.main import app as rag_app +from apps.web.main import app as webui_app -from config import ENV, FRONTEND_BUILD_DIR +from pydantic import BaseModel +from typing import List + + +from utils.utils import get_admin_user +from apps.rag.utils import rag_messages + +from config import ( + WEBUI_NAME, + ENV, + VERSION, + CHANGELOG, + FRONTEND_BUILD_DIR, + MODEL_FILTER_ENABLED, + MODEL_FILTER_LIST, + WEBHOOK_URL, +) +from constants import ERROR_MESSAGES class SPAStaticFiles(StaticFiles): @@ -32,8 +56,70 @@ class SPAStaticFiles(StaticFiles): app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) +app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.WEBHOOK_URL = WEBHOOK_URL + + origins = ["*"] + +class RAGMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.method == "POST" and ( + "/api/chat" in request.url.path or "/chat/completions" in request.url.path + ): + print(request.url.path) + + # Read the original request body + body = await request.body() + # Decode body to string + body_str = body.decode("utf-8") + # Parse string to JSON + data = json.loads(body_str) if body_str else {} + + # Example: Add a new key-value pair or modify existing ones + # data["modified"] = True # Example modification + 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.sentence_transformer_ef, + ) + del data["docs"] + + print(data["messages"]) + + modified_body_bytes = json.dumps(data).encode("utf-8") + + # Replace the request body with the modified one + request._body = modified_body_bytes + + # Set custom header to ensure content-length matches new body length + request.headers.__dict__["_list"] = [ + (b"content-length", str(len(modified_body_bytes)).encode("utf-8")), + *[ + (k, v) + for k, v in request.headers.raw + if k.lower() != b"content-length" + ], + ] + + response = await call_next(request) + return response + + async def _receive(self, body: bytes): + return {"type": "http.request", "body": body, "more_body": False} + + +app.add_middleware(RAGMiddleware) + + app.add_middleware( CORSMiddleware, allow_origins=origins, @@ -53,15 +139,124 @@ async def check_url(request: Request, call_next): return response -app.mount("/api/v1", webui_app) +@app.on_event("startup") +async def on_startup(): + await litellm_app_startup() -app.mount("/ollama/api", ollama_app) + +app.mount("/api/v1", webui_app) +app.mount("/litellm/api", litellm_app) + +app.mount("/ollama", ollama_app) app.mount("/openai/api", openai_app) +app.mount("/images/api/v1", images_app) app.mount("/audio/api/v1", audio_app) app.mount("/rag/api/v1", rag_app) +@app.get("/api/config") +async def get_app_config(): + + return { + "status": True, + "name": WEBUI_NAME, + "version": VERSION, + "images": images_app.state.ENABLED, + "default_models": webui_app.state.DEFAULT_MODELS, + "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, + } + + +@app.get("/api/config/model/filter") +async def get_model_filter_config(user=Depends(get_admin_user)): + return { + "enabled": app.state.MODEL_FILTER_ENABLED, + "models": app.state.MODEL_FILTER_LIST, + } + + +class ModelFilterConfigForm(BaseModel): + enabled: bool + models: List[str] + + +@app.post("/api/config/model/filter") +async def update_model_filter_config( + form_data: ModelFilterConfigForm, user=Depends(get_admin_user) +): + + app.state.MODEL_FILTER_ENABLED = 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.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST + + openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED + openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST + + return { + "enabled": app.state.MODEL_FILTER_ENABLED, + "models": app.state.MODEL_FILTER_LIST, + } + + +@app.get("/api/webhook") +async def get_webhook_url(user=Depends(get_admin_user)): + return { + "url": app.state.WEBHOOK_URL, + } + + +class UrlForm(BaseModel): + url: str + + +@app.post("/api/webhook") +async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): + app.state.WEBHOOK_URL = form_data.url + + webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL + + return { + "url": app.state.WEBHOOK_URL, + } + + +@app.get("/api/version") +async def get_app_config(): + + return { + "version": VERSION, + } + + +@app.get("/api/changelog") +async def get_app_changelog(): + return CHANGELOG + + +@app.get("/api/version/updates") +async def get_app_latest_release_version(): + try: + response = requests.get( + f"https://api.github.com/repos/open-webui/open-webui/releases/latest" + ) + response.raise_for_status() + latest_version = response.json()["tag_name"] + + return {"current": VERSION, "latest": latest_version[1:]} + except Exception as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, + ) + + +app.mount("/static", StaticFiles(directory="static"), name="static") +app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") + + app.mount( "/", SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), diff --git a/backend/requirements.txt b/backend/requirements.txt index 56e1d36e..29fb3492 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,8 +16,14 @@ aiohttp peewee bcrypt +litellm==1.30.7 +argon2-cffi +apscheduler +google-generativeai + langchain langchain-community +fake_useragent chromadb sentence_transformers pypdf @@ -30,6 +36,9 @@ openpyxl pyxlsb xlrd +opencv-python-headless +rapidocr-onnxruntime + faster-whisper PyJWT diff --git a/backend/static/favicon.png b/backend/static/favicon.png new file mode 100644 index 00000000..519af1db Binary files /dev/null and b/backend/static/favicon.png differ diff --git a/backend/utils/misc.py b/backend/utils/misc.py index 385a2c41..98528c40 100644 --- a/backend/utils/misc.py +++ b/backend/utils/misc.py @@ -1,5 +1,8 @@ +from pathlib import Path import hashlib import re +from datetime import timedelta +from typing import Optional def get_gravatar_url(email): @@ -38,3 +41,71 @@ def validate_email_format(email: str) -> bool: if not re.match(r"[^@]+@[^@]+\.[^@]+", email): return False return True + + +def sanitize_filename(file_name): + # Convert to lowercase + lower_case_file_name = file_name.lower() + + # Remove special characters using regular expression + sanitized_file_name = re.sub(r"[^\w\s]", "", lower_case_file_name) + + # Replace spaces with dashes + final_file_name = re.sub(r"\s+", "-", sanitized_file_name) + + return final_file_name + + +def extract_folders_after_data_docs(path): + # Convert the path to a Path object if it's not already + path = Path(path) + + # Extract parts of the path + parts = path.parts + + # Find the index of '/data/docs' in the path + try: + index_data_docs = parts.index("data") + 1 + index_docs = parts.index("docs", index_data_docs) + 1 + except ValueError: + return [] + + # Exclude the filename and accumulate folder names + tags = [] + + folders = parts[index_docs:-1] + for idx, part in enumerate(folders): + tags.append("/".join(folders[: idx + 1])) + + return tags + + +def parse_duration(duration: str) -> Optional[timedelta]: + if duration == "-1" or duration == "0": + return None + + # Regular expression to find number and unit pairs + pattern = r"(-?\d+(\.\d+)?)(ms|s|m|h|d|w)" + matches = re.findall(pattern, duration) + + if not matches: + raise ValueError("Invalid duration string") + + total_duration = timedelta() + + for number, _, unit in matches: + number = float(number) + if unit == "ms": + total_duration += timedelta(milliseconds=number) + elif unit == "s": + total_duration += timedelta(seconds=number) + elif unit == "m": + total_duration += timedelta(minutes=number) + elif unit == "h": + total_duration += timedelta(hours=number) + elif unit == "d": + total_duration += timedelta(days=number) + elif unit == "w": + total_duration += timedelta(weeks=number) + + return total_duration diff --git a/backend/utils/utils.py b/backend/utils/utils.py index c6d01814..32724af3 100644 --- a/backend/utils/utils.py +++ b/backend/utils/utils.py @@ -58,6 +58,14 @@ def extract_token_from_auth_header(auth_header: str): return auth_header[len("Bearer ") :] +def get_http_authorization_cred(auth_header: str): + try: + scheme, credentials = auth_header.split(" ") + return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) + except: + raise ValueError(ERROR_MESSAGES.INVALID_TOKEN) + + def get_current_user( auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), ): diff --git a/backend/utils/webhook.py b/backend/utils/webhook.py new file mode 100644 index 00000000..1bc5a604 --- /dev/null +++ b/backend/utils/webhook.py @@ -0,0 +1,20 @@ +import requests + + +def post_webhook(url: str, message: str, event_data: dict) -> bool: + try: + payload = {} + + if "https://hooks.slack.com" in url: + payload["text"] = message + elif "https://discord.com/api/webhooks" in url: + payload["content"] = message + else: + payload = {**event_data} + + r = requests.post(url, json=payload) + r.raise_for_status() + return True + except Exception as e: + print(e) + return False diff --git a/bun.lockb b/bun.lockb index 7768741d..e0a038da 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/confirm_remove.sh b/confirm_remove.sh new file mode 100755 index 00000000..729c2507 --- /dev/null +++ b/confirm_remove.sh @@ -0,0 +1,8 @@ +#!/bin/bash +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 +else + echo "Operation cancelled." +fi diff --git a/demo.gif b/demo.gif index 014f40e2..5a3f3626 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/docker-compose.yaml b/docker-compose.yaml index c45478ca..f69084b8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,7 @@ services: build: context: . args: - OLLAMA_API_BASE_URL: '/ollama/api' + OLLAMA_BASE_URL: '/ollama' dockerfile: Dockerfile image: ghcr.io/open-webui/open-webui:main container_name: open-webui @@ -23,9 +23,9 @@ services: depends_on: - ollama ports: - - ${OLLAMA_WEBUI_PORT-3000}:8080 + - ${OPEN_WEBUI_PORT-3000}:8080 environment: - - 'OLLAMA_API_BASE_URL=http://ollama:11434/api' + - 'OLLAMA_BASE_URL=http://ollama:11434' - 'WEBUI_SECRET_KEY=' extra_hosts: - host.docker.internal:host-gateway diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b9c4a539..f78436d5 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -50,6 +50,18 @@ We welcome pull requests. Before submitting one, please: Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI. +### 🌐 Translations and Internationalization + +Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project. + +We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language. + +To add a new language: + +- Create a new directory in the `src/lib/i18n/locales` path with the appropriate language code as its name. For instance, if you're adding translations for Spanish (Spain), create a new directory named `es-ES`. +- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object. +- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`. + ### 🤔 Questions & Feedback Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help! diff --git a/example.env b/example.env deleted file mode 100644 index 4a4fdaa6..00000000 --- a/example.env +++ /dev/null @@ -1,10 +0,0 @@ -# Ollama URL for the backend to connect -# The path '/ollama/api' will be redirected to the specified backend URL -OLLAMA_API_BASE_URL='http://localhost:11434/api' - -OPENAI_API_BASE_URL='' -OPENAI_API_KEY='' - -# DO NOT TRACK -SCARF_NO_ANALYTICS=true -DO_NOT_TRACK=true \ No newline at end of file diff --git a/i18next-parser.config.ts b/i18next-parser.config.ts new file mode 100644 index 00000000..37ce57ee --- /dev/null +++ b/i18next-parser.config.ts @@ -0,0 +1,38 @@ +// i18next-parser.config.ts +import { getLanguages } from './src/lib/i18n/index.ts'; + +const getLangCodes = async () => { + const languages = await getLanguages(); + return languages.map((l) => l.code); +}; + +export default { + contextSeparator: '_', + createOldCatalogs: false, + defaultNamespace: 'translation', + defaultValue: '', + indentation: 2, + keepRemoved: false, + keySeparator: false, + lexers: { + svelte: ['JavascriptLexer'], + js: ['JavascriptLexer'], + ts: ['JavascriptLexer'], + + default: ['JavascriptLexer'] + }, + lineEnding: 'auto', + locales: await getLangCodes(), + namespaceSeparator: false, + output: 'src/lib/i18n/locales/$LOCALE/$NAMESPACE.json', + pluralSeparator: '_', + input: 'src/**/*.{js,svelte}', + sort: true, + verbose: true, + failOnWarnings: false, + failOnUpdate: false, + customValueTemplate: null, + resetDefaultValueLocale: null, + i18nextOptions: null, + yamlOptions: null +}; diff --git a/kubernetes/helm/.helmignore b/kubernetes/helm/.helmignore index e69de29b..e8065247 100644 --- a/kubernetes/helm/.helmignore +++ b/kubernetes/helm/.helmignore @@ -0,0 +1 @@ +values-minikube.yaml diff --git a/kubernetes/helm/Chart.yaml b/kubernetes/helm/Chart.yaml index 52683b65..ab5b41df 100644 --- a/kubernetes/helm/Chart.yaml +++ b/kubernetes/helm/Chart.yaml @@ -1,5 +1,21 @@ apiVersion: v2 -name: ollama-webui -description: "Ollama Web UI: A User-Friendly Web Interface for Chat Interactions 👋" +name: open-webui version: 1.0.0 -icon: https://raw.githubusercontent.com/ollama-webui/ollama-webui/main/static/favicon.png +appVersion: "latest" + +home: https://www.openwebui.com/ +icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png + +description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋" +keywords: +- llm +- chat +- web-ui + +sources: +- https://github.com/open-webui/open-webui/tree/main/kubernetes/helm +- https://hub.docker.com/r/ollama/ollama +- https://github.com/open-webui/open-webui/pkgs/container/open-webui + +annotations: + licenses: MIT diff --git a/kubernetes/helm/templates/_helpers.tpl b/kubernetes/helm/templates/_helpers.tpl new file mode 100644 index 00000000..0647a42a --- /dev/null +++ b/kubernetes/helm/templates/_helpers.tpl @@ -0,0 +1,47 @@ +{{- define "open-webui.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end -}} + +{{- define "ollama.name" -}} +ollama +{{- end -}} + +{{- define "ollama.url" -}} +{{- printf "http://%s.%s.svc.cluster.local:%d/api" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }} +{{- end }} + +{{- define "chart.name" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "base.labels" -}} +helm.sh/chart: {{ include "chart.name" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "base.selectorLabels" -}} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "open-webui.selectorLabels" -}} +{{ include "base.selectorLabels" . }} +app.kubernetes.io/component: {{ .Chart.Name }} +{{- end }} + +{{- define "open-webui.labels" -}} +{{ include "base.labels" . }} +{{ include "open-webui.selectorLabels" . }} +{{- end }} + +{{- define "ollama.selectorLabels" -}} +{{ include "base.selectorLabels" . }} +app.kubernetes.io/component: {{ include "ollama.name" . }} +{{- end }} + +{{- define "ollama.labels" -}} +{{ include "base.labels" . }} +{{ include "ollama.selectorLabels" . }} +{{- end }} diff --git a/kubernetes/helm/templates/ollama-namespace.yaml b/kubernetes/helm/templates/ollama-namespace.yaml deleted file mode 100644 index 59f79447..00000000 --- a/kubernetes/helm/templates/ollama-namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: {{ .Values.namespace }} \ No newline at end of file diff --git a/kubernetes/helm/templates/ollama-service.yaml b/kubernetes/helm/templates/ollama-service.yaml index 54558473..becb6ad2 100644 --- a/kubernetes/helm/templates/ollama-service.yaml +++ b/kubernetes/helm/templates/ollama-service.yaml @@ -1,13 +1,21 @@ apiVersion: v1 kind: Service metadata: - name: ollama-service - namespace: {{ .Values.namespace }} + name: {{ include "ollama.name" . }} + labels: + {{- include "ollama.labels" . | nindent 4 }} + {{- with .Values.ollama.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: - type: {{ .Values.ollama.service.type }} selector: - app: ollama + {{- include "ollama.selectorLabels" . | nindent 4 }} +{{- with .Values.ollama.service }} + type: {{ .type }} ports: - protocol: TCP - port: {{ .Values.ollama.servicePort }} - targetPort: {{ .Values.ollama.servicePort }} \ No newline at end of file + name: http + port: {{ .port }} + targetPort: http +{{- end }} diff --git a/kubernetes/helm/templates/ollama-statefulset.yaml b/kubernetes/helm/templates/ollama-statefulset.yaml index 83cb6883..a87aeab0 100644 --- a/kubernetes/helm/templates/ollama-statefulset.yaml +++ b/kubernetes/helm/templates/ollama-statefulset.yaml @@ -1,24 +1,43 @@ apiVersion: apps/v1 kind: StatefulSet metadata: - name: ollama - namespace: {{ .Values.namespace }} + name: {{ include "ollama.name" . }} + labels: + {{- include "ollama.labels" . | nindent 4 }} + {{- with .Values.ollama.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: - serviceName: "ollama" + serviceName: {{ include "ollama.name" . }} replicas: {{ .Values.ollama.replicaCount }} selector: matchLabels: - app: ollama + {{- include "ollama.selectorLabels" . | nindent 6 }} template: metadata: labels: - app: ollama + {{- include "ollama.labels" . | nindent 8 }} + {{- with .Values.ollama.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} spec: + enableServiceLinks: false + automountServiceAccountToken: false + {{- with .Values.ollama.runtimeClassName }} + runtimeClassName: {{ . }} + {{- end }} containers: - - name: ollama - image: {{ .Values.ollama.image }} + - name: {{ include "ollama.name" . }} + {{- with .Values.ollama.image }} + image: {{ .repository }}:{{ .tag }} + imagePullPolicy: {{ .pullPolicy }} + {{- end }} + tty: true ports: - - containerPort: {{ .Values.ollama.servicePort }} + - name: http + containerPort: {{ .Values.ollama.service.containerPort }} env: {{- if .Values.ollama.gpu.enabled }} - name: PATH @@ -27,29 +46,51 @@ spec: value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64 - name: NVIDIA_DRIVER_CAPABILITIES value: compute,utility - {{- end}} - {{- if .Values.ollama.resources }} - resources: {{- toYaml .Values.ollama.resources | nindent 10 }} + {{- end }} + {{- with .Values.ollama.resources }} + resources: {{- toYaml . | nindent 10 }} {{- end }} volumeMounts: - - name: ollama-volume + - name: data mountPath: /root/.ollama - tty: true {{- with .Values.ollama.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.ollama.tolerations }} tolerations: - {{- if .Values.ollama.gpu.enabled }} - - key: nvidia.com/gpu - operator: Exists - effect: NoSchedule - {{- end }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- if and .Values.ollama.persistence.enabled .Values.ollama.persistence.existingClaim }} + - name: data + persistentVolumeClaim: + claimName: {{ .Values.ollama.persistence.existingClaim }} + {{- else if not .Values.ollama.persistence.enabled }} + - name: data + emptyDir: {} + {{- else if and .Values.ollama.persistence.enabled (not .Values.ollama.persistence.existingClaim) }} + [] volumeClaimTemplates: - metadata: - name: ollama-volume + name: data + labels: + {{- include "ollama.selectorLabels" . | nindent 8 }} + {{- with .Values.ollama.persistence.annotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} spec: - accessModes: [ "ReadWriteOnce" ] + accessModes: + {{- range .Values.ollama.persistence.accessModes }} + - {{ . | quote }} + {{- end }} resources: requests: - storage: {{ .Values.ollama.volumeSize }} \ No newline at end of file + storage: {{ .Values.ollama.persistence.size | quote }} + storageClass: {{ .Values.ollama.persistence.storageClass }} + {{- with .Values.ollama.persistence.selector }} + selector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} diff --git a/kubernetes/helm/templates/webui-deployment.yaml b/kubernetes/helm/templates/webui-deployment.yaml index d9721ee0..bbd5706d 100644 --- a/kubernetes/helm/templates/webui-deployment.yaml +++ b/kubernetes/helm/templates/webui-deployment.yaml @@ -1,38 +1,62 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: ollama-webui-deployment - namespace: {{ .Values.namespace }} + name: {{ include "open-webui.name" . }} + labels: + {{- include "open-webui.labels" . | nindent 4 }} + {{- with .Values.webui.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: - replicas: 1 + replicas: {{ .Values.webui.replicaCount }} selector: matchLabels: - app: ollama-webui + {{- include "open-webui.selectorLabels" . | nindent 6 }} template: metadata: labels: - app: ollama-webui + {{- include "open-webui.labels" . | nindent 8 }} + {{- with .Values.webui.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} spec: + enableServiceLinks: false + automountServiceAccountToken: false containers: - - name: ollama-webui - image: {{ .Values.webui.image }} + - name: {{ .Chart.Name }} + {{- with .Values.webui.image }} + image: {{ .repository }}:{{ .tag | default $.Chart.AppVersion }} + imagePullPolicy: {{ .pullPolicy }} + {{- end }} ports: - - containerPort: 8080 - {{- if .Values.webui.resources }} - resources: {{- toYaml .Values.webui.resources | nindent 10 }} + - name: http + containerPort: {{ .Values.webui.service.containerPort }} + {{- with .Values.webui.resources }} + resources: {{- toYaml . | nindent 10 }} {{- end }} volumeMounts: - - name: webui-volume + - name: data mountPath: /app/backend/data env: - - name: OLLAMA_API_BASE_URL - value: "http://ollama-service.{{ .Values.namespace }}.svc.cluster.local:{{ .Values.ollama.servicePort }}/api" + - name: OLLAMA_BASE_URL + value: {{ include "ollama.url" . | quote }} tty: true {{- with .Values.webui.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} volumes: - - name: webui-volume + {{- if and .Values.webui.persistence.enabled .Values.webui.persistence.existingClaim }} + - name: data persistentVolumeClaim: - claimName: ollama-webui-pvc \ No newline at end of file + claimName: {{ .Values.webui.persistence.existingClaim }} + {{- else if not .Values.webui.persistence.enabled }} + - name: data + emptyDir: {} + {{- else if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }} + - name: data + persistentVolumeClaim: + claimName: {{ include "open-webui.name" . }} + {{- end }} diff --git a/kubernetes/helm/templates/webui-ingress.yaml b/kubernetes/helm/templates/webui-ingress.yaml index 84f819f3..ea9f95e1 100644 --- a/kubernetes/helm/templates/webui-ingress.yaml +++ b/kubernetes/helm/templates/webui-ingress.yaml @@ -2,13 +2,23 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: ollama-webui-ingress - namespace: {{ .Values.namespace }} -{{- if .Values.webui.ingress.annotations }} + name: {{ include "open-webui.name" . }} + labels: + {{- include "open-webui.labels" . | nindent 4 }} + {{- with .Values.webui.ingress.annotations }} annotations: -{{ toYaml .Values.webui.ingress.annotations | trimSuffix "\n" | indent 4 }} -{{- end }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: + {{- with .Values.webui.ingress.class }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.webui.ingress.tls }} + tls: + - hosts: + - {{ .Values.webui.ingress.host | quote }} + secretName: {{ default (printf "%s-tls" .Release.Name) .Values.webui.ingress.existingSecret }} + {{- end }} rules: - host: {{ .Values.webui.ingress.host }} http: @@ -17,7 +27,7 @@ spec: pathType: Prefix backend: service: - name: ollama-webui-service + name: {{ include "open-webui.name" . }} port: - number: {{ .Values.webui.servicePort }} + name: http {{- end }} diff --git a/kubernetes/helm/templates/webui-pvc.yaml b/kubernetes/helm/templates/webui-pvc.yaml index e9961aa8..06b2cc4a 100644 --- a/kubernetes/helm/templates/webui-pvc.yaml +++ b/kubernetes/helm/templates/webui-pvc.yaml @@ -1,12 +1,25 @@ +{{- if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }} apiVersion: v1 kind: PersistentVolumeClaim metadata: + name: {{ include "open-webui.name" . }} labels: - app: ollama-webui - name: ollama-webui-pvc - namespace: {{ .Values.namespace }} + {{- include "open-webui.selectorLabels" . | nindent 4 }} + {{- with .Values.webui.persistence.annotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} spec: - accessModes: [ "ReadWriteOnce" ] + accessModes: + {{- range .Values.webui.persistence.accessModes }} + - {{ . | quote }} + {{- end }} resources: requests: - storage: {{ .Values.webui.volumeSize }} \ No newline at end of file + storage: {{ .Values.webui.persistence.size }} + storageClass: {{ .Values.webui.persistence.storageClass }} + {{- with .Values.webui.persistence.selector }} + selector: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/kubernetes/helm/templates/webui-service.yaml b/kubernetes/helm/templates/webui-service.yaml index 7fefa4fd..9ccd9b9e 100644 --- a/kubernetes/helm/templates/webui-service.yaml +++ b/kubernetes/helm/templates/webui-service.yaml @@ -1,15 +1,29 @@ apiVersion: v1 kind: Service metadata: - name: ollama-webui-service - namespace: {{ .Values.namespace }} + name: {{ include "open-webui.name" . }} + labels: + {{- include "open-webui.labels" . | nindent 4 }} + {{- with .Values.webui.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.webui.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: - type: {{ .Values.webui.service.type }} # Default: NodePort # Use LoadBalancer if you're on a cloud that supports it selector: - app: ollama-webui + {{- include "open-webui.selectorLabels" . | nindent 4 }} + type: {{ .Values.webui.service.type | default "ClusterIP" }} ports: - - protocol: TCP - port: {{ .Values.webui.servicePort }} - targetPort: {{ .Values.webui.servicePort }} - # If using NodePort, you can optionally specify the nodePort: - # nodePort: 30000 \ No newline at end of file + - protocol: TCP + name: http + port: {{ .Values.webui.service.port }} + targetPort: http + {{- if .Values.webui.service.nodePort }} + nodePort: {{ .Values.webui.service.nodePort | int }} + {{- end }} + {{- if .Values.webui.service.loadBalancerClass }} + loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }} + {{- end }} + diff --git a/kubernetes/helm/values-minikube.yaml b/kubernetes/helm/values-minikube.yaml new file mode 100644 index 00000000..1b67b0b7 --- /dev/null +++ b/kubernetes/helm/values-minikube.yaml @@ -0,0 +1,27 @@ +ollama: + resources: + requests: + cpu: "2000m" + memory: "2Gi" + limits: + cpu: "4000m" + memory: "4Gi" + nvidia.com/gpu: "0" + service: + type: ClusterIP + gpu: + enabled: false + +webui: + resources: + requests: + cpu: "500m" + memory: "500Mi" + limits: + cpu: "1000m" + memory: "1Gi" + ingress: + enabled: true + host: open-webui.minikube.local + service: + type: NodePort diff --git a/kubernetes/helm/values.yaml b/kubernetes/helm/values.yaml index 648b4050..394e5a49 100644 --- a/kubernetes/helm/values.yaml +++ b/kubernetes/helm/values.yaml @@ -1,38 +1,74 @@ -namespace: ollama-namespace +nameOverride: "" ollama: + annotations: {} + podAnnotations: {} replicaCount: 1 - image: ollama/ollama:latest - servicePort: 11434 - resources: - limits: - cpu: "2000m" - memory: "2Gi" - nvidia.com/gpu: "0" - volumeSize: 1Gi + image: + repository: ollama/ollama + tag: latest + pullPolicy: Always + resources: {} + persistence: + enabled: true + size: 30Gi + existingClaim: "" + accessModes: + - ReadWriteOnce + storageClass: "" + selector: {} + annotations: {} + nodeSelector: {} + # -- If using a special runtime container such as nvidia, set it here. + runtimeClassName: "" + tolerations: + - key: nvidia.com/gpu + operator: Exists + effect: NoSchedule + service: + type: ClusterIP + annotations: {} + port: 80 + containerPort: 11434 + gpu: + # -- Enable additional ENV values to help Ollama discover GPU usage + enabled: false + +webui: + annotations: {} + podAnnotations: {} + replicaCount: 1 + image: + repository: ghcr.io/open-webui/open-webui + tag: "" + pullPolicy: Always + resources: {} + ingress: + enabled: false + class: "" + # -- Use appropriate annotations for your Ingress controller, e.g., for NGINX: + # nginx.ingress.kubernetes.io/rewrite-target: / + annotations: {} + host: "" + tls: false + existingSecret: "" + persistence: + enabled: true + size: 2Gi + existingClaim: "" + # -- If using multiple replicas, you must update accessModes to ReadWriteMany + accessModes: + - ReadWriteOnce + storageClass: "" + selector: {} + annotations: {} nodeSelector: {} tolerations: [] service: type: ClusterIP - gpu: - enabled: false - -webui: - replicaCount: 1 - image: ghcr.io/ollama-webui/ollama-webui:main - servicePort: 8080 - resources: - limits: - cpu: "500m" - memory: "500Mi" - ingress: - enabled: true - annotations: - # Use appropriate annotations for your Ingress controller, e.g., for NGINX: - # nginx.ingress.kubernetes.io/rewrite-target: / - host: ollama.minikube.local - volumeSize: 1Gi - nodeSelector: {} - tolerations: [] - service: - type: NodePort \ No newline at end of file + annotations: {} + port: 80 + containerPort: 8080 + nodePort: "" + labels: {} + loadBalancerClass: "" diff --git a/kubernetes/manifest/base/ollama-service.yaml b/kubernetes/manifest/base/ollama-service.yaml index a9467fc4..8bab65b5 100644 --- a/kubernetes/manifest/base/ollama-service.yaml +++ b/kubernetes/manifest/base/ollama-service.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: name: ollama-service - namespace: ollama-namespace + namespace: open-webui spec: selector: app: ollama diff --git a/kubernetes/manifest/base/ollama-statefulset.yaml b/kubernetes/manifest/base/ollama-statefulset.yaml index ee63faa9..cd1144ca 100644 --- a/kubernetes/manifest/base/ollama-statefulset.yaml +++ b/kubernetes/manifest/base/ollama-statefulset.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: ollama - namespace: ollama-namespace + namespace: open-webui spec: serviceName: "ollama" replicas: 1 @@ -20,9 +20,13 @@ spec: ports: - containerPort: 11434 resources: - limits: + requests: cpu: "2000m" memory: "2Gi" + limits: + cpu: "4000m" + memory: "4Gi" + nvidia.com/gpu: "0" volumeMounts: - name: ollama-volume mountPath: /root/.ollama @@ -34,4 +38,4 @@ spec: accessModes: [ "ReadWriteOnce" ] resources: requests: - storage: 1Gi \ No newline at end of file + storage: 30Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/ollama-namespace.yaml b/kubernetes/manifest/base/open-webui.yaml similarity index 63% rename from kubernetes/manifest/base/ollama-namespace.yaml rename to kubernetes/manifest/base/open-webui.yaml index f296eb20..9c1a599f 100644 --- a/kubernetes/manifest/base/ollama-namespace.yaml +++ b/kubernetes/manifest/base/open-webui.yaml @@ -1,4 +1,4 @@ apiVersion: v1 kind: Namespace metadata: - name: ollama-namespace \ No newline at end of file + name: open-webui \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-deployment.yaml b/kubernetes/manifest/base/webui-deployment.yaml index 58de0368..38efd554 100644 --- a/kubernetes/manifest/base/webui-deployment.yaml +++ b/kubernetes/manifest/base/webui-deployment.yaml @@ -1,28 +1,38 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: ollama-webui-deployment - namespace: ollama-namespace + name: open-webui-deployment + namespace: open-webui spec: replicas: 1 selector: matchLabels: - app: ollama-webui + app: open-webui template: metadata: labels: - app: ollama-webui + app: open-webui spec: containers: - - name: ollama-webui - image: ghcr.io/ollama-webui/ollama-webui:main + - name: open-webui + image: ghcr.io/open-webui/open-webui:main ports: - containerPort: 8080 resources: - limits: + requests: cpu: "500m" memory: "500Mi" + limits: + cpu: "1000m" + memory: "1Gi" env: - - name: OLLAMA_API_BASE_URL - value: "http://ollama-service.ollama-namespace.svc.cluster.local:11434/api" - tty: true \ No newline at end of file + - name: OLLAMA_BASE_URL + value: "http://ollama-service.open-webui.svc.cluster.local:11434" + tty: true + volumeMounts: + - name: webui-volume + mountPath: /app/backend/data + volumes: + - name: webui-volume + persistentVolumeClaim: + claimName: ollama-webui-pvc \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-ingress.yaml b/kubernetes/manifest/base/webui-ingress.yaml index 0038807c..dc0b53cc 100644 --- a/kubernetes/manifest/base/webui-ingress.yaml +++ b/kubernetes/manifest/base/webui-ingress.yaml @@ -1,20 +1,20 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: ollama-webui-ingress - namespace: ollama-namespace + name: open-webui-ingress + namespace: open-webui #annotations: # Use appropriate annotations for your Ingress controller, e.g., for NGINX: # nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - - host: ollama.minikube.local + - host: open-webui.minikube.local http: paths: - path: / pathType: Prefix backend: service: - name: ollama-webui-service + name: open-webui-service port: number: 8080 diff --git a/kubernetes/manifest/base/webui-pvc.yaml b/kubernetes/manifest/base/webui-pvc.yaml new file mode 100644 index 00000000..5c75283a --- /dev/null +++ b/kubernetes/manifest/base/webui-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: ollama-webui + name: ollama-webui-pvc + namespace: open-webui +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-service.yaml b/kubernetes/manifest/base/webui-service.yaml index b41daeaf..d73845f0 100644 --- a/kubernetes/manifest/base/webui-service.yaml +++ b/kubernetes/manifest/base/webui-service.yaml @@ -1,12 +1,12 @@ apiVersion: v1 kind: Service metadata: - name: ollama-webui-service - namespace: ollama-namespace + name: open-webui-service + namespace: open-webui spec: type: NodePort # Use LoadBalancer if you're on a cloud that supports it selector: - app: ollama-webui + app: open-webui ports: - protocol: TCP port: 8080 diff --git a/kubernetes/manifest/kustomization.yaml b/kubernetes/manifest/kustomization.yaml index a4b03d96..907bff3e 100644 --- a/kubernetes/manifest/kustomization.yaml +++ b/kubernetes/manifest/kustomization.yaml @@ -1,10 +1,11 @@ resources: -- base/ollama-namespace.yaml +- base/open-webui.yaml - base/ollama-service.yaml - base/ollama-statefulset.yaml - base/webui-deployment.yaml - base/webui-service.yaml - base/webui-ingress.yaml +- base/webui-pvc.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization diff --git a/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml b/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml index 54e5aba6..3e424436 100644 --- a/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml +++ b/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: ollama - namespace: ollama-namespace + namespace: open-webui spec: selector: matchLabels: diff --git a/package-lock.json b/package-lock.json index 6f962e70..7fa490eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,27 @@ { - "name": "ollama-webui", - "version": "0.0.1", + "name": "open-webui", + "version": "0.1.112", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "ollama-webui", - "version": "0.0.1", + "name": "open-webui", + "version": "0.1.112", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", + "bits-ui": "^0.19.7", "dayjs": "^1.11.10", "file-saver": "^2.0.5", "highlight.js": "^11.9.0", + "i18next": "^23.10.0", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.9", "marked": "^9.1.0", - "svelte-french-toast": "^1.2.0", + "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "uuid": "^9.0.1" }, @@ -33,11 +37,13 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", + "i18next-parser": "^8.13.0", "postcss": "^8.4.31", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", "svelte": "^4.0.5", "svelte-check": "^3.4.3", + "svelte-confetti": "^1.3.2", "tailwindcss": "^3.3.3", "tslib": "^2.4.1", "typescript": "^5.0.0", @@ -77,6 +83,81 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", @@ -92,6 +173,294 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -149,13 +518,47 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "engines": { "node": ">=14" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -189,6 +592,14 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@internationalized/date": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.2.tgz", + "integrity": "sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -232,6 +643,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@melt-ui/svelte": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.0.tgz", + "integrity": "sha512-X1ktxKujjLjOBt8LBvfckHGDMrkHWceRt1jdsUTf0EH76ikNPP1ofSoiV0IhlduDoCBV+2YchJ8kXCDfDXfC9Q==", + "dependencies": { + "@floating-ui/core": "^1.3.1", + "@floating-ui/dom": "^1.4.5", + "@internationalized/date": "^3.5.0", + "dequal": "^2.0.3", + "focus-trap": "^7.5.2", + "nanoid": "^5.0.4" + }, + "peerDependencies": { + "svelte": ">=3 <5" + } + }, + "node_modules/@melt-ui/svelte/node_modules/nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -471,9 +915,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "1.30.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz", - "integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==", + "version": "1.30.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz", + "integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==", "hasInstallScript": true, "dependencies": { "@sveltejs/vite-plugin-svelte": "^2.5.0", @@ -488,7 +932,7 @@ "set-cookie-parser": "^2.6.0", "sirv": "^2.0.2", "tiny-glob": "^0.2.9", - "undici": "~5.26.2" + "undici": "^5.28.3" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -538,6 +982,14 @@ "vite": "^4.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", @@ -591,6 +1043,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "node_modules/@types/pug": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.7.tgz", @@ -608,6 +1066,12 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "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", + "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz", @@ -991,6 +1455,33 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.1.tgz", + "integrity": "sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1000,6 +1491,67 @@ "node": ">=8" } }, + "node_modules/bits-ui": { + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.19.7.tgz", + "integrity": "sha512-GHUpKvN7QyazhnZNkUy0lxg6W1M6KJHWSZ4a/UGCjPE6nQgk6vKbGysY67PkDtQMknZTZAzVoMj1Eic4IKeCRQ==", + "dependencies": { + "@internationalized/date": "^3.5.1", + "@melt-ui/svelte": "0.76.0", + "nanoid": "^5.0.5" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/bits-ui/node_modules/nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1022,6 +1574,85 @@ "node": ">=8" } }, + "node_modules/broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true + }, + "node_modules/broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "dependencies": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, "node_modules/browserslist": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", @@ -1054,6 +1685,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1134,6 +1789,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1173,6 +1866,21 @@ "node": ">= 6" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -1203,6 +1911,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1223,6 +1940,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -1231,6 +1954,12 @@ "node": ">= 0.6" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1245,6 +1974,22 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -1257,6 +2002,18 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1274,6 +2031,12 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1362,12 +2125,91 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.544", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz", "integrity": "sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w==", "dev": true }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -1410,6 +2252,321 @@ "@esbuild/win32-x64": "0.18.20" } }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1638,6 +2795,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -1752,6 +2915,14 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, "node_modules/fraction.js": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", @@ -1765,6 +2936,94 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + } + }, + "node_modules/fs-merger/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-merger/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-merger/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "dependencies": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1815,6 +3074,25 @@ "node": ">=10.13.0" } }, + "node_modules/glob-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.0.tgz", + "integrity": "sha512-CdIUuwOkYNv9ZadR3jJvap8CMooKziQZ/QCSPhEb7zqfsEI5YnPmvca7IvbaVE3z58ZdUYD2JsU6AUWjL8WZJA==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -1872,6 +3150,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", + "dev": true, + "dependencies": { + "through2": "^2.0.1" + } + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -1889,6 +3176,55 @@ "node": ">=8" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "dependencies": { + "rsvp": "~3.2.1" + } + }, + "node_modules/heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + } + }, + "node_modules/heimdalljs-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/heimdalljs-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/heimdalljs/node_modules/rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", + "dev": true + }, "node_modules/highlight.js": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", @@ -1897,11 +3233,209 @@ "node": ">=12.0.0" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/i18next": { + "version": "23.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.0.tgz", + "integrity": "sha512-/TgHOqsa7/9abUKJjdPeydoyDc0oTi/7u9F8lMSj6ufg4cbC1Oj3f/Jja7zj7WRIhEQKB7Q4eN6y68I9RDxxGQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", + "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-parser": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.13.0.tgz", + "integrity": "sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0-rc.2", + "colors": "1.4.0", + "commander": "~11.1.0", + "eol": "^0.9.1", + "esbuild": "^0.20.1", + "fs-extra": "^11.1.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1", + "js-yaml": "4.1.0", + "lilconfig": "^3.0.0", + "rsvp": "^4.8.2", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "~3.0.0", + "vinyl-fs": "^4.0.0", + "vue-template-compiler": "^2.6.11" + }, + "bin": { + "i18next": "bin/cli.js" + }, + "engines": { + "node": ">=16.0.0 || >=18.0.0 || >=20.0.0", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/i18next-parser/node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/i18next-parser/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/i18next-resources-to-backend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz", + "integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2023,6 +3557,15 @@ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2041,6 +3584,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -2049,6 +3604,21 @@ "@types/estree": "*" } }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2099,6 +3669,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/katex": { "version": "0.16.9", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", @@ -2145,6 +3727,15 @@ "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", "dev": true }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2245,6 +3836,19 @@ "node": ">= 16" } }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -2314,6 +3918,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mktemp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", + "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", + "dev": true, + "engines": { + "node": ">0.9" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2393,6 +4006,30 @@ "node": ">=0.10.0" } }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2478,6 +4115,31 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2510,6 +4172,12 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2769,6 +4437,21 @@ "svelte": "^3.2.0 || ^4.0.0-next.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", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true, + "engines": { + "node": "10.* || >= 12.*" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2798,6 +4481,35 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-temp": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", + "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", + "dev": true, + "dependencies": { + "mktemp": "~0.4.0", + "rimraf": "^2.5.4", + "underscore.string": "~3.3.4" + } + }, + "node_modules/quick-temp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2807,6 +4519,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2819,6 +4546,26 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -2844,6 +4591,18 @@ "node": ">=4" } }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -2884,6 +4643,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2918,6 +4686,18 @@ "node": ">=6" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -3020,6 +4800,21 @@ "sorcery": "bin/sorcery" } }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -3028,6 +4823,43 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3174,6 +5006,15 @@ "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" } }, + "node_modules/svelte-confetti": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz", + "integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==", + "dev": true, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.33.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", @@ -3201,17 +5042,6 @@ } } }, - "node_modules/svelte-french-toast": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz", - "integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==", - "dependencies": { - "svelte-writable-derived": "^3.1.0" - }, - "peerDependencies": { - "svelte": "^3.57.0 || ^4.0.0" - } - }, "node_modules/svelte-hmr": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", @@ -3297,17 +5127,25 @@ "node": ">=12" } }, - "node_modules/svelte-writable-derived": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz", - "integrity": "sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==", - "funding": { - "url": "https://ko-fi.com/pixievoltno1" - }, + "node_modules/svelte-sonner": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz", + "integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==", "peerDependencies": { - "svelte": "^3.2.1 || ^4.0.0-next.1" + "svelte": ">=3 <5" } }, + "node_modules/symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", @@ -3383,6 +5221,15 @@ "node": ">= 14" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3410,6 +5257,16 @@ "node": ">=0.8" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -3439,6 +5296,18 @@ "node": ">=8.0" } }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3468,8 +5337,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type-check": { "version": "0.4.0", @@ -3508,10 +5376,23 @@ "node": ">=14.17" } }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici": { - "version": "5.26.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", - "integrity": "sha512-OG+QOf0fTLtazL9P9X7yqWxQ+Z0395Wk6DSkyTxtaq3wQEjIroVe7Y4asCX/vcCxYpNGMnwz8F0qbRYUoaQVMw==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -3519,6 +5400,15 @@ "node": ">=14.0" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -3576,10 +5466,90 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -3643,6 +5613,31 @@ } } }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3663,6 +5658,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -3713,12 +5717,174 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "dev": true, + "optional": true + }, "@esbuild/darwin-arm64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "optional": true }, + "@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "dev": true, + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3758,9 +5924,40 @@ "dev": true }, "@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==" + }, + "@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "requires": { + "@floating-ui/utils": "^0.2.1" + } + }, + "@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "requires": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "requires": { + "is-negated-glob": "^1.0.0" + } }, "@humanwhocodes/config-array": { "version": "0.11.13", @@ -3785,6 +5982,14 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "@internationalized/date": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.2.tgz", + "integrity": "sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==", + "requires": { + "@swc/helpers": "^0.5.0" + } + }, "@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -3819,6 +6024,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@melt-ui/svelte": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.0.tgz", + "integrity": "sha512-X1ktxKujjLjOBt8LBvfckHGDMrkHWceRt1jdsUTf0EH76ikNPP1ofSoiV0IhlduDoCBV+2YchJ8kXCDfDXfC9Q==", + "requires": { + "@floating-ui/core": "^1.3.1", + "@floating-ui/dom": "^1.4.5", + "@internationalized/date": "^3.5.0", + "dequal": "^2.0.3", + "focus-trap": "^7.5.2", + "nanoid": "^5.0.4" + }, + "dependencies": { + "nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==" + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3985,9 +6210,9 @@ "requires": {} }, "@sveltejs/kit": { - "version": "1.30.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz", - "integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==", + "version": "1.30.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz", + "integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==", "requires": { "@sveltejs/vite-plugin-svelte": "^2.5.0", "@types/cookie": "^0.5.1", @@ -4001,7 +6226,7 @@ "set-cookie-parser": "^2.6.0", "sirv": "^2.0.2", "tiny-glob": "^0.2.9", - "undici": "~5.26.2" + "undici": "^5.28.3" } }, "@sveltejs/vite-plugin-svelte": { @@ -4026,6 +6251,14 @@ "debug": "^4.3.4" } }, + "@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "requires": { + "tslib": "^2.4.0" + } + }, "@tailwindcss/typography": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", @@ -4075,6 +6308,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "@types/pug": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.7.tgz", @@ -4092,6 +6331,12 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "@types/symlink-or-copy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz", + "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz", @@ -4332,12 +6577,72 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bare-events": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.1.tgz", + "integrity": "sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==", + "dev": true, + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bits-ui": { + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.19.7.tgz", + "integrity": "sha512-GHUpKvN7QyazhnZNkUy0lxg6W1M6KJHWSZ4a/UGCjPE6nQgk6vKbGysY67PkDtQMknZTZAzVoMj1Eic4IKeCRQ==", + "requires": { + "@internationalized/date": "^3.5.1", + "@melt-ui/svelte": "0.76.0", + "nanoid": "^5.0.5" + }, + "dependencies": { + "nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==" + } + } + }, + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4357,6 +6662,72 @@ "fill-range": "^7.0.1" } }, + "broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true + }, + "broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true + }, + "broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "requires": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "requires": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + } + }, "browserslist": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", @@ -4369,6 +6740,16 @@ "update-browserslist-db": "^1.0.13" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -4414,6 +6795,35 @@ "supports-color": "^7.1.0" } }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4441,6 +6851,18 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, "code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -4468,6 +6890,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4485,11 +6913,23 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4501,6 +6941,19 @@ "which": "^2.0.1" } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, "css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -4510,6 +6963,12 @@ "source-map-js": "^1.0.1" } }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4521,6 +6980,12 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4586,12 +7051,67 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "electron-to-chromium": { "version": "1.4.544", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz", "integrity": "sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w==", "dev": true }, + "ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true + }, "es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -4625,6 +7145,134 @@ "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "optional": true + } } }, "escalade": { @@ -4787,6 +7435,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -4882,12 +7536,95 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "requires": { + "tabbable": "^6.2.0" + } + }, "fraction.js": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", "dev": true }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "requires": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + } + }, + "fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "requires": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4922,6 +7659,22 @@ "is-glob": "^4.0.3" } }, + "glob-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.0.tgz", + "integrity": "sha512-CdIUuwOkYNv9ZadR3jJvap8CMooKziQZ/QCSPhEb7zqfsEI5YnPmvca7IvbaVE3z58ZdUYD2JsU6AUWjL8WZJA==", + "dev": true, + "requires": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + } + }, "globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -4967,6 +7720,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", + "dev": true, + "requires": { + "through2": "^2.0.1" + } + }, "has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -4978,16 +7740,195 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "requires": { + "rsvp": "~3.2.1" + }, + "dependencies": { + "rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", + "dev": true + } + } + }, + "heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "requires": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "highlight.js": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==" }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "i18next": { + "version": "23.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.0.tgz", + "integrity": "sha512-/TgHOqsa7/9abUKJjdPeydoyDc0oTi/7u9F8lMSj6ufg4cbC1Oj3f/Jja7zj7WRIhEQKB7Q4eN6y68I9RDxxGQ==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "i18next-browser-languagedetector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", + "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "i18next-parser": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.13.0.tgz", + "integrity": "sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==", + "dev": true, + "requires": { + "@babel/runtime": "^7.23.2", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0-rc.2", + "colors": "1.4.0", + "commander": "~11.1.0", + "eol": "^0.9.1", + "esbuild": "^0.20.1", + "fs-extra": "^11.1.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1", + "js-yaml": "4.1.0", + "lilconfig": "^3.0.0", + "rsvp": "^4.8.2", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "~3.0.0", + "vinyl-fs": "^4.0.0", + "vue-template-compiler": "^2.6.11" + }, + "dependencies": { + "@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "dev": true, + "optional": true + }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true + }, + "esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true + } + } + }, + "i18next-resources-to-backend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz", + "integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, "idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -5075,6 +8016,12 @@ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5087,6 +8034,12 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true + }, "is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -5095,6 +8048,18 @@ "@types/estree": "*" } }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5139,6 +8104,16 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "katex": { "version": "0.16.9", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", @@ -5174,6 +8149,12 @@ "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", "dev": true }, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5250,6 +8231,16 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.0.tgz", "integrity": "sha512-VZjm0PM5DMv7WodqOUps3g6Q7dmxs9YGiFUZ7a2majzQTTCgX+6S6NAJHPvOhgFBzYz8s4QZKWWMfZKFmsfOgA==" }, + "matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + } + }, "mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -5301,6 +8292,12 @@ "minimist": "^1.2.6" } }, + "mktemp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", + "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", + "dev": true + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -5356,6 +8353,24 @@ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true }, + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5417,6 +8432,25 @@ "callsites": "^3.0.0" } }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5440,6 +8474,12 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5576,6 +8616,18 @@ "dev": true, "requires": {} }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5588,6 +8640,34 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "quick-temp": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", + "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", + "dev": true, + "requires": { + "mktemp": "~0.4.0", + "rimraf": "^2.5.4", + "underscore.string": "~3.3.4" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5597,6 +8677,21 @@ "pify": "^2.3.0" } }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5606,6 +8701,23 @@ "picomatch": "^2.2.1" } }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, "resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -5622,6 +8734,15 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "requires": { + "value-or-function": "^4.0.0" + } + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5645,6 +8766,12 @@ "fsevents": "~2.3.2" } }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5662,6 +8789,18 @@ "mri": "^1.1.0" } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -5742,11 +8881,55 @@ "sander": "^0.5.0" } }, + "sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "requires": { + "is-plain-obj": "^4.0.0" + } + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "requires": { + "streamx": "^2.13.2" + } + }, + "streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5852,6 +9035,13 @@ "typescript": "^5.0.3" } }, + "svelte-confetti": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz", + "integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==", + "dev": true, + "requires": {} + }, "svelte-eslint-parser": { "version": "0.33.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", @@ -5865,14 +9055,6 @@ "postcss-scss": "^4.0.8" } }, - "svelte-french-toast": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz", - "integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==", - "requires": { - "svelte-writable-derived": "^3.1.0" - } - }, "svelte-hmr": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", @@ -5903,12 +9085,23 @@ } } }, - "svelte-writable-derived": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz", - "integrity": "sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==", + "svelte-sonner": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz", + "integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==", "requires": {} }, + "symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true + }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", @@ -5957,6 +9150,15 @@ } } }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5981,6 +9183,16 @@ "thenify": ">= 3.1.0 < 4" } }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -6007,6 +9219,15 @@ "is-number": "^7.0.0" } }, + "to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, "totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -6028,8 +9249,7 @@ "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "type-check": { "version": "0.4.0", @@ -6052,14 +9272,30 @@ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, + "underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "requires": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + } + }, "undici": { - "version": "5.26.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", - "integrity": "sha512-OG+QOf0fTLtazL9P9X7yqWxQ+Z0395Wk6DSkyTxtaq3wQEjIroVe7Y4asCX/vcCxYpNGMnwz8F0qbRYUoaQVMw==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "requires": { "@fastify/busboy": "^2.0.0" } }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, "update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -6090,10 +9326,75 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, + "value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + }, + "vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + } + }, + "vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "requires": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + } + }, "vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "requires": { "esbuild": "^0.18.10", "fsevents": "~2.3.2", @@ -6107,6 +9408,28 @@ "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", "requires": {} }, + "vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6121,6 +9444,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index bae50d1b..e9e1fecf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ollama-webui", - "version": "0.0.1", + "name": "open-webui", + "version": "0.1.114", "private": true, "scripts": { "dev": "vite dev --host", @@ -13,7 +13,8 @@ "lint:types": "npm run check", "lint:backend": "pylint backend/", "format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'", - "format:backend": "yapf --recursive backend -p -i" + "format:backend": "yapf --recursive backend -p -i", + "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", @@ -27,11 +28,13 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", + "i18next-parser": "^8.13.0", "postcss": "^8.4.31", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", "svelte": "^4.0.5", "svelte-check": "^3.4.3", + "svelte-confetti": "^1.3.2", "tailwindcss": "^3.3.3", "tslib": "^2.4.1", "typescript": "^5.0.0", @@ -41,15 +44,19 @@ "dependencies": { "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", + "bits-ui": "^0.19.7", "dayjs": "^1.11.10", "file-saver": "^2.0.5", "highlight.js": "^11.9.0", + "i18next": "^23.10.0", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.9", "marked": "^9.1.0", - "svelte-french-toast": "^1.2.0", + "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "uuid": "^9.0.1" } -} +} \ No newline at end of file diff --git a/run-compose.sh b/run-compose.sh index 7b0f8d2b..08fba272 100755 --- a/run-compose.sh +++ b/run-compose.sh @@ -182,7 +182,7 @@ else export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable fi if [[ -n $webui_port ]]; then - export OLLAMA_WEBUI_PORT=$webui_port # Set OLLAMA_WEBUI_PORT environment variable + export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable fi DEFAULT_COMPOSE_COMMAND+=" up -d" DEFAULT_COMPOSE_COMMAND+=" --remove-orphans" diff --git a/run.sh b/run.sh index c8ac77cc..6793fe16 100644 --- a/run.sh +++ b/run.sh @@ -1,7 +1,7 @@ #!/bin/bash -image_name="ollama-webui" -container_name="ollama-webui" +image_name="open-webui" +container_name="open-webui" host_port=3000 container_port=8080 diff --git a/src/app.css b/src/app.css index 9665d026..82b3caa3 100644 --- a/src/app.css +++ b/src/app.css @@ -28,6 +28,25 @@ math { @apply rounded-lg; } +ol > li { + counter-increment: list-number; + display: block; + margin-bottom: 0; + margin-top: 0; + min-height: 28px; +} + +.prose ol > li::before { + content: counters(list-number, '.') '.'; + padding-right: 0.5rem; + color: var(--tw-prose-counters); + font-weight: 400; +} + +li p { + display: inline; +} + ::-webkit-scrollbar-thumb { --tw-border-opacity: 1; background-color: rgba(217, 217, 227, 0.8); @@ -37,8 +56,8 @@ math { } ::-webkit-scrollbar { - height: 0.45rem; - width: 0.35rem; + height: 0.4rem; + width: 0.4rem; } ::-webkit-scrollbar-track { diff --git a/src/app.html b/src/app.html index 9b1099b0..c52cff98 100644 --- a/src/app.html +++ b/src/app.html @@ -5,20 +5,38 @@ + %sveltekit.head% diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 07858998..16999872 100644 --- a/src/lib/apis/auths/index.ts +++ b/src/lib/apis/auths/index.ts @@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => { return res; }; + +export const getJWTExpiresDuration = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateJWTExpiresDuration = async (token: string, duration: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + duration: duration + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index aadf3769..35b259d5 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -439,7 +439,7 @@ export const deleteAllChats = async (token: string) => { return json; }) .catch((err) => { - error = err; + error = err.detail; console.log(err); return null; diff --git a/src/lib/apis/documents/index.ts b/src/lib/apis/documents/index.ts index 2f7fb2b9..21e0a264 100644 --- a/src/lib/apis/documents/index.ts +++ b/src/lib/apis/documents/index.ts @@ -5,7 +5,8 @@ export const createNewDoc = async ( collection_name: string, filename: string, name: string, - title: string + title: string, + content: object | null = null ) => { let error = null; @@ -20,7 +21,8 @@ export const createNewDoc = async ( collection_name: collection_name, filename: filename, name: name, - title: title + title: title, + ...(content ? { content: JSON.stringify(content) } : {}) }) }) .then(async (res) => { diff --git a/src/lib/apis/images/index.ts b/src/lib/apis/images/index.ts new file mode 100644 index 00000000..1fb004a3 --- /dev/null +++ b/src/lib/apis/images/index.ts @@ -0,0 +1,473 @@ +import { IMAGES_API_BASE_URL } from '$lib/constants'; + +export const getImageGenerationConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateImageGenerationConfig = async ( + token: string = '', + engine: string, + enabled: boolean +) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + engine, + enabled + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIKey = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/key`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_KEY; +}; + +export const updateOpenAIKey = async (token: string = '', key: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/key/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + key: key + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_KEY; +}; + +export const getAUTOMATIC1111Url = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/url`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.AUTOMATIC1111_BASE_URL; +}; + +export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.AUTOMATIC1111_BASE_URL; +}; + +export const getImageSize = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/size`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_SIZE; +}; + +export const updateImageSize = async (token: string = '', size: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + size: size + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_SIZE; +}; + +export const getImageSteps = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/steps`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_STEPS; +}; + +export const updateImageSteps = async (token: string = '', steps: number) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/steps/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ steps }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_STEPS; +}; + +export const getImageGenerationModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDefaultImageGenerationModel = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.model; +}; + +export const updateDefaultImageGenerationModel = async (token: string = '', model: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + model: model + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.model; +}; + +export const imageGenerations = async (token: string = '', prompt: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + prompt: prompt + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 91512166..a610f721 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -1,9 +1,9 @@ -import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { WEBUI_BASE_URL } from '$lib/constants'; export const getBackendConfig = async () => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/`, { + const res = await fetch(`${WEBUI_BASE_URL}/api/config`, { method: 'GET', headers: { 'Content-Type': 'application/json' @@ -19,5 +19,180 @@ export const getBackendConfig = async () => { return null; }); + if (error) { + throw error; + } + return res; }; + +export const getChangelog = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/changelog`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getVersionUpdates = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelFilterConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateModelFilterConfig = async ( + token: string, + enabled: boolean, + models: string[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + enabled: enabled, + models: models + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getWebhookUrl = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const updateWebhookUrl = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; diff --git a/src/lib/apis/litellm/index.ts b/src/lib/apis/litellm/index.ts new file mode 100644 index 00000000..302e9c4a --- /dev/null +++ b/src/lib/apis/litellm/index.ts @@ -0,0 +1,150 @@ +import { LITELLM_API_BASE_URL } from '$lib/constants'; + +export const getLiteLLMModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${LITELLM_API_BASE_URL}/v1/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + const models = Array.isArray(res) ? res : res?.data ?? null; + + return models + ? models + .map((model) => ({ + id: model.id, + name: model.name ?? model.id, + external: true, + source: 'litellm' + })) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }) + : models; +}; + +export const getLiteLLMModelInfo = async (token: string = '') => { + let error = null; + + const res = await fetch(`${LITELLM_API_BASE_URL}/model/info`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + const models = Array.isArray(res) ? res : res?.data ?? null; + + return models; +}; + +type AddLiteLLMModelForm = { + name: string; + model: string; + api_base: string; + api_key: string; + rpm: string; + max_tokens: string; +}; + +export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMModelForm) => { + let error = null; + + const res = await fetch(`${LITELLM_API_BASE_URL}/model/new`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + model_name: payload.name, + litellm_params: { + model: payload.model, + ...(payload.api_base === '' ? {} : { api_base: payload.api_base }), + ...(payload.api_key === '' ? {} : { api_key: payload.api_key }), + ...(isNaN(parseInt(payload.rpm)) ? {} : { rpm: parseInt(payload.rpm) }), + ...(payload.max_tokens === '' ? {} : { max_tokens: payload.max_tokens }) + } + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteLiteLLMModel = async (token: string = '', id: string) => { + let error = null; + + const res = await fetch(`${LITELLM_API_BASE_URL}/model/delete`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + id: id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 62501966..2047fede 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -1,9 +1,9 @@ import { OLLAMA_API_BASE_URL } from '$lib/constants'; -export const getOllamaAPIUrl = async (token: string = '') => { +export const getOllamaUrls = async (token: string = '') => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, { method: 'GET', headers: { Accept: 'application/json', @@ -29,13 +29,13 @@ export const getOllamaAPIUrl = async (token: string = '') => { throw error; } - return res.OLLAMA_API_BASE_URL; + return res.OLLAMA_BASE_URLS; }; -export const updateOllamaAPIUrl = async (token: string = '', url: string) => { +export const updateOllamaUrls = async (token: string = '', urls: string[]) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/urls/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -43,7 +43,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - url: url + urls: urls }) }) .then(async (res) => { @@ -64,13 +64,13 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { throw error; } - return res.OLLAMA_API_BASE_URL; + return res.OLLAMA_BASE_URLS; }; export const getOllamaVersion = async (token: string = '') => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version`, { method: 'GET', headers: { Accept: 'application/json', @@ -102,7 +102,7 @@ export const getOllamaVersion = async (token: string = '') => { export const getOllamaModels = async (token: string = '') => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, { method: 'GET', headers: { Accept: 'application/json', @@ -128,23 +128,36 @@ export const getOllamaModels = async (token: string = '') => { throw error; } - return (res?.models ?? []).sort((a, b) => { - return a.name.localeCompare(b.name); - }); + return (res?.models ?? []) + .map((model) => ({ id: model.model, name: model.name ?? model.model, ...model })) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }); }; -export const generateTitle = async (token: string = '', model: string, prompt: string) => { +// TODO: migrate to backend +export const generateTitle = async ( + token: string = '', + template: string, + model: string, + prompt: string +) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { + template = template.replace(/{{prompt}}/g, prompt); + + console.log(template); + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { method: 'POST', headers: { - 'Content-Type': 'text/event-stream', + Accept: 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ model: model, - prompt: `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': ${prompt}`, + prompt: template, stream: false }) }) @@ -174,10 +187,11 @@ export const generatePrompt = async (token: string = '', model: string, conversa conversation = '[no existing conversation]'; } - const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { method: 'POST', headers: { - 'Content-Type': 'text/event-stream', + Accept: 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ @@ -205,15 +219,43 @@ export const generatePrompt = async (token: string = '', model: string, conversa return res; }; +export const generateTextCompletion = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: text, + stream: true + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const generateChatCompletion = async (token: string = '', body: object) => { let controller = new AbortController(); let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, { signal: controller.signal, method: 'POST', headers: { - 'Content-Type': 'text/event-stream', + Accept: 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify(body) @@ -253,10 +295,11 @@ export const cancelChatCompletion = async (token: string = '', requestId: string export const createModel = async (token: string, tagName: string, content: string) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, { method: 'POST', headers: { - 'Content-Type': 'text/event-stream', + Accept: 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ @@ -275,19 +318,23 @@ export const createModel = async (token: string, tagName: string, content: strin return res; }; -export const deleteModel = async (token: string, tagName: string) => { +export const deleteModel = async (token: string, tagName: string, urlIdx: string | null = null) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, { - method: 'DELETE', - headers: { - 'Content-Type': 'text/event-stream', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify({ - name: tagName - }) - }) + const res = await fetch( + `${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName + }) + } + ) .then(async (res) => { if (!res.ok) throw await res.json(); return res.json(); @@ -298,7 +345,12 @@ export const deleteModel = async (token: string, tagName: string) => { }) .catch((err) => { console.log(err); - error = err.error; + error = err; + + if ('detail' in err) { + error = err.detail; + } + return null; }); @@ -309,13 +361,14 @@ export const deleteModel = async (token: string, tagName: string) => { return res; }; -export const pullModel = async (token: string, tagName: string) => { +export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, { method: 'POST', headers: { - 'Content-Type': 'text/event-stream', + Accept: 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts index f9173f70..e38314a5 100644 --- a/src/lib/apis/openai/index.ts +++ b/src/lib/apis/openai/index.ts @@ -1,9 +1,9 @@ import { OPENAI_API_BASE_URL } from '$lib/constants'; -export const getOpenAIUrl = async (token: string = '') => { +export const getOpenAIUrls = async (token: string = '') => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/url`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, { method: 'GET', headers: { Accept: 'application/json', @@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => { throw error; } - return res.OPENAI_API_BASE_URL; + return res.OPENAI_API_BASE_URLS; }; -export const updateOpenAIUrl = async (token: string = '', url: string) => { +export const updateOpenAIUrls = async (token: string = '', urls: string[]) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - url: url + urls: urls }) }) .then(async (res) => { @@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { throw error; } - return res.OPENAI_API_BASE_URL; + return res.OPENAI_API_BASE_URLS; }; -export const getOpenAIKey = async (token: string = '') => { +export const getOpenAIKeys = async (token: string = '') => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/key`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, { method: 'GET', headers: { Accept: 'application/json', @@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => { throw error; } - return res.OPENAI_API_KEY; + return res.OPENAI_API_KEYS; }; -export const updateOpenAIKey = async (token: string = '', key: string) => { +export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - key: key + keys: keys }) }) .then(async (res) => { @@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { throw error; } - return res.OPENAI_API_KEY; + return res.OPENAI_API_KEYS; }; export const getOpenAIModels = async (token: string = '') => { @@ -150,7 +150,6 @@ export const getOpenAIModels = async (token: string = '') => { return res.json(); }) .catch((err) => { - console.log(err); error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; return []; }); @@ -163,7 +162,7 @@ export const getOpenAIModels = async (token: string = '') => { return models ? models - .map((model) => ({ name: model.id, external: true })) + .map((model) => ({ id: model.id, name: model.name ?? model.id, external: true })) .sort((a, b) => { return a.name.localeCompare(b.name); }) @@ -200,17 +199,21 @@ export const getOpenAIModelsDirect = async ( const models = Array.isArray(res) ? res : res?.data ?? null; return models - .map((model) => ({ name: model.id, external: true })) + .map((model) => ({ id: model.id, name: model.name ?? model.id, external: true })) .filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true)) .sort((a, b) => { return a.name.localeCompare(b.name); }); }; -export const generateOpenAIChatCompletion = async (token: string = '', body: object) => { +export const generateOpenAIChatCompletion = async ( + token: string = '', + body: object, + url: string = OPENAI_API_BASE_URL +) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/chat/completions`, { + const res = await fetch(`${url}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, diff --git a/src/lib/apis/rag/index.ts b/src/lib/apis/rag/index.ts index 3f4f30bf..668fe227 100644 --- a/src/lib/apis/rag/index.ts +++ b/src/lib/apis/rag/index.ts @@ -1,5 +1,161 @@ import { RAG_API_BASE_URL } from '$lib/constants'; +export const getRAGConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type ChunkConfigForm = { + chunk_size: number; + chunk_overlap: number; +}; + +type RAGConfigForm = { + pdf_extract_images: boolean; + chunk: ChunkConfigForm; +}; + +export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getRAGTemplate = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/template`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res?.template ?? ''; +}; + +export const getQuerySettings = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type QuerySettings = { + k: number | null; + template: string | null; +}; + +export const updateQuerySettings = async (token: string, settings: QuerySettings) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...settings + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const uploadDocToVectorDB = async (token: string, collection_name: string, file: File) => { const data = new FormData(); data.append('file', file); @@ -68,7 +224,7 @@ export const queryDoc = async ( token: string, collection_name: string, query: string, - k: number + k: number | null = null ) => { let error = null; @@ -105,7 +261,7 @@ export const queryCollection = async ( token: string, collection_names: string, query: string, - k: number + k: number | null = null ) => { let error = null; @@ -138,6 +294,32 @@ export const queryCollection = async ( return res; }; +export const scanDocs = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/scan`, { + method: 'GET', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const resetVectorDB = async (token: string) => { let error = null; diff --git a/src/lib/apis/utils/index.ts b/src/lib/apis/utils/index.ts index ed4d4e02..bcb55407 100644 --- a/src/lib/apis/utils/index.ts +++ b/src/lib/apis/utils/index.ts @@ -21,3 +21,35 @@ export const getGravatarUrl = async (email: string) => { return res; }; + +export const downloadDatabase = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/db/download`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then((response) => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.blob(); + }) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'webui.db'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); +}; diff --git a/src/lib/components/AddFilesPlaceholder.svelte b/src/lib/components/AddFilesPlaceholder.svelte index 3cbd0454..3bdbe928 100644 --- a/src/lib/components/AddFilesPlaceholder.svelte +++ b/src/lib/components/AddFilesPlaceholder.svelte @@ -1,8 +1,13 @@ + +
📄
-
Add Files
+
{$i18n.t('Add Files')}
- Drop any files here to add to the conversation + {$i18n.t('Drop any files here to add to the conversation')}
diff --git a/src/lib/components/ChangelogModal.svelte b/src/lib/components/ChangelogModal.svelte new file mode 100644 index 00000000..635d2c03 --- /dev/null +++ b/src/lib/components/ChangelogModal.svelte @@ -0,0 +1,118 @@ + + + +
+
+
+ {$i18n.t('What’s New in')} + {$WEBUI_NAME} + +
+ +
+
+
{$i18n.t('Release Notes')}
+
+
+ v{WEBUI_VERSION} +
+
+
+ +
+ +
+
+
+ {#if changelog} + {#each Object.keys(changelog) as version} +
+
+ v{version} - {changelog[version].date} +
+ +
+ + {#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section} +
+
+ {section} +
+ +
+ {#each Object.keys(changelog[version][section]) as item} +
+
+ {changelog[version][section][item].title} +
+
{changelog[version][section][item].content}
+
+ {/each} +
+
+ {/each} +
+ {/each} + {/if} +
+
+
+ +
+
+ diff --git a/src/lib/components/admin/EditUserModal.svelte b/src/lib/components/admin/EditUserModal.svelte index 09005b30..f5fa0837 100644 --- a/src/lib/components/admin/EditUserModal.svelte +++ b/src/lib/components/admin/EditUserModal.svelte @@ -1,12 +1,13 @@ + +
{ + saveHandler(); + }} +> +
+
+
{$i18n.t('Database')}
+ +
+ + + +
+
+
+ + +
diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 48ed41e7..be319e26 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -1,15 +1,23 @@
{ - // console.log('submit'); + updateJWTExpiresDurationHandler(JWTExpiresIn); + updateWebhookUrlHandler(); saveHandler(); }} >
-
General Settings
+
{$i18n.t('General Settings')}
-
Enable New Sign Ups
+
{$i18n.t('Enable New Sign Ups')}
-
Default User Role
+
{$i18n.t('Default User Role')}
+ +
+ +
+
+
{$i18n.t('Webhook URL')}
+
+ +
+ +
+
+ +
+ +
+
+
{$i18n.t('JWT Expiration')}
+
+ +
+ +
+ +
+ {$i18n.t('Valid time units:')} + {$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")} +
+
@@ -102,7 +162,7 @@ class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" type="submit" > - Save + {$i18n.t('Save')}
diff --git a/src/lib/components/admin/Settings/Users.svelte b/src/lib/components/admin/Settings/Users.svelte index 8a442c51..b6844be0 100644 --- a/src/lib/components/admin/Settings/Users.svelte +++ b/src/lib/components/admin/Settings/Users.svelte @@ -1,10 +1,17 @@ @@ -21,15 +35,17 @@ on:submit|preventDefault={async () => { // console.log('submit'); await updateUserPermissions(localStorage.token, permissions); + + await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels); saveHandler(); }} >
-
User Permissions
+
{$i18n.t('User Permissions')}
-
Allow Chat Deletion
+
{$i18n.t('Allow Chat Deletion')}
+ +
+ +
+
+
+
+
{$i18n.t('Manage Models')}
+
+
+ +
+
+
+
{$i18n.t('Model Whitelisting')}
+ + +
+
+ + {#if whitelistEnabled} +
+
+ {#each whitelistModels as modelId, modelIdx} +
+
+ +
+ + {#if modelIdx === 0} + + {:else} + + {/if} +
+ {/each} +
+ +
+
+ {whitelistModels.length} + {$i18n.t('Model(s) Whitelisted')} +
+
+
+ {/if} +
+
+
@@ -76,7 +193,7 @@ class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" type="submit" > - Save + {$i18n.t('Save')}
diff --git a/src/lib/components/admin/SettingsModal.svelte b/src/lib/components/admin/SettingsModal.svelte index 67f6be88..7b726214 100644 --- a/src/lib/components/admin/SettingsModal.svelte +++ b/src/lib/components/admin/SettingsModal.svelte @@ -1,9 +1,13 @@ -
- {#each prompts as prompt, promptIdx} -
- -
- {/each} + + + +
+ + + {/each} + diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index b02ba116..7afb5c37 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -2,15 +2,18 @@ import { v4 as uuidv4 } from 'uuid'; import { chats, config, modelfiles, settings, user } from '$lib/stores'; - import { tick } from 'svelte'; + import { tick, getContext } from 'svelte'; - import toast from 'svelte-french-toast'; + import { toast } from 'svelte-sonner'; import { getChatList, updateChatById } from '$lib/apis/chats'; import UserMessage from './Messages/UserMessage.svelte'; import ResponseMessage from './Messages/ResponseMessage.svelte'; import Placeholder from './Messages/Placeholder.svelte'; import Spinner from '../common/Spinner.svelte'; + import { imageGenerations } from '$lib/apis/images'; + + const i18n = getContext('i18n'); export let chatId = ''; export let sendPrompt: Function; @@ -66,7 +69,7 @@ navigator.clipboard.writeText(text).then( function () { console.log('Async: Copying to clipboard was successful!'); - toast.success('Copying to clipboard was successful!'); + toast.success($i18n.t('Copying to clipboard was successful!')); }, function (err) { console.error('Async: Could not copy text: ', err); @@ -221,6 +224,81 @@ scrollToBottom(); }, 100); }; + + const messageDeleteHandler = async (messageId) => { + const messageToDelete = history.messages[messageId]; + const messageParentId = messageToDelete.parentId; + const messageChildrenIds = messageToDelete.childrenIds ?? []; + const hasSibling = messageChildrenIds.some( + (childId) => history.messages[childId]?.childrenIds?.length > 0 + ); + messageChildrenIds.forEach((childId) => { + const child = history.messages[childId]; + if (child && child.childrenIds) { + if (child.childrenIds.length === 0 && !hasSibling) { + // if last prompt/response pair + history.messages[messageParentId].childrenIds = []; + history.currentId = messageParentId; + } else { + child.childrenIds.forEach((grandChildId) => { + if (history.messages[grandChildId]) { + history.messages[grandChildId].parentId = messageParentId; + history.messages[messageParentId].childrenIds.push(grandChildId); + } + }); + } + } + // remove response + history.messages[messageParentId].childrenIds = history.messages[ + messageParentId + ].childrenIds.filter((id) => id !== childId); + }); + // remove prompt + history.messages[messageParentId].childrenIds = history.messages[ + messageParentId + ].childrenIds.filter((id) => id !== messageId); + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }; + + // const messageDeleteHandler = async (messageId) => { + // const message = history.messages[messageId]; + // const parentId = message.parentId; + // const childrenIds = message.childrenIds ?? []; + // const grandchildrenIds = []; + + // // Iterate through childrenIds to find grandchildrenIds + // for (const childId of childrenIds) { + // const childMessage = history.messages[childId]; + // const grandChildrenIds = childMessage.childrenIds ?? []; + + // for (const grandchildId of grandchildrenIds) { + // const childMessage = history.messages[grandchildId]; + // childMessage.parentId = parentId; + // } + // grandchildrenIds.push(...grandChildrenIds); + // } + + // history.messages[parentId].childrenIds.push(...grandchildrenIds); + // history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter( + // (id) => id !== messageId + // ); + + // // Select latest message + // let currentMessageId = grandchildrenIds.at(-1); + // if (currentMessageId) { + // let messageChildrenIds = history.messages[currentMessageId].childrenIds; + // while (messageChildrenIds.length !== 0) { + // currentMessageId = messageChildrenIds.at(-1); + // messageChildrenIds = history.messages[currentMessageId].childrenIds; + // } + // history.currentId = currentMessageId; + // } + + // await updateChatById(localStorage.token, chatId, { messages, history }); + // }; {#if messages.length == 0} @@ -237,8 +315,10 @@ > {#if message.role === 'user'} messageDeleteHandler(message.id)} user={$user} {message} + isFirstMessage={messageIdx === 0} siblings={message.parentId !== null ? history.messages[message.parentId]?.childrenIds ?? [] : Object.values(history.messages) @@ -249,52 +329,6 @@ {showNextMessage} {copyToClipboard} /> - - {#if messages.length - 1 === messageIdx && processing !== ''} -
-
- -
-
- {processing} -
-
- {/if} {:else} { + console.log('save', e); + + const message = e.detail; + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }} /> {/if} diff --git a/src/lib/components/chat/Messages/Placeholder.svelte b/src/lib/components/chat/Messages/Placeholder.svelte index ae9ced14..a18cedbb 100644 --- a/src/lib/components/chat/Messages/Placeholder.svelte +++ b/src/lib/components/chat/Messages/Placeholder.svelte @@ -1,5 +1,9 @@
@@ -58,17 +67,18 @@ {#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)} {$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title} {:else} - You {message?.user ?? ''} + {$i18n.t('You')} + {message?.user ?? ''} {/if} {:else if $settings.showUsername} {user.name} {:else} - You + {$i18n.t('You')} {/if} {#if message.timestamp} {/if} @@ -116,7 +126,7 @@ {file.name}
-
Document
+
{$i18n.t('Document')}
{:else if file.type === 'collection'} @@ -145,7 +155,7 @@ {file?.title ?? `#${file.name}`} -
Collection
+
{$i18n.t('Collection')}
{/if} @@ -158,9 +168,11 @@