Parcourir la source

Merge pull request #2376 from open-webui/dev

0.1.125
Timothy Jaeryang Baek il y a 11 mois
Parent
commit
be5534c655
100 fichiers modifiés avec 7334 ajouts et 1635 suppressions
  1. 43 22
      .github/pull_request_template.md
  2. 18 0
      .github/workflows/format-build-frontend.yaml
  3. 4 0
      .gitignore
  4. 306 6
      .prettierignore
  5. 19 0
      CHANGELOG.md
  6. 52 32
      Dockerfile
  7. 20 19
      backend/apps/audio/main.py
  8. 89 68
      backend/apps/images/main.py
  9. 15 12
      backend/apps/litellm/main.py
  10. 39 32
      backend/apps/ollama/main.py
  11. 59 27
      backend/apps/openai/main.py
  12. 138 107
      backend/apps/rag/main.py
  13. 53 0
      backend/apps/web/internal/migrations/008_add_memory.py
  14. 18 9
      backend/apps/web/main.py
  15. 118 0
      backend/apps/web/models/memories.py
  16. 16 16
      backend/apps/web/routers/auths.py
  17. 2 2
      backend/apps/web/routers/chats.py
  18. 4 4
      backend/apps/web/routers/configs.py
  19. 145 0
      backend/apps/web/routers/memories.py
  20. 40 4
      backend/apps/web/routers/users.py
  21. 286 73
      backend/config.py
  22. 69 39
      backend/main.py
  23. 32 0
      cypress/e2e/chat.cy.ts
  24. 2 6
      cypress/e2e/settings.cy.ts
  25. 0 2
      docker-compose.api.yaml
  26. 0 2
      docker-compose.data.yaml
  27. 0 2
      docker-compose.gpu.yaml
  28. 0 2
      docker-compose.yaml
  29. 2 2
      docs/CONTRIBUTING.md
  30. 726 10
      package-lock.json
  31. 12 7
      package.json
  32. 39 0
      scripts/prepare-pyodide.js
  33. 24 4
      src/app.css
  34. 1 0
      src/app.html
  35. 155 0
      src/lib/apis/memories/index.ts
  36. 67 0
      src/lib/apis/openai/index.ts
  37. 7 0
      src/lib/apis/streaming/index.ts
  38. 27 0
      src/lib/apis/users/index.ts
  39. 1 1
      src/lib/components/ChangelogModal.svelte
  40. 5 2
      src/lib/components/admin/AddUserModal.svelte
  41. 2 2
      src/lib/components/admin/Settings/General.svelte
  42. 19 16
      src/lib/components/chat/MessageInput.svelte
  43. 69 31
      src/lib/components/chat/Messages.svelte
  44. 1 1
      src/lib/components/chat/Messages/CitationsModal.svelte
  45. 226 5
      src/lib/components/chat/Messages/CodeBlock.svelte
  46. 163 0
      src/lib/components/chat/Messages/CompareMessages.svelte
  47. 1 1
      src/lib/components/chat/Messages/Name.svelte
  48. 2 0
      src/lib/components/chat/Messages/Placeholder.svelte
  49. 15 2
      src/lib/components/chat/Messages/ProfileImage.svelte
  50. 2 2
      src/lib/components/chat/Messages/RateComment.svelte
  51. 139 122
      src/lib/components/chat/Messages/ResponseMessage.svelte
  52. 172 91
      src/lib/components/chat/Messages/UserMessage.svelte
  53. 3 3
      src/lib/components/chat/ModelSelector.svelte
  54. 15 12
      src/lib/components/chat/ModelSelector/Selector.svelte
  55. 2 2
      src/lib/components/chat/Settings/Account.svelte
  56. 1 1
      src/lib/components/chat/Settings/Advanced.svelte
  57. 2 2
      src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
  58. 2 2
      src/lib/components/chat/Settings/Audio.svelte
  59. 19 14
      src/lib/components/chat/Settings/Connections.svelte
  60. 2 2
      src/lib/components/chat/Settings/General.svelte
  61. 72 18
      src/lib/components/chat/Settings/Interface.svelte
  62. 96 0
      src/lib/components/chat/Settings/Personalization.svelte
  63. 125 0
      src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte
  64. 165 0
      src/lib/components/chat/Settings/Personalization/ManageModal.svelte
  65. 25 1
      src/lib/components/chat/SettingsModal.svelte
  66. 16 2
      src/lib/components/chat/ShareChatModal.svelte
  67. 2 2
      src/lib/components/common/Modal.svelte
  68. 1 1
      src/lib/components/common/Selector.svelte
  69. 22 0
      src/lib/components/common/Switch.svelte
  70. 4 2
      src/lib/components/documents/Settings/General.svelte
  71. 1 1
      src/lib/components/documents/Settings/QueryParams.svelte
  72. 19 0
      src/lib/components/icons/MenuLines.svelte
  73. 11 0
      src/lib/components/icons/User.svelte
  74. 65 37
      src/lib/components/layout/Navbar.svelte
  75. 62 88
      src/lib/components/layout/Navbar/Menu.svelte
  76. 133 280
      src/lib/components/layout/Sidebar.svelte
  77. 16 4
      src/lib/components/layout/Sidebar/ChatMenu.svelte
  78. 147 0
      src/lib/components/layout/Sidebar/UserMenu.svelte
  79. 611 0
      src/lib/components/workspace/Documents.svelte
  80. 409 0
      src/lib/components/workspace/Modelfiles.svelte
  81. 113 116
      src/lib/components/workspace/Playground.svelte
  82. 331 0
      src/lib/components/workspace/Prompts.svelte
  83. 2 2
      src/lib/constants.ts
  84. 1 1
      src/lib/i18n/index.ts
  85. 108 100
      src/lib/i18n/locales/ar-BH/translation.json
  86. 22 14
      src/lib/i18n/locales/bg-BG/translation.json
  87. 23 15
      src/lib/i18n/locales/bn-BD/translation.json
  88. 23 15
      src/lib/i18n/locales/ca-ES/translation.json
  89. 16 8
      src/lib/i18n/locales/de-DE/translation.json
  90. 20 12
      src/lib/i18n/locales/dg-DG/translation.json
  91. 14 6
      src/lib/i18n/locales/en-GB/translation.json
  92. 14 6
      src/lib/i18n/locales/en-US/translation.json
  93. 23 15
      src/lib/i18n/locales/es-ES/translation.json
  94. 23 15
      src/lib/i18n/locales/fa-IR/translation.json
  95. 15 7
      src/lib/i18n/locales/fi-FI/translation.json
  96. 23 15
      src/lib/i18n/locales/fr-CA/translation.json
  97. 23 15
      src/lib/i18n/locales/fr-FR/translation.json
  98. 503 0
      src/lib/i18n/locales/he-IL/translation.json
  99. 22 14
      src/lib/i18n/locales/hi-IN/translation.json
  100. 503 0
      src/lib/i18n/locales/hr-HR/translation.json

+ 43 - 22
.github/pull_request_template.md

@@ -1,38 +1,56 @@
-## Pull Request Checklist
+# Pull Request Checklist
 
-- [ ] **Target branch:** Pull requests should target the `dev` branch.
-- [ ] **Description:** Briefly describe the changes in this pull request.
+### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
+
+**Before submitting, make sure you've checked the following:**
+
+- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
+- [ ] **Description:** Provide a concise description of the changes made 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 [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
 - [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
-- [ ] **Testing:** Have you written and run sufficient tests for the changes?
-- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues?
-
----
-
-## Description
-
-[Insert a brief description of the changes made in this pull request, including any relevant motivation and impact.]
-
----
-
-### Changelog Entry
+- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
+- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
+- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
+  - **BREAKING CHANGE**: Significant changes that may affect compatibility
+  - **build**: Changes that affect the build system or external dependencies
+  - **ci**: Changes to our continuous integration processes or workflows
+  - **chore**: Refactor, cleanup, or other non-functional code changes
+  - **docs**: Documentation update or addition
+  - **feat**: Introduces a new feature or enhancement to the codebase
+  - **fix**: Bug fix or error correction
+  - **i18n**: Internationalization or localization changes
+  - **perf**: Performance improvement
+  - **refactor**: Code restructuring for better maintainability, readability, or scalability
+  - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
+  - **test**: Adding missing tests or correcting existing tests
+  - **WIP**: Work in progress, a temporary label for incomplete or ongoing work
+
+# Changelog Entry
+
+### Description
+
+- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
 
 ### Added
 
 - [List any new features, functionalities, or additions]
 
-### Fixed
-
-- [List any fixes, corrections, or bug fixes]
-
 ### Changed
 
 - [List any changes, updates, refactorings, or optimizations]
 
+### Deprecated
+
+- [List any deprecated functionality or features that have been removed]
+
 ### Removed
 
-- [List any removed features, files, or deprecated functionalities]
+- [List any removed features, files, or functionalities]
+
+### Fixed
+
+- [List any fixes, corrections, or bug fixes]
 
 ### Security
 
@@ -40,12 +58,15 @@
 
 ### Breaking Changes
 
-- [List any breaking changes affecting compatibility or functionality]
+- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
 
 ---
 
 ### Additional Information
 
 - [Insert any additional context, notes, or explanations for the changes]
+  - [Reference any related issues, commits, or other relevant information]
+
+### Screenshots or Videos
 
-- [Reference any related issues, commits, or other relevant information]
+- [Attach any relevant screenshots or videos demonstrating the changes]

+ 18 - 0
.github/workflows/format-build-frontend.yaml

@@ -37,3 +37,21 @@ jobs:
 
       - name: Build Frontend
         run: npm run build
+
+  test-frontend:
+    name: 'Frontend Unit Tests'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@v4
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+
+      - name: Install Dependencies
+        run: npm ci
+
+      - name: Run vitest
+        run: npm run test:frontend

+ 4 - 0
.gitignore

@@ -16,6 +16,10 @@ __pycache__/
 # C extensions
 *.so
 
+# Pyodide distribution
+static/pyodide/*
+!static/pyodide/pyodide-lock.json
+
 # Distribution / packaging
 .Python
 build/

+ 306 - 6
.prettierignore

@@ -1,3 +1,11 @@
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+
+kubernetes/
+
+# Copy of .gitignore
 .DS_Store
 node_modules
 /build
@@ -6,11 +14,303 @@ node_modules
 .env
 .env.*
 !.env.example
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# cypress artifacts
+cypress/videos
+cypress/screenshots
+
 
-# Ignore files for PNPM, NPM and YARN
-pnpm-lock.yaml
-package-lock.json
-yarn.lock
 
-# Ignore kubernetes files
-kubernetes
+/static/*

+ 19 - 0
CHANGELOG.md

@@ -5,6 +5,25 @@ 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.125] - 2024-05-19
+
+### Added
+
+- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
+- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
+- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
+- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
+- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
+- **💾 Persistent Settings**: Settings now saved as config.json for convenience.
+- **🩺 Health Check Endpoint**: Added for Docker deployment.
+- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
+- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
+- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
+
+### Changed
+
+- **👤 Shared Chat Update**: Shared chat now includes creator user information.
+
 ## [0.1.124] - 2024-05-08
 
 ### Added

+ 52 - 32
Dockerfile

@@ -11,6 +11,9 @@ ARG USE_CUDA_VER=cu121
 # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
 ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
 ARG USE_RERANKING_MODEL=""
+# Override at your own risk - non-root configurations are untested
+ARG UID=0
+ARG GID=0
 
 ######## WebUI frontend ########
 FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
@@ -32,6 +35,8 @@ ARG USE_OLLAMA
 ARG USE_CUDA_VER
 ARG USE_EMBEDDING_MODEL
 ARG USE_RERANKING_MODEL
+ARG UID
+ARG GID
 
 ## Basis ##
 ENV ENV=prod \
@@ -76,46 +81,57 @@ ENV HF_HOME="/app/backend/data/cache/embedding/models"
 WORKDIR /app/backend
 
 ENV HOME /root
+# Create user and group if not root
+RUN if [ $UID -ne 0 ]; then \
+      if [ $GID -ne 0 ]; then \
+        addgroup --gid $GID app; \
+      fi; \
+      adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
+    fi
+
 RUN mkdir -p $HOME/.cache/chroma
 RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
 
+# Make sure the user has access to the app and root directory
+RUN chown -R $UID:$GID /app $HOME
+
 RUN if [ "$USE_OLLAMA" = "true" ]; then \
-        apt-get update && \
-        # Install pandoc and netcat
-        apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
-        # for RAG OCR
-        apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
-        # install helper tools
-        apt-get install -y --no-install-recommends curl && \
-        # install ollama
-        curl -fsSL https://ollama.com/install.sh | sh && \
-        # cleanup
-        rm -rf /var/lib/apt/lists/*; \
+    apt-get update && \
+    # Install pandoc and netcat
+    apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
+    # for RAG OCR
+    apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
+    # install helper tools
+    apt-get install -y --no-install-recommends curl jq && \
+    # install ollama
+    curl -fsSL https://ollama.com/install.sh | sh && \
+    # cleanup
+    rm -rf /var/lib/apt/lists/*; \
     else \
-        apt-get update && \
-        # Install pandoc and netcat
-        apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
-        # for RAG OCR
-        apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
-        # cleanup
-        rm -rf /var/lib/apt/lists/*; \
+    apt-get update && \
+    # Install pandoc and netcat
+    apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
+    # for RAG OCR
+    apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
+    # cleanup
+    rm -rf /var/lib/apt/lists/*; \
     fi
 
 # install python dependencies
-COPY ./backend/requirements.txt ./requirements.txt
+COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
 
 RUN pip3 install uv && \
     if [ "$USE_CUDA" = "true" ]; then \
-        # If you use CUDA the whisper and embedding model will be downloaded on first use
-        pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
-        uv pip install --system -r requirements.txt --no-cache-dir && \
-        python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
-        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'])"; \
+    # If you use CUDA the whisper and embedding model will be downloaded on first use
+    pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
+    uv pip install --system -r requirements.txt --no-cache-dir && \
+    python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
+    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'])"; \
     else \
-        pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
-        uv pip install --system -r requirements.txt --no-cache-dir && \
-        python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
-        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'])"; \
+    pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
+    uv pip install --system -r requirements.txt --no-cache-dir && \
+    python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
+    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'])"; \
     fi
 
 
@@ -125,13 +141,17 @@ RUN pip3 install uv && \
 # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
 
 # 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 --chown=$UID:$GID --from=build /app/build /app/build
+COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
+COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
 
 # copy backend files
-COPY ./backend .
+COPY --chown=$UID:$GID ./backend .
 
 EXPOSE 8080
 
+HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
+
+USER $UID:$GID
+
 CMD [ "bash", "start.sh"]

+ 20 - 19
backend/apps/audio/main.py

@@ -45,6 +45,7 @@ from config import (
     AUDIO_OPENAI_API_KEY,
     AUDIO_OPENAI_API_MODEL,
     AUDIO_OPENAI_API_VOICE,
+    AppConfig,
 )
 
 log = logging.getLogger(__name__)
@@ -59,11 +60,11 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
-
-app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
-app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
-app.state.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
-app.state.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
+app.state.config = AppConfig()
+app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
+app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
+app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
+app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
 
 # setting device type for whisper model
 whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -83,10 +84,10 @@ class OpenAIConfigUpdateForm(BaseModel):
 @app.get("/config")
 async def get_openai_config(user=Depends(get_admin_user)):
     return {
-        "OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
-        "OPENAI_API_KEY": app.state.OPENAI_API_KEY,
-        "OPENAI_API_MODEL": app.state.OPENAI_API_MODEL,
-        "OPENAI_API_VOICE": app.state.OPENAI_API_VOICE,
+        "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
+        "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
+        "OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
+        "OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
     }
 
 
@@ -97,17 +98,17 @@ async def update_openai_config(
     if form_data.key == "":
         raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
 
-    app.state.OPENAI_API_BASE_URL = form_data.url
-    app.state.OPENAI_API_KEY = form_data.key
-    app.state.OPENAI_API_MODEL = form_data.model
-    app.state.OPENAI_API_VOICE = form_data.speaker
+    app.state.config.OPENAI_API_BASE_URL = form_data.url
+    app.state.config.OPENAI_API_KEY = form_data.key
+    app.state.config.OPENAI_API_MODEL = form_data.model
+    app.state.config.OPENAI_API_VOICE = form_data.speaker
 
     return {
         "status": True,
-        "OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
-        "OPENAI_API_KEY": app.state.OPENAI_API_KEY,
-        "OPENAI_API_MODEL": app.state.OPENAI_API_MODEL,
-        "OPENAI_API_VOICE": app.state.OPENAI_API_VOICE,
+        "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
+        "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
+        "OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
+        "OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
     }
 
 
@@ -124,13 +125,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
         return FileResponse(file_path)
 
     headers = {}
-    headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
+    headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
     headers["Content-Type"] = "application/json"
 
     r = None
     try:
         r = requests.post(
-            url=f"{app.state.OPENAI_API_BASE_URL}/audio/speech",
+            url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
             data=body,
             headers=headers,
             stream=True,

+ 89 - 68
backend/apps/images/main.py

@@ -42,6 +42,7 @@ from config import (
     IMAGE_GENERATION_MODEL,
     IMAGE_SIZE,
     IMAGE_STEPS,
+    AppConfig,
 )
 
 
@@ -60,26 +61,31 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
-app.state.ENGINE = IMAGE_GENERATION_ENGINE
-app.state.ENABLED = ENABLE_IMAGE_GENERATION
+app.state.config = AppConfig()
 
-app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
-app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
+app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
+app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
 
-app.state.MODEL = IMAGE_GENERATION_MODEL
+app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
+app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
 
+app.state.config.MODEL = IMAGE_GENERATION_MODEL
 
-app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
-app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
 
+app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
+app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
 
-app.state.IMAGE_SIZE = IMAGE_SIZE
-app.state.IMAGE_STEPS = IMAGE_STEPS
+
+app.state.config.IMAGE_SIZE = IMAGE_SIZE
+app.state.config.IMAGE_STEPS = IMAGE_STEPS
 
 
 @app.get("/config")
 async def get_config(request: Request, user=Depends(get_admin_user)):
-    return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
+    return {
+        "engine": app.state.config.ENGINE,
+        "enabled": app.state.config.ENABLED,
+    }
 
 
 class ConfigUpdateForm(BaseModel):
@@ -89,9 +95,12 @@ class ConfigUpdateForm(BaseModel):
 
 @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}
+    app.state.config.ENGINE = form_data.engine
+    app.state.config.ENABLED = form_data.enabled
+    return {
+        "engine": app.state.config.ENGINE,
+        "enabled": app.state.config.ENABLED,
+    }
 
 
 class EngineUrlUpdateForm(BaseModel):
@@ -102,8 +111,8 @@ class EngineUrlUpdateForm(BaseModel):
 @app.get("/url")
 async def get_engine_url(user=Depends(get_admin_user)):
     return {
-        "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
-        "COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
+        "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
+        "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
     }
 
 
@@ -113,29 +122,29 @@ async def update_engine_url(
 ):
 
     if form_data.AUTOMATIC1111_BASE_URL == None:
-        app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
+        app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
     else:
         url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
         try:
             r = requests.head(url)
-            app.state.AUTOMATIC1111_BASE_URL = url
+            app.state.config.AUTOMATIC1111_BASE_URL = url
         except Exception as e:
             raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
 
     if form_data.COMFYUI_BASE_URL == None:
-        app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
+        app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
     else:
         url = form_data.COMFYUI_BASE_URL.strip("/")
 
         try:
             r = requests.head(url)
-            app.state.COMFYUI_BASE_URL = url
+            app.state.config.COMFYUI_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,
-        "COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
+        "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
+        "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
         "status": True,
     }
 
@@ -148,8 +157,8 @@ class OpenAIConfigUpdateForm(BaseModel):
 @app.get("/openai/config")
 async def get_openai_config(user=Depends(get_admin_user)):
     return {
-        "OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
-        "OPENAI_API_KEY": app.state.OPENAI_API_KEY,
+        "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
+        "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
     }
 
 
@@ -160,13 +169,13 @@ async def update_openai_config(
     if form_data.key == "":
         raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
 
-    app.state.OPENAI_API_BASE_URL = form_data.url
-    app.state.OPENAI_API_KEY = form_data.key
+    app.state.config.OPENAI_API_BASE_URL = form_data.url
+    app.state.config.OPENAI_API_KEY = form_data.key
 
     return {
         "status": True,
-        "OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
-        "OPENAI_API_KEY": app.state.OPENAI_API_KEY,
+        "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
+        "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
     }
 
 
@@ -176,7 +185,7 @@ class ImageSizeUpdateForm(BaseModel):
 
 @app.get("/size")
 async def get_image_size(user=Depends(get_admin_user)):
-    return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
+    return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
 
 
 @app.post("/size/update")
@@ -185,9 +194,9 @@ async def update_image_size(
 ):
     pattern = r"^\d+x\d+$"  # Regular expression pattern
     if re.match(pattern, form_data.size):
-        app.state.IMAGE_SIZE = form_data.size
+        app.state.config.IMAGE_SIZE = form_data.size
         return {
-            "IMAGE_SIZE": app.state.IMAGE_SIZE,
+            "IMAGE_SIZE": app.state.config.IMAGE_SIZE,
             "status": True,
         }
     else:
@@ -203,7 +212,7 @@ class ImageStepsUpdateForm(BaseModel):
 
 @app.get("/steps")
 async def get_image_size(user=Depends(get_admin_user)):
-    return {"IMAGE_STEPS": app.state.IMAGE_STEPS}
+    return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
 
 
 @app.post("/steps/update")
@@ -211,9 +220,9 @@ 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
+        app.state.config.IMAGE_STEPS = form_data.steps
         return {
-            "IMAGE_STEPS": app.state.IMAGE_STEPS,
+            "IMAGE_STEPS": app.state.config.IMAGE_STEPS,
             "status": True,
         }
     else:
@@ -226,14 +235,14 @@ async def update_image_size(
 @app.get("/models")
 def get_models(user=Depends(get_current_user)):
     try:
-        if app.state.ENGINE == "openai":
+        if app.state.config.ENGINE == "openai":
             return [
                 {"id": "dall-e-2", "name": "DALL·E 2"},
                 {"id": "dall-e-3", "name": "DALL·E 3"},
             ]
-        elif app.state.ENGINE == "comfyui":
+        elif app.state.config.ENGINE == "comfyui":
 
-            r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
+            r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
             info = r.json()
 
             return list(
@@ -245,7 +254,7 @@ def get_models(user=Depends(get_current_user)):
 
         else:
             r = requests.get(
-                url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
+                url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
             )
             models = r.json()
             return list(
@@ -255,23 +264,29 @@ def get_models(user=Depends(get_current_user)):
                 )
             )
     except Exception as e:
-        app.state.ENABLED = False
+        app.state.config.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"}
-        elif app.state.ENGINE == "comfyui":
-            return {"model": app.state.MODEL if app.state.MODEL else ""}
+        if app.state.config.ENGINE == "openai":
+            return {
+                "model": (
+                    app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
+                )
+            }
+        elif app.state.config.ENGINE == "comfyui":
+            return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
         else:
-            r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
+            r = requests.get(
+                url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
+            )
             options = r.json()
             return {"model": options["sd_model_checkpoint"]}
     except Exception as e:
-        app.state.ENABLED = False
+        app.state.config.ENABLED = False
         raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
 
 
@@ -280,20 +295,20 @@ class UpdateModelForm(BaseModel):
 
 
 def set_model_handler(model: str):
-    if app.state.ENGINE == "openai":
-        app.state.MODEL = model
-        return app.state.MODEL
-    if app.state.ENGINE == "comfyui":
-        app.state.MODEL = model
-        return app.state.MODEL
+    if app.state.config.ENGINE in ["openai", "comfyui"]:
+        app.state.config.MODEL = model
+        return app.state.config.MODEL
     else:
-        r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
+        r = requests.get(
+            url=f"{app.state.config.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
+                url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
+                json=options,
             )
 
         return options
@@ -382,26 +397,32 @@ def generate_image(
     user=Depends(get_current_user),
 ):
 
-    width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
+    width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
 
     r = None
     try:
-        if app.state.ENGINE == "openai":
+        if app.state.config.ENGINE == "openai":
 
             headers = {}
-            headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
+            headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
             headers["Content-Type"] = "application/json"
 
             data = {
-                "model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
+                "model": (
+                    app.state.config.MODEL
+                    if app.state.config.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,
+                "size": (
+                    form_data.size if form_data.size else app.state.config.IMAGE_SIZE
+                ),
                 "response_format": "b64_json",
             }
 
             r = requests.post(
-                url=f"{app.state.OPENAI_API_BASE_URL}/images/generations",
+                url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
                 json=data,
                 headers=headers,
             )
@@ -421,7 +442,7 @@ def generate_image(
 
             return images
 
-        elif app.state.ENGINE == "comfyui":
+        elif app.state.config.ENGINE == "comfyui":
 
             data = {
                 "prompt": form_data.prompt,
@@ -430,19 +451,19 @@ def generate_image(
                 "n": form_data.n,
             }
 
-            if app.state.IMAGE_STEPS != None:
-                data["steps"] = app.state.IMAGE_STEPS
+            if app.state.config.IMAGE_STEPS is not None:
+                data["steps"] = app.state.config.IMAGE_STEPS
 
-            if form_data.negative_prompt != None:
+            if form_data.negative_prompt is not None:
                 data["negative_prompt"] = form_data.negative_prompt
 
             data = ImageGenerationPayload(**data)
 
             res = comfyui_generate_image(
-                app.state.MODEL,
+                app.state.config.MODEL,
                 data,
                 user.id,
-                app.state.COMFYUI_BASE_URL,
+                app.state.config.COMFYUI_BASE_URL,
             )
             log.debug(f"res: {res}")
 
@@ -469,14 +490,14 @@ def generate_image(
                 "height": height,
             }
 
-            if app.state.IMAGE_STEPS != None:
-                data["steps"] = app.state.IMAGE_STEPS
+            if app.state.config.IMAGE_STEPS is not None:
+                data["steps"] = app.state.config.IMAGE_STEPS
 
-            if form_data.negative_prompt != None:
+            if form_data.negative_prompt is not None:
                 data["negative_prompt"] = form_data.negative_prompt
 
             r = requests.post(
-                url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
+                url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
                 json=data,
             )
 

+ 15 - 12
backend/apps/litellm/main.py

@@ -1,4 +1,5 @@
 import sys
+from contextlib import asynccontextmanager
 
 from fastapi import FastAPI, Depends, HTTPException
 from fastapi.routing import APIRoute
@@ -46,7 +47,16 @@ import asyncio
 import subprocess
 import yaml
 
-app = FastAPI()
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    log.info("startup_event")
+    # TODO: Check config.yaml file and create one
+    asyncio.create_task(start_litellm_background())
+    yield
+
+
+app = FastAPI(lifespan=lifespan)
 
 origins = ["*"]
 
@@ -65,6 +75,10 @@ with open(LITELLM_CONFIG_DIR, "r") as file:
     litellm_config = yaml.safe_load(file)
 
 
+app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
+app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
+
+
 app.state.ENABLE = ENABLE_LITELLM
 app.state.CONFIG = litellm_config
 
@@ -141,17 +155,6 @@ async def shutdown_litellm_background():
         background_process = None
 
 
-@app.on_event("startup")
-async def startup_event():
-    log.info("startup_event")
-    # TODO: Check config.yaml file and create one
-    asyncio.create_task(start_litellm_background())
-
-
-app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
-
-
 @app.get("/")
 async def get_status():
     return {"status": True}

+ 39 - 32
backend/apps/ollama/main.py

@@ -46,6 +46,7 @@ from config import (
     ENABLE_MODEL_FILTER,
     MODEL_FILTER_LIST,
     UPLOAD_DIR,
+    AppConfig,
 )
 from utils.misc import calculate_sha256
 
@@ -61,11 +62,12 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
+app.state.config = AppConfig()
 
-app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
+app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
+app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
-app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
+app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
 app.state.MODELS = {}
 
 
@@ -96,7 +98,7 @@ async def get_status():
 
 @app.get("/urls")
 async def get_ollama_api_urls(user=Depends(get_admin_user)):
-    return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
+    return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
 
 
 class UrlUpdateForm(BaseModel):
@@ -105,10 +107,10 @@ class UrlUpdateForm(BaseModel):
 
 @app.post("/urls/update")
 async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
-    app.state.OLLAMA_BASE_URLS = form_data.urls
+    app.state.config.OLLAMA_BASE_URLS = form_data.urls
 
-    log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}")
-    return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
+    log.info(f"app.state.config.OLLAMA_BASE_URLS: {app.state.config.OLLAMA_BASE_URLS}")
+    return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
 
 
 @app.get("/cancel/{request_id}")
@@ -122,8 +124,9 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user))
 
 
 async def fetch_url(url):
+    timeout = aiohttp.ClientTimeout(total=5)
     try:
-        async with aiohttp.ClientSession() as session:
+        async with aiohttp.ClientSession(timeout=timeout) as session:
             async with session.get(url) as response:
                 return await response.json()
     except Exception as e:
@@ -153,7 +156,7 @@ def merge_models_lists(model_lists):
 
 async def get_all_models():
     log.info("get_all_models()")
-    tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
+    tasks = [fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS]
     responses = await asyncio.gather(*tasks)
 
     models = {
@@ -175,18 +178,19 @@ async def get_ollama_tags(
     if url_idx == None:
         models = await get_all_models()
 
-        if app.state.ENABLE_MODEL_FILTER:
+        if app.state.config.ENABLE_MODEL_FILTER:
             if user.role == "user":
                 models["models"] = list(
                     filter(
-                        lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
+                        lambda model: model["name"]
+                        in app.state.config.MODEL_FILTER_LIST,
                         models["models"],
                     )
                 )
                 return models
         return models
     else:
-        url = app.state.OLLAMA_BASE_URLS[url_idx]
+        url = app.state.config.OLLAMA_BASE_URLS[url_idx]
         try:
             r = requests.request(method="GET", url=f"{url}/api/tags")
             r.raise_for_status()
@@ -216,7 +220,9 @@ 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]
+        tasks = [
+            fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
+        ]
         responses = await asyncio.gather(*tasks)
         responses = list(filter(lambda x: x is not None, responses))
 
@@ -235,7 +241,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
                 detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
             )
     else:
-        url = app.state.OLLAMA_BASE_URLS[url_idx]
+        url = app.state.config.OLLAMA_BASE_URLS[url_idx]
         try:
             r = requests.request(method="GET", url=f"{url}/api/version")
             r.raise_for_status()
@@ -267,7 +273,7 @@ class ModelNameForm(BaseModel):
 async def pull_model(
     form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
 ):
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     r = None
@@ -355,7 +361,7 @@ async def push_model(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.debug(f"url: {url}")
 
     r = None
@@ -417,7 +423,7 @@ async def create_model(
     form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
 ):
     log.debug(f"form_data: {form_data}")
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     r = None
@@ -490,7 +496,7 @@ async def copy_model(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     try:
@@ -537,7 +543,7 @@ async def delete_model(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     try:
@@ -577,7 +583,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
         )
 
     url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     try:
@@ -634,7 +640,7 @@ async def generate_embeddings(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     try:
@@ -684,7 +690,7 @@ def generate_ollama_embeddings(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     try:
@@ -753,7 +759,7 @@ async def generate_completion(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     r = None
@@ -856,7 +862,7 @@ async def generate_chat_completion(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     r = None
@@ -965,7 +971,7 @@ async def generate_openai_chat_completion(
                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
             )
 
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
     r = None
@@ -1041,11 +1047,12 @@ async def get_openai_models(
     if url_idx == None:
         models = await get_all_models()
 
-        if app.state.ENABLE_MODEL_FILTER:
+        if app.state.config.ENABLE_MODEL_FILTER:
             if user.role == "user":
                 models["models"] = list(
                     filter(
-                        lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
+                        lambda model: model["name"]
+                        in app.state.config.MODEL_FILTER_LIST,
                         models["models"],
                     )
                 )
@@ -1064,7 +1071,7 @@ async def get_openai_models(
         }
 
     else:
-        url = app.state.OLLAMA_BASE_URLS[url_idx]
+        url = app.state.config.OLLAMA_BASE_URLS[url_idx]
         try:
             r = requests.request(method="GET", url=f"{url}/api/tags")
             r.raise_for_status()
@@ -1198,7 +1205,7 @@ async def download_model(
 
     if url_idx == None:
         url_idx = 0
-    url = app.state.OLLAMA_BASE_URLS[url_idx]
+    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
 
     file_name = parse_huggingface_url(form_data.url)
 
@@ -1217,7 +1224,7 @@ async def download_model(
 def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
     if url_idx == None:
         url_idx = 0
-    ollama_url = app.state.OLLAMA_BASE_URLS[url_idx]
+    ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
 
     file_path = f"{UPLOAD_DIR}/{file.filename}"
 
@@ -1282,7 +1289,7 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
 # async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
 #     if url_idx == None:
 #         url_idx = 0
-#     url = app.state.OLLAMA_BASE_URLS[url_idx]
+#     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
 
 #     file_location = os.path.join(UPLOAD_DIR, file.filename)
 #     total_size = file.size
@@ -1319,7 +1326,7 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
 async def deprecated_proxy(
     path: str, request: Request, user=Depends(get_verified_user)
 ):
-    url = app.state.OLLAMA_BASE_URLS[0]
+    url = app.state.config.OLLAMA_BASE_URLS[0]
     target_url = f"{url}/{path}"
 
     body = await request.body()

+ 59 - 27
backend/apps/openai/main.py

@@ -21,11 +21,13 @@ from utils.utils import (
 )
 from config import (
     SRC_LOG_LEVELS,
+    ENABLE_OPENAI_API,
     OPENAI_API_BASE_URLS,
     OPENAI_API_KEYS,
     CACHE_DIR,
     ENABLE_MODEL_FILTER,
     MODEL_FILTER_LIST,
+    AppConfig,
 )
 from typing import List, Optional
 
@@ -45,11 +47,16 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
-app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
-app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
-app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
+app.state.config = AppConfig()
+
+app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
+app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
+
+
+app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
+app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
+app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
 
 app.state.MODELS = {}
 
@@ -65,6 +72,21 @@ async def check_url(request: Request, call_next):
     return response
 
 
+@app.get("/config")
+async def get_config(user=Depends(get_admin_user)):
+    return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
+
+
+class OpenAIConfigForm(BaseModel):
+    enable_openai_api: Optional[bool] = None
+
+
+@app.post("/config/update")
+async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
+    app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
+    return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
+
+
 class UrlsUpdateForm(BaseModel):
     urls: List[str]
 
@@ -75,32 +97,32 @@ class KeysUpdateForm(BaseModel):
 
 @app.get("/urls")
 async def get_openai_urls(user=Depends(get_admin_user)):
-    return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
+    return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
 
 
 @app.post("/urls/update")
 async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
     await get_all_models()
-    app.state.OPENAI_API_BASE_URLS = form_data.urls
-    return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
+    app.state.config.OPENAI_API_BASE_URLS = form_data.urls
+    return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
 
 
 @app.get("/keys")
 async def get_openai_keys(user=Depends(get_admin_user)):
-    return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
+    return {"OPENAI_API_KEYS": app.state.config.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.state.config.OPENAI_API_KEYS = form_data.keys
+    return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
 
 
 @app.post("/audio/speech")
 async def speech(request: Request, user=Depends(get_verified_user)):
     idx = None
     try:
-        idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
+        idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
         body = await request.body()
         name = hashlib.sha256(body).hexdigest()
 
@@ -114,13 +136,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
             return FileResponse(file_path)
 
         headers = {}
-        headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
+        headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
         headers["Content-Type"] = "application/json"
-
+        if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
+            headers["HTTP-Referer"] = "https://openwebui.com/"
+            headers["X-Title"] = "Open WebUI"
         r = None
         try:
             r = requests.post(
-                url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
+                url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
                 data=body,
                 headers=headers,
                 stream=True,
@@ -159,11 +183,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
 
 
 async def fetch_url(url, key):
+    timeout = aiohttp.ClientTimeout(total=5)
     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()
+        if key != "":
+            headers = {"Authorization": f"Bearer {key}"}
+            async with aiohttp.ClientSession(timeout=timeout) as session:
+                async with session.get(url, headers=headers) as response:
+                    return await response.json()
+        else:
+            return None
     except Exception as e:
         # Handle connection error here
         log.error(f"Connection error: {e}")
@@ -180,7 +208,8 @@ def merge_models_lists(model_lists):
                 [
                     {**model, "urlIdx": idx}
                     for model in models
-                    if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx]
+                    if "api.openai.com"
+                    not in app.state.config.OPENAI_API_BASE_URLS[idx]
                     or "gpt" in model["id"]
                 ]
             )
@@ -191,12 +220,15 @@ def merge_models_lists(model_lists):
 async def get_all_models():
     log.info("get_all_models()")
 
-    if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
+    if (
+        len(app.state.config.OPENAI_API_KEYS) == 1
+        and app.state.config.OPENAI_API_KEYS[0] == ""
+    ) or not app.state.config.ENABLE_OPENAI_API:
         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)
+            fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
+            for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
         ]
 
         responses = await asyncio.gather(*tasks)
@@ -228,18 +260,18 @@ async def get_all_models():
 async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
     if url_idx == None:
         models = await get_all_models()
-        if app.state.ENABLE_MODEL_FILTER:
+        if app.state.config.ENABLE_MODEL_FILTER:
             if user.role == "user":
                 models["data"] = list(
                     filter(
-                        lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
+                        lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
                         models["data"],
                     )
                 )
                 return models
         return models
     else:
-        url = app.state.OPENAI_API_BASE_URLS[url_idx]
+        url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
 
         r = None
 
@@ -303,8 +335,8 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
     except json.JSONDecodeError as e:
         log.error("Error loading request body into a dictionary:", e)
 
-    url = app.state.OPENAI_API_BASE_URLS[idx]
-    key = app.state.OPENAI_API_KEYS[idx]
+    url = app.state.config.OPENAI_API_BASE_URLS[idx]
+    key = app.state.config.OPENAI_API_KEYS[idx]
 
     target_url = f"{url}/{path}"
 

+ 138 - 107
backend/apps/rag/main.py

@@ -69,6 +69,7 @@ from utils.misc import (
 from utils.utils import get_current_user, get_admin_user
 
 from config import (
+    ENV,
     SRC_LOG_LEVELS,
     UPLOAD_DIR,
     DOCS_DIR,
@@ -93,6 +94,7 @@ from config import (
     RAG_TEMPLATE,
     ENABLE_RAG_LOCAL_WEB_FETCH,
     YOUTUBE_LOADER_LANGUAGE,
+    AppConfig,
 )
 
 from constants import ERROR_MESSAGES
@@ -102,30 +104,32 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 app = FastAPI()
 
-app.state.TOP_K = RAG_TOP_K
-app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
+app.state.config = AppConfig()
 
-app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
-app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
+app.state.config.TOP_K = RAG_TOP_K
+app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
+
+app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
+app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
     ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
 )
 
-app.state.CHUNK_SIZE = CHUNK_SIZE
-app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
+app.state.config.CHUNK_SIZE = CHUNK_SIZE
+app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
 
-app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
-app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
-app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
-app.state.RAG_TEMPLATE = RAG_TEMPLATE
+app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
+app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
+app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
+app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
 
 
-app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
-app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
+app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
+app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
 
-app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
+app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
 
 
-app.state.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
+app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
 app.state.YOUTUBE_LOADER_TRANSLATION = None
 
 
@@ -133,7 +137,7 @@ def update_embedding_model(
     embedding_model: str,
     update_model: bool = False,
 ):
-    if embedding_model and app.state.RAG_EMBEDDING_ENGINE == "":
+    if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
         app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
             get_model_path(embedding_model, update_model),
             device=DEVICE_TYPE,
@@ -158,22 +162,22 @@ def update_reranking_model(
 
 
 update_embedding_model(
-    app.state.RAG_EMBEDDING_MODEL,
+    app.state.config.RAG_EMBEDDING_MODEL,
     RAG_EMBEDDING_MODEL_AUTO_UPDATE,
 )
 
 update_reranking_model(
-    app.state.RAG_RERANKING_MODEL,
+    app.state.config.RAG_RERANKING_MODEL,
     RAG_RERANKING_MODEL_AUTO_UPDATE,
 )
 
 
 app.state.EMBEDDING_FUNCTION = get_embedding_function(
-    app.state.RAG_EMBEDDING_ENGINE,
-    app.state.RAG_EMBEDDING_MODEL,
+    app.state.config.RAG_EMBEDDING_ENGINE,
+    app.state.config.RAG_EMBEDDING_MODEL,
     app.state.sentence_transformer_ef,
-    app.state.OPENAI_API_KEY,
-    app.state.OPENAI_API_BASE_URL,
+    app.state.config.OPENAI_API_KEY,
+    app.state.config.OPENAI_API_BASE_URL,
 )
 
 origins = ["*"]
@@ -200,12 +204,12 @@ class UrlForm(CollectionNameForm):
 async def get_status():
     return {
         "status": True,
-        "chunk_size": app.state.CHUNK_SIZE,
-        "chunk_overlap": app.state.CHUNK_OVERLAP,
-        "template": app.state.RAG_TEMPLATE,
-        "embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
-        "embedding_model": app.state.RAG_EMBEDDING_MODEL,
-        "reranking_model": app.state.RAG_RERANKING_MODEL,
+        "chunk_size": app.state.config.CHUNK_SIZE,
+        "chunk_overlap": app.state.config.CHUNK_OVERLAP,
+        "template": app.state.config.RAG_TEMPLATE,
+        "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
+        "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
+        "reranking_model": app.state.config.RAG_RERANKING_MODEL,
     }
 
 
@@ -213,18 +217,21 @@ async def get_status():
 async def get_embedding_config(user=Depends(get_admin_user)):
     return {
         "status": True,
-        "embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
-        "embedding_model": app.state.RAG_EMBEDDING_MODEL,
+        "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
+        "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
         "openai_config": {
-            "url": app.state.OPENAI_API_BASE_URL,
-            "key": app.state.OPENAI_API_KEY,
+            "url": app.state.config.OPENAI_API_BASE_URL,
+            "key": app.state.config.OPENAI_API_KEY,
         },
     }
 
 
 @app.get("/reranking")
 async def get_reraanking_config(user=Depends(get_admin_user)):
-    return {"status": True, "reranking_model": app.state.RAG_RERANKING_MODEL}
+    return {
+        "status": True,
+        "reranking_model": app.state.config.RAG_RERANKING_MODEL,
+    }
 
 
 class OpenAIConfigForm(BaseModel):
@@ -243,34 +250,34 @@ async def update_embedding_config(
     form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
 ):
     log.info(
-        f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
+        f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
     )
     try:
-        app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
-        app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
+        app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
+        app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
 
-        if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
+        if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
             if form_data.openai_config != None:
-                app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
-                app.state.OPENAI_API_KEY = form_data.openai_config.key
+                app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
+                app.state.config.OPENAI_API_KEY = form_data.openai_config.key
 
-        update_embedding_model(app.state.RAG_EMBEDDING_MODEL, True)
+        update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
 
         app.state.EMBEDDING_FUNCTION = get_embedding_function(
-            app.state.RAG_EMBEDDING_ENGINE,
-            app.state.RAG_EMBEDDING_MODEL,
+            app.state.config.RAG_EMBEDDING_ENGINE,
+            app.state.config.RAG_EMBEDDING_MODEL,
             app.state.sentence_transformer_ef,
-            app.state.OPENAI_API_KEY,
-            app.state.OPENAI_API_BASE_URL,
+            app.state.config.OPENAI_API_KEY,
+            app.state.config.OPENAI_API_BASE_URL,
         )
 
         return {
             "status": True,
-            "embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
-            "embedding_model": app.state.RAG_EMBEDDING_MODEL,
+            "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
+            "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
             "openai_config": {
-                "url": app.state.OPENAI_API_BASE_URL,
-                "key": app.state.OPENAI_API_KEY,
+                "url": app.state.config.OPENAI_API_BASE_URL,
+                "key": app.state.config.OPENAI_API_KEY,
             },
         }
     except Exception as e:
@@ -290,16 +297,16 @@ async def update_reranking_config(
     form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
 ):
     log.info(
-        f"Updating reranking model: {app.state.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
+        f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
     )
     try:
-        app.state.RAG_RERANKING_MODEL = form_data.reranking_model
+        app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
 
-        update_reranking_model(app.state.RAG_RERANKING_MODEL, True)
+        update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
 
         return {
             "status": True,
-            "reranking_model": app.state.RAG_RERANKING_MODEL,
+            "reranking_model": app.state.config.RAG_RERANKING_MODEL,
         }
     except Exception as e:
         log.exception(f"Problem updating reranking model: {e}")
@@ -313,14 +320,14 @@ async def update_reranking_config(
 async def get_rag_config(user=Depends(get_admin_user)):
     return {
         "status": True,
-        "pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
+        "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
         "chunk": {
-            "chunk_size": app.state.CHUNK_SIZE,
-            "chunk_overlap": app.state.CHUNK_OVERLAP,
+            "chunk_size": app.state.config.CHUNK_SIZE,
+            "chunk_overlap": app.state.config.CHUNK_OVERLAP,
         },
-        "web_loader_ssl_verification": app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+        "web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
         "youtube": {
-            "language": app.state.YOUTUBE_LOADER_LANGUAGE,
+            "language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
             "translation": app.state.YOUTUBE_LOADER_TRANSLATION,
         },
     }
@@ -345,50 +352,52 @@ class ConfigUpdateForm(BaseModel):
 
 @app.post("/config/update")
 async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
-    app.state.PDF_EXTRACT_IMAGES = (
+    app.state.config.PDF_EXTRACT_IMAGES = (
         form_data.pdf_extract_images
-        if form_data.pdf_extract_images != None
-        else app.state.PDF_EXTRACT_IMAGES
+        if form_data.pdf_extract_images is not None
+        else app.state.config.PDF_EXTRACT_IMAGES
     )
 
-    app.state.CHUNK_SIZE = (
-        form_data.chunk.chunk_size if form_data.chunk != None else app.state.CHUNK_SIZE
+    app.state.config.CHUNK_SIZE = (
+        form_data.chunk.chunk_size
+        if form_data.chunk is not None
+        else app.state.config.CHUNK_SIZE
     )
 
-    app.state.CHUNK_OVERLAP = (
+    app.state.config.CHUNK_OVERLAP = (
         form_data.chunk.chunk_overlap
-        if form_data.chunk != None
-        else app.state.CHUNK_OVERLAP
+        if form_data.chunk is not None
+        else app.state.config.CHUNK_OVERLAP
     )
 
-    app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
+    app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
         form_data.web_loader_ssl_verification
         if form_data.web_loader_ssl_verification != None
-        else app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
+        else app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
     )
 
-    app.state.YOUTUBE_LOADER_LANGUAGE = (
+    app.state.config.YOUTUBE_LOADER_LANGUAGE = (
         form_data.youtube.language
-        if form_data.youtube != None
-        else app.state.YOUTUBE_LOADER_LANGUAGE
+        if form_data.youtube is not None
+        else app.state.config.YOUTUBE_LOADER_LANGUAGE
     )
 
     app.state.YOUTUBE_LOADER_TRANSLATION = (
         form_data.youtube.translation
-        if form_data.youtube != None
+        if form_data.youtube is not None
         else app.state.YOUTUBE_LOADER_TRANSLATION
     )
 
     return {
         "status": True,
-        "pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
+        "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
         "chunk": {
-            "chunk_size": app.state.CHUNK_SIZE,
-            "chunk_overlap": app.state.CHUNK_OVERLAP,
+            "chunk_size": app.state.config.CHUNK_SIZE,
+            "chunk_overlap": app.state.config.CHUNK_OVERLAP,
         },
-        "web_loader_ssl_verification": app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+        "web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
         "youtube": {
-            "language": app.state.YOUTUBE_LOADER_LANGUAGE,
+            "language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
             "translation": app.state.YOUTUBE_LOADER_TRANSLATION,
         },
     }
@@ -398,7 +407,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
 async def get_rag_template(user=Depends(get_current_user)):
     return {
         "status": True,
-        "template": app.state.RAG_TEMPLATE,
+        "template": app.state.config.RAG_TEMPLATE,
     }
 
 
@@ -406,10 +415,10 @@ async def get_rag_template(user=Depends(get_current_user)):
 async def get_query_settings(user=Depends(get_admin_user)):
     return {
         "status": True,
-        "template": app.state.RAG_TEMPLATE,
-        "k": app.state.TOP_K,
-        "r": app.state.RELEVANCE_THRESHOLD,
-        "hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
+        "template": app.state.config.RAG_TEMPLATE,
+        "k": app.state.config.TOP_K,
+        "r": app.state.config.RELEVANCE_THRESHOLD,
+        "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
     }
 
 
@@ -424,16 +433,20 @@ class QuerySettingsForm(BaseModel):
 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
-    app.state.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
-    app.state.ENABLE_RAG_HYBRID_SEARCH = form_data.hybrid if form_data.hybrid else False
+    app.state.config.RAG_TEMPLATE = (
+        form_data.template if form_data.template else RAG_TEMPLATE
+    )
+    app.state.config.TOP_K = form_data.k if form_data.k else 4
+    app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
+    app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
+        form_data.hybrid if form_data.hybrid else False
+    )
     return {
         "status": True,
-        "template": app.state.RAG_TEMPLATE,
-        "k": app.state.TOP_K,
-        "r": app.state.RELEVANCE_THRESHOLD,
-        "hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
+        "template": app.state.config.RAG_TEMPLATE,
+        "k": app.state.config.TOP_K,
+        "r": app.state.config.RELEVANCE_THRESHOLD,
+        "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
     }
 
 
@@ -451,21 +464,23 @@ def query_doc_handler(
     user=Depends(get_current_user),
 ):
     try:
-        if app.state.ENABLE_RAG_HYBRID_SEARCH:
+        if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
             return query_doc_with_hybrid_search(
                 collection_name=form_data.collection_name,
                 query=form_data.query,
                 embedding_function=app.state.EMBEDDING_FUNCTION,
-                k=form_data.k if form_data.k else app.state.TOP_K,
+                k=form_data.k if form_data.k else app.state.config.TOP_K,
                 reranking_function=app.state.sentence_transformer_rf,
-                r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
+                r=(
+                    form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
+                ),
             )
         else:
             return query_doc(
                 collection_name=form_data.collection_name,
                 query=form_data.query,
                 embedding_function=app.state.EMBEDDING_FUNCTION,
-                k=form_data.k if form_data.k else app.state.TOP_K,
+                k=form_data.k if form_data.k else app.state.config.TOP_K,
             )
     except Exception as e:
         log.exception(e)
@@ -489,21 +504,23 @@ def query_collection_handler(
     user=Depends(get_current_user),
 ):
     try:
-        if app.state.ENABLE_RAG_HYBRID_SEARCH:
+        if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
             return query_collection_with_hybrid_search(
                 collection_names=form_data.collection_names,
                 query=form_data.query,
                 embedding_function=app.state.EMBEDDING_FUNCTION,
-                k=form_data.k if form_data.k else app.state.TOP_K,
+                k=form_data.k if form_data.k else app.state.config.TOP_K,
                 reranking_function=app.state.sentence_transformer_rf,
-                r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
+                r=(
+                    form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
+                ),
             )
         else:
             return query_collection(
                 collection_names=form_data.collection_names,
                 query=form_data.query,
                 embedding_function=app.state.EMBEDDING_FUNCTION,
-                k=form_data.k if form_data.k else app.state.TOP_K,
+                k=form_data.k if form_data.k else app.state.config.TOP_K,
             )
 
     except Exception as e:
@@ -520,7 +537,7 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
         loader = YoutubeLoader.from_youtube_url(
             form_data.url,
             add_video_info=True,
-            language=app.state.YOUTUBE_LOADER_LANGUAGE,
+            language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
             translation=app.state.YOUTUBE_LOADER_TRANSLATION,
         )
         data = loader.load()
@@ -548,7 +565,8 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
     # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
     try:
         loader = get_web_loader(
-            form_data.url, verify_ssl=app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
+            form_data.url,
+            verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
         )
         data = loader.load()
 
@@ -604,8 +622,8 @@ def resolve_hostname(hostname):
 def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
 
     text_splitter = RecursiveCharacterTextSplitter(
-        chunk_size=app.state.CHUNK_SIZE,
-        chunk_overlap=app.state.CHUNK_OVERLAP,
+        chunk_size=app.state.config.CHUNK_SIZE,
+        chunk_overlap=app.state.config.CHUNK_OVERLAP,
         add_start_index=True,
     )
 
@@ -622,8 +640,8 @@ def store_text_in_vector_db(
     text, metadata, collection_name, overwrite: bool = False
 ) -> bool:
     text_splitter = RecursiveCharacterTextSplitter(
-        chunk_size=app.state.CHUNK_SIZE,
-        chunk_overlap=app.state.CHUNK_OVERLAP,
+        chunk_size=app.state.config.CHUNK_SIZE,
+        chunk_overlap=app.state.config.CHUNK_OVERLAP,
         add_start_index=True,
     )
     docs = text_splitter.create_documents([text], metadatas=[metadata])
@@ -646,11 +664,11 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
         collection = CHROMA_CLIENT.create_collection(name=collection_name)
 
         embedding_func = get_embedding_function(
-            app.state.RAG_EMBEDDING_ENGINE,
-            app.state.RAG_EMBEDDING_MODEL,
+            app.state.config.RAG_EMBEDDING_ENGINE,
+            app.state.config.RAG_EMBEDDING_MODEL,
             app.state.sentence_transformer_ef,
-            app.state.OPENAI_API_KEY,
-            app.state.OPENAI_API_BASE_URL,
+            app.state.config.OPENAI_API_KEY,
+            app.state.config.OPENAI_API_BASE_URL,
         )
 
         embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
@@ -724,7 +742,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
     ]
 
     if file_ext == "pdf":
-        loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
+        loader = PyPDFLoader(
+            file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
+        )
     elif file_ext == "csv":
         loader = CSVLoader(file_path)
     elif file_ext == "rst":
@@ -932,3 +952,14 @@ def reset(user=Depends(get_admin_user)) -> bool:
         log.exception(e)
 
     return True
+
+
+if ENV == "dev":
+
+    @app.get("/ef")
+    async def get_embeddings():
+        return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
+
+    @app.get("/ef/{text}")
+    async def get_embeddings_text(text: str):
+        return {"result": app.state.EMBEDDING_FUNCTION(text)}

+ 53 - 0
backend/apps/web/internal/migrations/008_add_memory.py

@@ -0,0 +1,53 @@
+"""Peewee migrations -- 002_add_local_sharing.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    @migrator.create_model
+    class Memory(pw.Model):
+        id = pw.CharField(max_length=255, unique=True)
+        user_id = pw.CharField(max_length=255)
+        content = pw.TextField(null=False)
+        updated_at = pw.BigIntegerField(null=False)
+        created_at = pw.BigIntegerField(null=False)
+
+        class Meta:
+            table_name = "memory"
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    migrator.remove_model("memory")

+ 18 - 9
backend/apps/web/main.py

@@ -9,6 +9,7 @@ from apps.web.routers import (
     modelfiles,
     prompts,
     configs,
+    memories,
     utils,
 )
 from config import (
@@ -21,22 +22,27 @@ from config import (
     USER_PERMISSIONS,
     WEBHOOK_URL,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
+    JWT_EXPIRES_IN,
+    AppConfig,
 )
 
 app = FastAPI()
 
 origins = ["*"]
 
-app.state.ENABLE_SIGNUP = ENABLE_SIGNUP
-app.state.JWT_EXPIRES_IN = "-1"
+app.state.config = AppConfig()
 
-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.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
+app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
+
+app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
+app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
+app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
+app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
+app.state.config.WEBHOOK_URL = WEBHOOK_URL
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 
+
 app.add_middleware(
     CORSMiddleware,
     allow_origins=origins,
@@ -48,9 +54,12 @@ app.add_middleware(
 app.include_router(auths.router, prefix="/auths", tags=["auths"])
 app.include_router(users.router, prefix="/users", tags=["users"])
 app.include_router(chats.router, prefix="/chats", tags=["chats"])
+
 app.include_router(documents.router, prefix="/documents", tags=["documents"])
 app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
 app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
+app.include_router(memories.router, prefix="/memories", tags=["memories"])
+
 
 app.include_router(configs.router, prefix="/configs", tags=["configs"])
 app.include_router(utils.router, prefix="/utils", tags=["utils"])
@@ -61,6 +70,6 @@ async def get_status():
     return {
         "status": True,
         "auth": WEBUI_AUTH,
-        "default_models": app.state.DEFAULT_MODELS,
-        "default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,
+        "default_models": app.state.config.DEFAULT_MODELS,
+        "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
     }

+ 118 - 0
backend/apps/web/models/memories.py

@@ -0,0 +1,118 @@
+from pydantic import BaseModel
+from peewee import *
+from playhouse.shortcuts import model_to_dict
+from typing import List, Union, Optional
+
+from apps.web.internal.db import DB
+from apps.web.models.chats import Chats
+
+import time
+import uuid
+
+####################
+# Memory DB Schema
+####################
+
+
+class Memory(Model):
+    id = CharField(unique=True)
+    user_id = CharField()
+    content = TextField()
+    updated_at = BigIntegerField()
+    created_at = BigIntegerField()
+
+    class Meta:
+        database = DB
+
+
+class MemoryModel(BaseModel):
+    id: str
+    user_id: str
+    content: str
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class MemoriesTable:
+    def __init__(self, db):
+        self.db = db
+        self.db.create_tables([Memory])
+
+    def insert_new_memory(
+        self,
+        user_id: str,
+        content: str,
+    ) -> Optional[MemoryModel]:
+        id = str(uuid.uuid4())
+
+        memory = MemoryModel(
+            **{
+                "id": id,
+                "user_id": user_id,
+                "content": content,
+                "created_at": int(time.time()),
+                "updated_at": int(time.time()),
+            }
+        )
+        result = Memory.create(**memory.model_dump())
+        if result:
+            return memory
+        else:
+            return None
+
+    def get_memories(self) -> List[MemoryModel]:
+        try:
+            memories = Memory.select()
+            return [MemoryModel(**model_to_dict(memory)) for memory in memories]
+        except:
+            return None
+
+    def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
+        try:
+            memories = Memory.select().where(Memory.user_id == user_id)
+            return [MemoryModel(**model_to_dict(memory)) for memory in memories]
+        except:
+            return None
+
+    def get_memory_by_id(self, id) -> Optional[MemoryModel]:
+        try:
+            memory = Memory.get(Memory.id == id)
+            return MemoryModel(**model_to_dict(memory))
+        except:
+            return None
+
+    def delete_memory_by_id(self, id: str) -> bool:
+        try:
+            query = Memory.delete().where(Memory.id == id)
+            query.execute()  # Remove the rows, return number of rows removed.
+
+            return True
+
+        except:
+            return False
+
+    def delete_memories_by_user_id(self, user_id: str) -> bool:
+        try:
+            query = Memory.delete().where(Memory.user_id == user_id)
+            query.execute()
+
+            return True
+        except:
+            return False
+
+    def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
+        try:
+            query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
+            query.execute()
+
+            return True
+        except:
+            return False
+
+
+Memories = MemoriesTable(DB)

+ 16 - 16
backend/apps/web/routers/auths.py

@@ -140,7 +140,7 @@ async def signin(request: Request, form_data: SigninForm):
     if user:
         token = create_token(
             data={"id": user.id},
-            expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
+            expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
         )
 
         return {
@@ -163,7 +163,7 @@ async def signin(request: Request, form_data: SigninForm):
 
 @router.post("/signup", response_model=SigninResponse)
 async def signup(request: Request, form_data: SignupForm):
-    if not request.app.state.ENABLE_SIGNUP and WEBUI_AUTH:
+    if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
         raise HTTPException(
             status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
         )
@@ -180,7 +180,7 @@ async def signup(request: Request, form_data: SignupForm):
         role = (
             "admin"
             if Users.get_num_users() == 0
-            else request.app.state.DEFAULT_USER_ROLE
+            else request.app.state.config.DEFAULT_USER_ROLE
         )
         hashed = get_password_hash(form_data.password)
         user = Auths.insert_new_auth(
@@ -194,13 +194,13 @@ async def signup(request: Request, form_data: SignupForm):
         if user:
             token = create_token(
                 data={"id": user.id},
-                expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
+                expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
             )
             # response.set_cookie(key='token', value=token, httponly=True)
 
-            if request.app.state.WEBHOOK_URL:
+            if request.app.state.config.WEBHOOK_URL:
                 post_webhook(
-                    request.app.state.WEBHOOK_URL,
+                    request.app.state.config.WEBHOOK_URL,
                     WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                     {
                         "action": "signup",
@@ -276,13 +276,13 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
 
 @router.get("/signup/enabled", response_model=bool)
 async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
-    return request.app.state.ENABLE_SIGNUP
+    return request.app.state.config.ENABLE_SIGNUP
 
 
 @router.get("/signup/enabled/toggle", response_model=bool)
 async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
-    request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP
-    return request.app.state.ENABLE_SIGNUP
+    request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
+    return request.app.state.config.ENABLE_SIGNUP
 
 
 ############################
@@ -292,7 +292,7 @@ async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
 
 @router.get("/signup/user/role")
 async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
-    return request.app.state.DEFAULT_USER_ROLE
+    return request.app.state.config.DEFAULT_USER_ROLE
 
 
 class UpdateRoleForm(BaseModel):
@@ -304,8 +304,8 @@ async def update_default_user_role(
     request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
 ):
     if form_data.role in ["pending", "user", "admin"]:
-        request.app.state.DEFAULT_USER_ROLE = form_data.role
-    return request.app.state.DEFAULT_USER_ROLE
+        request.app.state.config.DEFAULT_USER_ROLE = form_data.role
+    return request.app.state.config.DEFAULT_USER_ROLE
 
 
 ############################
@@ -315,7 +315,7 @@ async def update_default_user_role(
 
 @router.get("/token/expires")
 async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
-    return request.app.state.JWT_EXPIRES_IN
+    return request.app.state.config.JWT_EXPIRES_IN
 
 
 class UpdateJWTExpiresDurationForm(BaseModel):
@@ -332,10 +332,10 @@ async def update_token_expires_duration(
 
     # 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
+        request.app.state.config.JWT_EXPIRES_IN = form_data.duration
+        return request.app.state.config.JWT_EXPIRES_IN
     else:
-        return request.app.state.JWT_EXPIRES_IN
+        return request.app.state.config.JWT_EXPIRES_IN
 
 
 ############################

+ 2 - 2
backend/apps/web/routers/chats.py

@@ -58,7 +58,7 @@ 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"]
+        and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]
     ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -266,7 +266,7 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
         result = Chats.delete_chat_by_id(id)
         return result
     else:
-        if not request.app.state.USER_PERMISSIONS["chat"]["deletion"]:
+        if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]:
             raise HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
                 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,

+ 4 - 4
backend/apps/web/routers/configs.py

@@ -44,8 +44,8 @@ class SetDefaultSuggestionsForm(BaseModel):
 async def set_global_default_models(
     request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user)
 ):
-    request.app.state.DEFAULT_MODELS = form_data.models
-    return request.app.state.DEFAULT_MODELS
+    request.app.state.config.DEFAULT_MODELS = form_data.models
+    return request.app.state.config.DEFAULT_MODELS
 
 
 @router.post("/default/suggestions", response_model=List[PromptSuggestion])
@@ -55,5 +55,5 @@ async def set_global_default_suggestions(
     user=Depends(get_admin_user),
 ):
     data = form_data.model_dump()
-    request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
-    return request.app.state.DEFAULT_PROMPT_SUGGESTIONS
+    request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
+    return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS

+ 145 - 0
backend/apps/web/routers/memories.py

@@ -0,0 +1,145 @@
+from fastapi import Response, Request
+from fastapi import Depends, FastAPI, HTTPException, status
+from datetime import datetime, timedelta
+from typing import List, Union, Optional
+
+from fastapi import APIRouter
+from pydantic import BaseModel
+import logging
+
+from apps.web.models.memories import Memories, MemoryModel
+
+from utils.utils import get_verified_user
+from constants import ERROR_MESSAGES
+
+from config import SRC_LOG_LEVELS, CHROMA_CLIENT
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+router = APIRouter()
+
+
+@router.get("/ef")
+async def get_embeddings(request: Request):
+    return {"result": request.app.state.EMBEDDING_FUNCTION("hello world")}
+
+
+############################
+# GetMemories
+############################
+
+
+@router.get("/", response_model=List[MemoryModel])
+async def get_memories(user=Depends(get_verified_user)):
+    return Memories.get_memories_by_user_id(user.id)
+
+
+############################
+# AddMemory
+############################
+
+
+class AddMemoryForm(BaseModel):
+    content: str
+
+
+@router.post("/add", response_model=Optional[MemoryModel])
+async def add_memory(
+    request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
+):
+    memory = Memories.insert_new_memory(user.id, form_data.content)
+    memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
+
+    collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
+    collection.upsert(
+        documents=[memory.content],
+        ids=[memory.id],
+        embeddings=[memory_embedding],
+        metadatas=[{"created_at": memory.created_at}],
+    )
+
+    return memory
+
+
+############################
+# QueryMemory
+############################
+
+
+class QueryMemoryForm(BaseModel):
+    content: str
+
+
+@router.post("/query")
+async def query_memory(
+    request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
+):
+    query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
+    collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
+
+    results = collection.query(
+        query_embeddings=[query_embedding],
+        n_results=1,  # how many results to return
+    )
+
+    return results
+
+
+############################
+# ResetMemoryFromVectorDB
+############################
+@router.get("/reset", response_model=bool)
+async def reset_memory_from_vector_db(
+    request: Request, user=Depends(get_verified_user)
+):
+    CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
+    collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
+
+    memories = Memories.get_memories_by_user_id(user.id)
+    for memory in memories:
+        memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
+        collection.upsert(
+            documents=[memory.content],
+            ids=[memory.id],
+            embeddings=[memory_embedding],
+        )
+    return True
+
+
+############################
+# DeleteMemoriesByUserId
+############################
+
+
+@router.delete("/user", response_model=bool)
+async def delete_memory_by_user_id(user=Depends(get_verified_user)):
+    result = Memories.delete_memories_by_user_id(user.id)
+
+    if result:
+        try:
+            CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
+        except Exception as e:
+            log.error(e)
+        return True
+
+    return False
+
+
+############################
+# DeleteMemoryById
+############################
+
+
+@router.delete("/{memory_id}", response_model=bool)
+async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
+    result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
+
+    if result:
+        collection = CHROMA_CLIENT.get_or_create_collection(
+            name=f"user-memory-{user.id}"
+        )
+        collection.delete(ids=[memory_id])
+        return True
+
+    return False

+ 40 - 4
backend/apps/web/routers/users.py

@@ -11,8 +11,9 @@ import logging
 
 from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
 from apps.web.models.auths import Auths
+from apps.web.models.chats import Chats
 
-from utils.utils import get_current_user, get_password_hash, get_admin_user
+from utils.utils import get_verified_user, get_password_hash, get_admin_user
 from constants import ERROR_MESSAGES
 
 from config import SRC_LOG_LEVELS
@@ -39,15 +40,15 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
 
 @router.get("/permissions/user")
 async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
-    return request.app.state.USER_PERMISSIONS
+    return request.app.state.config.USER_PERMISSIONS
 
 
 @router.post("/permissions/user")
 async def update_user_permissions(
     request: Request, form_data: dict, user=Depends(get_admin_user)
 ):
-    request.app.state.USER_PERMISSIONS = form_data
-    return request.app.state.USER_PERMISSIONS
+    request.app.state.config.USER_PERMISSIONS = form_data
+    return request.app.state.config.USER_PERMISSIONS
 
 
 ############################
@@ -67,6 +68,41 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
     )
 
 
+############################
+# GetUserById
+############################
+
+
+class UserResponse(BaseModel):
+    name: str
+    profile_image_url: str
+
+
+@router.get("/{user_id}", response_model=UserResponse)
+async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
+
+    if user_id.startswith("shared-"):
+        chat_id = user_id.replace("shared-", "")
+        chat = Chats.get_chat_by_id(chat_id)
+        if chat:
+            user_id = chat.user_id
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.USER_NOT_FOUND,
+            )
+
+    user = Users.get_user_by_id(user_id)
+
+    if user:
+        return UserResponse(name=user.name, profile_image_url=user.profile_image_url)
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.USER_NOT_FOUND,
+        )
+
+
 ############################
 # UpdateUserById
 ############################

+ 286 - 73
backend/config.py

@@ -5,6 +5,7 @@ import chromadb
 from chromadb import Settings
 from base64 import b64encode
 from bs4 import BeautifulSoup
+from typing import TypeVar, Generic, Union
 
 from pathlib import Path
 import json
@@ -17,7 +18,6 @@ import shutil
 from secrets import token_bytes
 from constants import ERROR_MESSAGES
 
-
 ####################################
 # Load .env file
 ####################################
@@ -71,7 +71,6 @@ for source in log_sources:
 
 log.setLevel(SRC_LOG_LEVELS["CONFIG"])
 
-
 WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
 if WEBUI_NAME != "Open WebUI":
     WEBUI_NAME += " (Open WebUI)"
@@ -161,16 +160,6 @@ CHANGELOG = changelog_json
 
 WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
 
-####################################
-# WEBUI_AUTH (Required for security)
-####################################
-
-WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
-WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
-    "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
-)
-
-
 ####################################
 # DATA/FRONTEND BUILD DIR
 ####################################
@@ -184,6 +173,108 @@ try:
 except:
     CONFIG_DATA = {}
 
+
+####################################
+# Config helpers
+####################################
+
+
+def save_config():
+    try:
+        with open(f"{DATA_DIR}/config.json", "w") as f:
+            json.dump(CONFIG_DATA, f, indent="\t")
+    except Exception as e:
+        log.exception(e)
+
+
+def get_config_value(config_path: str):
+    path_parts = config_path.split(".")
+    cur_config = CONFIG_DATA
+    for key in path_parts:
+        if key in cur_config:
+            cur_config = cur_config[key]
+        else:
+            return None
+    return cur_config
+
+
+T = TypeVar("T")
+
+
+class PersistentConfig(Generic[T]):
+    def __init__(self, env_name: str, config_path: str, env_value: T):
+        self.env_name = env_name
+        self.config_path = config_path
+        self.env_value = env_value
+        self.config_value = get_config_value(config_path)
+        if self.config_value is not None:
+            log.info(f"'{env_name}' loaded from config.json")
+            self.value = self.config_value
+        else:
+            self.value = env_value
+
+    def __str__(self):
+        return str(self.value)
+
+    @property
+    def __dict__(self):
+        raise TypeError(
+            "PersistentConfig object cannot be converted to dict, use config_get or .value instead."
+        )
+
+    def __getattribute__(self, item):
+        if item == "__dict__":
+            raise TypeError(
+                "PersistentConfig object cannot be converted to dict, use config_get or .value instead."
+            )
+        return super().__getattribute__(item)
+
+    def save(self):
+        # Don't save if the value is the same as the env value and the config value
+        if self.env_value == self.value:
+            if self.config_value == self.value:
+                return
+        log.info(f"Saving '{self.env_name}' to config.json")
+        path_parts = self.config_path.split(".")
+        config = CONFIG_DATA
+        for key in path_parts[:-1]:
+            if key not in config:
+                config[key] = {}
+            config = config[key]
+        config[path_parts[-1]] = self.value
+        save_config()
+        self.config_value = self.value
+
+
+class AppConfig:
+    _state: dict[str, PersistentConfig]
+
+    def __init__(self):
+        super().__setattr__("_state", {})
+
+    def __setattr__(self, key, value):
+        if isinstance(value, PersistentConfig):
+            self._state[key] = value
+        else:
+            self._state[key].value = value
+            self._state[key].save()
+
+    def __getattr__(self, key):
+        return self._state[key].value
+
+
+####################################
+# WEBUI_AUTH (Required for security)
+####################################
+
+WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
+WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
+    "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
+)
+JWT_EXPIRES_IN = PersistentConfig(
+    "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
+)
+
 ####################################
 # Static DIR
 ####################################
@@ -318,12 +409,22 @@ 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(";")]
-
+OLLAMA_BASE_URLS = PersistentConfig(
+    "OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
+)
 
 ####################################
 # OPENAI_API
 ####################################
 
+
+ENABLE_OPENAI_API = PersistentConfig(
+    "ENABLE_OPENAI_API",
+    "openai.enable",
+    os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true",
+)
+
+
 OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
 OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
 
@@ -335,7 +436,9 @@ 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_KEYS = PersistentConfig(
+    "OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS
+)
 
 OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
 OPENAI_API_BASE_URLS = (
@@ -346,37 +449,42 @@ OPENAI_API_BASE_URLS = [
     url.strip() if url != "" else "https://api.openai.com/v1"
     for url in OPENAI_API_BASE_URLS.split(";")
 ]
+OPENAI_API_BASE_URLS = PersistentConfig(
+    "OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
+)
 
 OPENAI_API_KEY = ""
 
 try:
-    OPENAI_API_KEY = OPENAI_API_KEYS[
-        OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
+    OPENAI_API_KEY = OPENAI_API_KEYS.value[
+        OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
     ]
 except:
     pass
 
 OPENAI_API_BASE_URL = "https://api.openai.com/v1"
 
-
 ####################################
 # WEBUI
 ####################################
 
-ENABLE_SIGNUP = (
-    False
-    if WEBUI_AUTH == False
-    else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
+ENABLE_SIGNUP = PersistentConfig(
+    "ENABLE_SIGNUP",
+    "ui.enable_signup",
+    (
+        False
+        if not WEBUI_AUTH
+        else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
+    ),
+)
+DEFAULT_MODELS = PersistentConfig(
+    "DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
 )
-DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
-
 
-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 [
+DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
+    "DEFAULT_PROMPT_SUGGESTIONS",
+    "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.",
@@ -404,23 +512,40 @@ DEFAULT_PROMPT_SUGGESTIONS = (
             "title": ["Overcome procrastination", "give me tips"],
             "content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
         },
-    ]
+    ],
 )
 
-
-DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
+DEFAULT_USER_ROLE = PersistentConfig(
+    "DEFAULT_USER_ROLE",
+    "ui.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}}
+USER_PERMISSIONS = PersistentConfig(
+    "USER_PERMISSIONS",
+    "ui.user_permissions",
+    {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
+)
 
-ENABLE_MODEL_FILTER = os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true"
+ENABLE_MODEL_FILTER = PersistentConfig(
+    "ENABLE_MODEL_FILTER",
+    "model_filter.enable",
+    os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
+)
 MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
-MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
+MODEL_FILTER_LIST = PersistentConfig(
+    "MODEL_FILTER_LIST",
+    "model_filter.list",
+    [model.strip() for model in MODEL_FILTER_LIST.split(";")],
+)
 
-WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
+WEBHOOK_URL = PersistentConfig(
+    "WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
+)
 
 ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
 
@@ -458,26 +583,45 @@ else:
 CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
 # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
 
-RAG_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
-RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
-
-ENABLE_RAG_HYBRID_SEARCH = (
-    os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
+RAG_TOP_K = PersistentConfig(
+    "RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
+)
+RAG_RELEVANCE_THRESHOLD = PersistentConfig(
+    "RAG_RELEVANCE_THRESHOLD",
+    "rag.relevance_threshold",
+    float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")),
 )
 
+ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
+    "ENABLE_RAG_HYBRID_SEARCH",
+    "rag.enable_hybrid_search",
+    os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
+)
 
-ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
-    os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true"
+ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
+    "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
+    "rag.enable_web_loader_ssl_verification",
+    os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
 )
 
-RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
+RAG_EMBEDDING_ENGINE = PersistentConfig(
+    "RAG_EMBEDDING_ENGINE",
+    "rag.embedding_engine",
+    os.environ.get("RAG_EMBEDDING_ENGINE", ""),
+)
 
-PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
+PDF_EXTRACT_IMAGES = PersistentConfig(
+    "PDF_EXTRACT_IMAGES",
+    "rag.pdf_extract_images",
+    os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true",
+)
 
-RAG_EMBEDDING_MODEL = os.environ.get(
-    "RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
+RAG_EMBEDDING_MODEL = PersistentConfig(
+    "RAG_EMBEDDING_MODEL",
+    "rag.embedding_model",
+    os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
 )
-log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
+log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
 
 RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
     os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
@@ -487,9 +631,13 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
     os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
 )
 
-RAG_RERANKING_MODEL = os.environ.get("RAG_RERANKING_MODEL", "")
-if not RAG_RERANKING_MODEL == "":
-    log.info(f"Reranking model set: {RAG_RERANKING_MODEL}"),
+RAG_RERANKING_MODEL = PersistentConfig(
+    "RAG_RERANKING_MODEL",
+    "rag.reranking_model",
+    os.environ.get("RAG_RERANKING_MODEL", ""),
+)
+if RAG_RERANKING_MODEL.value != "":
+    log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
 
 RAG_RERANKING_MODEL_AUTO_UPDATE = (
     os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
@@ -527,9 +675,14 @@ if USE_CUDA.lower() == "true":
 else:
     DEVICE_TYPE = "cpu"
 
-
-CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
-CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
+CHUNK_SIZE = PersistentConfig(
+    "CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500"))
+)
+CHUNK_OVERLAP = PersistentConfig(
+    "CHUNK_OVERLAP",
+    "rag.chunk_overlap",
+    int(os.environ.get("CHUNK_OVERLAP", "100")),
+)
 
 DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
 <context>
@@ -545,16 +698,32 @@ And answer according to the language of the user's question.
 Given the context information, answer the query.
 Query: [query]"""
 
-RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
+RAG_TEMPLATE = PersistentConfig(
+    "RAG_TEMPLATE",
+    "rag.template",
+    os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE),
+)
 
-RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
-RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
+RAG_OPENAI_API_BASE_URL = PersistentConfig(
+    "RAG_OPENAI_API_BASE_URL",
+    "rag.openai_api_base_url",
+    os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
+)
+RAG_OPENAI_API_KEY = PersistentConfig(
+    "RAG_OPENAI_API_KEY",
+    "rag.openai_api_key",
+    os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
+)
 
 ENABLE_RAG_LOCAL_WEB_FETCH = (
     os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
 )
 
-YOUTUBE_LOADER_LANGUAGE = os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(",")
+YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
+    "YOUTUBE_LOADER_LANGUAGE",
+    "rag.youtube_loader_language",
+    os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
+)
 
 ####################################
 # Transcribe
@@ -571,34 +740,78 @@ WHISPER_MODEL_AUTO_UPDATE = (
 # Images
 ####################################
 
-IMAGE_GENERATION_ENGINE = os.getenv("IMAGE_GENERATION_ENGINE", "")
+IMAGE_GENERATION_ENGINE = PersistentConfig(
+    "IMAGE_GENERATION_ENGINE",
+    "image_generation.engine",
+    os.getenv("IMAGE_GENERATION_ENGINE", ""),
+)
 
-ENABLE_IMAGE_GENERATION = (
-    os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
+ENABLE_IMAGE_GENERATION = PersistentConfig(
+    "ENABLE_IMAGE_GENERATION",
+    "image_generation.enable",
+    os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
+)
+AUTOMATIC1111_BASE_URL = PersistentConfig(
+    "AUTOMATIC1111_BASE_URL",
+    "image_generation.automatic1111.base_url",
+    os.getenv("AUTOMATIC1111_BASE_URL", ""),
 )
-AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
 
-COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
+COMFYUI_BASE_URL = PersistentConfig(
+    "COMFYUI_BASE_URL",
+    "image_generation.comfyui.base_url",
+    os.getenv("COMFYUI_BASE_URL", ""),
+)
 
-IMAGES_OPENAI_API_BASE_URL = os.getenv(
-    "IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
+IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
+    "IMAGES_OPENAI_API_BASE_URL",
+    "image_generation.openai.api_base_url",
+    os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
+)
+IMAGES_OPENAI_API_KEY = PersistentConfig(
+    "IMAGES_OPENAI_API_KEY",
+    "image_generation.openai.api_key",
+    os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
 )
-IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
 
-IMAGE_SIZE = os.getenv("IMAGE_SIZE", "512x512")
+IMAGE_SIZE = PersistentConfig(
+    "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
+)
 
-IMAGE_STEPS = int(os.getenv("IMAGE_STEPS", 50))
+IMAGE_STEPS = PersistentConfig(
+    "IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
+)
 
-IMAGE_GENERATION_MODEL = os.getenv("IMAGE_GENERATION_MODEL", "")
+IMAGE_GENERATION_MODEL = PersistentConfig(
+    "IMAGE_GENERATION_MODEL",
+    "image_generation.model",
+    os.getenv("IMAGE_GENERATION_MODEL", ""),
+)
 
 ####################################
 # Audio
 ####################################
 
-AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
-AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
-AUDIO_OPENAI_API_MODEL = os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1")
-AUDIO_OPENAI_API_VOICE = os.getenv("AUDIO_OPENAI_API_VOICE", "alloy")
+AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
+    "AUDIO_OPENAI_API_BASE_URL",
+    "audio.openai.api_base_url",
+    os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
+)
+AUDIO_OPENAI_API_KEY = PersistentConfig(
+    "AUDIO_OPENAI_API_KEY",
+    "audio.openai.api_key",
+    os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
+)
+AUDIO_OPENAI_API_MODEL = PersistentConfig(
+    "AUDIO_OPENAI_API_MODEL",
+    "audio.openai.api_model",
+    os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
+)
+AUDIO_OPENAI_API_VOICE = PersistentConfig(
+    "AUDIO_OPENAI_API_VOICE",
+    "audio.openai.api_voice",
+    os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
+)
 
 ####################################
 # LiteLLM

+ 69 - 39
backend/main.py

@@ -1,3 +1,4 @@
+from contextlib import asynccontextmanager
 from bs4 import BeautifulSoup
 import json
 import markdown
@@ -58,6 +59,7 @@ from config import (
     SRC_LOG_LEVELS,
     WEBHOOK_URL,
     ENABLE_ADMIN_EXPORT,
+    AppConfig,
 )
 from constants import ERROR_MESSAGES
 
@@ -92,16 +94,41 @@ https://github.com/open-webui/open-webui
 """
 )
 
-app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
 
-app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
-app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    if ENABLE_LITELLM:
+        asyncio.create_task(start_litellm_background())
+    yield
+    if ENABLE_LITELLM:
+        await shutdown_litellm_background()
+
+
+app = FastAPI(
+    docs_url="/docs" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan
+)
 
-app.state.WEBHOOK_URL = WEBHOOK_URL
+app.state.config = AppConfig()
+app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
+app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
+
+app.state.config.WEBHOOK_URL = WEBHOOK_URL
 
 origins = ["*"]
 
 
+# Custom middleware to add security headers
+# class SecurityHeadersMiddleware(BaseHTTPMiddleware):
+#     async def dispatch(self, request: Request, call_next):
+#         response: Response = await call_next(request)
+#         response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
+#         response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
+#         return response
+
+
+# app.add_middleware(SecurityHeadersMiddleware)
+
+
 class RAGMiddleware(BaseHTTPMiddleware):
     async def dispatch(self, request: Request, call_next):
         return_citations = False
@@ -129,12 +156,12 @@ class RAGMiddleware(BaseHTTPMiddleware):
                 data["messages"], citations = rag_messages(
                     docs=data["docs"],
                     messages=data["messages"],
-                    template=rag_app.state.RAG_TEMPLATE,
+                    template=rag_app.state.config.RAG_TEMPLATE,
                     embedding_function=rag_app.state.EMBEDDING_FUNCTION,
-                    k=rag_app.state.TOP_K,
+                    k=rag_app.state.config.TOP_K,
                     reranking_function=rag_app.state.sentence_transformer_rf,
-                    r=rag_app.state.RELEVANCE_THRESHOLD,
-                    hybrid_search=rag_app.state.ENABLE_RAG_HYBRID_SEARCH,
+                    r=rag_app.state.config.RELEVANCE_THRESHOLD,
+                    hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
                 )
                 del data["docs"]
 
@@ -211,15 +238,15 @@ async def check_url(request: Request, call_next):
     return response
 
 
-@app.on_event("startup")
-async def on_startup():
-    if ENABLE_LITELLM:
-        asyncio.create_task(start_litellm_background())
+@app.middleware("http")
+async def update_embedding_function(request: Request, call_next):
+    response = await call_next(request)
+    if "/embedding/update" in request.url.path:
+        webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
+    return response
 
 
-app.mount("/api/v1", webui_app)
 app.mount("/litellm/api", litellm_app)
-
 app.mount("/ollama", ollama_app)
 app.mount("/openai/api", openai_app)
 
@@ -227,6 +254,10 @@ app.mount("/images/api/v1", images_app)
 app.mount("/audio/api/v1", audio_app)
 app.mount("/rag/api/v1", rag_app)
 
+app.mount("/api/v1", webui_app)
+
+webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
+
 
 @app.get("/api/config")
 async def get_app_config():
@@ -243,9 +274,9 @@ async def get_app_config():
         "version": VERSION,
         "auth": WEBUI_AUTH,
         "default_locale": default_locale,
-        "images": images_app.state.ENABLED,
-        "default_models": webui_app.state.DEFAULT_MODELS,
-        "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
+        "images": images_app.state.config.ENABLED,
+        "default_models": webui_app.state.config.DEFAULT_MODELS,
+        "default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
         "trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
         "admin_export_enabled": ENABLE_ADMIN_EXPORT,
     }
@@ -254,8 +285,8 @@ async def get_app_config():
 @app.get("/api/config/model/filter")
 async def get_model_filter_config(user=Depends(get_admin_user)):
     return {
-        "enabled": app.state.ENABLE_MODEL_FILTER,
-        "models": app.state.MODEL_FILTER_LIST,
+        "enabled": app.state.config.ENABLE_MODEL_FILTER,
+        "models": app.state.config.MODEL_FILTER_LIST,
     }
 
 
@@ -268,28 +299,28 @@ class ModelFilterConfigForm(BaseModel):
 async def update_model_filter_config(
     form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
 ):
-    app.state.ENABLE_MODEL_FILTER = form_data.enabled
-    app.state.MODEL_FILTER_LIST = form_data.models
+    app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
+    app.state.config.MODEL_FILTER_LIST = form_data.models
 
-    ollama_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
-    ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
+    ollama_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
+    ollama_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
 
-    openai_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
-    openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
+    openai_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
+    openai_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
 
-    litellm_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
-    litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
+    litellm_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
+    litellm_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
 
     return {
-        "enabled": app.state.ENABLE_MODEL_FILTER,
-        "models": app.state.MODEL_FILTER_LIST,
+        "enabled": app.state.config.ENABLE_MODEL_FILTER,
+        "models": app.state.config.MODEL_FILTER_LIST,
     }
 
 
 @app.get("/api/webhook")
 async def get_webhook_url(user=Depends(get_admin_user)):
     return {
-        "url": app.state.WEBHOOK_URL,
+        "url": app.state.config.WEBHOOK_URL,
     }
 
 
@@ -299,12 +330,12 @@ class UrlForm(BaseModel):
 
 @app.post("/api/webhook")
 async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
-    app.state.WEBHOOK_URL = form_data.url
+    app.state.config.WEBHOOK_URL = form_data.url
 
-    webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL
+    webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
 
     return {
-        "url": app.state.WEBHOOK_URL,
+        "url": app.state.config.WEBHOOK_URL,
     }
 
 
@@ -368,6 +399,11 @@ async def get_opensearch_xml():
     return Response(content=xml_content, media_type="application/xml")
 
 
+@app.get("/health")
+async def healthcheck():
+    return {"status": True}
+
+
 app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
 app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
 
@@ -381,9 +417,3 @@ else:
     log.warning(
         f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
     )
-
-
-@app.on_event("shutdown")
-async def shutdown_event():
-    if ENABLE_LITELLM:
-        await shutdown_litellm_background()

+ 32 - 0
cypress/e2e/chat.cy.ts

@@ -42,5 +42,37 @@ describe('Settings', () => {
 				.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
 				.should('exist');
 		});
+
+		it('user can share chat', () => {
+			// Click on the model selector
+			cy.get('button[aria-label="Select a model"]').click();
+			// Select the first model
+			cy.get('button[aria-label="model-item"]').first().click();
+			// Type a message
+			cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
+				force: true
+			});
+			// Send the message
+			cy.get('button[type="submit"]').click();
+			// User's message should be visible
+			cy.get('.chat-user').should('exist');
+			// Wait for the response
+			cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
+				.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
+				.should('exist');
+			// spy on requests
+			const spy = cy.spy();
+			cy.intercept('GET', '/api/v1/chats/*', spy);
+			// Open context menu
+			cy.get('#chat-context-menu-button').click();
+			// Click share button
+			cy.get('#chat-share-button').click();
+			// Check if the share dialog is visible
+			cy.get('#copy-and-share-chat-button').should('exist');
+			cy.wrap({}, { timeout: 5000 }).should(() => {
+				// Check if the request was made twice (once for to replace chat object and once more due to change event)
+				expect(spy).to.be.callCount(2);
+			});
+		});
 	});
 });

+ 2 - 6
cypress/e2e/settings.cy.ts

@@ -15,12 +15,8 @@ describe('Settings', () => {
 		cy.loginAdmin();
 		// Visit the home page
 		cy.visit('/');
-		// Open the sidebar if it is not already open
-		cy.get('[aria-label="Open sidebar"]').then(() => {
-			cy.get('button[id="sidebar-toggle-button"]').click();
-		});
-		// Click on the profile link
-		cy.get('button').contains(adminUser.name).click();
+		// Click on the user menu
+		cy.get('button[aria-label="User Menu"]').click();
 		// Click on the settings link
 		cy.get('button').contains('Settings').click();
 	});

+ 0 - 2
docker-compose.api.yaml

@@ -1,5 +1,3 @@
-version: '3.8'
-
 services:
   ollama:
     # Expose Ollama API outside the container stack

+ 0 - 2
docker-compose.data.yaml

@@ -1,5 +1,3 @@
-version: '3.8'
-
 services:
   ollama:
     volumes:

+ 0 - 2
docker-compose.gpu.yaml

@@ -1,5 +1,3 @@
-version: '3.8'
-
 services:
   ollama:
     # GPU support

+ 0 - 2
docker-compose.yaml

@@ -1,5 +1,3 @@
-version: '3.8'
-
 services:
   ollama:
     volumes:

+ 2 - 2
docs/CONTRIBUTING.md

@@ -17,7 +17,7 @@ If your issue or contribution pertains directly to the core Ollama technology, p
 
 ### 🚨 Reporting Issues
 
-Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/oopen-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
+Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/open-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
 
 > [!IMPORTANT]
 >
@@ -54,7 +54,7 @@ Help us make Open WebUI more accessible by improving documentation, writing tuto
 
 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.
+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:
 

Fichier diff supprimé car celui-ci est trop grand
+ 726 - 10
package-lock.json


+ 12 - 7
package.json

@@ -1,10 +1,10 @@
 {
 	"name": "open-webui",
-	"version": "0.1.124",
+	"version": "0.1.125",
 	"private": true,
 	"scripts": {
-		"dev": "vite dev --host",
-		"build": "vite build",
+		"dev": "npm run pyodide:fetch && vite dev --host",
+		"build": "npm run pyodide:fetch && vite build",
 		"preview": "vite preview",
 		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
 		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -12,10 +12,12 @@
 		"lint:frontend": "eslint . --fix",
 		"lint:types": "npm run check",
 		"lint:backend": "pylint backend/",
-		"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
+		"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"",
 		"format:backend": "black . --exclude \"/venv/\"",
-		"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'",
-		"cy:open": "cypress open"
+		"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"",
+		"cy:open": "cypress open",
+		"test:frontend": "vitest",
+		"pyodide:fetch": "node scripts/prepare-pyodide.js"
 	},
 	"devDependencies": {
 		"@sveltejs/adapter-auto": "^2.0.0",
@@ -41,10 +43,12 @@
 		"tailwindcss": "^3.3.3",
 		"tslib": "^2.4.1",
 		"typescript": "^5.0.0",
-		"vite": "^4.4.2"
+		"vite": "^4.4.2",
+		"vitest": "^1.6.0"
 	},
 	"type": "module",
 	"dependencies": {
+		"@pyscript/core": "^0.4.32",
 		"@sveltejs/adapter-node": "^1.3.1",
 		"async": "^3.2.5",
 		"bits-ui": "^0.19.7",
@@ -59,6 +63,7 @@
 		"js-sha256": "^0.10.1",
 		"katex": "^0.16.9",
 		"marked": "^9.1.0",
+		"pyodide": "^0.26.0-alpha.4",
 		"svelte-sonner": "^0.3.19",
 		"tippy.js": "^6.3.7",
 		"uuid": "^9.0.1"

+ 39 - 0
scripts/prepare-pyodide.js

@@ -0,0 +1,39 @@
+const packages = [
+	'requests',
+	'beautifulsoup4',
+	'numpy',
+	'pandas',
+	'matplotlib',
+	'scikit-learn',
+	'scipy',
+	'regex',
+	'seaborn'
+];
+
+import { loadPyodide } from 'pyodide';
+import { writeFile, copyFile, readdir } from 'fs/promises';
+
+async function downloadPackages() {
+	console.log('Setting up pyodide + micropip');
+	const pyodide = await loadPyodide({
+		packageCacheDir: 'static/pyodide'
+	});
+	await pyodide.loadPackage('micropip');
+	const micropip = pyodide.pyimport('micropip');
+	console.log('Downloading Pyodide packages:', packages);
+	await micropip.install(packages);
+	console.log('Pyodide packages downloaded, freezing into lock file');
+	const lockFile = await micropip.freeze();
+	await writeFile('static/pyodide/pyodide-lock.json', lockFile);
+}
+
+async function copyPyodide() {
+	console.log('Copying Pyodide files into static directory');
+	// Copy all files from node_modules/pyodide to static/pyodide
+	for await (const entry of await readdir('node_modules/pyodide')) {
+		await copyFile(`node_modules/pyodide/${entry}`, `static/pyodide/${entry}`);
+	}
+}
+
+await downloadPackages();
+await copyPyodide();

+ 24 - 4
src/app.css

@@ -83,11 +83,31 @@ select {
 	display: none;
 }
 
-.scrollbar-none:active::-webkit-scrollbar-thumb,
-.scrollbar-none:focus::-webkit-scrollbar-thumb,
-.scrollbar-none:hover::-webkit-scrollbar-thumb {
+.scrollbar-hidden:active::-webkit-scrollbar-thumb,
+.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
+.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
 	visibility: visible;
 }
-.scrollbar-none::-webkit-scrollbar-thumb {
+.scrollbar-hidden::-webkit-scrollbar-thumb {
 	visibility: hidden;
 }
+
+.scrollbar-none::-webkit-scrollbar {
+	display: none; /* for Chrome, Safari and Opera */
+}
+
+.scrollbar-none {
+	-ms-overflow-style: none; /* IE and Edge */
+	scrollbar-width: none; /* Firefox */
+}
+
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+	/* display: none; <- Crashes Chrome on hover */
+	-webkit-appearance: none;
+	margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
+}
+
+input[type='number'] {
+	-moz-appearance: textfield; /* Firefox */
+}

+ 1 - 0
src/app.html

@@ -12,6 +12,7 @@
 			title="Open WebUI"
 			href="/opensearch.xml"
 		/>
+
 		<script>
 			// On page load or when changing themes, best to add inline in `head` to avoid FOUC
 			(() => {

+ 155 - 0
src/lib/apis/memories/index.ts

@@ -0,0 +1,155 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+export const getMemories = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/memories`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const addNewMemory = async (token: string, content: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/memories/add`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			content: content
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const queryMemory = async (token: string, content: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/memories/query`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			content: content
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteMemoryById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteMemoriesByUserId = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/memories/user`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 67 - 0
src/lib/apis/openai/index.ts

@@ -1,6 +1,73 @@
 import { OPENAI_API_BASE_URL } from '$lib/constants';
 import { promptTemplate } from '$lib/utils';
 
+export const getOpenAIConfig = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${OPENAI_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 updateOpenAIConfig = async (token: string = '', enable_openai_api: boolean) => {
+	let error = null;
+
+	const res = await fetch(`${OPENAI_API_BASE_URL}/config/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			enable_openai_api: enable_openai_api
+		})
+	})
+		.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 getOpenAIUrls = async (token: string = '') => {
 	let error = null;
 

+ 7 - 0
src/lib/apis/streaming/index.ts

@@ -6,6 +6,8 @@ type TextStreamUpdate = {
 	value: string;
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	citations?: any;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	error?: any;
 };
 
 // createOpenAITextStream takes a responseBody with a SSE response,
@@ -47,6 +49,11 @@ async function* openAIStreamToIterator(
 			const parsedData = JSON.parse(data);
 			console.log(parsedData);
 
+			if (parsedData.error) {
+				yield { done: true, value: '', error: parsedData.error };
+				break;
+			}
+
 			if (parsedData.citations) {
 				yield { done: false, value: '', citations: parsedData.citations };
 				continue;

+ 27 - 0
src/lib/apis/users/index.ts

@@ -115,6 +115,33 @@ export const getUsers = async (token: string) => {
 	return res ? res : [];
 };
 
+export const getUserById = async (token: string, userId: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, {
+		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 deleteUserById = async (token: string, userId: string) => {
 	let error = null;
 

+ 1 - 1
src/lib/components/ChangelogModal.svelte

@@ -58,7 +58,7 @@
 	</div>
 
 	<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
-		<div class=" overflow-y-scroll max-h-80 scrollbar-none">
+		<div class=" overflow-y-scroll max-h-80 scrollbar-hidden">
 			<div class="mb-3">
 				{#if changelog}
 					{#each Object.keys(changelog) as version}

+ 5 - 2
src/lib/components/admin/AddUserModal.svelte

@@ -73,13 +73,16 @@
 						console.log(idx, columns);
 
 						if (idx > 0) {
-							if (columns.length === 4 && ['admin', 'user', 'pending'].includes(columns[3])) {
+							if (
+								columns.length === 4 &&
+								['admin', 'user', 'pending'].includes(columns[3].toLowerCase())
+							) {
 								const res = await addUser(
 									localStorage.token,
 									columns[0],
 									columns[1],
 									columns[2],
-									columns[3]
+									columns[3].toLowerCase()
 								).catch((error) => {
 									toast.error(`Row ${idx + 1}: ${error}`);
 									return null;

+ 2 - 2
src/lib/components/admin/Settings/General.svelte

@@ -123,7 +123,7 @@
 
 				<div class="flex mt-2 space-x-2">
 					<input
-						class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 						type="text"
 						placeholder={`https://example.com/webhook`}
 						bind:value={webhookUrl}
@@ -140,7 +140,7 @@
 
 				<div class="flex mt-2 space-x-2">
 					<input
-						class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 						type="text"
 						placeholder={`e.g.) "30m","1h", "10d". `}
 						bind:value={JWTExpiresIn}

+ 19 - 16
src/lib/components/chat/MessageInput.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { onMount, tick, getContext } from 'svelte';
-	import { modelfiles, settings, showSidebar } from '$lib/stores';
+	import { mobile, modelfiles, settings, showSidebar } from '$lib/stores';
 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
 
 	import {
@@ -327,7 +327,6 @@
 	};
 
 	onMount(() => {
-		console.log(document.getElementById('sidebar'));
 		window.setTimeout(() => chatTextAreaElement?.focus(), 0);
 
 		const dropZone = document.querySelector('body');
@@ -358,7 +357,7 @@
 				if (inputFiles && inputFiles.length > 0) {
 					inputFiles.forEach((file) => {
 						console.log(file, file.name.split('.').at(-1));
-						if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
+						if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
 							let reader = new FileReader();
 							reader.onload = (event) => {
 								files = [
@@ -412,7 +411,7 @@
 {#if dragged}
 	<div
 		class="fixed {$showSidebar
-			? 'left-0 lg:left-[260px] lg:w-[calc(100%-260px)]'
+			? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
 			: 'left-0'}  w-full h-full flex z-50 touch-none pointer-events-none"
 		id="dropzone"
 		role="region"
@@ -428,9 +427,9 @@
 	</div>
 {/if}
 
-<div class="fixed bottom-0 {$showSidebar ? 'left-0 lg:left-[260px]' : 'left-0'} right-0">
+<div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0">
 	<div class="w-full">
-		<div class="px-2.5 lg:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
+		<div class="px-2.5 md:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
 			<div class="flex flex-col max-w-5xl w-full">
 				<div class="relative">
 					{#if autoScroll === false && messages.length > 0}
@@ -506,6 +505,7 @@
 						>
 							<div class="flex items-center gap-2 text-sm dark:text-gray-500">
 								<img
+									crossorigin="anonymous"
 									alt="model profile"
 									class="size-5 max-w-[28px] object-cover rounded-full"
 									src={$modelfiles.find((modelfile) => modelfile.tagName === selectedModel.id)
@@ -535,7 +535,7 @@
 		</div>
 
 		<div class="bg-white dark:bg-gray-900">
-			<div class="max-w-6xl px-2.5 lg:px-16 mx-auto inset-x-0">
+			<div class="max-w-6xl px-2.5 md:px-16 mx-auto inset-x-0">
 				<div class=" pb-2">
 					<input
 						bind:this={filesInputElement}
@@ -547,7 +547,9 @@
 							if (inputFiles && inputFiles.length > 0) {
 								const _inputFiles = Array.from(inputFiles);
 								_inputFiles.forEach((file) => {
-									if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
+									if (
+										['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])
+									) {
 										let reader = new FileReader();
 										reader.onload = (event) => {
 											files = [
@@ -584,7 +586,8 @@
 						}}
 					/>
 					<form
-						class=" flex flex-col relative w-full rounded-3xl px-1.5 border border-gray-100 dark:border-gray-850 bg-white dark:bg-gray-900 dark:text-gray-100"
+						dir={$settings?.chatDirection ?? 'LTR'}
+						class=" flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
 						on:submit|preventDefault={() => {
 							submitPrompt(prompt, user);
 						}}
@@ -754,7 +757,7 @@
 							<textarea
 								id="chat-textarea"
 								bind:this={chatTextAreaElement}
-								class="scrollbar-none dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
+								class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
 									? ''
 									: ' pl-4'} rounded-xl resize-none h-[48px]"
 								placeholder={chatInputPlaceholder !== ''
@@ -765,7 +768,7 @@
 								bind:value={prompt}
 								on:keypress={(e) => {
 									if (
-										window.innerWidth > 1024 ||
+										!$mobile ||
 										!(
 											'ontouchstart' in window ||
 											navigator.maxTouchPoints > 0 ||
@@ -995,7 +998,7 @@
 											id="send-message-button"
 											class="{prompt !== ''
 												? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
-												: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
+												: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
 											type="submit"
 											disabled={prompt === ''}
 										>
@@ -1046,12 +1049,12 @@
 </div>
 
 <style>
-	.scrollbar-none:active::-webkit-scrollbar-thumb,
-	.scrollbar-none:focus::-webkit-scrollbar-thumb,
-	.scrollbar-none:hover::-webkit-scrollbar-thumb {
+	.scrollbar-hidden:active::-webkit-scrollbar-thumb,
+	.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
+	.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
 		visibility: visible;
 	}
-	.scrollbar-none::-webkit-scrollbar-thumb {
+	.scrollbar-hidden::-webkit-scrollbar-thumb {
 		visibility: hidden;
 	}
 </style>

+ 69 - 31
src/lib/components/chat/Messages.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { v4 as uuidv4 } from 'uuid';
 
-	import { chats, config, modelfiles, settings, user } from '$lib/stores';
+	import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores';
 	import { tick, getContext } from 'svelte';
 
 	import { toast } from 'svelte-sonner';
@@ -13,6 +13,8 @@
 	import Spinner from '../common/Spinner.svelte';
 	import { imageGenerations } from '$lib/apis/images';
 	import { copyToClipboard, findWordIndices } from '$lib/utils';
+	import CompareMessages from './Messages/CompareMessages.svelte';
+	import { stringify } from 'postcss';
 
 	const i18n = getContext('i18n');
 
@@ -22,15 +24,16 @@
 	export let continueGeneration: Function;
 	export let regenerateResponse: Function;
 
+	export let user = $_user;
 	export let prompt;
-	export let suggestionPrompts;
+	export let suggestionPrompts = [];
 	export let processing = '';
 	export let bottomPadding = false;
 	export let autoScroll;
-	export let selectedModels;
 	export let history = {};
 	export let messages = [];
 
+	export let selectedModels;
 	export let selectedModelfiles = [];
 
 	$: if (autoScroll && bottomPadding) {
@@ -62,7 +65,8 @@
 			childrenIds: [],
 			role: 'user',
 			content: userPrompt,
-			...(history.messages[messageId].files && { files: history.messages[messageId].files })
+			...(history.messages[messageId].files && { files: history.messages[messageId].files }),
+			models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
 		};
 
 		let messageParentId = history.messages[messageId].parentId;
@@ -78,7 +82,7 @@
 		history.currentId = userMessageId;
 
 		await tick();
-		await sendPrompt(userPrompt, userMessageId, chatId);
+		await sendPrompt(userPrompt, userMessageId);
 	};
 
 	const updateChatMessages = async () => {
@@ -294,7 +298,7 @@
 							{#if message.role === 'user'}
 								<UserMessage
 									on:delete={() => messageDeleteHandler(message.id)}
-									user={$user}
+									{user}
 									{readOnly}
 									{message}
 									isFirstMessage={messageIdx === 0}
@@ -308,32 +312,66 @@
 									{showNextMessage}
 									copyToClipboard={copyToClipboardWithToast}
 								/>
+							{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
+								{#key message.id}
+									<ResponseMessage
+										{message}
+										modelfiles={selectedModelfiles}
+										siblings={history.messages[message.parentId]?.childrenIds ?? []}
+										isLastMessage={messageIdx + 1 === messages.length}
+										{readOnly}
+										{updateChatMessages}
+										{confirmEditResponseMessage}
+										{showPreviousMessage}
+										{showNextMessage}
+										{rateMessage}
+										copyToClipboard={copyToClipboardWithToast}
+										{continueGeneration}
+										{regenerateResponse}
+										on:save={async (e) => {
+											console.log('save', e);
+
+											const message = e.detail;
+											history.messages[message.id] = message;
+											await updateChatById(localStorage.token, chatId, {
+												messages: messages,
+												history: history
+											});
+										}}
+									/>
+								{/key}
 							{:else}
-								<ResponseMessage
-									{message}
-									modelfiles={selectedModelfiles}
-									siblings={history.messages[message.parentId]?.childrenIds ?? []}
-									isLastMessage={messageIdx + 1 === messages.length}
-									{readOnly}
-									{updateChatMessages}
-									{confirmEditResponseMessage}
-									{showPreviousMessage}
-									{showNextMessage}
-									{rateMessage}
-									copyToClipboard={copyToClipboardWithToast}
-									{continueGeneration}
-									{regenerateResponse}
-									on:save={async (e) => {
-										console.log('save', e);
-
-										const message = e.detail;
-										history.messages[message.id] = message;
-										await updateChatById(localStorage.token, chatId, {
-											messages: messages,
-											history: history
-										});
-									}}
-								/>
+								{#key message.parentId}
+									<CompareMessages
+										bind:history
+										{messages}
+										{chatId}
+										parentMessage={history.messages[message.parentId]}
+										{messageIdx}
+										{selectedModelfiles}
+										{updateChatMessages}
+										{confirmEditResponseMessage}
+										{rateMessage}
+										copyToClipboard={copyToClipboardWithToast}
+										{continueGeneration}
+										{regenerateResponse}
+										on:change={async () => {
+											await updateChatById(localStorage.token, chatId, {
+												messages: messages,
+												history: history
+											});
+
+											if (autoScroll) {
+												const element = document.getElementById('messages-container');
+												autoScroll =
+													element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
+												setTimeout(() => {
+													scrollToBottom();
+												}, 100);
+											}
+										}}
+									/>
+								{/key}
 							{/if}
 						</div>
 					</div>

+ 1 - 1
src/lib/components/chat/Messages/CitationsModal.svelte

@@ -47,7 +47,7 @@
 
 		<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
 			<div
-				class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-none"
+				class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden"
 			>
 				{#each mergedDocuments as document, documentIdx}
 					<div class="flex flex-col w-full">

+ 226 - 5
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -1,11 +1,23 @@
 <script lang="ts">
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import { copyToClipboard } from '$lib/utils';
 	import hljs from 'highlight.js';
 	import 'highlight.js/styles/github-dark.min.css';
+	import { loadPyodide } from 'pyodide';
+	import { tick } from 'svelte';
+	import PyodideWorker from '$lib/workers/pyodide.worker?worker';
+
+	export let id = '';
 
 	export let lang = '';
 	export let code = '';
 
+	let executing = false;
+
+	let stdout = null;
+	let stderr = null;
+	let result = null;
+
 	let copied = false;
 
 	const copyCode = async () => {
@@ -17,24 +29,233 @@
 		}, 1000);
 	};
 
+	const checkPythonCode = (str) => {
+		// Check if the string contains typical Python syntax characters
+		const pythonSyntax = [
+			'def ',
+			'else:',
+			'elif ',
+			'try:',
+			'except:',
+			'finally:',
+			'yield ',
+			'lambda ',
+			'assert ',
+			'nonlocal ',
+			'del ',
+			'True',
+			'False',
+			'None',
+			' and ',
+			' or ',
+			' not ',
+			' in ',
+			' is ',
+			' with '
+		];
+
+		for (let syntax of pythonSyntax) {
+			if (str.includes(syntax)) {
+				return true;
+			}
+		}
+
+		// If none of the above conditions met, it's probably not Python code
+		return false;
+	};
+
+	const executePython = async (code) => {
+		if (!code.includes('input') && !code.includes('matplotlib')) {
+			executePythonAsWorker(code);
+		} else {
+			result = null;
+			stdout = null;
+			stderr = null;
+
+			executing = true;
+
+			document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
+
+			let pyodide = await loadPyodide({
+				indexURL: '/pyodide/',
+				stdout: (text) => {
+					console.log('Python output:', text);
+
+					if (stdout) {
+						stdout += `${text}\n`;
+					} else {
+						stdout = `${text}\n`;
+					}
+				},
+				stderr: (text) => {
+					console.log('An error occured:', text);
+					if (stderr) {
+						stderr += `${text}\n`;
+					} else {
+						stderr = `${text}\n`;
+					}
+				},
+				packages: ['micropip']
+			});
+
+			try {
+				const micropip = pyodide.pyimport('micropip');
+
+				await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
+
+				let packages = [
+					code.includes('requests') ? 'requests' : null,
+					code.includes('bs4') ? 'beautifulsoup4' : null,
+					code.includes('numpy') ? 'numpy' : null,
+					code.includes('pandas') ? 'pandas' : null,
+					code.includes('matplotlib') ? 'matplotlib' : null,
+					code.includes('sklearn') ? 'scikit-learn' : null,
+					code.includes('scipy') ? 'scipy' : null,
+					code.includes('re') ? 'regex' : null,
+					code.includes('seaborn') ? 'seaborn' : null
+				].filter(Boolean);
+
+				console.log(packages);
+				await micropip.install(packages);
+
+				result = await pyodide.runPythonAsync(`from js import prompt
+def input(p):
+    return prompt(p)
+__builtins__.input = input`);
+
+				result = await pyodide.runPython(code);
+
+				if (!result) {
+					result = '[NO OUTPUT]';
+				}
+
+				console.log(result);
+				console.log(stdout);
+				console.log(stderr);
+
+				const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
+
+				if (pltCanvasElement?.innerHTML !== '') {
+					pltCanvasElement.classList.add('pt-4');
+				}
+			} catch (error) {
+				console.error('Error:', error);
+				stderr = error;
+			}
+
+			executing = false;
+		}
+	};
+
+	const executePythonAsWorker = async (code) => {
+		result = null;
+		stdout = null;
+		stderr = null;
+
+		executing = true;
+
+		let packages = [
+			code.includes('requests') ? 'requests' : null,
+			code.includes('bs4') ? 'beautifulsoup4' : null,
+			code.includes('numpy') ? 'numpy' : null,
+			code.includes('pandas') ? 'pandas' : null,
+			code.includes('sklearn') ? 'scikit-learn' : null,
+			code.includes('scipy') ? 'scipy' : null,
+			code.includes('re') ? 'regex' : null,
+			code.includes('seaborn') ? 'seaborn' : null
+		].filter(Boolean);
+
+		console.log(packages);
+
+		const pyodideWorker = new PyodideWorker();
+
+		pyodideWorker.postMessage({
+			id: id,
+			code: code,
+			packages: packages
+		});
+
+		setTimeout(() => {
+			if (executing) {
+				executing = false;
+				stderr = 'Execution Time Limit Exceeded';
+				pyodideWorker.terminate();
+			}
+		}, 60000);
+
+		pyodideWorker.onmessage = (event) => {
+			console.log('pyodideWorker.onmessage', event);
+			const { id, ...data } = event.data;
+
+			console.log(id, data);
+
+			data['stdout'] && (stdout = data['stdout']);
+			data['stderr'] && (stderr = data['stderr']);
+			data['result'] && (result = data['result']);
+
+			executing = false;
+		};
+
+		pyodideWorker.onerror = (event) => {
+			console.log('pyodideWorker.onerror', event);
+			executing = false;
+		};
+	};
+
 	$: highlightedCode = code ? hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value : '';
 </script>
 
 {#if code}
-	<div class="mb-4">
+	<div class="mb-4" dir="ltr">
 		<div
 			class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
 		>
 			<div class="p-1">{@html lang}</div>
-			<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
-				>{copied ? 'Copied' : 'Copy Code'}</button
-			>
+
+			<div class="flex items-center">
+				{#if ['', 'python'].includes(lang) && (lang === 'python' || checkPythonCode(code))}
+					{#if executing}
+						<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
+					{:else}
+						<button
+							class="copy-code-button bg-none border-none p-1"
+							on:click={() => {
+								executePython(code);
+							}}>Run</button
+						>
+					{/if}
+				{/if}
+				<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
+					>{copied ? 'Copied' : 'Copy Code'}</button
+				>
+			</div>
 		</div>
 
 		<pre
 			class=" hljs p-4 px-5 overflow-x-auto"
-			style="border-top-left-radius: 0px; border-top-right-radius: 0px;"><code
+			style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
+				stdout ||
+				stderr ||
+				result) &&
+				'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
 				class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
 			></pre>
+
+		<div
+			id="plt-canvas-{id}"
+			class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
+		/>
+
+		{#if executing}
+			<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
+				<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
+				<div class="text-sm">Running...</div>
+			</div>
+		{:else if stdout || stderr || result}
+			<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
+				<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
+				<div class="text-sm">{stdout || stderr || result}</div>
+			</div>
+		{/if}
 	</div>
 {/if}

+ 163 - 0
src/lib/components/chat/Messages/CompareMessages.svelte

@@ -0,0 +1,163 @@
+<script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+
+	import { updateChatById } from '$lib/apis/chats';
+	import { onMount, tick } from 'svelte';
+	import ResponseMessage from './ResponseMessage.svelte';
+
+	export let chatId;
+
+	export let history;
+	export let messages = [];
+	export let messageIdx;
+
+	export let parentMessage;
+
+	export let selectedModelfiles;
+
+	export let updateChatMessages: Function;
+	export let confirmEditResponseMessage: Function;
+	export let rateMessage: Function;
+
+	export let copyToClipboard: Function;
+	export let continueGeneration: Function;
+	export let regenerateResponse: Function;
+
+	const dispatch = createEventDispatcher();
+
+	let currentMessageId;
+
+	let groupedMessagesIdx = {};
+	let groupedMessages = {};
+
+	$: groupedMessages = parentMessage?.models.reduce((a, model) => {
+		const modelMessages = parentMessage?.childrenIds
+			.map((id) => history.messages[id])
+			.filter((m) => m.model === model);
+
+		return {
+			...a,
+			[model]: { messages: modelMessages }
+		};
+	}, {});
+
+	const showPreviousMessage = (model) => {
+		groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1);
+		let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+
+		console.log(messageId);
+		let messageChildrenIds = history.messages[messageId].childrenIds;
+
+		while (messageChildrenIds.length !== 0) {
+			messageId = messageChildrenIds.at(-1);
+			messageChildrenIds = history.messages[messageId].childrenIds;
+		}
+
+		history.currentId = messageId;
+
+		dispatch('change');
+	};
+
+	const showNextMessage = (model) => {
+		groupedMessagesIdx[model] = Math.min(
+			groupedMessages[model].messages.length - 1,
+			groupedMessagesIdx[model] + 1
+		);
+
+		let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+		console.log(messageId);
+
+		let messageChildrenIds = history.messages[messageId].childrenIds;
+
+		while (messageChildrenIds.length !== 0) {
+			messageId = messageChildrenIds.at(-1);
+			messageChildrenIds = history.messages[messageId].childrenIds;
+		}
+
+		history.currentId = messageId;
+
+		dispatch('change');
+	};
+
+	onMount(async () => {
+		await tick();
+		currentMessageId = messages[messageIdx].id;
+
+		for (const model of parentMessage?.models) {
+			const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId);
+
+			if (idx !== -1) {
+				groupedMessagesIdx[model] = idx;
+			} else {
+				groupedMessagesIdx[model] = 0;
+			}
+		}
+	});
+</script>
+
+<div>
+	<div
+		class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
+		id="responses-container-{parentMessage.id}"
+	>
+		{#each Object.keys(groupedMessages) as model}
+			{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
+				<!-- svelte-ignore a11y-no-static-element-interactions -->
+				<!-- svelte-ignore a11y-click-events-have-key-events -->
+
+				<div
+					class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
+						currentMessageId
+					].model === model
+						? 'border-gray-100 dark:border-gray-700 border-[1.5px]'
+						: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
+					on:click={() => {
+						currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+
+						let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+
+						console.log(messageId);
+						let messageChildrenIds = history.messages[messageId].childrenIds;
+
+						while (messageChildrenIds.length !== 0) {
+							messageId = messageChildrenIds.at(-1);
+							messageChildrenIds = history.messages[messageId].childrenIds;
+						}
+
+						history.currentId = messageId;
+						dispatch('change');
+					}}
+				>
+					<ResponseMessage
+						message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
+						modelfiles={selectedModelfiles}
+						siblings={groupedMessages[model].messages.map((m) => m.id)}
+						isLastMessage={true}
+						{updateChatMessages}
+						{confirmEditResponseMessage}
+						showPreviousMessage={() => showPreviousMessage(model)}
+						showNextMessage={() => showNextMessage(model)}
+						{rateMessage}
+						{copyToClipboard}
+						{continueGeneration}
+						regenerateResponse={async (message) => {
+							regenerateResponse(message);
+							await tick();
+							groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
+						}}
+						on:save={async (e) => {
+							console.log('save', e);
+
+							const message = e.detail;
+							history.messages[message.id] = message;
+							await updateChatById(localStorage.token, chatId, {
+								messages: messages,
+								history: history
+							});
+						}}
+					/>
+				</div>
+			{/if}
+		{/each}
+	</div>
+</div>

+ 1 - 1
src/lib/components/chat/Messages/Name.svelte

@@ -1,3 +1,3 @@
-<div class=" self-center font-bold mb-0.5 capitalize line-clamp-1">
+<div class=" self-center font-bold mb-0.5 line-clamp-1 contents">
 	<slot />
 </div>

+ 2 - 0
src/lib/components/chat/Messages/Placeholder.svelte

@@ -43,6 +43,7 @@
 					>
 						{#if model in modelfiles}
 							<img
+								crossorigin="anonymous"
 								src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
 								alt="modelfile"
 								class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
@@ -50,6 +51,7 @@
 							/>
 						{:else}
 							<img
+								crossorigin="anonymous"
 								src={$i18n.language === 'dg-DG'
 									? `/doge.png`
 									: `${WEBUI_BASE_URL}/static/favicon.png`}

+ 15 - 2
src/lib/components/chat/Messages/ProfileImage.svelte

@@ -1,7 +1,20 @@
 <script lang="ts">
+	import { settings } from '$lib/stores';
+	import { WEBUI_BASE_URL } from '$lib/constants';
+
 	export let src = '/user.png';
 </script>
 
-<div class=" mr-4">
-	<img {src} class=" max-w-[28px] object-cover rounded-full" alt="profile" draggable="false" />
+<div class={($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}>
+	<img
+		crossorigin="anonymous"
+		src={src.startsWith(WEBUI_BASE_URL) ||
+		src.startsWith('https://www.gravatar.com/avatar/') ||
+		src.startsWith('data:')
+			? src
+			: `/user.png`}
+		class=" w-8 object-cover rounded-full"
+		alt="profile"
+		draggable="false"
+	/>
 </div>

+ 2 - 2
src/lib/components/chat/Messages/RateComment.svelte

@@ -39,9 +39,9 @@
 	let selectedReason = null;
 	let comment = '';
 
-	$: if (message.annotation.rating === 1) {
+	$: if (message?.annotation?.rating === 1) {
 		reasons = LIKE_REASONS;
-	} else if (message.annotation.rating === -1) {
+	} else if (message?.annotation?.rating === -1) {
 		reasons = DISLIKE_REASONS;
 	}
 

+ 139 - 122
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -65,11 +65,11 @@
 	let generatingImage = false;
 
 	let showRateComment = false;
-
 	let showCitationModal = false;
+
 	let selectedCitation = null;
 
-	$: tokens = marked.lexer(sanitizeResponseContent(message.content));
+	$: tokens = marked.lexer(sanitizeResponseContent(message?.content));
 
 	const renderer = new marked.Renderer();
 
@@ -332,13 +332,17 @@
 <CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
 
 {#key message.id}
-	<div class=" flex w-full message-{message.id}" id="message-{message.id}">
+	<div
+		class=" flex w-full message-{message.id}"
+		id="message-{message.id}"
+		dir={$settings.chatDirection}
+	>
 		<ProfileImage
 			src={modelfiles[message.model]?.imageUrl ??
 				($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
 		/>
 
-		<div class="w-full overflow-hidden">
+		<div class="w-full overflow-hidden pl-1">
 			<Name>
 				{#if message.model in modelfiles}
 					{modelfiles[message.model]?.title}
@@ -347,8 +351,10 @@
 				{/if}
 
 				{#if message.timestamp}
-					<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
-						{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
+					<span
+						class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
+					>
+						{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
 					</span>
 				{/if}
 			</Name>
@@ -370,7 +376,7 @@
 			>
 				<div>
 					{#if edit === true}
-						<div class=" w-full">
+						<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
 							<textarea
 								id="message-edit-{message.id}"
 								bind:this={editTextAreaElement}
@@ -382,23 +388,25 @@
 								}}
 							/>
 
-							<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
+							<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
 								<button
-									class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
+									id="close-edit-message-button"
+									class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
 									on:click={() => {
-										editMessageConfirmHandler();
+										cancelEditMessage();
 									}}
 								>
-									{$i18n.t('Save')}
+									{$i18n.t('Cancel')}
 								</button>
 
 								<button
-									class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
+									id="save-edit-message-button"
+									class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
 									on:click={() => {
-										cancelEditMessage();
+										editMessageConfirmHandler();
 									}}
 								>
-									{$i18n.t('Cancel')}
+									{$i18n.t('Save')}
 								</button>
 							</div>
 						</div>
@@ -430,9 +438,10 @@
 							{:else if message.content === ''}
 								<Skeleton />
 							{:else}
-								{#each tokens as token}
+								{#each tokens as token, tokenIdx}
 									{#if token.type === 'code'}
 										<CodeBlock
+											id={`${message.id}-${tokenIdx}`}
 											lang={token.lang}
 											code={revertSanitizedResponseContent(token.text)}
 										/>
@@ -476,7 +485,7 @@
 												<div class="bg-white dark:bg-gray-700 rounded-full size-4">
 													{idx + 1}
 												</div>
-												<div class=" mx-2">
+												<div class="flex-1 mx-2 line-clamp-1">
 													{citation.source.name}
 												</div>
 											</button>
@@ -487,50 +496,56 @@
 
 							{#if message.done || siblings.length > 1}
 								<div
-									class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
+									class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
 								>
 									{#if siblings.length > 1}
-										<div class="flex self-center min-w-fit">
+										<div class="flex self-center min-w-fit" dir="ltr">
 											<button
-												class="self-center dark:hover:text-white hover:text-black transition"
+												class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
 												on:click={() => {
 													showPreviousMessage(message);
 												}}
 											>
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 20 20"
-													fill="currentColor"
-													class="w-4 h-4"
+													fill="none"
+													viewBox="0 0 24 24"
+													stroke="currentColor"
+													stroke-width="2.5"
+													class="size-3.5"
 												>
 													<path
-														fill-rule="evenodd"
-														d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
-														clip-rule="evenodd"
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														d="M15.75 19.5 8.25 12l7.5-7.5"
 													/>
 												</svg>
 											</button>
 
-											<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
-												{siblings.indexOf(message.id) + 1} / {siblings.length}
+											<div
+												class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
+											>
+												{siblings.indexOf(message.id) + 1}/{siblings.length}
 											</div>
 
 											<button
-												class="self-center dark:hover:text-white hover:text-black transition"
+												class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
 												on:click={() => {
 													showNextMessage(message);
 												}}
 											>
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 20 20"
-													fill="currentColor"
-													class="w-4 h-4"
+													fill="none"
+													viewBox="0 0 24 24"
+													stroke="currentColor"
+													stroke-width="2.5"
+													class="size-3.5"
 												>
 													<path
-														fill-rule="evenodd"
-														d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
-														clip-rule="evenodd"
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														d="m8.25 4.5 7.5 7.5-7.5 7.5"
 													/>
 												</svg>
 											</button>
@@ -543,7 +558,7 @@
 												<button
 													class="{isLastMessage
 														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
 													on:click={() => {
 														editMessageHandler();
 													}}
@@ -552,7 +567,7 @@
 														xmlns="http://www.w3.org/2000/svg"
 														fill="none"
 														viewBox="0 0 24 24"
-														stroke-width="2"
+														stroke-width="2.3"
 														stroke="currentColor"
 														class="w-4 h-4"
 													>
@@ -570,7 +585,7 @@
 											<button
 												class="{isLastMessage
 													? 'visible'
-													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
+													: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
 												on:click={() => {
 													copyToClipboard(message.content);
 												}}
@@ -579,7 +594,7 @@
 													xmlns="http://www.w3.org/2000/svg"
 													fill="none"
 													viewBox="0 0 24 24"
-													stroke-width="2"
+													stroke-width="2.3"
 													stroke="currentColor"
 													class="w-4 h-4"
 												>
@@ -592,83 +607,12 @@
 											</button>
 										</Tooltip>
 
-										{#if !readOnly}
-											<Tooltip content={$i18n.t('Good Response')} placement="bottom">
-												<button
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
-														?.rating === 1
-														? 'bg-gray-100 dark:bg-gray-800'
-														: ''} dark:hover:text-white hover:text-black transition"
-													on:click={() => {
-														rateMessage(message.id, 1);
-														showRateComment = true;
-
-														window.setTimeout(() => {
-															document
-																.getElementById(`message-feedback-${message.id}`)
-																?.scrollIntoView();
-														}, 0);
-													}}
-												>
-													<svg
-														stroke="currentColor"
-														fill="none"
-														stroke-width="2"
-														viewBox="0 0 24 24"
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														class="w-4 h-4"
-														xmlns="http://www.w3.org/2000/svg"
-														><path
-															d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
-														/></svg
-													>
-												</button>
-											</Tooltip>
-
-											<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
-												<button
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
-														?.rating === -1
-														? 'bg-gray-100 dark:bg-gray-800'
-														: ''} dark:hover:text-white hover:text-black transition"
-													on:click={() => {
-														rateMessage(message.id, -1);
-														showRateComment = true;
-														window.setTimeout(() => {
-															document
-																.getElementById(`message-feedback-${message.id}`)
-																?.scrollIntoView();
-														}, 0);
-													}}
-												>
-													<svg
-														stroke="currentColor"
-														fill="none"
-														stroke-width="2"
-														viewBox="0 0 24 24"
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														class="w-4 h-4"
-														xmlns="http://www.w3.org/2000/svg"
-														><path
-															d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
-														/></svg
-													>
-												</button>
-											</Tooltip>
-										{/if}
-
 										<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
 											<button
 												id="speak-button-{message.id}"
 												class="{isLastMessage
 													? 'visible'
-													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
+													: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
 												on:click={() => {
 													if (!loadingSpeech) {
 														toggleSpeakMessage(message);
@@ -715,7 +659,7 @@
 														xmlns="http://www.w3.org/2000/svg"
 														fill="none"
 														viewBox="0 0 24 24"
-														stroke-width="2"
+														stroke-width="2.3"
 														stroke="currentColor"
 														class="w-4 h-4"
 													>
@@ -730,7 +674,7 @@
 														xmlns="http://www.w3.org/2000/svg"
 														fill="none"
 														viewBox="0 0 24 24"
-														stroke-width="2"
+														stroke-width="2.3"
 														stroke="currentColor"
 														class="w-4 h-4"
 													>
@@ -749,7 +693,7 @@
 												<button
 													class="{isLastMessage
 														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
+														: 'invisible group-hover:visible'}  p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
 													on:click={() => {
 														if (!generatingImage) {
 															generateImage(message);
@@ -796,7 +740,7 @@
 															xmlns="http://www.w3.org/2000/svg"
 															fill="none"
 															viewBox="0 0 24 24"
-															stroke-width="2"
+															stroke-width="2.3"
 															stroke="currentColor"
 															class="w-4 h-4"
 														>
@@ -816,7 +760,7 @@
 												<button
 													class=" {isLastMessage
 														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
 													on:click={() => {
 														console.log(message);
 													}}
@@ -826,7 +770,7 @@
 														xmlns="http://www.w3.org/2000/svg"
 														fill="none"
 														viewBox="0 0 24 24"
-														stroke-width="2"
+														stroke-width="2.3"
 														stroke="currentColor"
 														class="w-4 h-4"
 													>
@@ -840,13 +784,84 @@
 											</Tooltip>
 										{/if}
 
+										{#if !readOnly}
+											<Tooltip content={$i18n.t('Good Response')} placement="bottom">
+												<button
+													class="{isLastMessage
+														? 'visible'
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
+														?.annotation?.rating === 1
+														? 'bg-gray-100 dark:bg-gray-800'
+														: ''} dark:hover:text-white hover:text-black transition"
+													on:click={() => {
+														rateMessage(message.id, 1);
+														showRateComment = true;
+
+														window.setTimeout(() => {
+															document
+																.getElementById(`message-feedback-${message.id}`)
+																?.scrollIntoView();
+														}, 0);
+													}}
+												>
+													<svg
+														stroke="currentColor"
+														fill="none"
+														stroke-width="2.3"
+														viewBox="0 0 24 24"
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														class="w-4 h-4"
+														xmlns="http://www.w3.org/2000/svg"
+														><path
+															d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
+														/></svg
+													>
+												</button>
+											</Tooltip>
+
+											<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
+												<button
+													class="{isLastMessage
+														? 'visible'
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
+														?.annotation?.rating === -1
+														? 'bg-gray-100 dark:bg-gray-800'
+														: ''} dark:hover:text-white hover:text-black transition"
+													on:click={() => {
+														rateMessage(message.id, -1);
+														showRateComment = true;
+														window.setTimeout(() => {
+															document
+																.getElementById(`message-feedback-${message.id}`)
+																?.scrollIntoView();
+														}, 0);
+													}}
+												>
+													<svg
+														stroke="currentColor"
+														fill="none"
+														stroke-width="2.3"
+														viewBox="0 0 24 24"
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														class="w-4 h-4"
+														xmlns="http://www.w3.org/2000/svg"
+														><path
+															d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
+														/></svg
+													>
+												</button>
+											</Tooltip>
+										{/if}
+
 										{#if isLastMessage && !readOnly}
 											<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
 												<button
 													type="button"
 													class="{isLastMessage
 														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
 													on:click={() => {
 														continueGeneration();
 													}}
@@ -855,7 +870,7 @@
 														xmlns="http://www.w3.org/2000/svg"
 														fill="none"
 														viewBox="0 0 24 24"
-														stroke-width="2"
+														stroke-width="2.3"
 														stroke="currentColor"
 														class="w-4 h-4"
 													>
@@ -878,14 +893,16 @@
 													type="button"
 													class="{isLastMessage
 														? 'visible'
-														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
-													on:click={regenerateResponse}
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
+													on:click={() => {
+														regenerateResponse(message);
+													}}
 												>
 													<svg
 														xmlns="http://www.w3.org/2000/svg"
 														fill="none"
 														viewBox="0 0 24 24"
-														stroke-width="2"
+														stroke-width="2.3"
 														stroke="currentColor"
 														class="w-4 h-4"
 													>
@@ -902,7 +919,7 @@
 								</div>
 							{/if}
 
-							{#if showRateComment}
+							{#if message.done && showRateComment}
 								<RateComment
 									messageId={message.id}
 									bind:show={showRateComment}

+ 172 - 91
src/lib/components/chat/Messages/UserMessage.svelte

@@ -7,6 +7,8 @@
 	import { modelfiles, settings } from '$lib/stores';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
+	import { user as _user } from '$lib/stores';
+
 	const i18n = getContext('i18n');
 
 	const dispatch = createEventDispatcher();
@@ -54,49 +56,55 @@
 	};
 </script>
 
-<div class=" flex w-full">
-	<ProfileImage
-		src={message.user
-			? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ?? '/user.png'
-			: user?.profile_image_url ?? '/user.png'}
-	/>
-
-	<div class="w-full overflow-hidden">
-		<div class="user-message">
-			<Name>
-				{#if message.user}
-					{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
-						{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
+<div class=" flex w-full user-message" dir={$settings.chatDirection}>
+	{#if !($settings?.chatBubble ?? true)}
+		<ProfileImage
+			src={message.user
+				? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ??
+				  '/user.png'
+				: user?.profile_image_url ?? '/user.png'}
+		/>
+	{/if}
+	<div class="w-full overflow-hidden pl-1">
+		{#if !($settings?.chatBubble ?? true)}
+			<div>
+				<Name>
+					{#if message.user}
+						{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
+							{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
+						{:else}
+							{$i18n.t('You')}
+							<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
+						{/if}
+					{:else if $settings.showUsername || $_user.name !== user.name}
+						{user.name}
 					{:else}
 						{$i18n.t('You')}
-						<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
 					{/if}
-				{:else if $settings.showUsername}
-					{user.name}
-				{:else}
-					{$i18n.t('You')}
-				{/if}
-
-				{#if message.timestamp}
-					<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
-						{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
-					</span>
-				{/if}
-			</Name>
-		</div>
+
+					{#if message.timestamp}
+						<span
+							class=" invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
+						>
+							{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
+						</span>
+					{/if}
+				</Name>
+			</div>
+		{/if}
 
 		<div
-			class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
+			class="prose chat-{message.role} w-full max-w-full flex flex-col justify-end dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
 		>
 			{#if message.files}
-				<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
+				<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
 					{#each message.files as file}
-						<div>
+						<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
 							{#if file.type === 'image'}
 								<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
 							{:else if file.type === 'doc'}
 								<button
-									class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
+									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
 									type="button"
 									on:click={() => {
 										if (file?.url) {
@@ -132,7 +140,7 @@
 								</button>
 							{:else if file.type === 'collection'}
 								<button
-									class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
+									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
 									type="button"
 								>
 									<div class="p-2.5 bg-red-400 text-white rounded-lg">
@@ -166,7 +174,7 @@
 			{/if}
 
 			{#if edit === true}
-				<div class=" w-full">
+				<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
 					<textarea
 						id="message-edit-{message.id}"
 						bind:this={messageEditTextAreaElement}
@@ -190,85 +198,104 @@
 						}}
 					/>
 
-					<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
+					<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
 						<button
-							id="save-edit-message-button"
-							class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
+							id="close-edit-message-button"
+							class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
 							on:click={() => {
-								editMessageConfirmHandler();
+								cancelEditMessage();
 							}}
 						>
-							{$i18n.t('Save & Submit')}
+							{$i18n.t('Cancel')}
 						</button>
 
 						<button
-							id="close-edit-message-button"
-							class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
+							id="save-edit-message-button"
+							class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
 							on:click={() => {
-								cancelEditMessage();
+								editMessageConfirmHandler();
 							}}
 						>
-							{$i18n.t('Cancel')}
+							{$i18n.t('Send')}
 						</button>
 					</div>
 				</div>
 			{:else}
 				<div class="w-full">
-					<pre id="user-message">{message.content}</pre>
+					<div class="flex {$settings?.chatBubble ?? true ? 'justify-end' : ''} mb-2">
+						<div
+							class="rounded-3xl {$settings?.chatBubble ?? true
+								? `max-w-[90%] px-5 py-2  bg-gray-50 dark:bg-gray-850 ${
+										message.files ? 'rounded-tr-lg' : ''
+								  }`
+								: ''}  "
+						>
+							<pre id="user-message">{message.content}</pre>
+						</div>
+					</div>
 
-					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
-						{#if siblings.length > 1}
-							<div class="flex self-center">
-								<button
-									class="self-center dark:hover:text-white hover:text-black transition"
-									on:click={() => {
-										showPreviousMessage(message);
-									}}
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 20 20"
-										fill="currentColor"
-										class="w-4 h-4"
+					<div
+						class=" flex {$settings?.chatBubble ?? true
+							? 'justify-end'
+							: ''}  text-gray-600 dark:text-gray-500"
+					>
+						{#if !($settings?.chatBubble ?? true)}
+							{#if siblings.length > 1}
+								<div class="flex self-center" dir="ltr">
+									<button
+										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										on:click={() => {
+											showPreviousMessage(message);
+										}}
 									>
-										<path
-											fill-rule="evenodd"
-											d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								</button>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke="currentColor"
+											stroke-width="2.5"
+											class="size-3.5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M15.75 19.5 8.25 12l7.5-7.5"
+											/>
+										</svg>
+									</button>
 
-								<div class="text-xs font-bold self-center dark:text-gray-100">
-									{siblings.indexOf(message.id) + 1} / {siblings.length}
-								</div>
+									<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
+										{siblings.indexOf(message.id) + 1}/{siblings.length}
+									</div>
 
-								<button
-									class="self-center dark:hover:text-white hover:text-black transition"
-									on:click={() => {
-										showNextMessage(message);
-									}}
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 20 20"
-										fill="currentColor"
-										class="w-4 h-4"
+									<button
+										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										on:click={() => {
+											showNextMessage(message);
+										}}
 									>
-										<path
-											fill-rule="evenodd"
-											d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								</button>
-							</div>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke="currentColor"
+											stroke-width="2.5"
+											class="size-3.5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="m8.25 4.5 7.5 7.5-7.5 7.5"
+											/>
+										</svg>
+									</button>
+								</div>
+							{/if}
 						{/if}
-
 						{#if !readOnly}
 							<Tooltip content={$i18n.t('Edit')} placement="bottom">
 								<button
-									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
+									class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
 									on:click={() => {
 										editMessageHandler();
 									}}
@@ -277,7 +304,7 @@
 										xmlns="http://www.w3.org/2000/svg"
 										fill="none"
 										viewBox="0 0 24 24"
-										stroke-width="2"
+										stroke-width="2.3"
 										stroke="currentColor"
 										class="w-4 h-4"
 									>
@@ -293,7 +320,7 @@
 
 						<Tooltip content={$i18n.t('Copy')} placement="bottom">
 							<button
-								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
+								class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
 								on:click={() => {
 									copyToClipboard(message.content);
 								}}
@@ -302,7 +329,7 @@
 									xmlns="http://www.w3.org/2000/svg"
 									fill="none"
 									viewBox="0 0 24 24"
-									stroke-width="2"
+									stroke-width="2.3"
 									stroke="currentColor"
 									class="w-4 h-4"
 								>
@@ -340,6 +367,60 @@
 								</button>
 							</Tooltip>
 						{/if}
+
+						{#if $settings?.chatBubble ?? true}
+							{#if siblings.length > 1}
+								<div class="flex self-center" dir="ltr">
+									<button
+										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										on:click={() => {
+											showPreviousMessage(message);
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke="currentColor"
+											stroke-width="2.5"
+											class="size-3.5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M15.75 19.5 8.25 12l7.5-7.5"
+											/>
+										</svg>
+									</button>
+
+									<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
+										{siblings.indexOf(message.id) + 1}/{siblings.length}
+									</div>
+
+									<button
+										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										on:click={() => {
+											showNextMessage(message);
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke="currentColor"
+											stroke-width="2.5"
+											class="size-3.5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="m8.25 4.5 7.5 7.5-7.5 7.5"
+											/>
+										</svg>
+									</button>
+								</div>
+							{/if}
+						{/if}
 					</div>
 				</div>
 			{/if}

+ 3 - 3
src/lib/components/chat/ModelSelector.svelte

@@ -2,7 +2,7 @@
 	import { Collapsible } from 'bits-ui';
 
 	import { setDefaultModels } from '$lib/apis/configs';
-	import { models, showSettings, settings, user } from '$lib/stores';
+	import { models, showSettings, settings, user, mobile } from '$lib/stores';
 	import { onMount, tick, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import Selector from './ModelSelector/Selector.svelte';
@@ -38,7 +38,7 @@
 	}
 </script>
 
-<div class="flex flex-col mt-0.5 w-full">
+<div class="flex flex-col w-full items-center md:items-start">
 	{#each selectedModels as selectedModel, selectedModelIdx}
 		<div class="flex w-full max-w-fit">
 			<div class="overflow-hidden w-full">
@@ -108,7 +108,7 @@
 	{/each}
 </div>
 
-{#if showSetDefault}
+{#if showSetDefault && !$mobile}
 	<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
 		<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
 	</div>

+ 15 - 12
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -10,7 +10,7 @@
 
 	import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
 
-	import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
+	import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
 	import { toast } from 'svelte-sonner';
 	import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -25,7 +25,7 @@
 
 	export let items = [{ value: 'mango', label: 'Mango' }];
 
-	export let className = ' w-[32rem]';
+	export let className = 'w-[30rem]';
 
 	let show = false;
 
@@ -36,7 +36,7 @@
 	let ollamaVersion = null;
 
 	$: filteredItems = searchValue
-		? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
+		? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase()))
 		: items;
 
 	const pullModelHandler = async () => {
@@ -201,10 +201,13 @@
 			<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
 		</div>
 	</DropdownMenu.Trigger>
+
 	<DropdownMenu.Content
-		class=" z-40 {className} max-w-[calc(100vw-1rem)] justify-start rounded-lg  bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50  outline-none "
+		class=" z-40 {$mobile
+			? `w-full`
+			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50  outline-none "
 		transition={flyAndScale}
-		side={'bottom-start'}
+		side={$mobile ? 'bottom' : 'bottom-start'}
 		sideOffset={4}
 	>
 		<slot>
@@ -224,11 +227,11 @@
 				<hr class="border-gray-100 dark:border-gray-800" />
 			{/if}
 
-			<div class="px-3 my-2 max-h-72 overflow-y-auto scrollbar-none">
+			<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden">
 				{#each filteredItems as item}
 					<button
 						aria-label="model-item"
-						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
 						on:click={() => {
 							value = item.value;
 
@@ -312,7 +315,7 @@
 
 				{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
 					<button
-						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
 						on:click={() => {
 							pullModelHandler();
 						}}
@@ -406,12 +409,12 @@
 </DropdownMenu.Root>
 
 <style>
-	.scrollbar-none:active::-webkit-scrollbar-thumb,
-	.scrollbar-none:focus::-webkit-scrollbar-thumb,
-	.scrollbar-none:hover::-webkit-scrollbar-thumb {
+	.scrollbar-hidden:active::-webkit-scrollbar-thumb,
+	.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
+	.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
 		visibility: visible;
 	}
-	.scrollbar-none::-webkit-scrollbar-thumb {
+	.scrollbar-hidden::-webkit-scrollbar-thumb {
 		visibility: hidden;
 	}
 </style>

+ 2 - 2
src/lib/components/chat/Settings/Account.svelte

@@ -71,7 +71,7 @@
 </script>
 
 <div class="flex flex-col h-full justify-between text-sm">
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
 		<input
 			id="profile-image-input"
 			bind:this={profileImageInputElement}
@@ -127,7 +127,7 @@
 
 				if (
 					files.length > 0 &&
-					['image/gif', 'image/jpeg', 'image/png'].includes(files[0]['type'])
+					['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type'])
 				) {
 					reader.readAsDataURL(files[0]);
 				}

+ 1 - 1
src/lib/components/chat/Settings/Advanced.svelte

@@ -84,7 +84,7 @@
 			{#if keepAlive !== null}
 				<div class="flex mt-1 space-x-2">
 					<input
-						class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 						type="text"
 						placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
 						bind:value={keepAlive}

+ 2 - 2
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte

@@ -27,7 +27,7 @@
 			<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
 			<div class=" flex-1 self-center">
 				<input
-					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 					type="number"
 					placeholder="Enter Seed"
 					bind:value={options.seed}
@@ -43,7 +43,7 @@
 			<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
 			<div class=" flex-1 self-center">
 				<input
-					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 					type="text"
 					placeholder={$i18n.t('Enter stop sequence')}
 					bind:value={options.stop}

+ 2 - 2
src/lib/components/chat/Settings/Audio.svelte

@@ -147,7 +147,7 @@
 		dispatch('save');
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
 		<div>
 			<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
 
@@ -345,7 +345,7 @@
 		{/if}
 	</div>
 
-	<div class="flex justify-end pt-3 text-sm font-medium">
+	<div class="flex justify-end text-sm font-medium">
 		<button
 			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
 			type="submit"

+ 19 - 14
src/lib/components/chat/Settings/Connections.svelte

@@ -5,28 +5,27 @@
 
 	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
 	import {
+		getOpenAIConfig,
 		getOpenAIKeys,
 		getOpenAIUrls,
+		updateOpenAIConfig,
 		updateOpenAIKeys,
 		updateOpenAIUrls
 	} from '$lib/apis/openai';
 	import { toast } from 'svelte-sonner';
+	import Switch from '$lib/components/common/Switch.svelte';
 
 	const i18n = getContext('i18n');
 
 	export let getModels: Function;
 
 	// External
-	let OLLAMA_BASE_URL = '';
 	let OLLAMA_BASE_URLS = [''];
 
-	let OPENAI_API_KEY = '';
-	let OPENAI_API_BASE_URL = '';
-
 	let OPENAI_API_KEYS = [''];
 	let OPENAI_API_BASE_URLS = [''];
 
-	let showOpenAI = false;
+	let ENABLE_OPENAI_API = false;
 
 	const updateOpenAIHandler = async () => {
 		OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
@@ -52,6 +51,10 @@
 	onMount(async () => {
 		if ($user.role === 'admin') {
 			OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
+
+			const config = await getOpenAIConfig(localStorage.token);
+			ENABLE_OPENAI_API = config.ENABLE_OPENAI_API;
+
 			OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
 			OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
 		}
@@ -65,21 +68,23 @@
 		dispatch('save');
 	}}
 >
-	<div class="  pr-1.5 overflow-y-scroll max-h-[22rem] space-y-3">
+	<div class="  pr-1.5 overflow-y-scroll max-h-[25rem] space-y-3">
 		<div class=" space-y-3">
 			<div class="mt-2 space-y-2 pr-1.5">
 				<div class="flex justify-between items-center text-sm">
 					<div class="  font-medium">{$i18n.t('OpenAI API')}</div>
-					<button
-						class=" text-xs font-medium text-gray-500"
-						type="button"
-						on:click={() => {
-							showOpenAI = !showOpenAI;
-						}}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button
-					>
+
+					<div class="mt-1">
+						<Switch
+							bind:state={ENABLE_OPENAI_API}
+							on:change={async () => {
+								updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API);
+							}}
+						/>
+					</div>
 				</div>
 
-				{#if showOpenAI}
+				{#if ENABLE_OPENAI_API}
 					<div class="flex flex-col gap-1">
 						{#each OPENAI_API_BASE_URLS as url, idx}
 							<div class="flex w-full gap-2">

+ 2 - 2
src/lib/components/chat/Settings/General.svelte

@@ -130,7 +130,7 @@
 </script>
 
 <div class="flex flex-col h-full justify-between text-sm">
-	<div class="  pr-1.5 overflow-y-scroll max-h-[22rem]">
+	<div class="  pr-1.5 overflow-y-scroll max-h-[25rem]">
 		<div class="">
 			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
 
@@ -253,7 +253,7 @@
 					{#if keepAlive !== null}
 						<div class="flex mt-1 space-x-2">
 							<input
-								class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
+								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 								type="text"
 								placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
 								bind:value={keepAlive}

+ 72 - 18
src/lib/components/chat/Settings/Interface.svelte

@@ -22,6 +22,8 @@
 	// Interface
 	let promptSuggestions = [];
 	let showUsername = false;
+	let chatBubble = true;
+	let chatDirection: 'LTR' | 'RTL' = 'LTR';
 
 	const toggleSplitLargeChunks = async () => {
 		splitLargeChunks = !splitLargeChunks;
@@ -33,6 +35,11 @@
 		saveSettings({ fullScreenMode: fullScreenMode });
 	};
 
+	const toggleChatBubble = async () => {
+		chatBubble = !chatBubble;
+		saveSettings({ chatBubble: chatBubble });
+	};
+
 	const toggleShowUsername = async () => {
 		showUsername = !showUsername;
 		saveSettings({ showUsername: showUsername });
@@ -70,6 +77,11 @@
 		}
 	};
 
+	const toggleChangeChatDirection = async () => {
+		chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
+		saveSettings({ chatDirection });
+	};
+
 	const updateInterfaceHandler = async () => {
 		if ($user.role === 'admin') {
 			promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
@@ -105,8 +117,10 @@
 
 		responseAutoCopy = settings.responseAutoCopy ?? false;
 		showUsername = settings.showUsername ?? false;
+		chatBubble = settings.chatBubble ?? true;
 		fullScreenMode = settings.fullScreenMode ?? false;
 		splitLargeChunks = settings.splitLargeChunks ?? false;
+		chatDirection = settings.chatDirection ?? 'LTR';
 	});
 </script>
 
@@ -117,22 +131,22 @@
 		dispatch('save');
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
 		<div>
 			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
+					<div class=" self-center text-xs font-medium">{$i18n.t('Chat Bubble UI')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleTitleAutoGenerate();
+							toggleChatBubble();
 						}}
 						type="button"
 					>
-						{#if titleAutoGenerate === true}
+						{#if chatBubble === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -143,18 +157,16 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Response AutoCopy to Clipboard')}
-					</div>
+					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleResponseAutoCopy();
+							toggleTitleAutoGenerate();
 						}}
 						type="button"
 					>
-						{#if responseAutoCopy === true}
+						{#if titleAutoGenerate === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -165,16 +177,18 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
+					<div class=" self-center text-xs font-medium">
+						{$i18n.t('Response AutoCopy to Clipboard')}
+					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleFullScreenMode();
+							toggleResponseAutoCopy();
 						}}
 						type="button"
 					>
-						{#if fullScreenMode === true}
+						{#if responseAutoCopy === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -185,18 +199,16 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Display the username instead of You in the Chat')}
-					</div>
+					<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleShowUsername();
+							toggleFullScreenMode();
 						}}
 						type="button"
 					>
-						{#if showUsername === true}
+						{#if fullScreenMode === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -205,6 +217,30 @@
 				</div>
 			</div>
 
+			{#if !$settings.chatBubble}
+				<div>
+					<div class=" py-0.5 flex w-full justify-between">
+						<div class=" self-center text-xs font-medium">
+							{$i18n.t('Display the username instead of You in the Chat')}
+						</div>
+
+						<button
+							class="p-1 px-3 text-xs flex rounded transition"
+							on:click={() => {
+								toggleShowUsername();
+							}}
+							type="button"
+						>
+							{#if showUsername === true}
+								<span class="ml-2 self-center">{$i18n.t('On')}</span>
+							{:else}
+								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+							{/if}
+						</button>
+					</div>
+				</div>
+			{/if}
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
 					<div class=" self-center text-xs font-medium">
@@ -228,6 +264,24 @@
 			</div>
 		</div>
 
+		<div>
+			<div class=" py-0.5 flex w-full justify-between">
+				<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>
+
+				<button
+					class="p-1 px-3 text-xs flex rounded transition"
+					on:click={toggleChangeChatDirection}
+					type="button"
+				>
+					{#if chatDirection === 'LTR'}
+						<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
+					{:else}
+						<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
+					{/if}
+				</button>
+			</div>
+		</div>
+
 		<hr class=" dark:border-gray-700" />
 
 		<div>
@@ -283,7 +337,7 @@
 		{#if $user.role === 'admin'}
 			<hr class=" dark:border-gray-700" />
 
-			<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
+			<div class=" space-y-3 pr-1.5">
 				<div class="flex w-full justify-between mb-2">
 					<div class=" self-center text-sm font-semibold">
 						{$i18n.t('Default Prompt Suggestions')}

+ 96 - 0
src/lib/components/chat/Settings/Personalization.svelte

@@ -0,0 +1,96 @@
+<script lang="ts">
+	import { getBackendConfig } from '$lib/apis';
+	import { setDefaultPromptSuggestions } from '$lib/apis/configs';
+	import Switch from '$lib/components/common/Switch.svelte';
+	import { config, models, settings, user } from '$lib/stores';
+	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
+	import { toast } from 'svelte-sonner';
+	import ManageModal from './Personalization/ManageModal.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	const dispatch = createEventDispatcher();
+
+	const i18n = getContext('i18n');
+
+	export let saveSettings: Function;
+
+	let showManageModal = false;
+
+	// Addons
+	let enableMemory = false;
+
+	onMount(async () => {
+		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
+		enableMemory = settings?.memory ?? false;
+	});
+</script>
+
+<ManageModal bind:show={showManageModal} />
+
+<form
+	class="flex flex-col h-full justify-between space-y-3 text-sm"
+	on:submit|preventDefault={() => {
+		dispatch('save');
+	}}
+>
+	<div class="  pr-1.5 overflow-y-scroll max-h-[25rem]">
+		<div>
+			<div class="flex items-center justify-between mb-1">
+				<Tooltip
+					content="This is an experimental feature, it may not function as expected and is subject to change at any time."
+				>
+					<div class="text-sm font-medium">
+						{$i18n.t('Memory')}
+
+						<span class=" text-xs text-gray-500">({$i18n.t('Experimental')})</span>
+					</div>
+				</Tooltip>
+
+				<div class="mt-1">
+					<Switch
+						bind:state={enableMemory}
+						on:change={async () => {
+							saveSettings({ memory: enableMemory });
+						}}
+					/>
+				</div>
+			</div>
+		</div>
+
+		<div class="text-xs text-gray-600 dark:text-gray-400">
+			<div>
+				You can personalize your interactions with LLMs by adding memories through the 'Manage'
+				button below, making them more helpful and tailored to you.
+			</div>
+
+			<!-- <div class="mt-3">
+				To understand what LLM remembers or teach it something new, just chat with it:
+
+				<div>- “Remember that I like concise responses.”</div>
+				<div>- “I just got a puppy!”</div>
+				<div>- “What do you remember about me?”</div>
+				<div>- “Where did we leave off on my last project?”</div>
+			</div> -->
+		</div>
+
+		<div class="mt-3 mb-1 ml-1">
+			<button
+				type="button"
+				class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
+				on:click={() => {
+					showManageModal = true;
+				}}
+			>
+				Manage
+			</button>
+		</div>
+	</div>
+
+	<div class="flex justify-end text-sm font-medium">
+		<button
+			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
+			type="submit"
+		>
+			{$i18n.t('Save')}
+		</button>
+	</div>
+</form>

+ 125 - 0
src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte

@@ -0,0 +1,125 @@
+<script>
+	import { createEventDispatcher, getContext } from 'svelte';
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import { addNewMemory } from '$lib/apis/memories';
+	import { toast } from 'svelte-sonner';
+
+	const dispatch = createEventDispatcher();
+
+	export let show;
+
+	const i18n = getContext('i18n');
+
+	let loading = false;
+	let content = '';
+
+	const submitHandler = async () => {
+		loading = true;
+
+		const res = await addNewMemory(localStorage.token, content).catch((error) => {
+			toast.error(error);
+
+			return null;
+		});
+
+		if (res) {
+			console.log(res);
+			toast.success('Memory added successfully');
+			content = '';
+			show = false;
+			dispatch('save');
+		}
+
+		loading = false;
+	};
+</script>
+
+<Modal bind:show size="sm">
+	<div>
+		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
+			<div class=" text-lg font-medium self-center">{$i18n.t('Add Memory')}</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
+			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
+				<form
+					class="flex flex-col w-full"
+					on:submit|preventDefault={() => {
+						submitHandler();
+					}}
+				>
+					<div class="">
+						<textarea
+							bind:value={content}
+							class=" bg-transparent w-full text-sm resize-none rounded-xl p-3 outline outline-1 outline-gray-100 dark:outline-gray-800"
+							rows="3"
+							placeholder={$i18n.t('Enter a detail about yourself for your LLMs to recall')}
+						/>
+
+						<div class="text-xs text-gray-500">
+							ⓘ Refer to yourself as "User" (e.g., "User is learning Spanish")
+						</div>
+					</div>
+
+					<div class="flex justify-end pt-1 text-sm font-medium">
+						<button
+							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading
+								? ' cursor-not-allowed'
+								: ''}"
+							type="submit"
+							disabled={loading}
+						>
+							{$i18n.t('Add')}
+
+							{#if loading}
+								<div class="ml-2 self-center">
+									<svg
+										class=" w-4 h-4"
+										viewBox="0 0 24 24"
+										fill="currentColor"
+										xmlns="http://www.w3.org/2000/svg"
+										><style>
+											.spinner_ajPY {
+												transform-origin: center;
+												animation: spinner_AtaB 0.75s infinite linear;
+											}
+											@keyframes spinner_AtaB {
+												100% {
+													transform: rotate(360deg);
+												}
+											}
+										</style><path
+											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+											opacity=".25"
+										/><path
+											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+											class="spinner_ajPY"
+										/></svg
+									>
+								</div>
+							{/if}
+						</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+</Modal>

+ 165 - 0
src/lib/components/chat/Settings/Personalization/ManageModal.svelte

@@ -0,0 +1,165 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import dayjs from 'dayjs';
+	import { getContext, createEventDispatcher } from 'svelte';
+
+	const dispatch = createEventDispatcher();
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import AddMemoryModal from './AddMemoryModal.svelte';
+	import { deleteMemoriesByUserId, deleteMemoryById, getMemories } from '$lib/apis/memories';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import { error } from '@sveltejs/kit';
+
+	const i18n = getContext('i18n');
+
+	export let show = false;
+
+	let memories = [];
+
+	let showAddMemoryModal = false;
+
+	$: if (show) {
+		(async () => {
+			memories = await getMemories(localStorage.token);
+		})();
+	}
+</script>
+
+<Modal size="xl" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
+			<div class=" text-lg font-medium self-center">{$i18n.t('Memory')}</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col w-full px-5 pb-5 dark:text-gray-200">
+			<div
+				class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6 h-[28rem] max-h-screen outline outline-1 rounded-xl outline-gray-100 dark:outline-gray-800 mb-4 mt-1"
+			>
+				{#if memories.length > 0}
+					<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
+						<div class="relative overflow-x-auto">
+							<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
+								<thead
+									class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
+								>
+									<tr>
+										<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
+										<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created At')} </th>
+										<th scope="col" class="px-3 py-2 text-right" />
+									</tr>
+								</thead>
+								<tbody>
+									{#each memories as memory}
+										<tr class="border-b dark:border-gray-800 items-center">
+											<td class="px-3 py-1">
+												<div class="line-clamp-1">
+													{memory.content}
+												</div>
+											</td>
+											<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
+												<div class="my-auto whitespace-nowrap">
+													{dayjs(memory.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+												</div>
+											</td>
+											<td class="px-3 py-1">
+												<div class="flex justify-end w-full">
+													<Tooltip content="Delete">
+														<button
+															class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+															on:click={async () => {
+																const res = await deleteMemoryById(
+																	localStorage.token,
+																	memory.id
+																).catch((error) => {
+																	toast.error(error);
+																	return null;
+																});
+
+																if (res) {
+																	toast.success('Memory deleted successfully');
+																	memories = await getMemories(localStorage.token);
+																}
+															}}
+														>
+															<svg
+																xmlns="http://www.w3.org/2000/svg"
+																fill="none"
+																viewBox="0 0 24 24"
+																stroke-width="1.5"
+																stroke="currentColor"
+																class="w-4 h-4"
+															>
+																<path
+																	stroke-linecap="round"
+																	stroke-linejoin="round"
+																	d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+																/>
+															</svg>
+														</button>
+													</Tooltip>
+												</div>
+											</td>
+										</tr>
+									{/each}
+								</tbody>
+							</table>
+						</div>
+					</div>
+				{:else}
+					<div class="text-center flex h-full text-sm w-full">
+						<div class=" my-auto pb-10 px-4 w-full text-gray-500">
+							{$i18n.t('Memories accessible by LLMs will be shown here.')}
+						</div>
+					</div>
+				{/if}
+			</div>
+			<div class="flex text-sm font-medium gap-1.5">
+				<button
+					class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
+					on:click={() => {
+						showAddMemoryModal = true;
+					}}>Add memory</button
+				>
+				<button
+					class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
+					on:click={async () => {
+						const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
+							toast.error(error);
+							return null;
+						});
+
+						if (res) {
+							toast.success('Memory cleared successfully');
+							memories = [];
+						}
+					}}>Clear memory</button
+				>
+			</div>
+		</div>
+	</div>
+</Modal>
+
+<AddMemoryModal
+	bind:show={showAddMemoryModal}
+	on:save={async () => {
+		memories = await getMemories(localStorage.token);
+	}}
+/>

+ 25 - 1
src/lib/components/chat/SettingsModal.svelte

@@ -15,6 +15,8 @@
 	import Chats from './Settings/Chats.svelte';
 	import Connections from './Settings/Connections.svelte';
 	import Images from './Settings/Images.svelte';
+	import User from '../icons/User.svelte';
+	import Personalization from './Settings/Personalization.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -165,6 +167,21 @@
 					<div class=" self-center">{$i18n.t('Interface')}</div>
 				</button>
 
+				<button
+					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+					'personalization'
+						? 'bg-gray-200 dark:bg-gray-700'
+						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
+					on:click={() => {
+						selectedTab = 'personalization';
+					}}
+				>
+					<div class=" self-center mr-2">
+						<User />
+					</div>
+					<div class=" self-center">{$i18n.t('Personalization')}</div>
+				</button>
+
 				<button
 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 					'audio'
@@ -298,7 +315,7 @@
 					<div class=" self-center">{$i18n.t('About')}</div>
 				</button>
 			</div>
-			<div class="flex-1 md:min-h-[25rem]">
+			<div class="flex-1 md:min-h-[28rem]">
 				{#if selectedTab === 'general'}
 					<General
 						{getModels}
@@ -323,6 +340,13 @@
 							toast.success($i18n.t('Settings saved successfully!'));
 						}}
 					/>
+				{:else if selectedTab === 'personalization'}
+					<Personalization
+						{saveSettings}
+						on:save={() => {
+							toast.success($i18n.t('Settings saved successfully!'));
+						}}
+					/>
 				{:else if selectedTab === 'audio'}
 					<Audio
 						{saveSettings}

+ 16 - 2
src/lib/components/chat/ShareChatModal.svelte

@@ -57,10 +57,23 @@
 
 	export let show = false;
 
+	const isDifferentChat = (_chat) => {
+		if (!chat) {
+			return true;
+		}
+		if (!_chat) {
+			return false;
+		}
+		return chat.id !== _chat.id || chat.share_id !== _chat.share_id;
+	};
+
 	$: if (show) {
 		(async () => {
 			if (chatId) {
-				chat = await getChatById(localStorage.token, chatId);
+				const _chat = await getChatById(localStorage.token, chatId);
+				if (isDifferentChat(_chat)) {
+					chat = _chat;
+				}
 			} else {
 				chat = null;
 				console.log(chat);
@@ -115,7 +128,7 @@
 						{$i18n.t('and create a new shared link.')}
 					{:else}
 						{$i18n.t(
-							"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat."
+							"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat."
 						)}
 					{/if}
 				</div>
@@ -137,6 +150,7 @@
 							<button
 								class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
 								type="button"
+								id="copy-and-share-chat-button"
 								on:click={async () => {
 									const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
 

+ 2 - 2
src/lib/components/common/Modal.svelte

@@ -16,9 +16,9 @@
 		} else if (size === 'sm') {
 			return 'w-[30rem]';
 		} else if (size === 'md') {
-			return 'w-[44rem]';
-		} else {
 			return 'w-[48rem]';
+		} else {
+			return 'w-[56rem]';
 		}
 	};
 

+ 1 - 1
src/lib/components/common/Selector.svelte

@@ -26,7 +26,7 @@
 	let searchValue = '';
 
 	$: filteredItems = searchValue
-		? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
+		? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase()))
 		: items;
 </script>
 

+ 22 - 0
src/lib/components/common/Switch.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	import { createEventDispatcher, tick } from 'svelte';
+	import { Switch } from 'bits-ui';
+	export let state = true;
+
+	const dispatch = createEventDispatcher();
+</script>
+
+<Switch.Root
+	bind:checked={state}
+	onCheckedChange={async (e) => {
+		await tick();
+		dispatch('change', e);
+	}}
+	class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] transition  {state
+		? ' bg-emerald-600'
+		: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
+>
+	<Switch.Thumb
+		class="pointer-events-none block size-4 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "
+	/>
+</Switch.Root>

+ 4 - 2
src/lib/components/documents/Settings/General.svelte

@@ -190,13 +190,13 @@
 		saveHandler();
 	}}
 >
-	<div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[22rem]">
+	<div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[28rem]">
 		<div class="flex flex-col gap-0.5">
 			<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
 
 			<div class="  flex w-full justify-between">
 				<div class=" self-center text-xs font-medium">
-					{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
+					{$i18n.t('Scan for documents from {{path}}', { path: 'DOCS_DIR (/data/docs)' })}
 				</div>
 
 				<button
@@ -254,6 +254,8 @@
 								embeddingModel = '';
 							} else if (e.target.value === 'openai') {
 								embeddingModel = 'text-embedding-3-small';
+							} else if (e.target.value === '') {
+								embeddingModel = 'sentence-transformers/all-MiniLM-L6-v2';
 							}
 						}}
 					>

+ 1 - 1
src/lib/components/documents/Settings/QueryParams.svelte

@@ -46,7 +46,7 @@
 		saveHandler();
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
 		<div class=" ">
 			<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
 

+ 19 - 0
src/lib/components/icons/MenuLines.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'size-5';
+	export let strokeWidth = '2';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
+	/>
+</svg>

+ 11 - 0
src/lib/components/icons/User.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
+	<path
+		fill-rule="evenodd"
+		d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
+		clip-rule="evenodd"
+	/>
+</svg>

+ 65 - 37
src/lib/components/layout/Navbar.svelte

@@ -2,7 +2,17 @@
 	import { getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 
-	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
+	import {
+		WEBUI_NAME,
+		chatId,
+		mobile,
+		modelfiles,
+		settings,
+		showArchivedChats,
+		showSettings,
+		showSidebar,
+		user
+	} from '$lib/stores';
 
 	import { slide } from 'svelte/transition';
 	import ShareChatModal from '../chat/ShareChatModal.svelte';
@@ -10,6 +20,8 @@
 	import Tooltip from '../common/Tooltip.svelte';
 	import Menu from './Navbar/Menu.svelte';
 	import { page } from '$app/stores';
+	import UserMenu from './Sidebar/UserMenu.svelte';
+	import MenuLines from '../icons/MenuLines.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -28,48 +40,35 @@
 
 <ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
 <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
-	<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1.3rem]">
+	<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]">
 		<div class="flex items-center w-full max-w-full">
+			<div
+				class="{$showSidebar
+					? 'md:hidden'
+					: ''} mr-3 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
+			>
+				<button
+					id="sidebar-toggle-button"
+					class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+					on:click={() => {
+						showSidebar.set(!$showSidebar);
+					}}
+				>
+					<div class=" m-auto self-center">
+						<MenuLines />
+					</div>
+				</button>
+			</div>
 			<div class="flex-1 overflow-hidden max-w-full">
 				{#if showModelSelector}
 					<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
 				{/if}
 			</div>
 
-			<div class="self-start flex flex-none items-center">
-				<div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
+			<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
+				<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
 
-				{#if !shareEnabled}
-					<Tooltip content={$i18n.t('Settings')}>
-						<button
-							class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
-							id="open-settings-button"
-							on:click={async () => {
-								await showSettings.set(!$showSettings);
-							}}
-						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								fill="none"
-								viewBox="0 0 24 24"
-								stroke-width="1.5"
-								stroke="currentColor"
-								class="w-5 h-5"
-							>
-								<path
-									stroke-linecap="round"
-									stroke-linejoin="round"
-									d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
-								/>
-								<path
-									stroke-linecap="round"
-									stroke-linejoin="round"
-									d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
-								/>
-							</svg>
-						</button>
-					</Tooltip>
-				{:else}
+				{#if shareEnabled}
 					<Menu
 						{chat}
 						{shareEnabled}
@@ -81,7 +80,8 @@
 						}}
 					>
 						<button
-							class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
+							class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+							id="chat-context-menu-button"
 						>
 							<div class=" m-auto self-center">
 								<svg
@@ -105,7 +105,9 @@
 				<Tooltip content={$i18n.t('New Chat')}>
 					<button
 						id="new-chat-button"
-						class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
+						class=" flex {$showSidebar
+							? 'md:hidden'
+							: ''} cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 						on:click={() => {
 							initNewChat();
 						}}
@@ -127,6 +129,32 @@
 						</div>
 					</button>
 				</Tooltip>
+
+				{#if $user !== undefined}
+					<UserMenu
+						className="max-w-[200px]"
+						role={$user.role}
+						on:show={(e) => {
+							if (e.detail === 'archived-chat') {
+								showArchivedChats.set(true);
+							}
+						}}
+					>
+						<button
+							class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+							aria-label="User Menu"
+						>
+							<div class=" self-center">
+								<img
+									src={$user.profile_image_url}
+									class="size-6 object-cover rounded-full"
+									alt="User profile"
+									draggable="false"
+								/>
+							</div>
+						</button>
+					</UserMenu>
+				{/if}
 			</div>
 		</div>
 	</div>

+ 62 - 88
src/lib/components/layout/Navbar/Menu.svelte

@@ -76,14 +76,14 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
+			class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
 			sideOffset={8}
 			side="bottom"
 			align="end"
 			transition={flyAndScale}
 		>
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+			<!-- <DropdownMenu.Item
+				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-800 rounded-md"
 				on:click={async () => {
 					await showSettings.set(!$showSettings);
 				}}
@@ -108,113 +108,87 @@
 					/>
 				</svg>
 				<div class="flex items-center">{$i18n.t('Settings')}</div>
-			</DropdownMenu.Item>
+			</DropdownMenu.Item> -->
 
-			{#if shareEnabled}
-				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
-					on:click={() => {
-						shareHandler();
-					}}
+			<DropdownMenu.Item
+				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				id="chat-share-button"
+				on:click={() => {
+					shareHandler();
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 24 24"
+					fill="currentColor"
+					class="size-4"
 				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 24 24"
-						fill="currentColor"
-						class="size-4"
-					>
-						<path
-							fill-rule="evenodd"
-							d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
-							clip-rule="evenodd"
-						/>
-					</svg>
-					<div class="flex items-center">{$i18n.t('Share')}</div>
-				</DropdownMenu.Item>
+					<path
+						fill-rule="evenodd"
+						d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+				<div class="flex items-center">{$i18n.t('Share')}</div>
+			</DropdownMenu.Item>
 
-				<!-- <DropdownMenu.Item
+			<!-- <DropdownMenu.Item
 					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer"
 					on:click={() => {
 						downloadHandler();
 					}}
 				/> -->
-				<DropdownMenu.Sub>
-					<DropdownMenu.SubTrigger
-						class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
-					>
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="1.5"
-							stroke="currentColor"
-							class="size-4"
-						>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
-							/>
-						</svg>
-
-						<div class="flex items-center">{$i18n.t('Download')}</div>
-					</DropdownMenu.SubTrigger>
-					<DropdownMenu.SubContent
-						class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
-						transition={flyAndScale}
-						sideOffset={8}
-					>
-						<DropdownMenu.Item
-							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
-							on:click={() => {
-								downloadTxt();
-							}}
-						>
-							<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
-						</DropdownMenu.Item>
-
-						<DropdownMenu.Item
-							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
-							on:click={() => {
-								downloadPdf();
-							}}
-						>
-							<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
-						</DropdownMenu.Item>
-					</DropdownMenu.SubContent>
-				</DropdownMenu.Sub>
-
-				<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
-
-				<div class="flex p-1">
-					<Tags chatId={chat.id} />
-				</div>
-
-				<!-- <DropdownMenu.Item
-					class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer"
-					on:click={() => {
-						tagHandler();
-					}}
+			<DropdownMenu.Sub>
+				<DropdownMenu.SubTrigger
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				>
 					<svg
 						xmlns="http://www.w3.org/2000/svg"
 						fill="none"
 						viewBox="0 0 24 24"
-						stroke-width="2"
+						stroke-width="1.5"
 						stroke="currentColor"
 						class="size-4"
 					>
 						<path
 							stroke-linecap="round"
 							stroke-linejoin="round"
-							d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z"
+							d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
 						/>
-						<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
 					</svg>
 
-					<div class="flex items-center">Tag</div>
-				</DropdownMenu.Item> -->
-			{/if}
+					<div class="flex items-center">{$i18n.t('Download')}</div>
+				</DropdownMenu.SubTrigger>
+				<DropdownMenu.SubContent
+					class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
+					transition={flyAndScale}
+					sideOffset={8}
+				>
+					<DropdownMenu.Item
+						class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+						on:click={() => {
+							downloadTxt();
+						}}
+					>
+						<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
+					</DropdownMenu.Item>
+
+					<DropdownMenu.Item
+						class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+						on:click={() => {
+							downloadPdf();
+						}}
+					>
+						<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
+					</DropdownMenu.Item>
+				</DropdownMenu.SubContent>
+			</DropdownMenu.Sub>
+
+			<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
+
+			<div class="flex p-1">
+				<Tags chatId={chat.id} />
+			</div>
 		</DropdownMenu.Content>
 	</div>
 </Dropdown>

+ 133 - 280
src/lib/components/layout/Sidebar.svelte

@@ -1,6 +1,16 @@
 <script lang="ts">
 	import { goto } from '$app/navigation';
-	import { user, chats, settings, showSettings, chatId, tags, showSidebar } from '$lib/stores';
+	import {
+		user,
+		chats,
+		settings,
+		showSettings,
+		chatId,
+		tags,
+		showSidebar,
+		mobile,
+		showArchivedChats
+	} from '$lib/stores';
 	import { onMount, getContext } from 'svelte';
 
 	const i18n = getContext('i18n');
@@ -22,8 +32,9 @@
 	import ShareChatModal from '../chat/ShareChatModal.svelte';
 	import ArchiveBox from '../icons/ArchiveBox.svelte';
 	import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
+	import UserMenu from './Sidebar/UserMenu.svelte';
 
-	const BREAKPOINT = 1024;
+	const BREAKPOINT = 768;
 
 	let show = false;
 	let navElement;
@@ -39,7 +50,6 @@
 	let chatTitleEditId = null;
 	let chatTitle = '';
 
-	let showArchivedChatsModal = false;
 	let showShareChatModal = false;
 	let showDropdown = false;
 	let isEditing = false;
@@ -66,7 +76,24 @@
 		}
 	});
 
+	mobile;
+	const onResize = () => {
+		if ($showSidebar && window.innerWidth < BREAKPOINT) {
+			showSidebar.set(false);
+		}
+	};
+
 	onMount(async () => {
+		mobile.subscribe((e) => {
+			if ($showSidebar && e) {
+				showSidebar.set(false);
+			}
+
+			if (!$showSidebar && !e) {
+				showSidebar.set(true);
+			}
+		});
+
 		showSidebar.set(window.innerWidth > BREAKPOINT);
 		await chats.set(await getChatList(localStorage.token));
 
@@ -96,20 +123,12 @@
 			checkDirection();
 		};
 
-		const onResize = () => {
-			if ($showSidebar && window.innerWidth < BREAKPOINT) {
-				showSidebar.set(false);
-			}
-		};
-
 		window.addEventListener('touchstart', onTouchStart);
 		window.addEventListener('touchend', onTouchEnd);
-		window.addEventListener('resize', onResize);
 
 		return () => {
 			window.removeEventListener('touchstart', onTouchStart);
 			window.removeEventListener('touchend', onTouchEnd);
-			window.removeEventListener('resize', onResize);
 		};
 	});
 
@@ -176,31 +195,43 @@
 
 <ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
 <ArchivedChatsModal
-	bind:show={showArchivedChatsModal}
+	bind:show={$showArchivedChats}
 	on:change={async () => {
 		await chats.set(await getChatList(localStorage.token));
 	}}
 />
 
+<!-- svelte-ignore a11y-no-static-element-interactions -->
+
+{#if $showSidebar}
+	<div
+		class=" fixed md:hidden z-40 top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center overflow-hidden overscroll-contain"
+		on:mousedown={() => {
+			showSidebar.set(!$showSidebar);
+		}}
+	/>
+{/if}
+
 <div
 	bind:this={navElement}
 	id="sidebar"
-	class="h-screen max-h-[100dvh] min-h-screen {$showSidebar
-		? 'lg:relative w-[260px]'
+	class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
+		? 'md:relative w-[260px]'
 		: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 rounded-r-2xl
         "
 	data-state={$showSidebar}
 >
 	<div
-		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {$showSidebar
+		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
 			? ''
 			: 'invisible'}"
 	>
-		<div class="px-2 flex justify-center space-x-2">
+		<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
 			<a
 				id="sidebar-new-chat-button"
-				class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				class="flex flex-1 justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 				href="/"
+				draggable="false"
 				on:click={async () => {
 					selectedChatId = null;
 
@@ -208,27 +239,30 @@
 					const newChatButton = document.getElementById('new-chat-button');
 					setTimeout(() => {
 						newChatButton?.click();
+
+						if ($mobile) {
+							showSidebar.set(false);
+						}
 					}, 0);
 				}}
 			>
-				<div class="flex self-center">
-					<div class="self-center mr-1.5">
-						<img
-							src="{WEBUI_BASE_URL}/static/favicon.png"
-							class=" size-6 -translate-x-1.5 rounded-full"
-							alt="logo"
-						/>
-					</div>
-
-					<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
+				<div class="self-center mx-1.5">
+					<img
+						crossorigin="anonymous"
+						src="{WEBUI_BASE_URL}/static/favicon.png"
+						class=" size-6 -translate-x-1.5 rounded-full"
+						alt="logo"
+					/>
 				</div>
-
-				<div class="self-center">
+				<div class=" self-center font-medium text-sm text-gray-850 dark:text-white">
+					{$i18n.t('New Chat')}
+				</div>
+				<div class="self-center ml-auto">
 					<svg
 						xmlns="http://www.w3.org/2000/svg"
 						viewBox="0 0 20 20"
 						fill="currentColor"
-						class="w-4 h-4"
+						class="size-5"
 					>
 						<path
 							d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
@@ -239,17 +273,42 @@
 					</svg>
 				</div>
 			</a>
+
+			<button
+				class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+				on:click={() => {
+					showSidebar.set(!$showSidebar);
+				}}
+			>
+				<div class=" m-auto self-center">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="2"
+						stroke="currentColor"
+						class="size-5"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
+						/>
+					</svg>
+				</div>
+			</button>
 		</div>
 
 		{#if $user?.role === 'admin'}
-			<div class="px-2 flex justify-center mt-0.5">
+			<div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200">
 				<a
-					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
-					href="/modelfiles"
+					class="flex-grow flex space-x-3 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+					href="/workspace"
 					on:click={() => {
 						selectedChatId = null;
 						chatId.set('');
 					}}
+					draggable="false"
 				>
 					<div class="self-center">
 						<svg
@@ -258,7 +317,7 @@
 							viewBox="0 0 24 24"
 							stroke-width="2"
 							stroke="currentColor"
-							class="w-4 h-4"
+							class="size-[1.1rem]"
 						>
 							<path
 								stroke-linecap="round"
@@ -269,71 +328,7 @@
 					</div>
 
 					<div class="flex self-center">
-						<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
-					</div>
-				</a>
-			</div>
-
-			<div class="px-2 flex justify-center">
-				<a
-					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
-					href="/prompts"
-					on:click={() => {
-						selectedChatId = null;
-						chatId.set('');
-					}}
-				>
-					<div class="self-center">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="2"
-							stroke="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
-							/>
-						</svg>
-					</div>
-
-					<div class="flex self-center">
-						<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
-					</div>
-				</a>
-			</div>
-
-			<div class="px-2 flex justify-center mb-1">
-				<a
-					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
-					href="/documents"
-					on:click={() => {
-						selectedChatId = null;
-						chatId.set('');
-					}}
-				>
-					<div class="self-center">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="2"
-							stroke="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
-							/>
-						</svg>
-					</div>
-
-					<div class="flex self-center">
-						<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
+						<div class=" self-center font-medium text-sm">{$i18n.t('Workspace')}</div>
 					</div>
 				</a>
 			</div>
@@ -383,9 +378,9 @@
 				</div>
 			{/if}
 
-			<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
-				<div class="flex w-full" id="chat-search">
-					<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
+			<div class="px-2 mt-0.5 mb-2 flex justify-center space-x-2">
+				<div class="flex w-full rounded-xl" id="chat-search">
+					<div class="self-center pl-3 py-2 rounded-l-xl bg-transparent">
 						<svg
 							xmlns="http://www.w3.org/2000/svg"
 							viewBox="0 0 20 20"
@@ -401,7 +396,7 @@
 					</div>
 
 					<input
-						class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm dark:text-gray-300 dark:bg-gray-950 outline-none"
+						class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
 						placeholder={$i18n.t('Search')}
 						bind:value={search}
 						on:focus={() => {
@@ -412,9 +407,9 @@
 			</div>
 
 			{#if $tags.length > 0}
-				<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
+				<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
 					<button
-						class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
+						class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
 						on:click={async () => {
 							await chats.set(await getChatList(localStorage.token));
 						}}
@@ -423,7 +418,7 @@
 					</button>
 					{#each $tags as tag}
 						<button
-							class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
+							class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
 							on:click={async () => {
 								let chatIds = await getChatListByTagName(localStorage.token, tag.name);
 								if (chatIds.length === 0) {
@@ -439,7 +434,7 @@
 				</div>
 			{/if}
 
-			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-none">
+			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
 				{#each filteredChatList as chat, idx}
 					{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
 						<div
@@ -494,7 +489,7 @@
 								href="/c/{chat.id}"
 								on:click={() => {
 									selectedChatId = chat.id;
-									if (window.innerWidth < 1024) {
+									if ($mobile) {
 										showSidebar.set(false);
 									}
 								}}
@@ -609,6 +604,9 @@
 											shareChatId = selectedChatId;
 											showShareChatModal = true;
 										}}
+										archiveChatHandler={() => {
+											archiveChatHandler(chat.id);
+										}}
 										renameHandler={() => {
 											chatTitle = chat.title;
 											chatTitleEditId = chat.id;
@@ -640,18 +638,6 @@
 										</button>
 									</ChatMenu>
 
-									<Tooltip content={$i18n.t('Archive')}>
-										<button
-											aria-label="Archive"
-											class=" self-center dark:hover:text-white transition"
-											on:click={() => {
-												archiveChatHandler(chat.id);
-											}}
-										>
-											<ArchiveBox />
-										</button>
-									</Tooltip>
-
 									{#if chat.id === $chatId}
 										<button
 											id="delete-chat-button"
@@ -685,171 +671,38 @@
 
 			<div class="flex flex-col">
 				{#if $user !== undefined}
-					<button
-						class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
-						on:click={() => {
-							showDropdown = !showDropdown;
+					<UserMenu
+						role={$user.role}
+						on:show={(e) => {
+							if (e.detail === 'archived-chat') {
+								showArchivedChats.set(true);
+							}
 						}}
 					>
-						<div class=" self-center mr-3">
-							<img
-								src={$user.profile_image_url}
-								class=" max-w-[30px] object-cover rounded-full"
-								alt="User profile"
-							/>
-						</div>
-						<div class=" self-center font-semibold">{$user.name}</div>
-					</button>
-
-					{#if showDropdown}
-						<div
-							id="dropdownDots"
-							class="absolute z-40 bottom-[70px] rounded-lg shadow w-[240px] bg-white dark:bg-gray-900"
-							transition:fade|slide={{ duration: 100 }}
+						<button
+							class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+							on:click={() => {
+								showDropdown = !showDropdown;
+							}}
 						>
-							<div class="p-1 py-2 w-full">
-								{#if $user.role === 'admin'}
-									<button
-										class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
-										on:click={() => {
-											goto('/admin');
-											showDropdown = false;
-										}}
-									>
-										<div class=" self-center mr-3">
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												fill="none"
-												viewBox="0 0 24 24"
-												stroke-width="1.5"
-												stroke="currentColor"
-												class="w-5 h-5"
-											>
-												<path
-													stroke-linecap="round"
-													stroke-linejoin="round"
-													d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
-												/>
-											</svg>
-										</div>
-										<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
-									</button>
-
-									<button
-										class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
-										on:click={() => {
-											goto('/playground');
-											showDropdown = false;
-										}}
-									>
-										<div class=" self-center mr-3">
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												fill="none"
-												viewBox="0 0 24 24"
-												stroke-width="1.5"
-												stroke="currentColor"
-												class="w-5 h-5"
-											>
-												<path
-													stroke-linecap="round"
-													stroke-linejoin="round"
-													d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
-												/>
-											</svg>
-										</div>
-										<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
-									</button>
-								{/if}
-
-								<button
-									class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
-									on:click={() => {
-										showArchivedChatsModal = true;
-										showDropdown = false;
-									}}
-								>
-									<div class=" self-center mr-3">
-										<ArchiveBox className="size-5" strokeWidth="1.5" />
-									</div>
-									<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
-								</button>
-
-								<button
-									class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
-									on:click={async () => {
-										await showSettings.set(true);
-										showDropdown = false;
-									}}
-								>
-									<div class=" self-center mr-3">
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											fill="none"
-											viewBox="0 0 24 24"
-											stroke-width="1.5"
-											stroke="currentColor"
-											class="w-5 h-5"
-										>
-											<path
-												stroke-linecap="round"
-												stroke-linejoin="round"
-												d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
-											/>
-											<path
-												stroke-linecap="round"
-												stroke-linejoin="round"
-												d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
-											/>
-										</svg>
-									</div>
-									<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
-								</button>
-							</div>
-
-							<hr class=" dark:border-gray-800 m-0 p-0" />
-
-							<div class="p-1 py-2 w-full">
-								<button
-									class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
-									on:click={() => {
-										localStorage.removeItem('token');
-										location.href = '/auth';
-										showDropdown = false;
-									}}
-								>
-									<div class=" self-center mr-3">
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 20 20"
-											fill="currentColor"
-											class="w-5 h-5"
-										>
-											<path
-												fill-rule="evenodd"
-												d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
-												clip-rule="evenodd"
-											/>
-											<path
-												fill-rule="evenodd"
-												d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
-												clip-rule="evenodd"
-											/>
-										</svg>
-									</div>
-									<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
-								</button>
+							<div class=" self-center mr-3">
+								<img
+									src={$user.profile_image_url}
+									class=" max-w-[30px] object-cover rounded-full"
+									alt="User profile"
+								/>
 							</div>
-						</div>
-					{/if}
+							<div class=" self-center font-semibold">{$user.name}</div>
+						</button>
+					</UserMenu>
 				{/if}
 			</div>
 		</div>
 	</div>
 
-	<div
+	<!-- <div
 		id="sidebar-handle"
-		class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
+		class=" hidden md:fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
 	>
 		<Tooltip
 			placement="right"
@@ -882,16 +735,16 @@
 				</span>
 			</button>
 		</Tooltip>
-	</div>
+	</div> -->
 </div>
 
 <style>
-	.scrollbar-none:active::-webkit-scrollbar-thumb,
-	.scrollbar-none:focus::-webkit-scrollbar-thumb,
-	.scrollbar-none:hover::-webkit-scrollbar-thumb {
+	.scrollbar-hidden:active::-webkit-scrollbar-thumb,
+	.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
+	.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
 		visibility: visible;
 	}
-	.scrollbar-none::-webkit-scrollbar-thumb {
+	.scrollbar-hidden::-webkit-scrollbar-thumb {
 		visibility: hidden;
 	}
 </style>

+ 16 - 4
src/lib/components/layout/Sidebar/ChatMenu.svelte

@@ -9,10 +9,12 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tags from '$lib/components/chat/Tags.svelte';
 	import Share from '$lib/components/icons/Share.svelte';
+	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
 
 	const i18n = getContext('i18n');
 
 	export let shareHandler: Function;
+	export let archiveChatHandler: Function;
 	export let renameHandler: Function;
 	export let deleteHandler: Function;
 	export let onClose: Function;
@@ -36,14 +38,14 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
+			class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
 			sideOffset={-2}
 			side="bottom"
 			align="start"
 			transition={flyAndScale}
 		>
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-md"
 				on:click={() => {
 					shareHandler();
 				}}
@@ -53,7 +55,7 @@
 			</DropdownMenu.Item>
 
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 					renameHandler();
 				}}
@@ -63,7 +65,17 @@
 			</DropdownMenu.Item>
 
 			<DropdownMenu.Item
-				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				on:click={() => {
+					archiveChatHandler();
+				}}
+			>
+				<ArchiveBox strokeWidth="2" />
+				<div class="flex items-center">{$i18n.t('Archive')}</div>
+			</DropdownMenu.Item>
+
+			<DropdownMenu.Item
+				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 					deleteHandler();
 				}}

+ 147 - 0
src/lib/components/layout/Sidebar/UserMenu.svelte

@@ -0,0 +1,147 @@
+<script lang="ts">
+	import { DropdownMenu } from 'bits-ui';
+	import { createEventDispatcher, getContext } from 'svelte';
+
+	import { flyAndScale } from '$lib/utils/transitions';
+	import { goto } from '$app/navigation';
+	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
+	import { showSettings } from '$lib/stores';
+	import { fade, slide } from 'svelte/transition';
+
+	const i18n = getContext('i18n');
+
+	export let show = false;
+	export let role = '';
+	export let className = 'max-w-[240px]';
+
+	const dispatch = createEventDispatcher();
+</script>
+
+<DropdownMenu.Root
+	bind:open={show}
+	onOpenChange={(state) => {
+		dispatch('change', state);
+	}}
+>
+	<DropdownMenu.Trigger>
+		<slot />
+	</DropdownMenu.Trigger>
+
+	<slot name="content">
+		<DropdownMenu.Content
+			class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
+			sideOffset={8}
+			side="bottom"
+			align="start"
+			transition={(e) => fade(e, { duration: 100 })}
+		>
+			<button
+				class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+				on:click={async () => {
+					await showSettings.set(true);
+					show = false;
+				}}
+			>
+				<div class=" self-center mr-3">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-5 h-5"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
+						/>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
+						/>
+					</svg>
+				</div>
+				<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
+			</button>
+
+			<button
+				class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+				on:click={() => {
+					dispatch('show', 'archived-chat');
+					show = false;
+				}}
+			>
+				<div class=" self-center mr-3">
+					<ArchiveBox className="size-5" strokeWidth="1.5" />
+				</div>
+				<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
+			</button>
+
+			{#if role === 'admin'}
+				<button
+					class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+					on:click={() => {
+						goto('/admin');
+						show = false;
+					}}
+				>
+					<div class=" self-center mr-3">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-5 h-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
+							/>
+						</svg>
+					</div>
+					<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
+				</button>
+			{/if}
+
+			<hr class=" dark:border-gray-800 my-2 p-0" />
+
+			<button
+				class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+				on:click={() => {
+					localStorage.removeItem('token');
+					location.href = '/auth';
+					show = false;
+				}}
+			>
+				<div class=" self-center mr-3">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 20 20"
+						fill="currentColor"
+						class="w-5 h-5"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
+							clip-rule="evenodd"
+						/>
+						<path
+							fill-rule="evenodd"
+							d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
+				<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
+			</button>
+
+			<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm  font-medium">
+				<div class="flex items-center">Profile</div>
+			</DropdownMenu.Item> -->
+		</DropdownMenu.Content>
+	</slot>
+</DropdownMenu.Root>

+ 611 - 0
src/lib/components/workspace/Documents.svelte

@@ -0,0 +1,611 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import { onMount, getContext } from 'svelte';
+	import { WEBUI_NAME, documents } from '$lib/stores';
+	import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
+
+	import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
+	import { uploadDocToVectorDB } from '$lib/apis/rag';
+	import { transformFileName } from '$lib/utils';
+
+	import Checkbox from '$lib/components/common/Checkbox.svelte';
+
+	import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
+	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
+	import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
+	import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
+
+	const i18n = getContext('i18n');
+
+	let importFiles = '';
+
+	let inputFiles = '';
+	let query = '';
+	let documentsImportInputElement: HTMLInputElement;
+	let tags = [];
+
+	let showSettingsModal = false;
+	let showAddDocModal = false;
+	let showEditDocModal = false;
+	let selectedDoc;
+	let selectedTag = '';
+
+	let dragged = false;
+
+	const deleteDoc = async (name) => {
+		await deleteDocByName(localStorage.token, name);
+		await documents.set(await getDocs(localStorage.token));
+	};
+
+	const deleteDocs = async (docs) => {
+		const res = await Promise.all(
+			docs.map(async (doc) => {
+				return await deleteDocByName(localStorage.token, doc.name);
+			})
+		);
+
+		await documents.set(await getDocs(localStorage.token));
+	};
+
+	const uploadDoc = async (file) => {
+		const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			await createNewDoc(
+				localStorage.token,
+				res.collection_name,
+				res.filename,
+				transformFileName(res.filename),
+				res.filename
+			).catch((error) => {
+				toast.error(error);
+				return null;
+			});
+			await documents.set(await getDocs(localStorage.token));
+		}
+	};
+
+	onMount(() => {
+		documents.subscribe((docs) => {
+			tags = docs.reduce((a, e, i, arr) => {
+				return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
+			}, []);
+		});
+		const dropZone = document.querySelector('body');
+
+		const onDragOver = (e) => {
+			e.preventDefault();
+			dragged = true;
+		};
+
+		const onDragLeave = () => {
+			dragged = false;
+		};
+
+		const onDrop = async (e) => {
+			e.preventDefault();
+
+			if (e.dataTransfer?.files) {
+				let reader = new FileReader();
+
+				reader.onload = (event) => {
+					files = [
+						...files,
+						{
+							type: 'image',
+							url: `${event.target.result}`
+						}
+					];
+				};
+
+				const inputFiles = e.dataTransfer?.files;
+
+				if (inputFiles && inputFiles.length > 0) {
+					for (const file of inputFiles) {
+						console.log(file, file.name.split('.').at(-1));
+						if (
+							SUPPORTED_FILE_TYPE.includes(file['type']) ||
+							SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
+						) {
+							uploadDoc(file);
+						} else {
+							toast.error(
+								`Unknown File Type '${file['type']}', but accepting and treating as plain text`
+							);
+							uploadDoc(file);
+						}
+					}
+				} else {
+					toast.error($i18n.t(`File not found.`));
+				}
+			}
+
+			dragged = false;
+		};
+
+		dropZone?.addEventListener('dragover', onDragOver);
+		dropZone?.addEventListener('drop', onDrop);
+		dropZone?.addEventListener('dragleave', onDragLeave);
+
+		return () => {
+			dropZone?.removeEventListener('dragover', onDragOver);
+			dropZone?.removeEventListener('drop', onDrop);
+			dropZone?.removeEventListener('dragleave', onDragLeave);
+		};
+	});
+
+	let filteredDocs;
+
+	$: filteredDocs = $documents.filter(
+		(doc) =>
+			(selectedTag === '' ||
+				(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
+			(query === '' || doc.name.includes(query))
+	);
+</script>
+
+<svelte:head>
+	<title>
+		{$i18n.t('Documents')} | {$WEBUI_NAME}
+	</title>
+</svelte:head>
+
+{#if dragged}
+	<div
+		class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
+		id="dropzone"
+		role="region"
+		aria-label="Drag and Drop Container"
+	>
+		<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
+			<div class="m-auto pt-64 flex flex-col justify-center">
+				<div class="max-w-md">
+					<AddFilesPlaceholder>
+						<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
+							Drop any files here to add to my documents
+						</div>
+					</AddFilesPlaceholder>
+				</div>
+			</div>
+		</div>
+	</div>
+{/if}
+
+{#key selectedDoc}
+	<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
+{/key}
+
+<AddDocModal bind:show={showAddDocModal} />
+
+<SettingsModal bind:show={showSettingsModal} />
+
+<div class="mb-3">
+	<div class="flex justify-between items-center">
+		<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
+
+		<div>
+			<button
+				class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition"
+				type="button"
+				on:click={() => {
+					showSettingsModal = !showSettingsModal;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+
+				<div class=" text-xs">{$i18n.t('Document Settings')}</div>
+			</button>
+		</div>
+	</div>
+	<div class=" text-gray-500 text-xs mt-1">
+		ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
+	</div>
+</div>
+
+<div class=" flex w-full space-x-2">
+	<div class="flex flex-1">
+		<div class=" self-center ml-1 mr-3">
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 20 20"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					fill-rule="evenodd"
+					d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+		</div>
+		<input
+			class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+			bind:value={query}
+			placeholder={$i18n.t('Search Documents')}
+		/>
+	</div>
+
+	<div>
+		<button
+			class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
+			on:click={() => {
+				showAddDocModal = true;
+			}}
+		>
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 16 16"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
+				/>
+			</svg>
+		</button>
+	</div>
+</div>
+
+<!-- <div>
+    <div
+        class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600 {dragged &&
+            ' dark:bg-gray-700'} "
+        role="region"
+        on:drop={onDrop}
+        on:dragover={onDragOver}
+        on:dragleave={onDragLeave}
+    >
+        <div class="  pointer-events-none">
+            <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
+
+            <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
+                Drop any files here to add to my documents
+            </div>
+        </div>
+    </div>
+</div> -->
+
+<hr class=" dark:border-gray-850 my-2.5" />
+
+{#if tags.length > 0}
+	<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
+		<div class="ml-0.5 pr-3 my-auto flex items-center">
+			<Checkbox
+				state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
+				filteredDocs.length
+					? 'checked'
+					: 'unchecked'}
+				indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
+					filteredDocs.filter((doc) => doc?.selected === 'checked').length !== filteredDocs.length}
+				on:change={(e) => {
+					if (e.detail === 'checked') {
+						filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
+					} else if (e.detail === 'unchecked') {
+						filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
+					}
+				}}
+			/>
+		</div>
+
+		{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
+			<button
+				class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+				on:click={async () => {
+					selectedTag = '';
+					// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+				}}
+			>
+				<div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div>
+			</button>
+
+			{#each tags as tag}
+				<button
+					class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+					on:click={async () => {
+						selectedTag = tag;
+						// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+					}}
+				>
+					<div class=" text-xs font-medium self-center line-clamp-1">
+						#{tag}
+					</div>
+				</button>
+			{/each}
+		{:else}
+			<div class="flex-1 flex w-full justify-between items-center">
+				<div class="text-xs font-medium py-0.5 self-center mr-1">
+					{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
+				</div>
+
+				<div class="flex gap-1">
+					<!-- <button
+                        class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+                        on:click={async () => {
+                            selectedTag = '';
+                            // await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+                        }}
+                    >
+                        <div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
+                    </button> -->
+
+					<button
+						class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+						on:click={async () => {
+							deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
+							// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+						}}
+					>
+						<div class=" text-xs font-medium self-center line-clamp-1">
+							{$i18n.t('delete')}
+						</div>
+					</button>
+				</div>
+			</div>
+		{/if}
+	</div>
+{/if}
+
+<div class="my-3 mb-5">
+	{#each filteredDocs as doc}
+		<button
+			class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+			on:click={() => {
+				if (doc?.selected === 'checked') {
+					doc.selected = 'unchecked';
+				} else {
+					doc.selected = 'checked';
+				}
+			}}
+		>
+			<div class="my-auto flex items-center">
+				<Checkbox state={doc?.selected ?? 'unchecked'} />
+			</div>
+			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
+				<div class=" flex items-center space-x-3">
+					<div class="p-2.5 bg-red-400 text-white rounded-lg">
+						{#if doc}
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								class="w-6 h-6"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
+									clip-rule="evenodd"
+								/>
+								<path
+									d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
+								/>
+							</svg>
+						{:else}
+							<svg
+								class=" w-6 h-6 translate-y-[0.5px]"
+								fill="currentColor"
+								viewBox="0 0 24 24"
+								xmlns="http://www.w3.org/2000/svg"
+								><style>
+									.spinner_qM83 {
+										animation: spinner_8HQG 1.05s infinite;
+									}
+									.spinner_oXPr {
+										animation-delay: 0.1s;
+									}
+									.spinner_ZTLf {
+										animation-delay: 0.2s;
+									}
+									@keyframes spinner_8HQG {
+										0%,
+										57.14% {
+											animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
+											transform: translate(0);
+										}
+										28.57% {
+											animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
+											transform: translateY(-6px);
+										}
+										100% {
+											transform: translate(0);
+										}
+									}
+								</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
+									class="spinner_qM83 spinner_oXPr"
+									cx="12"
+									cy="12"
+									r="2.5"
+								/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
+							>
+						{/if}
+					</div>
+					<div class=" self-center flex-1">
+						<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div>
+						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+							{doc.title}
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="flex flex-row space-x-1 self-center">
+				<button
+					class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={async (e) => {
+						e.stopPropagation();
+						showEditDocModal = !showEditDocModal;
+						selectedDoc = doc;
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
+						/>
+					</svg>
+				</button>
+
+				<!-- <button
+            class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
+            type="button"
+            on:click={() => {
+                console.log('download file');
+            }}
+        >
+            <svg
+                xmlns="http://www.w3.org/2000/svg"
+                viewBox="0 0 16 16"
+                fill="currentColor"
+                class="w-4 h-4"
+            >
+                <path
+                    d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
+                />
+                <path
+                    d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+                />
+            </svg>
+        </button> -->
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={(e) => {
+						e.stopPropagation();
+
+						deleteDoc(doc.name);
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
+						/>
+					</svg>
+				</button>
+			</div>
+		</button>
+	{/each}
+</div>
+
+<div class=" flex justify-end w-full mb-2">
+	<div class="flex space-x-2">
+		<input
+			id="documents-import-input"
+			bind:this={documentsImportInputElement}
+			bind:files={importFiles}
+			type="file"
+			accept=".json"
+			hidden
+			on:change={() => {
+				console.log(importFiles);
+
+				const reader = new FileReader();
+				reader.onload = async (event) => {
+					const savedDocs = JSON.parse(event.target.result);
+					console.log(savedDocs);
+
+					for (const doc of savedDocs) {
+						await createNewDoc(
+							localStorage.token,
+							doc.collection_name,
+							doc.filename,
+							doc.name,
+							doc.title
+						).catch((error) => {
+							toast.error(error);
+							return null;
+						});
+					}
+
+					await documents.set(await getDocs(localStorage.token));
+				};
+
+				reader.readAsText(importFiles[0]);
+			}}
+		/>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={() => {
+				documentsImportInputElement.click();
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Import Documents Mapping')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={async () => {
+				let blob = new Blob([JSON.stringify($documents)], {
+					type: 'application/json'
+				});
+				saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Export Documents Mapping')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+	</div>
+</div>

+ 409 - 0
src/lib/components/workspace/Modelfiles.svelte

@@ -0,0 +1,409 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import { onMount, getContext } from 'svelte';
+
+	import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores';
+	import { createModel, deleteModel } from '$lib/apis/ollama';
+	import {
+		createNewModelfile,
+		deleteModelfileByTagName,
+		getModelfiles
+	} from '$lib/apis/modelfiles';
+	import { goto } from '$app/navigation';
+
+	const i18n = getContext('i18n');
+
+	let localModelfiles = [];
+	let importFiles;
+	let modelfilesImportInputElement: HTMLInputElement;
+	const deleteModelHandler = async (tagName) => {
+		let success = null;
+
+		success = await deleteModel(localStorage.token, tagName).catch((err) => {
+			toast.error(err);
+			return null;
+		});
+
+		if (success) {
+			toast.success($i18n.t(`Deleted {{tagName}}`, { tagName }));
+		}
+
+		return success;
+	};
+
+	const deleteModelfile = async (tagName) => {
+		await deleteModelHandler(tagName);
+		await deleteModelfileByTagName(localStorage.token, tagName);
+		await modelfiles.set(await getModelfiles(localStorage.token));
+	};
+
+	const shareModelfile = async (modelfile) => {
+		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
+
+		const url = 'https://openwebui.com';
+
+		const tab = await window.open(`${url}/modelfiles/create`, '_blank');
+		window.addEventListener(
+			'message',
+			(event) => {
+				if (event.origin !== url) return;
+				if (event.data === 'loaded') {
+					tab.postMessage(JSON.stringify(modelfile), '*');
+				}
+			},
+			false
+		);
+	};
+
+	const saveModelfiles = async (modelfiles) => {
+		let blob = new Blob([JSON.stringify(modelfiles)], {
+			type: 'application/json'
+		});
+		saveAs(blob, `modelfiles-export-${Date.now()}.json`);
+	};
+
+	onMount(() => {
+		localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
+
+		if (localModelfiles) {
+			console.log(localModelfiles);
+		}
+	});
+</script>
+
+<svelte:head>
+	<title>
+		{$i18n.t('Modelfiles')} | {$WEBUI_NAME}
+	</title>
+</svelte:head>
+
+<div class=" text-lg font-semibold mb-3">{$i18n.t('Modelfiles')}</div>
+
+<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/modelfiles/create">
+	<div class=" self-center w-10">
+		<div
+			class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+		>
+			<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
+				<path
+					fill-rule="evenodd"
+					d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+		</div>
+	</div>
+
+	<div class=" self-center">
+		<div class=" font-bold">{$i18n.t('Create a modelfile')}</div>
+		<div class=" text-sm">{$i18n.t('Customize Ollama models for a specific purpose')}</div>
+	</div>
+</a>
+
+<hr class=" dark:border-gray-850" />
+
+<div class=" my-2 mb-5">
+	{#each $modelfiles as modelfile}
+		<div
+			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+		>
+			<a
+				class=" flex flex-1 space-x-4 cursor-pointer w-full"
+				href={`/?models=${encodeURIComponent(modelfile.tagName)}`}
+			>
+				<div class=" self-center w-10">
+					<div class=" rounded-full bg-stone-700">
+						<img
+							src={modelfile.imageUrl ?? '/user.png'}
+							alt="modelfile profile"
+							class=" rounded-full w-full h-auto object-cover"
+						/>
+					</div>
+				</div>
+
+				<div class=" flex-1 self-center">
+					<div class=" font-bold capitalize">{modelfile.title}</div>
+					<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
+						{modelfile.desc}
+					</div>
+				</div>
+			</a>
+			<div class="flex flex-row space-x-1 self-center">
+				<a
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					href={`/workspace/modelfiles/edit?tag=${encodeURIComponent(modelfile.tagName)}`}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
+						/>
+					</svg>
+				</a>
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={() => {
+						// console.log(modelfile);
+						sessionStorage.modelfile = JSON.stringify(modelfile);
+						goto('/workspace/modelfiles/create');
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
+						/>
+					</svg>
+				</button>
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={() => {
+						shareModelfile(modelfile);
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z"
+						/>
+					</svg>
+				</button>
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={() => {
+						deleteModelfile(modelfile.tagName);
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+						/>
+					</svg>
+				</button>
+			</div>
+		</div>
+	{/each}
+</div>
+
+<div class=" flex justify-end w-full mb-3">
+	<div class="flex space-x-1">
+		<input
+			id="modelfiles-import-input"
+			bind:this={modelfilesImportInputElement}
+			bind:files={importFiles}
+			type="file"
+			accept=".json"
+			hidden
+			on:change={() => {
+				console.log(importFiles);
+
+				let reader = new FileReader();
+				reader.onload = async (event) => {
+					let savedModelfiles = JSON.parse(event.target.result);
+					console.log(savedModelfiles);
+
+					for (const modelfile of savedModelfiles) {
+						await createNewModelfile(localStorage.token, modelfile).catch((error) => {
+							return null;
+						});
+					}
+
+					await modelfiles.set(await getModelfiles(localStorage.token));
+				};
+
+				reader.readAsText(importFiles[0]);
+			}}
+		/>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={() => {
+				modelfilesImportInputElement.click();
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Import Modelfiles')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-3.5 h-3.5"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={async () => {
+				saveModelfiles($modelfiles);
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Export Modelfiles')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-3.5 h-3.5"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+	</div>
+
+	{#if localModelfiles.length > 0}
+		<div class="flex">
+			<div class=" self-center text-sm font-medium mr-4">
+				{localModelfiles.length} Local Modelfiles Detected
+			</div>
+
+			<div class="flex space-x-1">
+				<button
+					class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
+					on:click={async () => {
+						for (const modelfile of localModelfiles) {
+							await createNewModelfile(localStorage.token, modelfile).catch((error) => {
+								return null;
+							});
+						}
+
+						saveModelfiles(localModelfiles);
+						localStorage.removeItem('modelfiles');
+						localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
+						await modelfiles.set(await getModelfiles(localStorage.token));
+					}}
+				>
+					<div class=" self-center mr-2 font-medium">{$i18n.t('Sync All')}</div>
+
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-3.5 h-3.5"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</button>
+
+				<button
+					class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
+					on:click={async () => {
+						saveModelfiles(localModelfiles);
+
+						localStorage.removeItem('modelfiles');
+						localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
+						await modelfiles.set(await getModelfiles(localStorage.token));
+					}}
+				>
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
+							/>
+						</svg>
+					</div>
+				</button>
+			</div>
+		</div>
+	{/if}
+</div>
+
+<div class=" my-16">
+	<div class=" text-lg font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div>
+
+	<a
+		class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
+		href="https://openwebui.com/"
+		target="_blank"
+	>
+		<div class=" self-center w-10">
+			<div
+				class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+			>
+				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
+					<path
+						fill-rule="evenodd"
+						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</div>
+
+		<div class=" self-center">
+			<div class=" font-bold">{$i18n.t('Discover a modelfile')}</div>
+			<div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div>
+		</div>
+	</a>
+</div>

+ 113 - 116
src/routes/(app)/playground/+page.svelte → src/lib/components/workspace/Playground.svelte

@@ -268,75 +268,73 @@
 	</title>
 </svelte:head>
 
-<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
-	<div class=" flex flex-col justify-between w-full overflow-y-auto h-[100dvh]">
-		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10 h-full">
-			<div class=" flex flex-col h-full">
-				<div class="flex flex-col justify-between mb-2.5 gap-1">
-					<div class="flex justify-between items-center gap-2">
-						<div class=" text-2xl font-semibold self-center flex">
-							{$i18n.t('Playground')}
-							<span class=" text-xs text-gray-500 self-center ml-1">{$i18n.t('(Beta)')}</span>
-						</div>
-
-						<div>
-							<button
-								class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode ===
-									'chat' && 'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' &&
-									'text-green-600 dark:text-green-200 bg-green-200/30'} "
-								on:click={() => {
-									if (mode === 'complete') {
-										mode = 'chat';
-									} else {
-										mode = 'complete';
-									}
-								}}
-							>
-								{#if mode === 'complete'}
-									{$i18n.t('Text Completion')}
-								{:else if mode === 'chat'}
-									{$i18n.t('Chat')}
-								{/if}
-
-								<div>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 16 16"
-										fill="currentColor"
-										class="w-3 h-3"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								</div>
-							</button>
-						</div>
+<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
+	<div class="mx-auto w-full md:px-0 h-full">
+		<div class=" flex flex-col h-full">
+			<div class="flex flex-col justify-between mb-2.5 gap-1">
+				<div class="flex justify-between items-center gap-2">
+					<div class=" text-lg font-semibold self-center flex">
+						{$i18n.t('Playground')}
+						<span class=" text-xs text-gray-500 self-center ml-1">{$i18n.t('(Beta)')}</span>
 					</div>
 
-					<div class="flex flex-col gap-1 w-full">
-						<div class="flex w-full">
-							<div class="overflow-hidden w-full">
-								<div class="max-w-full">
-									<Selector
-										placeholder={$i18n.t('Select a model')}
-										items={$models
-											.filter((model) => model.name !== 'hr')
-											.map((model) => ({
-												value: model.id,
-												label: model.name,
-												info: model
-											}))}
-										bind:value={selectedModelId}
-										className="w-[42rem]"
+					<div>
+						<button
+							class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode === 'chat' &&
+								'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' &&
+								'text-green-600 dark:text-green-200 bg-green-200/30'} "
+							on:click={() => {
+								if (mode === 'complete') {
+									mode = 'chat';
+								} else {
+									mode = 'complete';
+								}
+							}}
+						>
+							{#if mode === 'complete'}
+								{$i18n.t('Text Completion')}
+							{:else if mode === 'chat'}
+								{$i18n.t('Chat')}
+							{/if}
+
+							<div>
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									viewBox="0 0 16 16"
+									fill="currentColor"
+									class="w-3 h-3"
+								>
+									<path
+										fill-rule="evenodd"
+										d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z"
+										clip-rule="evenodd"
 									/>
-								</div>
+								</svg>
+							</div>
+						</button>
+					</div>
+				</div>
+
+				<div class="flex flex-col gap-1 w-full">
+					<div class="flex w-full">
+						<div class="overflow-hidden w-full">
+							<div class="max-w-full">
+								<Selector
+									placeholder={$i18n.t('Select a model')}
+									items={$models
+										.filter((model) => model.name !== 'hr')
+										.map((model) => ({
+											value: model.id,
+											label: model.name,
+											info: model
+										}))}
+									bind:value={selectedModelId}
+								/>
 							</div>
 						</div>
+					</div>
 
-						<!-- <button
+					<!-- <button
 							class=" self-center dark:hover:text-gray-300"
 							id="open-settings-button"
 							on:click={async () => {}}
@@ -361,67 +359,66 @@
 								/>
 							</svg>
 						</button> -->
-					</div>
 				</div>
+			</div>
 
-				{#if mode === 'chat'}
-					<div class="p-1">
-						<div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg">
-							<div class=" text-sm font-medium">{$i18n.t('System')}</div>
+			{#if mode === 'chat'}
+				<div class="p-1">
+					<div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg">
+						<div class=" text-sm font-medium">{$i18n.t('System')}</div>
+						<textarea
+							id="system-textarea"
+							class="w-full h-full bg-transparent resize-none outline-none text-sm"
+							bind:value={system}
+							placeholder={$i18n.t("You're a helpful assistant.")}
+							rows="4"
+						/>
+					</div>
+				</div>
+			{/if}
+
+			<div
+				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
+				id="messages-container"
+				bind:this={messagesContainerElement}
+			>
+				<div class=" h-full w-full flex flex-col">
+					<div class="flex-1 p-1">
+						{#if mode === 'complete'}
 							<textarea
-								id="system-textarea"
-								class="w-full h-full bg-transparent resize-none outline-none text-sm"
-								bind:value={system}
+								id="text-completion-textarea"
+								bind:this={textCompletionAreaElement}
+								class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
+								bind:value={text}
 								placeholder={$i18n.t("You're a helpful assistant.")}
-								rows="4"
 							/>
-						</div>
-					</div>
-				{/if}
-
-				<div
-					class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
-					id="messages-container"
-					bind:this={messagesContainerElement}
-				>
-					<div class=" h-full w-full flex flex-col">
-						<div class="flex-1 p-1">
-							{#if mode === 'complete'}
-								<textarea
-									id="text-completion-textarea"
-									bind:this={textCompletionAreaElement}
-									class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
-									bind:value={text}
-									placeholder={$i18n.t("You're a helpful assistant.")}
-								/>
-							{:else if mode === 'chat'}
-								<ChatCompletion bind:messages />
-							{/if}
-						</div>
+						{:else if mode === 'chat'}
+							<ChatCompletion bind:messages />
+						{/if}
 					</div>
 				</div>
+			</div>
 
-				<div class="pb-2">
-					{#if !loading}
-						<button
-							class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
-							on:click={() => {
-								submitHandler();
-							}}
-						>
-							{$i18n.t('Submit')}
-						</button>
-					{:else}
-						<button
-							class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg"
-							on:click={() => {
-								stopResponse();
-							}}
-						>
-							{$i18n.t('Cancel')}
-						</button>
-					{/if}
-				</div>
+			<div class="pb-3">
+				{#if !loading}
+					<button
+						class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
+						on:click={() => {
+							submitHandler();
+						}}
+					>
+						{$i18n.t('Submit')}
+					</button>
+				{:else}
+					<button
+						class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg"
+						on:click={() => {
+							stopResponse();
+						}}
+					>
+						{$i18n.t('Cancel')}
+					</button>
+				{/if}
 			</div>
 		</div>
 	</div>

+ 331 - 0
src/lib/components/workspace/Prompts.svelte

@@ -0,0 +1,331 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import { onMount, getContext } from 'svelte';
+	import { WEBUI_NAME, prompts } from '$lib/stores';
+	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
+	import { error } from '@sveltejs/kit';
+	import { goto } from '$app/navigation';
+
+	const i18n = getContext('i18n');
+
+	let importFiles = '';
+	let query = '';
+	let promptsImportInputElement: HTMLInputElement;
+	const sharePrompt = async (prompt) => {
+		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
+
+		const url = 'https://openwebui.com';
+
+		const tab = await window.open(`${url}/prompts/create`, '_blank');
+		window.addEventListener(
+			'message',
+			(event) => {
+				if (event.origin !== url) return;
+				if (event.data === 'loaded') {
+					tab.postMessage(JSON.stringify(prompt), '*');
+				}
+			},
+			false
+		);
+	};
+
+	const deletePrompt = async (command) => {
+		await deletePromptByCommand(localStorage.token, command);
+		await prompts.set(await getPrompts(localStorage.token));
+	};
+</script>
+
+<svelte:head>
+	<title>
+		{$i18n.t('Prompts')} | {$WEBUI_NAME}
+	</title>
+</svelte:head>
+
+<div class="mb-3 flex justify-between items-center">
+	<div class=" text-lg font-semibold self-center">{$i18n.t('Prompts')}</div>
+</div>
+
+<div class=" flex w-full space-x-2">
+	<div class="flex flex-1">
+		<div class=" self-center ml-1 mr-3">
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 20 20"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					fill-rule="evenodd"
+					d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+		</div>
+		<input
+			class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+			bind:value={query}
+			placeholder={$i18n.t('Search Prompts')}
+		/>
+	</div>
+
+	<div>
+		<a
+			class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
+			href="/workspace/prompts/create"
+		>
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 16 16"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
+				/>
+			</svg>
+		</a>
+	</div>
+</div>
+<hr class=" dark:border-gray-850 my-2.5" />
+
+<div class="my-3 mb-5">
+	{#each $prompts.filter((p) => query === '' || p.command.includes(query)) as prompt}
+		<div
+			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+		>
+			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
+				<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
+					<div class=" flex-1 self-center pl-5">
+						<div class=" font-bold">{prompt.command}</div>
+						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+							{prompt.title}
+						</div>
+					</div>
+				</a>
+			</div>
+			<div class="flex flex-row space-x-1 self-center">
+				<a
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
+						/>
+					</svg>
+				</a>
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={() => {
+						// console.log(modelfile);
+						sessionStorage.prompt = JSON.stringify(prompt);
+						goto('/workspace/prompts/create');
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
+						/>
+					</svg>
+				</button>
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={() => {
+						sharePrompt(prompt);
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
+						/>
+					</svg>
+				</button>
+
+				<button
+					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+					type="button"
+					on:click={() => {
+						deletePrompt(prompt.command);
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="1.5"
+						stroke="currentColor"
+						class="w-4 h-4"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
+						/>
+					</svg>
+				</button>
+			</div>
+		</div>
+	{/each}
+</div>
+
+<div class=" flex justify-end w-full mb-3">
+	<div class="flex space-x-2">
+		<input
+			id="prompts-import-input"
+			bind:this={promptsImportInputElement}
+			bind:files={importFiles}
+			type="file"
+			accept=".json"
+			hidden
+			on:change={() => {
+				console.log(importFiles);
+
+				const reader = new FileReader();
+				reader.onload = async (event) => {
+					const savedPrompts = JSON.parse(event.target.result);
+					console.log(savedPrompts);
+
+					for (const prompt of savedPrompts) {
+						await createNewPrompt(
+							localStorage.token,
+							prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command,
+							prompt.title,
+							prompt.content
+						).catch((error) => {
+							toast.error(error);
+							return null;
+						});
+					}
+
+					await prompts.set(await getPrompts(localStorage.token));
+				};
+
+				reader.readAsText(importFiles[0]);
+			}}
+		/>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={() => {
+				promptsImportInputElement.click();
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Import Prompts')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={async () => {
+				// promptsImportInputElement.click();
+				let blob = new Blob([JSON.stringify($prompts)], {
+					type: 'application/json'
+				});
+				saveAs(blob, `prompts-export-${Date.now()}.json`);
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Export Prompts')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+
+		<!-- <button
+						on:click={() => {
+							loadDefaultPrompts();
+						}}
+					>
+						dd
+					</button> -->
+	</div>
+</div>
+
+<div class=" my-16">
+	<div class=" text-lg font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div>
+
+	<a
+		class=" flex space-x-4 cursor-pointer w-full mb-3 px-3 py-2"
+		href="https://openwebui.com/?type=prompts"
+		target="_blank"
+	>
+		<div class=" self-center w-10">
+			<div
+				class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+			>
+				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
+					<path
+						fill-rule="evenodd"
+						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</div>
+
+		<div class=" self-center">
+			<div class=" font-bold">{$i18n.t('Discover a prompt')}</div>
+			<div class=" text-sm">{$i18n.t('Discover, download, and explore custom prompts')}</div>
+		</div>
+	</a>
+</div>

+ 2 - 2
src/lib/constants.ts

@@ -1,8 +1,8 @@
-import { dev } from '$app/environment';
+import { browser, dev } from '$app/environment';
 // import { version } from '../../package.json';
 
 export const APP_NAME = 'Open WebUI';
-export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
+export const WEBUI_BASE_URL = browser ? (dev ? `http://${location.hostname}:8080` : ``) : ``;
 
 export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
 

+ 1 - 1
src/lib/i18n/index.ts

@@ -37,7 +37,7 @@ const createIsLoadingStore = (i18n: i18nType) => {
 	return isLoading;
 };
 
-export const initI18n = (defaultLocale: string) => {
+export const initI18n = (defaultLocale: string | undefined) => {
 	let detectionOrder = defaultLocale
 		? ['querystring', 'localStorage']
 		: ['querystring', 'localStorage', 'navigator'];

+ 108 - 100
src/lib/i18n/locales/ar-BH/translation.json

@@ -1,34 +1,36 @@
 {
 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "",
 	"(Beta)": "(تجريبي)",
-	"(e.g. `sh webui.sh --api`)": "(مثال `sh webui.sh --api`)",
+	"(e.g. `sh webui.sh --api`)": "( `sh webui.sh --api`مثال)",
 	"(latest)": "(الأخير)",
 	"{{modelName}} is thinking...": "{{modelName}} ...يفكر",
 	"{{user}}'s Chats": "{{user}}' الدردشات",
-	"{{webUIName}} Backend Required": "",
+	"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
 	"a user": "المستخدم",
 	"About": "عن",
 	"Account": "الحساب",
 	"Accurate information": "معلومات دقيقة",
+	"Add": "",
 	"Add a model": "أضافة موديل",
 	"Add a model tag name": "ضع تاق للأسم الموديل",
 	"Add a short description about what this modelfile does": "أضف وصفًا قصيرًا حول ما يفعله ملف الموديل هذا",
 	"Add a short title for this prompt": "أضف عنوانًا قصيرًا لبداء المحادثة",
 	"Add a tag": "أضافة تاق",
-	"Add custom prompt": "",
+	"Add custom prompt": "أضافة مطالبة مخصصه",
 	"Add Docs": "إضافة المستندات",
 	"Add Files": "إضافة ملفات",
+	"Add Memory": "",
 	"Add message": "اضافة رسالة",
 	"Add Model": "اضافة موديل",
 	"Add Tags": "اضافة تاق",
 	"Add User": "اضافة مستخدم",
-	"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين.",
+	"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين",
 	"admin": "المشرف",
 	"Admin Panel": "لوحة التحكم",
 	"Admin Settings": "اعدادات المشرف",
 	"Advanced Parameters": "التعليمات المتقدمة",
 	"all": "الكل",
-	"All Documents": "",
+	"All Documents": "جميع الملفات",
 	"All Users": "جميع المستخدمين",
 	"Allow": "يسمح",
 	"Allow Chat Deletion": "يستطيع حذف المحادثات",
@@ -42,23 +44,23 @@
 	"API Key created.": "API تم أنشاء المفتاح",
 	"API keys": "API المفاتيح",
 	"API RPM": "API RPM",
-	"April": "",
+	"April": "أبريل",
 	"Archive": "الأرشيف",
 	"Archived Chats": "الأرشيف المحادثات",
 	"are allowed - Activate this command by typing": "مسموح - قم بتنشيط هذا الأمر عن طريق الكتابة",
 	"Are you sure?": "هل أنت متأكد ؟",
-	"Attach file": "",
+	"Attach file": "أرفق ملف",
 	"Attention to detail": "انتبه للتفاصيل",
 	"Audio": "صوتي",
-	"August": "",
+	"August": "أغسطس",
 	"Auto-playback response": "استجابة التشغيل التلقائي",
-	"Auto-send input after 3 sec.": "إرسال تلقائي للإدخال بعد 3 ثوانٍ.",
+	"Auto-send input after 3 sec.": "إرسال تلقائي للإدخال بعد 3 ثواني.",
 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي",
 	"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب",
 	"available!": "متاح",
 	"Back": "خلف",
 	"Bad Response": "استجابة خطاء",
-	"before": "",
+	"before": "قبل",
 	"Being lazy": "كون كسول",
 	"Builder Mode": "بناء الموديل",
 	"Bypass SSL verification for Websites": "",
@@ -66,8 +68,10 @@
 	"Categories": "التصنيفات",
 	"Change Password": "تغير الباسورد",
 	"Chat": "المحادثة",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "تاريخ المحادثة",
-	"Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح.",
+	"Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح",
 	"Chats": "المحادثات",
 	"Check Again": "تحقق مرة اخرى",
 	"Check for updates": "تحقق من التحديثات",
@@ -76,13 +80,13 @@
 	"Chunk Overlap": "Chunk تداخل",
 	"Chunk Params": "Chunk المتغيرات",
 	"Chunk Size": "Chunk حجم",
-	"Citation": "",
+	"Citation": "اقتباس",
 	"Click here for help.": "أضغط هنا للمساعدة",
-	"Click here to": "",
-	"Click here to check other modelfiles.": "انقر هنا للتحقق من ملفات الموديلات الأخرى.",
+	"Click here to": "أضغط هنا الانتقال",
+	"Click here to check other modelfiles.": "انقر هنا للتحقق من ملفات الموديلات الأخرى",
 	"Click here to select": "أضغط هنا للاختيار",
 	"Click here to select a csv file.": "أضغط هنا للاختيار ملف csv",
-	"Click here to select documents.": "انقر هنا لاختيار المستندات.",
+	"Click here to select documents.": "انقر هنا لاختيار المستندات",
 	"click here.": "أضغط هنا",
 	"Click on the user role button to change a user's role.": "أضغط على أسم الصلاحيات لتغيرها للمستخدم",
 	"Close": "أغلق",
@@ -97,17 +101,17 @@
 	"Context Length": "طول السياق",
 	"Continue Response": "متابعة الرد",
 	"Conversation Mode": "وضع المحادثة",
-	"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة!",
+	"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
 	"Copy": "نسخ",
 	"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
 	"Copy last response": "انسخ الرد الأخير",
 	"Copy Link": "أنسخ الرابط",
-	"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح!",
+	"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح",
 	"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "قم بإنشاء عبارة موجزة مكونة من 3-5 كلمات كرأس للاستعلام التالي، مع الالتزام الصارم بالحد الأقصى لعدد الكلمات الذي يتراوح بين 3-5 كلمات وتجنب استخدام الكلمة 'عنوان':",
 	"Create a modelfile": "إنشاء ملف نموذجي",
 	"Create Account": "إنشاء حساب",
-	"Create new key": "",
-	"Create new secret key": "",
+	"Create new key": "عمل مفتاح جديد",
+	"Create new secret key": "عمل سر جديد",
 	"Created at": "أنشئت في",
 	"Created At": "أنشئت من",
 	"Current Model": "الموديل المختار",
@@ -115,27 +119,26 @@
 	"Custom": "مخصص",
 	"Customize Ollama models for a specific purpose": "تخصيص الموديل Ollama لغرض محدد",
 	"Dark": "مظلم",
-	"Dashboard": "",
+	"Dashboard": "لوحة التحكم",
 	"Database": "قاعدة البيانات",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
-	"December": "",
+	"December": "ديسمبر",
 	"Default": "الإفتراضي",
-	"Default (Automatic1111)": "الإفتراضي (Automatic1111)",
-	"Default (SentenceTransformers)": "الإفتراضي (SentenceTransformers)",
-	"Default (Web API)": "الإفتراضي (Web API)",
+	"Default (Automatic1111)": "(Automatic1111) الإفتراضي",
+	"Default (SentenceTransformers)": "(SentenceTransformers) الإفتراضي",
+	"Default (Web API)": "(Web API) الإفتراضي",
 	"Default model updated": "الإفتراضي تحديث الموديل",
 	"Default Prompt Suggestions": "الإفتراضي Prompt الاقتراحات",
 	"Default User Role": "الإفتراضي صلاحيات المستخدم",
 	"delete": "حذف",
-	"Delete": "حذف.",
+	"Delete": "حذف",
 	"Delete a model": "حذف الموديل",
 	"Delete chat": "حذف المحادثه",
 	"Delete Chat": "حذف المحادثه.",
-	"Delete Chats": "حذ المحادثات",
-	"delete this link": "",
+	"Delete Chats": "حذف المحادثات",
+	"delete this link": "أحذف هذا الرابط",
 	"Delete User": "حذف المستخدم",
-	"Deleted {{deleteModelTag}}": "حذف {{deleteModelTag}}",
-	"Deleted {{tagName}}": "حذف {{tagName}}",
+	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
+	"Deleted {{tagName}}": "{{tagName}} حذف",
 	"Description": "وصف",
 	"Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل",
 	"Disabled": "تعطيل",
@@ -152,7 +155,7 @@
 	"Don't have an account?": "ليس لديك حساب؟",
 	"Don't like the style": "لا أحب النمط",
 	"Download": "تحميل",
-	"Download canceled": "",
+	"Download canceled": "تم اللغاء التحميل",
 	"Download Database": "تحميل قاعدة البيانات",
 	"Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.",
@@ -168,17 +171,18 @@
 	"Enabled": "تفعيل",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
 	"Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "أدخل Chunk المتداخل",
 	"Enter Chunk Size": "أدخل Chunk الحجم",
-	"Enter Image Size (e.g. 512x512)": "أدخل حجم الصورة (e.g. 512x512)",
-	"Enter language codes": "",
+	"Enter Image Size (e.g. 512x512)": "(e.g. 512x512) أدخل حجم الصورة ",
+	"Enter language codes": "أدخل كود اللغة",
 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "أدخل عنوان URL الأساسي لواجهة برمجة تطبيقات LiteLLM (litellm_params.api_base)",
 	"Enter LiteLLM API Key (litellm_params.api_key)": "أدخل مفتاح LiteLLM API (litellm_params.api_key)",
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "أدخل LiteLLM API RPM (litllm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "أدخل LiteLLM الموديل (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "أدخل أكثر Tokens (litellm_params.max_tokens)",
-	"Enter model tag (e.g. {{modelTag}})": "أدخل الموديل تاق (e.g. {{modelTag}})",
-	"Enter Number of Steps (e.g. 50)": "أدخل عدد الخطوات (e.g. 50)",
+	"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
+	"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
 	"Enter Score": "أدخل النتيجة",
 	"Enter stop sequence": "أدخل تسلسل التوقف",
 	"Enter Top K": "Enter Top K",
@@ -196,7 +200,7 @@
 	"Export Prompts": "مطالبات التصدير",
 	"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
 	"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
-	"February": "",
+	"February": "فبراير",
 	"Feel free to add specific details": "لا تتردد في إضافة تفاصيل محددة",
 	"File Mode": "وضع الملف",
 	"File not found.": "لم يتم العثور على الملف.",
@@ -211,9 +215,10 @@
 	"General Settings": "الاعدادات العامة",
 	"Generation Info": "معلومات الجيل",
 	"Good Response": "استجابة جيدة",
+	"h:mm a": "",
 	"has no conversations.": "ليس لديه محادثات.",
-	"Hello, {{name}}": "مرحبا, {{name}}",
-	"Help": "",
+	"Hello, {{name}}": " {{name}} مرحبا",
+	"Help": "مساعدة",
 	"Hide": "أخفاء",
 	"Hide Additional Params": "إخفاء المعلمات الإضافية",
 	"How can I help you today?": "كيف استطيع مساعدتك اليوم؟",
@@ -227,14 +232,14 @@
 	"Import Modelfiles": "استيراد ملفات النماذج",
 	"Import Prompts": "مطالبات الاستيراد",
 	"Include `--api` flag when running stable-diffusion-webui": "قم بتضمين علامة `-api` عند تشغيل Stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "إدخال الأوامر",
 	"Interface": "واجهه المستخدم",
-	"Invalid Tag": "",
-	"January": "",
+	"Invalid Tag": "تاق غير صالحة",
+	"January": "يناير",
 	"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
 	"JSON": "JSON",
-	"July": "",
-	"June": "",
+	"July": "يوليو",
+	"June": "يونيو",
 	"JWT Expiration": "JWT تجريبي",
 	"JWT Token": "JWT Token",
 	"Keep Alive": "Keep Alive",
@@ -242,18 +247,21 @@
 	"Language": "اللغة",
 	"Last Active": "آخر نشاط",
 	"Light": "فاتح",
-	"Listening...": "جاري الاستماع...",
-	"LLMs can make mistakes. Verify important information.": "يمكن أن يرتكب LLM الأخطاء. التحقق من المعلومات الهامة.",
-	"Made by OpenWebUI Community": "تم إنشاؤه بواسطة مجتمع OpenWebUI",
+	"Listening...": "جاري الاستماع",
+	"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
+	"LTR": "",
+	"Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
 	"Make sure to enclose them with": "تأكد من إرفاقها",
-	"Manage LiteLLM Models": "إدارة نماذج LiteLLM",
+	"Manage LiteLLM Models": "LiteLLM إدارة نماذج ",
 	"Manage Models": "إدارة النماذج",
-	"Manage Ollama Models": "إدارة موديلات Ollama",
+	"Manage Ollama Models": "Ollama إدارة موديلات ",
 	"March": "",
 	"Max Tokens": "Max Tokens",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "الحد الأدنى من النقاط",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -263,7 +271,7 @@
 	"Model '{{modelName}}' has been successfully downloaded.": "موديل '{{modelName}}'تم تحميله بنجاح",
 	"Model '{{modelTag}}' is already in queue for downloading.": "موديل '{{modelTag}}' جاري تحميلة الرجاء الانتظار",
 	"Model {{modelId}} not found": "موديل {{modelId}} لم يوجد",
-	"Model {{modelName}} already exists.": "موديل {{modelName}} موجود",
+	"Model {{modelName}} already exists.": "موجود {{modelName}} موديل ",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
 	"Model Name": "أسم الموديل",
 	"Model not selected": "لم تختار موديل",
@@ -276,23 +284,20 @@
 	"Modelfiles": "ملفات الموديل",
 	"Models": "الموديلات",
 	"More": "المزيد",
-	"My Documents": "مستنداتي",
-	"My Modelfiles": "ملفاتي النموذجية",
-	"My Prompts": "مطالباتي",
 	"Name": "الأسم",
 	"Name Tag": "أسم التاق",
 	"Name your modelfile": "قم بتسمية ملف النموذج الخاص بك",
 	"New Chat": "دردشة جديدة",
 	"New Password": "كلمة المرور الجديدة",
-	"No results found": "",
-	"No source available": "",
+	"No results found": "لا توجد نتايج",
+	"No source available": "لا يوجد مصدر متاح",
 	"Not factually correct": "ليس صحيحا من حيث الواقع",
 	"Not sure what to add?": "لست متأكدا ما يجب إضافته؟",
 	"Not sure what to write? Switch to": "لست متأكدا ماذا أكتب؟ التبديل إلى",
 	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.",
 	"Notifications": "إشعارات",
-	"November": "",
-	"October": "",
+	"November": "نوفمبر",
+	"October": "اكتوبر",
 	"Off": "أغلاق",
 	"Okay, Let's Go!": "حسنا دعنا نذهب!",
 	"OLED Dark": "OLED داكن",
@@ -306,50 +311,51 @@
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
 	"Open": "فتح",
-	"Open AI": "فتح AI",
-	"Open AI (Dall-E)": "فتح AI (Dall-E)",
+	"Open AI": "AI فتح",
+	"Open AI (Dall-E)": "AI (Dall-E) فتح",
 	"Open new chat": "فتح محادثة جديده",
 	"OpenAI": "OpenAI",
 	"OpenAI API": "OpenAI API",
 	"OpenAI API Config": "OpenAI API إعدادات",
-	"OpenAI API Key is required.": "مطلوب مفتاح OpenAI API.",
-	"OpenAI URL/Key required.": "مطلوب عنوان URL/مفتاح OpenAI.",
+	"OpenAI API Key is required.": "OpenAI API.مطلوب مفتاح ",
+	"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
 	"or": "أو",
 	"Other": "آخر",
-	"Overview": "",
+	"Overview": "عرض",
 	"Parameters": "Parameters",
 	"Password": "الباسورد",
 	"PDF document (.pdf)": "PDF ملف (.pdf)",
 	"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
 	"pending": "قيد الانتظار",
-	"Permission denied when accessing microphone: {{error}}": "تم رفض الإذن عند الوصول إلى الميكروفون: {{error}}",
+	"Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ",
+	"Personalization": "",
 	"Plain text (.txt)": "نص عادي (.txt)",
 	"Playground": "مكان التجربة",
 	"Positive attitude": "موقف ايجابي",
-	"Previous 30 days": "",
-	"Previous 7 days": "",
+	"Previous 30 days": "أخر 30 يوم",
+	"Previous 7 days": "أخر 7 أيام",
 	"Profile Image": "صورة الملف الشخصي",
-	"Prompt": "",
+	"Prompt": "مطالبة",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "موجه (على سبيل المثال: أخبرني بحقيقة ممتعة عن الإمبراطورية الرومانية)",
 	"Prompt Content": "محتوى عاجل",
 	"Prompt suggestions": "اقتراحات سريعة",
-	"Prompts": "حث",
-	"Pull \"{{searchValue}}\" from Ollama.com": "",
-	"Pull a model from Ollama.com": "سحب الموديل من Ollama.com",
+	"Prompts": "مطالبات",
+	"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com \"{{searchValue}}\" أسحب من ",
+	"Pull a model from Ollama.com": "Ollama.com سحب الموديل من ",
 	"Pull Progress": "سحب التقدم",
 	"Query Params": "Query Params",
 	"RAG Template": "RAG تنمبلت",
 	"Raw Format": "Raw فورمات",
 	"Read Aloud": "أقراء لي",
 	"Record voice": "سجل صوت",
-	"Redirecting you to OpenWebUI Community": "إعادة توجيهك إلى مجتمع OpenWebUI",
+	"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
 	"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
 	"Regenerate": "تجديد",
 	"Release Notes": "ملاحظات الإصدار",
 	"Remove": "إزالة",
-	"Remove Model": "",
-	"Rename": "",
-	"Repeat Last N": "كرر آخر N",
+	"Remove Model": "حذف الموديل",
+	"Rename": "إعادة تسمية",
+	"Repeat Last N": "N كرر آخر",
 	"Repeat Penalty": "كرر المخالفة",
 	"Request Mode": "وضع الطلب",
 	"Reranking Model": "",
@@ -360,28 +366,29 @@
 	"Role": "منصب",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
 	"Save": "حفظ",
 	"Save & Create": "حفظ وإنشاء",
-	"Save & Submit": "حفظ وإرسال",
 	"Save & Update": "حفظ وتحديث",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله",
 	"Scan": "مسح",
 	"Scan complete!": "تم المسح",
-	"Scan for documents from {{path}}": " مسح على الملفات من {{path}}",
+	"Scan for documents from {{path}}": "{{path}} مسح على الملفات من",
 	"Search": "البحث",
 	"Search a model": "البحث عن موديل",
 	"Search Documents": "البحث المستندات",
 	"Search Prompts": "أبحث حث",
-	"See readme.md for instructions": "راجع readme.md للحصول على التعليمات",
+	"See readme.md for instructions": "readme.md للحصول على التعليمات",
 	"See what's new": "ما الجديد",
 	"Seed": "Seed",
 	"Select a mode": "أختار موديل",
 	"Select a model": "أختار الموديل",
-	"Select an Ollama instance": "أختار سيرفر Ollama",
-	"Select model": "",
-	"Send a Message": "أرسل رسالة.",
-	"Send message": "أرسل رسالة",
-	"September": "",
+	"Select an Ollama instance": "أختار سيرفر ",
+	"Select model": " أختار موديل",
+	"Send": "",
+	"Send a Message": "يُرجى إدخال طلبك هنا",
+	"Send message": "يُرجى إدخال طلبك هنا.",
+	"September": "سبتمبر",
 	"Server connection verified": "تم التحقق من اتصال الخادم",
 	"Set as default": "الافتراضي",
 	"Set Default Model": "تفعيد الموديل الافتراضي",
@@ -396,7 +403,7 @@
 	"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
 	"Share": "كشاركة",
 	"Share Chat": "مشاركة الدردشة",
-	"Share to OpenWebUI Community": "شارك في مجتمع OpenWebUI",
+	"Share to OpenWebUI Community": "OpenWebUI شارك في مجتمع",
 	"short-summary": "ملخص قصير",
 	"Show": "عرض",
 	"Show Additional Params": "إظهار المعلمات الإضافية",
@@ -407,17 +414,17 @@
 	"Sign Out": "تسجيل الخروج",
 	"Sign up": "تسجيل",
 	"Signing in": "جاري الدخول",
-	"Source": "",
-	"Speech recognition error: {{error}}": "خطأ في التعرف على الكلام: {{error}}",
+	"Source": "المصدر",
+	"Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام",
 	"Speech-to-Text Engine": "محرك تحويل الكلام إلى نص",
 	"SpeechRecognition API is not supported in this browser.": "API SpeechRecognition غير مدعومة في هذا المتصفح.",
 	"Stop Sequence": "وقف التسلسل",
 	"STT Settings": "STT اعدادات",
 	"Submit": "إرسال",
-	"Subtitle (e.g. about the Roman Empire)": "الترجمة (e.g. about the Roman Empire)",
+	"Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة",
 	"Success": "نجاح",
-	"Successfully updated.": "تم التحديث بنجاح.",
-	"Suggested": "",
+	"Successfully updated.": "تم التحديث بنجاح",
+	"Suggested": "مقترحات",
 	"Sync All": "مزامنة الكل",
 	"System": "النظام",
 	"System Prompt": "محادثة النظام",
@@ -436,33 +443,33 @@
 	"Thorough explanation": "شرح شامل",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
 	"Title": "العنوان",
-	"Title (e.g. Tell me a fun fact)": "العناون (e.g. Tell me a fun fact)",
+	"Title (e.g. Tell me a fun fact)": "(e.g. Tell me a fun fact) العناون",
 	"Title Auto-Generation": "توليد تلقائي للعنوان",
-	"Title cannot be an empty string.": "",
+	"Title cannot be an empty string.": "العنوان مطلوب",
 	"Title Generation Prompt": "موجه إنشاء العنوان",
 	"to": "الى",
 	"To access the available model names for downloading,": "للوصول إلى أسماء الموديلات المتاحة للتنزيل،",
 	"To access the GGUF models available for downloading,": "للوصول إلى الموديلات GGUF المتاحة للتنزيل،",
 	"to chat input.": "الى كتابة المحادثه",
-	"Today": "",
+	"Today": "اليوم",
 	"Toggle settings": "فتح وأغلاق الاعدادات",
 	"Toggle sidebar": "فتح وأغلاق الشريط الجانبي",
 	"Top K": "Top K",
 	"Top P": "Top P",
-	"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول إلى Olma؟",
+	"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
 	"TTS Settings": "TTS اعدادات",
 	"Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).",
-	"Uh-oh! There was an issue connecting to {{provider}}.": "خطاء أوه! حدثت مشكلة في الاتصال بـ {{provider}}.",
+	"Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ",
 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع ملف غير معروف '{{file_type}}', ولكن القبول والتعامل كنص عادي ",
 	"Update and Copy Link": "تحديث ونسخ الرابط",
 	"Update password": "تحديث كلمة المرور",
-	"Upload a GGUF model": "رفع موديل نوع GGUF",
+	"Upload a GGUF model": "GGUF رفع موديل نوع",
 	"Upload files": "رفع الملفات",
 	"Upload Progress": "جاري التحميل",
 	"URL Mode": "رابط الموديل",
 	"Use '#' in the prompt input to load and select your documents.": "أستخدم '#' في المحادثة لربطهامن المستندات",
-	"Use Gravatar": "أستخدم Gravatar",
-	"Use Initials": "أستخدم Initials",
+	"Use Gravatar": "Gravatar أستخدم",
+	"Use Initials": "Initials أستخدم",
 	"user": "مستخدم",
 	"User Permissions": "صلاحيات المستخدم",
 	"Users": "المستخدمين",
@@ -482,12 +489,13 @@
 	"What’s New in": "ما هو الجديد",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "عند إيقاف تشغيل السجل، لن تظهر الدردشات الجديدة على هذا المتصفح في سجلك على أي من أجهزتك.",
 	"Whisper (Local)": "Whisper (Local)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "اكتب اقتراحًا سريعًا (على سبيل المثال، من أنت؟)",
-	"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية].",
-	"Yesterday": "",
-	"You": "أنت",
-	"You have no archived conversations.": "",
-	"You have shared this chat": "",
+	"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
+	"Yesterday": "أمس",
+	"You": "",
+	"You have no archived conversations.": "لا تملك محادثات محفوظه",
+	"You have shared this chat": "تم مشاركة هذه المحادثة",
 	"You're a helpful assistant.": "مساعدك المفيد هنا",
 	"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
 	"Youtube": "Youtube",

+ 22 - 14
src/lib/i18n/locales/bg-BG/translation.json

@@ -10,14 +10,16 @@
 	"About": "Относно",
 	"Account": "Акаунт",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "Добавяне на модел",
 	"Add a model tag name": "Добавяне на име на таг за модел",
 	"Add a short description about what this modelfile does": "Добавяне на кратко описание за това какво прави този модфайл",
 	"Add a short title for this prompt": "Добавяне на кратко заглавие за този промпт",
 	"Add a tag": "Добавяне на таг",
-	"Add custom prompt": "",
+	"Add custom prompt": "Добавяне на собствен промпт",
 	"Add Docs": "Добавяне на Документи",
 	"Add Files": "Добавяне на Файлове",
+	"Add Memory": "",
 	"Add message": "Добавяне на съобщение",
 	"Add Model": "",
 	"Add Tags": "добавяне на тагове",
@@ -47,7 +49,7 @@
 	"Archived Chats": "",
 	"are allowed - Activate this command by typing": "са разрешени - Активирайте тази команда чрез въвеждане",
 	"Are you sure?": "Сигурни ли сте?",
-	"Attach file": "",
+	"Attach file": "Прикачване на файл",
 	"Attention to detail": "",
 	"Audio": "Аудио",
 	"August": "",
@@ -66,6 +68,8 @@
 	"Categories": "Категории",
 	"Change Password": "Промяна на Парола",
 	"Chat": "Чат",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Чат История",
 	"Chat History is off for this browser.": "Чат История е изключен за този браузър.",
 	"Chats": "Чатове",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "Chunk Overlap",
 	"Chunk Params": "Chunk Params",
 	"Chunk Size": "Chunk Size",
-	"Citation": "",
+	"Citation": "Цитат",
 	"Click here for help.": "Натиснете тук за помощ.",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "Натиснете тук за проверка на други моделфайлове.",
@@ -117,7 +121,6 @@
 	"Dark": "Тъмен",
 	"Dashboard": "",
 	"Database": "База данни",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "По подразбиране",
 	"Default (Automatic1111)": "По подразбиране (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Включено",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Въведете съобщение за {{role}} тук",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Въведете Chunk Overlap",
 	"Enter Chunk Size": "Въведете Chunk Size",
 	"Enter Image Size (e.g. 512x512)": "Въведете размер на изображението (напр. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Основни Настройки",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Здравей, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "Импортване на модфайлове",
 	"Import Prompts": "Импортване на промптове",
 	"Include `--api` flag when running stable-diffusion-webui": "Включете флага `--api`, когато стартирате stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "Въведете команди",
 	"Interface": "Интерфейс",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "Светъл",
 	"Listening...": "Слушам...",
 	"LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Направено от OpenWebUI общността",
 	"Make sure to enclose them with": "Уверете се, че са заключени с",
 	"Manage LiteLLM Models": "Управление на LiteLLM Моделите",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Max Tokens",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модели могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "Модфайлове",
 	"Models": "Модели",
 	"More": "",
-	"My Documents": "Мои документи",
-	"My Modelfiles": "Мои модфайлове",
-	"My Prompts": "Мои промптове",
 	"Name": "Име",
 	"Name Tag": "Име Таг",
 	"Name your modelfile": "Име на модфайла",
 	"New Chat": "Нов чат",
 	"New Password": "Нова парола",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "Няма наличен източник",
 	"Not factually correct": "",
 	"Not sure what to add?": "Не сте сигурни, какво да добавите?",
 	"Not sure what to write? Switch to": "Не сте сигурни, какво да напишете? Превключете към",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
 	"pending": "в очакване",
 	"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "Плейграунд",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "Роля",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
 	"Save": "Запис",
 	"Save & Create": "Запис & Създаване",
-	"Save & Submit": "Запис & Изпращане",
 	"Save & Update": "Запис & Актуализиране",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
 	"Scan": "Сканиране",
@@ -378,7 +384,8 @@
 	"Select a mode": "Изберете режим",
 	"Select a model": "Изберете модел",
 	"Select an Ollama instance": "Изберете Ollama инстанция",
-	"Select model": "",
+	"Select model": "Изберете модел",
+	"Send": "",
 	"Send a Message": "Изпращане на Съобщение",
 	"Send message": "Изпращане на съобщение",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "Изход",
 	"Sign up": "Регистрация",
 	"Signing in": "",
-	"Source": "",
+	"Source": "Източник",
 	"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
 	"Speech-to-Text Engine": "Speech-to-Text Engine",
 	"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.",
@@ -482,10 +489,11 @@
 	"What’s New in": "Какво е новото в",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Когато историята е изключена, нови чатове в този браузър ще не се показват в историята на никои от вашия профил.",
 	"Whisper (Local)": "Whisper (Локален)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Напиши предложение за промпт (напр. Кой сте вие?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 знака, което описва [тема или ключова дума].",
 	"Yesterday": "",
-	"You": "Вие",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "Вие сте полезен асистент.",

+ 23 - 15
src/lib/i18n/locales/bn-BD/translation.json

@@ -10,14 +10,16 @@
 	"About": "সম্পর্কে",
 	"Account": "একাউন্ট",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "একটি মডেল যোগ করুন",
 	"Add a model tag name": "একটি মডেল ট্যাগ যোগ করুন",
 	"Add a short description about what this modelfile does": "এই মডেলফাইলটির সম্পর্কে সংক্ষিপ্ত বিবরণ যোগ করুন",
 	"Add a short title for this prompt": "এই প্রম্পটের জন্য একটি সংক্ষিপ্ত টাইটেল যোগ করুন",
 	"Add a tag": "একটি ট্যাগ যোগ করুন",
-	"Add custom prompt": "",
+	"Add custom prompt": "একটি কাস্টম প্রম্পট যোগ করুন",
 	"Add Docs": "ডকুমেন্ট যোগ করুন",
 	"Add Files": "ফাইল যোগ করুন",
+	"Add Memory": "",
 	"Add message": "মেসেজ যোগ করুন",
 	"Add Model": "",
 	"Add Tags": "ট্যাগ যোগ করুন",
@@ -47,8 +49,8 @@
 	"Archived Chats": "চ্যাট ইতিহাস সংরক্ষণাগার",
 	"are allowed - Activate this command by typing": "অনুমোদিত - কমান্ডটি চালু করার জন্য লিখুন",
 	"Are you sure?": "আপনি নিশ্চিত?",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "ফাইল যুক্ত করুন",
+	"Attention to detail": "বিস্তারিত বিশেষতা",
 	"Audio": "অডিও",
 	"August": "",
 	"Auto-playback response": "রেসপন্স অটো-প্লেব্যাক",
@@ -66,6 +68,8 @@
 	"Categories": "ক্যাটাগরিসমূহ",
 	"Change Password": "পাসওয়ার্ড পরিবর্তন করুন",
 	"Chat": "চ্যাট",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "চ্যাট হিস্টোরি",
 	"Chat History is off for this browser.": "এই ব্রাউজারের জন্য চ্যাট হিস্টোরি বন্ধ আছে",
 	"Chats": "চ্যাটসমূহ",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "চাঙ্ক ওভারল্যাপ",
 	"Chunk Params": "চাঙ্ক প্যারামিটার্স",
 	"Chunk Size": "চাঙ্ক সাইজ",
-	"Citation": "",
+	"Citation": "উদ্ধৃতি",
 	"Click here for help.": "সাহায্যের জন্য এখানে ক্লিক করুন",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "অন্যান্য মডেলফাইল চেক করার জন্য এখানে ক্লিক করুন",
@@ -117,7 +121,6 @@
 	"Dark": "ডার্ক",
 	"Dashboard": "",
 	"Database": "ডেটাবেজ",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "ডিফল্ট",
 	"Default (Automatic1111)": "ডিফল্ট (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "চালু করা হয়েছে",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "{{role}} মেসেজ এখানে লিখুন",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন",
 	"Enter Chunk Size": "চাংক সাইজ লিখুন",
 	"Enter Image Size (e.g. 512x512)": "ছবির মাপ লিখুন (যেমন 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "সাধারণ সেটিংসমূহ",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "হ্যালো, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "মডেলফাইলগুলো ইমপোর্ট করুন",
 	"Import Prompts": "প্রম্পটগুলো ইমপোর্ট করুন",
 	"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui চালু করার সময় `--api` ফ্ল্যাগ সংযুক্ত করুন",
-	"Input commands": "",
+	"Input commands": "ইনপুট কমান্ডস",
 	"Interface": "ইন্টারফেস",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "লাইট",
 	"Listening...": "শুনছে...",
 	"LLMs can make mistakes. Verify important information.": "LLM ভুল করতে পারে। গুরুত্বপূর্ণ তথ্য যাচাই করে নিন।",
+	"LTR": "",
 	"Made by OpenWebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
 	"Make sure to enclose them with": "এটা দিয়ে বন্ধনী দিতে ভুলবেন না",
 	"Manage LiteLLM Models": "LiteLLM মডেল ব্যবস্থাপনা করুন",
@@ -253,7 +259,9 @@
 	"Max Tokens": "সর্বোচ্চ টোকন",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "একসঙ্গে সর্বোচ্চ তিনটি মডেল ডাউনলোড করা যায়। দয়া করে পরে আবার চেষ্টা করুন।",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "মডেলফাইলসমূহ",
 	"Models": "মডেলসমূহ",
 	"More": "",
-	"My Documents": "আমার ডকুমেন্টসমূহ",
-	"My Modelfiles": "আমার মডেলফাইলসমূহ",
-	"My Prompts": "আমার প্রম্পটসমূহ",
 	"Name": "নাম",
 	"Name Tag": "নামের ট্যাগ",
 	"Name your modelfile": "আপনার মডেলফাইলের নাম দিন",
 	"New Chat": "নতুন চ্যাট",
 	"New Password": "নতুন পাসওয়ার্ড",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "কোন উৎস পাওয়া যায়নি",
 	"Not factually correct": "",
 	"Not sure what to add?": "কী যুক্ত করতে হবে নিশ্চিত না?",
 	"Not sure what to write? Switch to": "কী লিখতে হবে নিশ্চিত না? পরিবর্তন করুন:",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "পিডিএফ এর ছবি থেকে লেখা বের করুন (OCR)",
 	"pending": "অপেক্ষমান",
 	"Permission denied when accessing microphone: {{error}}": "মাইক্রোফোন ব্যবহারের অনুমতি পাওয়া যায়নি: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "খেলাঘর",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "পদবি",
 	"Rosé Pine": "রোজ পাইন",
 	"Rosé Pine Dawn": "ভোরের রোজ পাইন",
+	"RTL": "",
 	"Save": "সংরক্ষণ",
 	"Save & Create": "সংরক্ষণ এবং তৈরি করুন",
-	"Save & Submit": "সংরক্ষণ এবং সাবমিট করুন",
 	"Save & Update": "সংরক্ষণ এবং আপডেট করুন",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "মাধ্যমে",
 	"Scan": "স্ক্যান",
@@ -378,7 +384,8 @@
 	"Select a mode": "একটি মডেল নির্বাচন করুন",
 	"Select a model": "একটি মডেল নির্বাচন করুন",
 	"Select an Ollama instance": "একটি Ollama ইন্সট্যান্স নির্বাচন করুন",
-	"Select model": "",
+	"Select model": "মডেল নির্বাচন করুন",
+	"Send": "",
 	"Send a Message": "একটি মেসেজ পাঠান",
 	"Send message": "মেসেজ পাঠান",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "সাইন আউট",
 	"Sign up": "সাইন আপ",
 	"Signing in": "",
-	"Source": "",
+	"Source": "উৎস",
 	"Speech recognition error: {{error}}": "স্পিচ রিকগনিশনে সমস্যা: {{error}}",
 	"Speech-to-Text Engine": "স্পিচ-টু-টেক্সট ইঞ্জিন",
 	"SpeechRecognition API is not supported in this browser.": "এই ব্রাউজার স্পিচরিকগনিশন এপিআই সাপোর্ট করে না।",
@@ -482,10 +489,11 @@
 	"What’s New in": "এতে নতুন কী",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "যদি হিস্টোরি বন্ধ থাকে তাহলে এই ব্রাউজারের নতুন চ্যাটগুলো আপনার কোন ডিভাইসের হিস্টোরিতেই দেখা যাবে না।",
 	"Whisper (Local)": "Whisper (লোকাল)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "একটি প্রম্পট সাজেশন লিখুন (যেমন Who are you?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "৫০ শব্দের মধ্যে [topic or keyword] এর একটি সারসংক্ষেপ লিখুন।",
 	"Yesterday": "",
-	"You": "আপনি",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "আপনি একজন উপকারী এসিস্ট্যান্ট",

+ 23 - 15
src/lib/i18n/locales/ca-ES/translation.json

@@ -10,14 +10,16 @@
 	"About": "Sobre",
 	"Account": "Compte",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "Afegeix un model",
 	"Add a model tag name": "Afegeix un nom d'etiqueta de model",
 	"Add a short description about what this modelfile does": "Afegeix una descripció curta del que fa aquest arxiu de model",
 	"Add a short title for this prompt": "Afegeix un títol curt per aquest prompt",
 	"Add a tag": "Afegeix una etiqueta",
-	"Add custom prompt": "",
+	"Add custom prompt": "Afegir un prompt personalitzat",
 	"Add Docs": "Afegeix Documents",
 	"Add Files": "Afegeix Arxius",
+	"Add Memory": "",
 	"Add message": "Afegeix missatge",
 	"Add Model": "",
 	"Add Tags": "afegeix etiquetes",
@@ -47,8 +49,8 @@
 	"Archived Chats": "Arxiu d'historial de xat",
 	"are allowed - Activate this command by typing": "estan permesos - Activa aquesta comanda escrivint",
 	"Are you sure?": "Estàs segur?",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "Adjuntar arxiu",
+	"Attention to detail": "Detall atent",
 	"Audio": "Àudio",
 	"August": "",
 	"Auto-playback response": "Resposta de reproducció automàtica",
@@ -66,6 +68,8 @@
 	"Categories": "Categories",
 	"Change Password": "Canvia la Contrasenya",
 	"Chat": "Xat",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Històric del Xat",
 	"Chat History is off for this browser.": "L'historial de xat està desactivat per a aquest navegador.",
 	"Chats": "Xats",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "Solapament de Blocs",
 	"Chunk Params": "Paràmetres de Blocs",
 	"Chunk Size": "Mida del Bloc",
-	"Citation": "",
+	"Citation": "Citació",
 	"Click here for help.": "Fes clic aquí per ajuda.",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "Fes clic aquí per comprovar altres fitxers de model.",
@@ -117,7 +121,6 @@
 	"Dark": "Fosc",
 	"Dashboard": "",
 	"Database": "Base de Dades",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "Per defecte",
 	"Default (Automatic1111)": "Per defecte (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Activat",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Introdueix aquí el missatge de {{role}}",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Introdueix el Solapament de Blocs",
 	"Enter Chunk Size": "Introdueix la Mida del Bloc",
 	"Enter Image Size (e.g. 512x512)": "Introdueix la Mida de la Imatge (p. ex. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Configuració General",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Hola, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "Importa Fitxers de Model",
 	"Import Prompts": "Importa Prompts",
 	"Include `--api` flag when running stable-diffusion-webui": "Inclou la bandera `--api` quan executis stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "Entra ordres",
 	"Interface": "Interfície",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "Clar",
 	"Listening...": "Escoltant...",
 	"LLMs can make mistakes. Verify important information.": "Els LLMs poden cometre errors. Verifica la informació important.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI",
 	"Make sure to enclose them with": "Assegura't d'envoltar-los amb",
 	"Manage LiteLLM Models": "Gestiona Models LiteLLM",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Màxim de Tokens",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es poden descarregar un màxim de 3 models simultàniament. Si us plau, prova-ho més tard.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Eta de Mirostat",
@@ -276,16 +284,13 @@
 	"Modelfiles": "Fitxers de Model",
 	"Models": "Models",
 	"More": "",
-	"My Documents": "Els Meus Documents",
-	"My Modelfiles": "Els Meus Fitxers de Model",
-	"My Prompts": "Els Meus Prompts",
 	"Name": "Nom",
 	"Name Tag": "Etiqueta de Nom",
 	"Name your modelfile": "Nomena el teu fitxer de model",
 	"New Chat": "Xat Nou",
 	"New Password": "Nova Contrasenya",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "Sense font disponible",
 	"Not factually correct": "",
 	"Not sure what to add?": "No estàs segur del que afegir?",
 	"Not sure what to write? Switch to": "No estàs segur del que escriure? Canvia a",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "Extreu Imatges de PDF (OCR)",
 	"pending": "pendent",
 	"Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "Zona de Jocs",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "Rol",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Albada Rosé Pine",
+	"RTL": "",
 	"Save": "Guarda",
 	"Save & Create": "Guarda i Crea",
-	"Save & Submit": "Guarda i Envia",
 	"Save & Update": "Guarda i Actualitza",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Guardar registres de xat directament a l'emmagatzematge del teu navegador ja no és suportat. Si us plau, pren un moment per descarregar i eliminar els teus registres de xat fent clic al botó de sota. No et preocupis, pots reimportar fàcilment els teus registres de xat al backend a través de",
 	"Scan": "Escaneja",
@@ -378,7 +384,8 @@
 	"Select a mode": "Selecciona un mode",
 	"Select a model": "Selecciona un model",
 	"Select an Ollama instance": "Selecciona una instància d'Ollama",
-	"Select model": "",
+	"Select model": "Selecciona un model",
+	"Send": "",
 	"Send a Message": "Envia un Missatge",
 	"Send message": "Envia missatge",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "Tanca sessió",
 	"Sign up": "Registra't",
 	"Signing in": "",
-	"Source": "",
+	"Source": "Font",
 	"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
 	"Speech-to-Text Engine": "Motor de Veu a Text",
 	"SpeechRecognition API is not supported in this browser.": "L'API de Reconèixer Veu no és compatible amb aquest navegador.",
@@ -482,10 +489,11 @@
 	"What’s New in": "Què hi ha de Nou en",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quan l'historial està desactivat, els nous xats en aquest navegador no apareixeran en el teu historial en cap dels teus dispositius.",
 	"Whisper (Local)": "Whisper (Local)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Escriu una suggerència de prompt (p. ex. Qui ets tu?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].",
 	"Yesterday": "",
-	"You": "Tu",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "Ets un assistent útil.",

+ 16 - 8
src/lib/i18n/locales/de-DE/translation.json

@@ -10,6 +10,7 @@
 	"About": "Über",
 	"Account": "Account",
 	"Accurate information": "Genaue Information",
+	"Add": "",
 	"Add a model": "Füge ein Modell hinzu",
 	"Add a model tag name": "Benenne deinen Modell-Tag",
 	"Add a short description about what this modelfile does": "Füge eine kurze Beschreibung hinzu, was dieses Modelfile kann",
@@ -18,6 +19,7 @@
 	"Add custom prompt": "Eigenen Prompt hinzufügen",
 	"Add Docs": "Dokumente hinzufügen",
 	"Add Files": "Dateien hinzufügen",
+	"Add Memory": "",
 	"Add message": "Nachricht eingeben",
 	"Add Model": "Modell hinzufügen",
 	"Add Tags": "Tags hinzufügen",
@@ -66,6 +68,8 @@
 	"Categories": "Kategorien",
 	"Change Password": "Passwort ändern",
 	"Chat": "Chat",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Chat Verlauf",
 	"Chat History is off for this browser.": "Chat Verlauf ist für diesen Browser ausgeschaltet.",
 	"Chats": "Chats",
@@ -117,7 +121,6 @@
 	"Dark": "Dunkel",
 	"Dashboard": "Dashboard",
 	"Database": "Datenbank",
-	"DD/MM/YYYY HH:mm": "DD.MM.YYYY HH:mm",
 	"December": "Dezember",
 	"Default": "Standard",
 	"Default (Automatic1111)": "Standard (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Aktiviert",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Gib die {{role}} Nachricht hier ein",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Gib den Chunk Overlap ein",
 	"Enter Chunk Size": "Gib die Chunk Size ein",
 	"Enter Image Size (e.g. 512x512)": "Gib die Bildgröße ein (z.B. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Allgemeine Einstellungen",
 	"Generation Info": "Generierungsinformationen",
 	"Good Response": "Gute Antwort",
+	"h:mm a": "",
 	"has no conversations.": "hat keine Unterhaltungen.",
 	"Hello, {{name}}": "Hallo, {{name}}",
 	"Help": "Hilfe",
@@ -244,6 +249,7 @@
 	"Light": "Hell",
 	"Listening...": "Hören...",
 	"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Von der OpenWebUI-Community",
 	"Make sure to enclose them with": "Formatiere deine Variablen mit:",
 	"Manage LiteLLM Models": "LiteLLM-Modelle verwalten",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Maximale Tokens",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es können maximal 3 Modelle gleichzeitig heruntergeladen werden. Bitte versuche es später erneut.",
 	"May": "Mai",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "Fortlaudende Nachrichten in diesem Chat werden nicht automatisch geteilt. Benutzer mit dem Link können den Chat einsehen.",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "Mindestscore",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,9 +284,6 @@
 	"Modelfiles": "Modelfiles",
 	"Models": "Modelle",
 	"More": "Mehr",
-	"My Documents": "Meine Dokumente",
-	"My Modelfiles": "Meine Modelfiles",
-	"My Prompts": "Meine Prompts",
 	"Name": "Name",
 	"Name Tag": "Namens-Tag",
 	"Name your modelfile": "Benenne dein modelfile",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "Text von Bildern aus PDFs extrahieren (OCR)",
 	"pending": "ausstehend",
 	"Permission denied when accessing microphone: {{error}}": "Zugriff auf das Mikrofon verweigert: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "Nur Text (.txt)",
 	"Playground": "Testumgebung",
 	"Positive attitude": "Positive Einstellung",
@@ -360,9 +366,9 @@
 	"Role": "Rolle",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
 	"Save": "Speichern",
 	"Save & Create": "Speichern und erstellen",
-	"Save & Submit": "Speichern und senden",
 	"Save & Update": "Speichern und aktualisieren",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Das direkte Speichern von Chat-Protokollen im Browser-Speicher wird nicht mehr unterstützt. Bitte nimm dir einen Moment Zeit, um deine Chat-Protokolle herunterzuladen und zu löschen, indem du auf die Schaltfläche unten klickst. Keine Sorge, du kannst deine Chat-Protokolle problemlos über das Backend wieder importieren.",
 	"Scan": "Scannen",
@@ -378,7 +384,8 @@
 	"Select a mode": "Einen Modus auswählen",
 	"Select a model": "Ein Modell auswählen",
 	"Select an Ollama instance": "Eine Ollama Instanz auswählen",
-	"Select model": "",
+	"Select model": "Modell auswählen",
+	"Send": "",
 	"Send a Message": "Eine Nachricht senden",
 	"Send message": "Nachricht senden",
 	"September": "September",
@@ -482,10 +489,11 @@
 	"What’s New in": "Was gibt's Neues in",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Wenn die Historie ausgeschaltet ist, werden neue Chats nicht in deiner Historie auf deine Geräte angezeigt.",
 	"Whisper (Local)": "Whisper (Lokal)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Gebe einen Prompt-Vorschlag ein (z.B. Wer bist du?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.",
 	"Yesterday": "Gestern",
-	"You": "Du",
+	"You": "",
 	"You have no archived conversations.": "Du hast keine archivierten Unterhaltungen.",
 	"You have shared this chat": "Du hast diesen Chat",
 	"You're a helpful assistant.": "Du bist ein hilfreicher Assistent.",

+ 20 - 12
src/lib/i18n/locales/dg-DG/translation.json

@@ -10,6 +10,7 @@
 	"About": "Much About",
 	"Account": "Account",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "Add a model",
 	"Add a model tag name": "Add a model tag name",
 	"Add a short description about what this modelfile does": "Add short description about what this modelfile does",
@@ -18,6 +19,7 @@
 	"Add custom prompt": "",
 	"Add Docs": "Add Docs",
 	"Add Files": "Add Files",
+	"Add Memory": "",
 	"Add message": "Add Prompt",
 	"Add Model": "",
 	"Add Tags": "",
@@ -47,7 +49,7 @@
 	"Archived Chats": "",
 	"are allowed - Activate this command by typing": "are allowed. Activate typing",
 	"Are you sure?": "Such certainty?",
-	"Attach file": "",
+	"Attach file": "Attach file",
 	"Attention to detail": "",
 	"Audio": "Audio",
 	"August": "",
@@ -66,6 +68,8 @@
 	"Categories": "Categories",
 	"Change Password": "Change Password",
 	"Chat": "Chat",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Chat History",
 	"Chat History is off for this browser.": "Chat History off for this browser. Such sadness.",
 	"Chats": "Chats",
@@ -117,7 +121,6 @@
 	"Dark": "Dark",
 	"Dashboard": "",
 	"Database": "Database",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "Default",
 	"Default (Automatic1111)": "Default (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "So Activated",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Enter {{role}} bork here",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Enter Overlap of Chunks",
 	"Enter Chunk Size": "Enter Size of Chunk",
 	"Enter Image Size (e.g. 512x512)": "Enter Size of Wow (e.g. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "General Doge Settings",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Much helo, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "Import Modelfiles",
 	"Import Prompts": "Import Promptos",
 	"Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "Input commands",
 	"Interface": "Interface",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "Light",
 	"Listening...": "Listening...",
 	"LLMs can make mistakes. Verify important information.": "LLMs can make borks. Verify important info.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Made by OpenWebUI Community",
 	"Make sure to enclose them with": "Make sure to enclose them with",
 	"Manage LiteLLM Models": "Manage LiteLLM Models",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Max Tokens",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maximum of 3 models can be downloaded simultaneously. Please try again later.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "Modelfiles",
 	"Models": "Wowdels",
 	"More": "",
-	"My Documents": "My Doguments",
-	"My Modelfiles": "My Modelfiles",
-	"My Prompts": "My Promptos",
 	"Name": "Name",
 	"Name Tag": "Name Tag",
 	"Name your modelfile": "Name your modelfile",
 	"New Chat": "New Bark",
 	"New Password": "New Barkword",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "No source available",
 	"Not factually correct": "",
 	"Not sure what to add?": "Not sure what to add?",
 	"Not sure what to write? Switch to": "Not sure what to write? Switch to",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "PDF Extract Wowmages (OCR)",
 	"pending": "pending",
 	"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "Playground",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "Role",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
 	"Save": "Save much wow",
 	"Save & Create": "Save & Create much create",
-	"Save & Submit": "Save & Submit very submit",
 	"Save & Update": "Save & Update much update",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Saving chat logs in browser storage not support anymore. Pls download and delete your chat logs by click button below. Much easy re-import to backend through",
 	"Scan": "Scan much scan",
@@ -378,7 +384,8 @@
 	"Select a mode": "Select a mode very choose",
 	"Select a model": "Select a model much choice",
 	"Select an Ollama instance": "Select an Ollama instance very choose",
-	"Select model": "",
+	"Select model": "Select model much choice",
+	"Send": "",
 	"Send a Message": "Send a Message much message",
 	"Send message": "Send message very send",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "Sign Out much logout",
 	"Sign up": "Sign up much join",
 	"Signing in": "",
-	"Source": "",
+	"Source": "Source",
 	"Speech recognition error: {{error}}": "Speech recognition error: {{error}} so error",
 	"Speech-to-Text Engine": "Speech-to-Text Engine much speak",
 	"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser. Much sad.",
@@ -482,10 +489,11 @@
 	"What’s New in": "What’s New in much new",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "When history is turned off, new chats on this browser won't appear in your history on any of your devices. Much history.",
 	"Whisper (Local)": "Whisper (Local) much whisper",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Write a prompt suggestion (e.g. Who are you?) much suggest",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword]. Much summarize.",
 	"Yesterday": "",
-	"You": "You very you",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "You're a helpful assistant. Much helpful.",

+ 14 - 6
src/lib/i18n/locales/en-GB/translation.json

@@ -10,6 +10,7 @@
 	"About": "",
 	"Account": "",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "",
 	"Add a model tag name": "",
 	"Add a short description about what this modelfile does": "",
@@ -18,6 +19,7 @@
 	"Add custom prompt": "",
 	"Add Docs": "",
 	"Add Files": "",
+	"Add Memory": "",
 	"Add message": "",
 	"Add Model": "",
 	"Add Tags": "",
@@ -66,6 +68,8 @@
 	"Categories": "",
 	"Change Password": "",
 	"Chat": "",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "",
 	"Chat History is off for this browser.": "",
 	"Chats": "",
@@ -117,7 +121,6 @@
 	"Dark": "",
 	"Dashboard": "",
 	"Database": "",
-	"DD/MM/YYYY HH:mm": "",
 	"December": "",
 	"Default": "",
 	"Default (Automatic1111)": "",
@@ -168,6 +171,7 @@
 	"Enabled": "",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "",
 	"Enter Chunk Size": "",
 	"Enter Image Size (e.g. 512x512)": "",
@@ -211,6 +215,7 @@
 	"General Settings": "",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "",
 	"Help": "",
@@ -244,6 +249,7 @@
 	"Light": "",
 	"Listening...": "",
 	"LLMs can make mistakes. Verify important information.": "",
+	"LTR": "",
 	"Made by OpenWebUI Community": "",
 	"Make sure to enclose them with": "",
 	"Manage LiteLLM Models": "",
@@ -253,7 +259,9 @@
 	"Max Tokens": "",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "",
 	"Mirostat Eta": "",
@@ -276,9 +284,6 @@
 	"Modelfiles": "",
 	"Models": "",
 	"More": "",
-	"My Documents": "",
-	"My Modelfiles": "",
-	"My Prompts": "",
 	"Name": "",
 	"Name Tag": "",
 	"Name your modelfile": "",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "",
 	"pending": "",
 	"Permission denied when accessing microphone: {{error}}": "",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "",
 	"Rosé Pine": "",
 	"Rosé Pine Dawn": "",
+	"RTL": "",
 	"Save": "",
 	"Save & Create": "",
-	"Save & Submit": "",
 	"Save & Update": "",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
 	"Scan": "",
@@ -379,6 +385,7 @@
 	"Select a model": "",
 	"Select an Ollama instance": "",
 	"Select model": "",
+	"Send": "",
 	"Send a Message": "",
 	"Send message": "",
 	"September": "",
@@ -482,6 +489,7 @@
 	"What’s New in": "",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "",
 	"Whisper (Local)": "",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "",
 	"Yesterday": "",

+ 14 - 6
src/lib/i18n/locales/en-US/translation.json

@@ -10,6 +10,7 @@
 	"About": "",
 	"Account": "",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "",
 	"Add a model tag name": "",
 	"Add a short description about what this modelfile does": "",
@@ -18,6 +19,7 @@
 	"Add custom prompt": "",
 	"Add Docs": "",
 	"Add Files": "",
+	"Add Memory": "",
 	"Add message": "",
 	"Add Model": "",
 	"Add Tags": "",
@@ -66,6 +68,8 @@
 	"Categories": "",
 	"Change Password": "",
 	"Chat": "",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "",
 	"Chat History is off for this browser.": "",
 	"Chats": "",
@@ -117,7 +121,6 @@
 	"Dark": "",
 	"Dashboard": "",
 	"Database": "",
-	"DD/MM/YYYY HH:mm": "",
 	"December": "",
 	"Default": "",
 	"Default (Automatic1111)": "",
@@ -168,6 +171,7 @@
 	"Enabled": "",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "",
 	"Enter Chunk Size": "",
 	"Enter Image Size (e.g. 512x512)": "",
@@ -211,6 +215,7 @@
 	"General Settings": "",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "",
 	"Help": "",
@@ -244,6 +249,7 @@
 	"Light": "",
 	"Listening...": "",
 	"LLMs can make mistakes. Verify important information.": "",
+	"LTR": "",
 	"Made by OpenWebUI Community": "",
 	"Make sure to enclose them with": "",
 	"Manage LiteLLM Models": "",
@@ -253,7 +259,9 @@
 	"Max Tokens": "",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "",
 	"Mirostat Eta": "",
@@ -276,9 +284,6 @@
 	"Modelfiles": "",
 	"Models": "",
 	"More": "",
-	"My Documents": "",
-	"My Modelfiles": "",
-	"My Prompts": "",
 	"Name": "",
 	"Name Tag": "",
 	"Name your modelfile": "",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "",
 	"pending": "",
 	"Permission denied when accessing microphone: {{error}}": "",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "",
 	"Rosé Pine": "",
 	"Rosé Pine Dawn": "",
+	"RTL": "",
 	"Save": "",
 	"Save & Create": "",
-	"Save & Submit": "",
 	"Save & Update": "",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
 	"Scan": "",
@@ -379,6 +385,7 @@
 	"Select a model": "",
 	"Select an Ollama instance": "",
 	"Select model": "",
+	"Send": "",
 	"Send a Message": "",
 	"Send message": "",
 	"September": "",
@@ -482,6 +489,7 @@
 	"What’s New in": "",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "",
 	"Whisper (Local)": "",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "",
 	"Yesterday": "",

+ 23 - 15
src/lib/i18n/locales/es-ES/translation.json

@@ -10,14 +10,16 @@
 	"About": "Sobre nosotros",
 	"Account": "Cuenta",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "Agregar un modelo",
 	"Add a model tag name": "Agregar un nombre de etiqueta de modelo",
 	"Add a short description about what this modelfile does": "Agregue una descripción corta de lo que este modelfile hace",
 	"Add a short title for this prompt": "Agregue un título corto para este Prompt",
 	"Add a tag": "Agregar una etiqueta",
-	"Add custom prompt": "",
+	"Add custom prompt": "Agregar un prompt personalizado",
 	"Add Docs": "Agregar Documentos",
 	"Add Files": "Agregar Archivos",
+	"Add Memory": "",
 	"Add message": "Agregar Prompt",
 	"Add Model": "",
 	"Add Tags": "agregar etiquetas",
@@ -47,8 +49,8 @@
 	"Archived Chats": "Chats archivados",
 	"are allowed - Activate this command by typing": "están permitidos - Active este comando escribiendo",
 	"Are you sure?": "¿Está seguro?",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "Adjuntar archivo",
+	"Attention to detail": "Detalle preciso",
 	"Audio": "Audio",
 	"August": "",
 	"Auto-playback response": "Respuesta de reproducción automática",
@@ -66,6 +68,8 @@
 	"Categories": "Categorías",
 	"Change Password": "Cambia la Contraseña",
 	"Chat": "Chat",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Historial del Chat",
 	"Chat History is off for this browser.": "El Historial del Chat está apagado para este navegador.",
 	"Chats": "Chats",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "Superposición de fragmentos",
 	"Chunk Params": "Parámetros de fragmentos",
 	"Chunk Size": "Tamaño de fragmentos",
-	"Citation": "",
+	"Citation": "Cita",
 	"Click here for help.": "Presiona aquí para obtener ayuda.",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "Presiona aquí para consultar otros modelfiles.",
@@ -117,7 +121,6 @@
 	"Dark": "Oscuro",
 	"Dashboard": "",
 	"Database": "Base de datos",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "Por defecto",
 	"Default (Automatic1111)": "Por defecto (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Activado",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Ingrese el mensaje {{role}} aquí",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Ingresar superposición de fragmentos",
 	"Enter Chunk Size": "Ingrese el tamaño del fragmento",
 	"Enter Image Size (e.g. 512x512)": "Ingrese el tamaño de la imagen (p.ej. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Opciones Generales",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Hola, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "Importar Modelfiles",
 	"Import Prompts": "Importar Prompts",
 	"Include `--api` flag when running stable-diffusion-webui": "Incluir el indicador `--api` al ejecutar stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "Ingresar comandos",
 	"Interface": "Interfaz",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "Claro",
 	"Listening...": "Escuchando...",
 	"LLMs can make mistakes. Verify important information.": "Los LLM pueden cometer errores. Verifica la información importante.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Hecho por la comunidad de OpenWebUI",
 	"Make sure to enclose them with": "Asegúrese de adjuntarlos con",
 	"Manage LiteLLM Models": "Administrar Modelos LiteLLM",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Máximo de Tokens",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Se pueden descargar un máximo de 3 modelos simultáneamente. Por favor, inténtelo de nuevo más tarde.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "Modelfiles",
 	"Models": "Modelos",
 	"More": "",
-	"My Documents": "Mis Documentos",
-	"My Modelfiles": "Mis Modelfiles",
-	"My Prompts": "Mis Prompts",
 	"Name": "Nombre",
 	"Name Tag": "Nombre de etiqueta",
 	"Name your modelfile": "Nombra tu modelfile",
 	"New Chat": "Nuevo Chat",
 	"New Password": "Nueva Contraseña",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "No hay fuente disponible",
 	"Not factually correct": "",
 	"Not sure what to add?": "¿No sabes qué añadir?",
 	"Not sure what to write? Switch to": "¿No sabes qué escribir? Cambia a",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "Extraer imágenes de PDF (OCR)",
 	"pending": "pendiente",
 	"Permission denied when accessing microphone: {{error}}": "Permiso denegado al acceder al micrófono: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "Patio de juegos",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "Rol",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
 	"Save": "Guardar",
 	"Save & Create": "Guardar y Crear",
-	"Save & Submit": "Guardar y Enviar",
 	"Save & Update": "Guardar y Actualizar",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Ya no se admite guardar registros de chat directamente en el almacenamiento de su navegador. Tómese un momento para descargar y eliminar sus registros de chat haciendo clic en el botón a continuación. No te preocupes, puedes volver a importar fácilmente tus registros de chat al backend a través de",
 	"Scan": "Escanear",
@@ -378,7 +384,8 @@
 	"Select a mode": "Selecciona un modo",
 	"Select a model": "Selecciona un modelo",
 	"Select an Ollama instance": "Seleccione una instancia de Ollama",
-	"Select model": "",
+	"Select model": "Selecciona un modelo",
+	"Send": "",
 	"Send a Message": "Enviar un Mensaje",
 	"Send message": "Enviar Mensaje",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "Cerrar sesión",
 	"Sign up": "Crear una cuenta",
 	"Signing in": "",
-	"Source": "",
+	"Source": "Fuente",
 	"Speech recognition error: {{error}}": "Error de reconocimiento de voz: {{error}}",
 	"Speech-to-Text Engine": "Motor de voz a texto",
 	"SpeechRecognition API is not supported in this browser.": "La API SpeechRecognition no es compatible con este navegador.",
@@ -482,10 +489,11 @@
 	"What’s New in": "Novedades en",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Cuando el historial está desactivado, los nuevos chats en este navegador no aparecerán en el historial de ninguno de sus dispositivos..",
 	"Whisper (Local)": "Whisper (Local)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Escribe una sugerencia para un prompt (por ejemplo, ¿quién eres?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escribe un resumen en 50 palabras que resuma [tema o palabra clave].",
 	"Yesterday": "",
-	"You": "Usted",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "Eres un asistente útil.",

+ 23 - 15
src/lib/i18n/locales/fa-IR/translation.json

@@ -10,14 +10,16 @@
 	"About": "درباره",
 	"Account": "حساب کاربری",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "اضافه کردن یک مدل",
 	"Add a model tag name": "اضافه کردن یک نام تگ برای مدل",
 	"Add a short description about what this modelfile does": "توضیح کوتاهی در مورد کاری که این فایل\u200cمدل انجام می دهد اضافه کنید",
 	"Add a short title for this prompt": "یک عنوان کوتاه برای این درخواست اضافه کنید",
 	"Add a tag": "اضافه کردن یک تگ",
-	"Add custom prompt": "",
+	"Add custom prompt": "اضافه کردن یک درخواست سفارشی",
 	"Add Docs": "اضافه کردن اسناد",
 	"Add Files": "اضافه کردن فایل\u200cها",
+	"Add Memory": "",
 	"Add message": "اضافه کردن پیغام",
 	"Add Model": "",
 	"Add Tags": "اضافه کردن تگ\u200cها",
@@ -47,8 +49,8 @@
 	"Archived Chats": "آرشیو تاریخچه چت",
 	"are allowed - Activate this command by typing": "مجاز هستند - این دستور را با تایپ کردن این فعال کنید:",
 	"Are you sure?": "آیا مطمئن هستید؟",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "پیوست فایل",
+	"Attention to detail": "دقیق",
 	"Audio": "صدا",
 	"August": "",
 	"Auto-playback response": "پخش خودکار پاسخ ",
@@ -66,6 +68,8 @@
 	"Categories": "دسته\u200cبندی\u200cها",
 	"Change Password": "تغییر رمز عبور",
 	"Chat": "گپ",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "تاریخچه\u200cی گفتگو",
 	"Chat History is off for this browser.": "سابقه گپ برای این مرورگر خاموش است.",
 	"Chats": "گپ\u200cها",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "همپوشانی تکه",
 	"Chunk Params": "پارامترهای تکه",
 	"Chunk Size": "اندازه تکه",
-	"Citation": "",
+	"Citation": "استناد",
 	"Click here for help.": "برای کمک اینجا را کلیک کنید.",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "برای بررسی سایر فایل\u200cهای مدل اینجا را کلیک کنید.",
@@ -117,7 +121,6 @@
 	"Dark": "تیره",
 	"Dashboard": "",
 	"Database": "پایگاه داده",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "پیشفرض",
 	"Default (Automatic1111)": "پیشفرض (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "فعال",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "پیام {{role}} را اینجا وارد کنید",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "مقدار Chunk Overlap را وارد کنید",
 	"Enter Chunk Size": "مقدار Chunk Size را وارد کنید",
 	"Enter Image Size (e.g. 512x512)": "اندازه تصویر را وارد کنید (مثال: 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "تنظیمات عمومی",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "سلام، {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "ایمپورت فایل\u200cهای مدل",
 	"Import Prompts": "ایمپورت پرامپت\u200cها",
 	"Include `--api` flag when running stable-diffusion-webui": "فلگ `--api` را هنکام اجرای stable-diffusion-webui استفاده کنید.",
-	"Input commands": "",
+	"Input commands": "ورودی دستورات",
 	"Interface": "رابط",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "روشن",
 	"Listening...": "در حال گوش دادن...",
 	"LLMs can make mistakes. Verify important information.": "مدل\u200cهای زبانی بزرگ می\u200cتوانند اشتباه کنند. اطلاعات مهم را راستی\u200cآزمایی کنید.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "ساخته شده توسط OpenWebUI Community",
 	"Make sure to enclose them with": "مطمئن شوید که آنها را با این محصور کنید:",
 	"Manage LiteLLM Models": "Manage LiteLLM Models",
@@ -253,7 +259,9 @@
 	"Max Tokens": "حداکثر توکن",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "حداکثر 3 مدل را می توان به طور همزمان دانلود کرد. لطفاً بعداً دوباره امتحان کنید.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "فایل\u200cهای مدل",
 	"Models": "مدل\u200cها",
 	"More": "",
-	"My Documents": "اسناد من",
-	"My Modelfiles": "فایل\u200cهای مدل من",
-	"My Prompts": "پرامپت\u200cهای من",
 	"Name": "نام",
 	"Name Tag": "نام تگ",
 	"Name your modelfile": "فایل مدل را نام\u200cگذاری کنید",
 	"New Chat": "گپ جدید",
 	"New Password": "رمز عبور جدید",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "منبعی در دسترس نیست",
 	"Not factually correct": "",
 	"Not sure what to add?": "مطمئن نیستید چه چیزی را اضافه کنید؟",
 	"Not sure what to write? Switch to": "مطمئن نیستید چه بنویسید؟ تغییر به",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "استخراج تصاویر از PDF (OCR)",
 	"pending": "در انتظار",
 	"Permission denied when accessing microphone: {{error}}": "هنگام دسترسی به میکروفون، اجازه داده نشد: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "زمین بازی",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "نقش",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
 	"Save": "ذخیره",
 	"Save & Create": "ذخیره و ایجاد",
-	"Save & Submit": "ذخیره و ارسال",
 	"Save & Update": "ذخیره و به\u200cروزرسانی",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "ذخیره گزارش\u200cهای چت مستقیماً در حافظه مرورگر شما دیگر پشتیبانی نمی\u200cشود. لطفاً با کلیک بر روی دکمه زیر، چند لحظه برای دانلود و حذف گزارش های چت خود وقت بگذارید. نگران نباشید، شما به راحتی می توانید گزارش های چت خود را از طریق بکند دوباره وارد کنید",
 	"Scan": "اسکن",
@@ -378,7 +384,8 @@
 	"Select a mode": "یک حالت انتخاب کنید",
 	"Select a model": "انتخاب یک مدل",
 	"Select an Ollama instance": "انتخاب یک نمونه از اولاما",
-	"Select model": "",
+	"Select model": "انتخاب یک مدل",
+	"Send": "",
 	"Send a Message": "ارسال یک پیام",
 	"Send message": "ارسال پیام",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "خروج",
 	"Sign up": "ثبت نام",
 	"Signing in": "",
-	"Source": "",
+	"Source": "منبع",
 	"Speech recognition error: {{error}}": "خطای تشخیص گفتار: {{error}}",
 	"Speech-to-Text Engine": "موتور گفتار به متن",
 	"SpeechRecognition API is not supported in this browser.": "API تشخیص گفتار در این مرورگر پشتیبانی نمی شود.",
@@ -482,10 +489,11 @@
 	"What’s New in": "موارد جدید در",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "وقتی سابقه خاموش است، چت\u200cهای جدید در این مرورگر در سابقه شما در هیچ یک از دستگاه\u200cهایتان ظاهر نمی\u200cشوند.",
 	"Whisper (Local)": "ویسپر (محلی)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "یک پیشنهاد پرامپت بنویسید (مثلاً شما کی هستید؟)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "خلاصه ای در 50 کلمه بنویسید که [موضوع یا کلمه کلیدی] را خلاصه کند.",
 	"Yesterday": "",
-	"You": "شما",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "تو یک دستیار سودمند هستی.",

+ 15 - 7
src/lib/i18n/locales/fi-FI/translation.json

@@ -10,6 +10,7 @@
 	"About": "Tietoja",
 	"Account": "Tili",
 	"Accurate information": "Tarkkaa tietoa",
+	"Add": "",
 	"Add a model": "Lisää malli",
 	"Add a model tag name": "Lisää mallitagi",
 	"Add a short description about what this modelfile does": "Lisää lyhyt kuvaus siitä, mitä tämä mallitiedosto tekee",
@@ -18,6 +19,7 @@
 	"Add custom prompt": "Lisää mukautettu kehote",
 	"Add Docs": "Lisää asiakirjoja",
 	"Add Files": "Lisää tiedostoja",
+	"Add Memory": "",
 	"Add message": "Lisää viesti",
 	"Add Model": "Lisää malli",
 	"Add Tags": "Lisää tageja",
@@ -66,6 +68,8 @@
 	"Categories": "Kategoriat",
 	"Change Password": "Vaihda salasana",
 	"Chat": "Keskustelu",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Keskusteluhistoria",
 	"Chat History is off for this browser.": "Keskusteluhistoria on pois päältä tällä selaimella.",
 	"Chats": "Keskustelut",
@@ -117,7 +121,6 @@
 	"Dark": "Tumma",
 	"Dashboard": "Kojelauta",
 	"Database": "Tietokanta",
-	"DD/MM/YYYY HH:mm": "DD.MM.YYYY HH:mm",
 	"December": "joulukuu",
 	"Default": "Oletus",
 	"Default (Automatic1111)": "Oletus (AUTOMATIC1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Käytössä",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Varmista, että CSV-tiedostossasi on 4 saraketta seuraavassa järjestyksessä: Nimi, Sähköposti, Salasana, Rooli.",
 	"Enter {{role}} message here": "Kirjoita {{role}} viesti tähän",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Syötä osien päällekkäisyys",
 	"Enter Chunk Size": "Syötä osien koko",
 	"Enter Image Size (e.g. 512x512)": "Syötä kuvan koko (esim. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Yleisasetukset",
 	"Generation Info": "Generointitiedot",
 	"Good Response": "Hyvä vastaus",
+	"h:mm a": "",
 	"has no conversations.": "ei ole keskusteluja.",
 	"Hello, {{name}}": "Terve, {{name}}",
 	"Help": "Apua",
@@ -244,6 +249,7 @@
 	"Light": "Vaalea",
 	"Listening...": "Kuunnellaan...",
 	"LLMs can make mistakes. Verify important information.": "Kielimallit voivat tehdä virheitä. Varmista tärkeät tiedot.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Tehnyt OpenWebUI-yhteisö",
 	"Make sure to enclose them with": "Varmista, että suljet ne",
 	"Manage LiteLLM Models": "Hallitse LiteLLM-malleja",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Maksimitokenit",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Enintään 3 mallia voidaan ladata samanaikaisesti. Yritä myöhemmin uudelleen.",
 	"May": "toukokuu",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "Viestejä, jotka lähetät luotuasi linkin, ei jaeta. Käyttäjät, joilla on tämä osoite voivat tarkastella jaettua keskustelua.",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "Vähimmäispisteet",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,9 +284,6 @@
 	"Modelfiles": "Mallitiedostot",
 	"Models": "Mallit",
 	"More": "Lisää",
-	"My Documents": "Omat asiakirjat",
-	"My Modelfiles": "Omat mallitiedostot",
-	"My Prompts": "Omat kehotteet",
 	"Name": "Nimi",
 	"Name Tag": "Nimitagi",
 	"Name your modelfile": "Nimeä mallitiedostosi",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "PDF-tiedoston kuvien erottelu (OCR)",
 	"pending": "odottaa",
 	"Permission denied when accessing microphone: {{error}}": "Mikrofonin käyttöoikeus evätty: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "Pelkkä teksti (.txt)",
 	"Playground": "Leikkipaikka",
 	"Positive attitude": "Positiivinen asenne",
@@ -360,9 +366,9 @@
 	"Role": "Rooli",
 	"Rosé Pine": "Rosee-mänty",
 	"Rosé Pine Dawn": "Aamuinen Rosee-mänty",
+	"RTL": "",
 	"Save": "Tallenna",
 	"Save & Create": "Tallenna ja luo",
-	"Save & Submit": "Tallenna ja lähetä",
 	"Save & Update": "Tallenna ja päivitä",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Keskustelulokien tallentaminen suoraan selaimen tallennustilaan ei ole enää tuettua. Lataa ja poista keskustelulokit napsauttamalla alla olevaa painiketta. Älä huoli, voit helposti tuoda keskustelulokit takaisin backendiin",
 	"Scan": "Skannaa",
@@ -379,6 +385,7 @@
 	"Select a model": "Valitse malli",
 	"Select an Ollama instance": "Valitse Ollama-instanssi",
 	"Select model": "Valitse malli",
+	"Send": "",
 	"Send a Message": "Lähetä viesti",
 	"Send message": "Lähetä viesti",
 	"September": "syyskuu",
@@ -482,10 +489,11 @@
 	"What’s New in": "Mitä uutta",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kun historia on pois päältä, uudet keskustelut tässä selaimessa eivät näy historiassasi millään laitteellasi.",
 	"Whisper (Local)": "Whisper (paikallinen)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Kirjoita ehdotettu kehote (esim. Kuka olet?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Kirjoita 50 sanan yhteenveto, joka tiivistää [aihe tai avainsana].",
 	"Yesterday": "Eilen",
-	"You": "Sinä",
+	"You": "",
 	"You have no archived conversations.": "Sinulla ei ole arkistoituja keskusteluja.",
 	"You have shared this chat": "Olet jakanut tämän keskustelun",
 	"You're a helpful assistant.": "Olet avulias apulainen.",

+ 23 - 15
src/lib/i18n/locales/fr-CA/translation.json

@@ -10,14 +10,16 @@
 	"About": "À propos",
 	"Account": "Compte",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "Ajouter un modèle",
 	"Add a model tag name": "Ajouter un nom de tag pour le modèle",
 	"Add a short description about what this modelfile does": "Ajouter une courte description de ce que fait ce fichier de modèle",
 	"Add a short title for this prompt": "Ajouter un court titre pour ce prompt",
 	"Add a tag": "Ajouter un tag",
-	"Add custom prompt": "",
+	"Add custom prompt": "Ajouter un prompt personnalisé",
 	"Add Docs": "Ajouter des documents",
 	"Add Files": "Ajouter des fichiers",
+	"Add Memory": "",
 	"Add message": "Ajouter un message",
 	"Add Model": "",
 	"Add Tags": "ajouter des tags",
@@ -47,8 +49,8 @@
 	"Archived Chats": "enregistrement du chat",
 	"are allowed - Activate this command by typing": "sont autorisés - Activez cette commande en tapant",
 	"Are you sure?": "Êtes-vous sûr ?",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "Joindre un fichier",
+	"Attention to detail": "Attention aux détails",
 	"Audio": "Audio",
 	"August": "",
 	"Auto-playback response": "Réponse en lecture automatique",
@@ -66,6 +68,8 @@
 	"Categories": "Catégories",
 	"Change Password": "Changer le mot de passe",
 	"Chat": "Discussion",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Historique des discussions",
 	"Chat History is off for this browser.": "L'historique des discussions est désactivé pour ce navigateur.",
 	"Chats": "Discussions",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "Chevauchement de bloc",
 	"Chunk Params": "Paramètres de bloc",
 	"Chunk Size": "Taille de bloc",
-	"Citation": "",
+	"Citation": "Citations",
 	"Click here for help.": "Cliquez ici pour de l'aide.",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "Cliquez ici pour vérifier d'autres fichiers de modèle.",
@@ -117,7 +121,6 @@
 	"Dark": "Sombre",
 	"Dashboard": "",
 	"Database": "Base de données",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "Par défaut",
 	"Default (Automatic1111)": "Par défaut (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Activé",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Entrez le message {{role}} ici",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Entrez le chevauchement de bloc",
 	"Enter Chunk Size": "Entrez la taille du bloc",
 	"Enter Image Size (e.g. 512x512)": "Entrez la taille de l'image (p. ex. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Paramètres généraux",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Bonjour, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "Importer les fichiers de modèle",
 	"Import Prompts": "Importer les prompts",
 	"Include `--api` flag when running stable-diffusion-webui": "Inclure l'indicateur `--api` lors de l'exécution de stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "Entrez des commandes d'entrée",
 	"Interface": "Interface",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "Lumière",
 	"Listening...": "Écoute...",
 	"LLMs can make mistakes. Verify important information.": "Les LLMs peuvent faire des erreurs. Vérifiez les informations importantes.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
 	"Make sure to enclose them with": "Assurez-vous de les entourer avec",
 	"Manage LiteLLM Models": "Gérer les modèles LiteLLM",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Tokens maximaux",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Un maximum de 3 modèles peut être téléchargé simultanément. Veuillez réessayer plus tard.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "Fichiers de modèle",
 	"Models": "Modèles",
 	"More": "",
-	"My Documents": "Mes documents",
-	"My Modelfiles": "Mes fichiers de modèle",
-	"My Prompts": "Mes prompts",
 	"Name": "Nom",
 	"Name Tag": "Tag de nom",
 	"Name your modelfile": "Nommez votre fichier de modèle",
 	"New Chat": "Nouvelle discussion",
 	"New Password": "Nouveau mot de passe",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "Aucune source disponible",
 	"Not factually correct": "",
 	"Not sure what to add?": "Pas sûr de quoi ajouter ?",
 	"Not sure what to write? Switch to": "Pas sûr de quoi écrire ? Changez pour",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "Extraction d'images PDF (OCR)",
 	"pending": "en attente",
 	"Permission denied when accessing microphone: {{error}}": "Permission refusée lors de l'accès au microphone : {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "Aire de jeu",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "Rôle",
 	"Rosé Pine": "Pin Rosé",
 	"Rosé Pine Dawn": "Aube Pin Rosé",
+	"RTL": "",
 	"Save": "Enregistrer",
 	"Save & Create": "Enregistrer & Créer",
-	"Save & Submit": "Enregistrer & Soumettre",
 	"Save & Update": "Enregistrer & Mettre à jour",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "La sauvegarde des journaux de discussion directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un moment pour télécharger et supprimer vos journaux de discussion en cliquant sur le bouton ci-dessous. Ne vous inquiétez pas, vous pouvez facilement réimporter vos journaux de discussion dans le backend via",
 	"Scan": "Scanner",
@@ -378,7 +384,8 @@
 	"Select a mode": "Sélectionnez un mode",
 	"Select a model": "Sélectionnez un modèle",
 	"Select an Ollama instance": "Sélectionner une instance Ollama",
-	"Select model": "",
+	"Select model": "Sélectionnez un modèle",
+	"Send": "",
 	"Send a Message": "Envoyer un message",
 	"Send message": "Envoyer un message",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "Se déconnecter",
 	"Sign up": "S'inscrire",
 	"Signing in": "",
-	"Source": "",
+	"Source": "Source",
 	"Speech recognition error: {{error}}": "Erreur de reconnaissance vocale : {{error}}",
 	"Speech-to-Text Engine": "Moteur reconnaissance vocale",
 	"SpeechRecognition API is not supported in this browser.": "L'API SpeechRecognition n'est pas prise en charge dans ce navigateur.",
@@ -482,10 +489,11 @@
 	"What’s New in": "Quoi de neuf dans",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Lorsque l'historique est désactivé, les nouvelles discussions sur ce navigateur n'apparaîtront pas dans votre historique sur aucun de vos appareils.",
 	"Whisper (Local)": "Whisper (Local)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Rédigez une suggestion de prompt (p. ex. Qui êtes-vous ?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Rédigez un résumé en 50 mots qui résume [sujet ou mot-clé].",
 	"Yesterday": "",
-	"You": "You",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "Vous êtes un assistant utile",

+ 23 - 15
src/lib/i18n/locales/fr-FR/translation.json

@@ -10,14 +10,16 @@
 	"About": "À propos",
 	"Account": "Compte",
 	"Accurate information": "",
+	"Add": "",
 	"Add a model": "Ajouter un modèle",
 	"Add a model tag name": "Ajouter un nom de tag pour le modèle",
 	"Add a short description about what this modelfile does": "Ajouter une courte description de ce que fait ce fichier de modèle",
 	"Add a short title for this prompt": "Ajouter un court titre pour ce prompt",
 	"Add a tag": "Ajouter un tag",
-	"Add custom prompt": "",
+	"Add custom prompt": "Ajouter un prompt personnalisé",
 	"Add Docs": "Ajouter des documents",
 	"Add Files": "Ajouter des fichiers",
+	"Add Memory": "",
 	"Add message": "Ajouter un message",
 	"Add Model": "",
 	"Add Tags": "ajouter des tags",
@@ -47,8 +49,8 @@
 	"Archived Chats": "enregistrement du chat",
 	"are allowed - Activate this command by typing": "sont autorisés - Activez cette commande en tapant",
 	"Are you sure?": "Êtes-vous sûr ?",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "Joindre un fichier",
+	"Attention to detail": "Attention aux détails",
 	"Audio": "Audio",
 	"August": "",
 	"Auto-playback response": "Réponse en lecture automatique",
@@ -66,6 +68,8 @@
 	"Categories": "Catégories",
 	"Change Password": "Changer le mot de passe",
 	"Chat": "Chat",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "Historique du chat",
 	"Chat History is off for this browser.": "L'historique du chat est désactivé pour ce navigateur.",
 	"Chats": "Chats",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "Chevauchement de bloc",
 	"Chunk Params": "Paramètres de bloc",
 	"Chunk Size": "Taille de bloc",
-	"Citation": "",
+	"Citation": "Citations",
 	"Click here for help.": "Cliquez ici pour de l'aide.",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "Cliquez ici pour vérifier d'autres fichiers de modèle.",
@@ -117,7 +121,6 @@
 	"Dark": "Sombre",
 	"Dashboard": "",
 	"Database": "Base de données",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "Par défaut",
 	"Default (Automatic1111)": "Par défaut (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "Activé",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
 	"Enter {{role}} message here": "Entrez le message {{role}} ici",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "Entrez le chevauchement de bloc",
 	"Enter Chunk Size": "Entrez la taille du bloc",
 	"Enter Image Size (e.g. 512x512)": "Entrez la taille de l'image (p. ex. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "Paramètres généraux",
 	"Generation Info": "",
 	"Good Response": "",
+	"h:mm a": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Bonjour, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "Importer les fichiers de modèle",
 	"Import Prompts": "Importer les prompts",
 	"Include `--api` flag when running stable-diffusion-webui": "Inclure le drapeau `--api` lors de l'exécution de stable-diffusion-webui",
-	"Input commands": "",
+	"Input commands": "Entrez les commandes d'entrée",
 	"Interface": "Interface",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "Clair",
 	"Listening...": "Écoute...",
 	"LLMs can make mistakes. Verify important information.": "Les LLMs peuvent faire des erreurs. Vérifiez les informations importantes.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
 	"Make sure to enclose them with": "Assurez-vous de les entourer avec",
 	"Manage LiteLLM Models": "Gérer les modèles LiteLLM",
@@ -253,7 +259,9 @@
 	"Max Tokens": "Tokens maximaux",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Un maximum de 3 modèles peut être téléchargé simultanément. Veuillez réessayer plus tard.",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -276,16 +284,13 @@
 	"Modelfiles": "Fichiers de modèle",
 	"Models": "Modèles",
 	"More": "",
-	"My Documents": "Mes documents",
-	"My Modelfiles": "Mes fichiers de modèle",
-	"My Prompts": "Mes prompts",
 	"Name": "Nom",
 	"Name Tag": "Tag de nom",
 	"Name your modelfile": "Nommez votre fichier de modèle",
 	"New Chat": "Nouveau chat",
 	"New Password": "Nouveau mot de passe",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "Aucune source disponible",
 	"Not factually correct": "",
 	"Not sure what to add?": "Vous ne savez pas quoi ajouter ?",
 	"Not sure what to write? Switch to": "Vous ne savez pas quoi écrire ? Basculer vers",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "Extraction d'images PDF (OCR)",
 	"pending": "en attente",
 	"Permission denied when accessing microphone: {{error}}": "Permission refusée lors de l'accès au microphone : {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "",
 	"Playground": "Aire de jeu",
 	"Positive attitude": "",
@@ -360,9 +366,9 @@
 	"Role": "Rôle",
 	"Rosé Pine": "Pin Rosé",
 	"Rosé Pine Dawn": "Aube Pin Rosé",
+	"RTL": "",
 	"Save": "Enregistrer",
 	"Save & Create": "Enregistrer & Créer",
-	"Save & Submit": "Enregistrer & Soumettre",
 	"Save & Update": "Enregistrer & Mettre à jour",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "La sauvegarde des chat directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un moment pour télécharger et supprimer vos journaux de chat en cliquant sur le bouton ci-dessous. Ne vous inquiétez pas, vous pouvez facilement importer vos sauvegardes de chat via",
 	"Scan": "Scanner",
@@ -378,7 +384,8 @@
 	"Select a mode": "Sélectionnez un mode",
 	"Select a model": "Sélectionner un modèle",
 	"Select an Ollama instance": "Sélectionner une instance Ollama",
-	"Select model": "",
+	"Select model": "Sélectionner un modèle",
+	"Send": "",
 	"Send a Message": "Envoyer un message",
 	"Send message": "Envoyer un message",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "Se déconnecter",
 	"Sign up": "S'inscrire",
 	"Signing in": "",
-	"Source": "",
+	"Source": "Source",
 	"Speech recognition error: {{error}}": "Erreur de reconnaissance vocale : {{error}}",
 	"Speech-to-Text Engine": "Moteur de reconnaissance vocale",
 	"SpeechRecognition API is not supported in this browser.": "L'API SpeechRecognition n'est pas prise en charge dans ce navigateur.",
@@ -482,10 +489,11 @@
 	"What’s New in": "Quoi de neuf dans",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Lorsque l'historique est désactivé, les nouveaux chats sur ce navigateur n'apparaîtront pas dans votre historique sur aucun de vos appareils.",
 	"Whisper (Local)": "Whisper (Local)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Écrivez un prompt (e.x. Qui est-tu ?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Ecrivez un résumé en 50 mots [sujet ou mot-clé]",
 	"Yesterday": "",
-	"You": "You",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "Vous êtes un assistant utile",

+ 503 - 0
src/lib/i18n/locales/he-IL/translation.json

@@ -0,0 +1,503 @@
+{
+	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' או '-1' ללא תפוגה.",
+	"(Beta)": "(בטא)",
+	"(e.g. `sh webui.sh --api`)": "(למשל `sh webui.sh --api`)",
+	"(latest)": "(האחרון)",
+	"{{modelName}} is thinking...": "{{modelName}} חושב...",
+	"{{user}}'s Chats": "צ'אטים של {{user}}",
+	"{{webUIName}} Backend Required": "נדרש Backend של {{webUIName}}",
+	"a user": "משתמש",
+	"About": "אודות",
+	"Account": "חשבון",
+	"Accurate information": "מידע מדויק",
+	"Add": "",
+	"Add a model": "הוסף מודל",
+	"Add a model tag name": "הוסף שם תג למודל",
+	"Add a short description about what this modelfile does": "הוסף תיאור קצר על מה שהקובץ מודל עושה",
+	"Add a short title for this prompt": "הוסף כותרת קצרה לפקודה זו",
+	"Add a tag": "הוסף תג",
+	"Add custom prompt": "הוסף פקודה מותאמת אישית",
+	"Add Docs": "הוסף מסמכים",
+	"Add Files": "הוסף קבצים",
+	"Add Memory": "",
+	"Add message": "הוסף הודעה",
+	"Add Model": "הוסף מודל",
+	"Add Tags": "הוסף תגים",
+	"Add User": "הוסף משתמש",
+	"Adjusting these settings will apply changes universally to all users.": "התאמת הגדרות אלו תחול על כל המשתמשים.",
+	"admin": "מנהל",
+	"Admin Panel": "לוח בקרה למנהל",
+	"Admin Settings": "הגדרות מנהל",
+	"Advanced Parameters": "פרמטרים מתקדמים",
+	"all": "הכל",
+	"All Documents": "כל המסמכים",
+	"All Users": "כל המשתמשים",
+	"Allow": "אפשר",
+	"Allow Chat Deletion": "אפשר מחיקת צ'אט",
+	"alphanumeric characters and hyphens": "תווים אלפאנומריים ומקפים",
+	"Already have an account?": "כבר יש לך חשבון?",
+	"an assistant": "עוזר",
+	"and": "וגם",
+	"and create a new shared link.": "וצור קישור משותף חדש.",
+	"API Base URL": "כתובת URL בסיסית ל-API",
+	"API Key": "מפתח API",
+	"API Key created.": "מפתח API נוצר.",
+	"API keys": "מפתחות API",
+	"API RPM": "RPM של API",
+	"April": "אפריל",
+	"Archive": "ארכיון",
+	"Archived Chats": "צ'אטים מאורכבים",
+	"are allowed - Activate this command by typing": "מותרים - הפעל פקודה זו על ידי הקלדה",
+	"Are you sure?": "האם אתה בטוח?",
+	"Attach file": "צרף קובץ",
+	"Attention to detail": "תשומת לב לפרטים",
+	"Audio": "אודיו",
+	"August": "אוגוסט",
+	"Auto-playback response": "תגובת השמעה אוטומטית",
+	"Auto-send input after 3 sec.": "שליחת קלט אוטומטית אחרי 3 שניות",
+	"AUTOMATIC1111 Base URL": "כתובת URL בסיסית של AUTOMATIC1111",
+	"AUTOMATIC1111 Base URL is required.": "נדרשת כתובת URL בסיסית של AUTOMATIC1111",
+	"available!": "זמין!",
+	"Back": "חזור",
+	"Bad Response": "תגובה שגויה",
+	"before": "לפני",
+	"Being lazy": "להיות עצלן",
+	"Builder Mode": "מצב בונה",
+	"Bypass SSL verification for Websites": "עקוף אימות SSL עבור אתרים",
+	"Cancel": "בטל",
+	"Categories": "קטגוריות",
+	"Change Password": "שנה סיסמה",
+	"Chat": "צ'אט",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
+	"Chat History": "היסטוריית צ'אט",
+	"Chat History is off for this browser.": "היסטוריית הצ'אט כבויה לדפדפן זה.",
+	"Chats": "צ'אטים",
+	"Check Again": "בדוק שוב",
+	"Check for updates": "בדוק עדכונים",
+	"Checking for updates...": "בודק עדכונים...",
+	"Choose a model before saving...": "בחר מודל לפני השמירה...",
+	"Chunk Overlap": "חפיפת נתונים",
+	"Chunk Params": "פרמטרי נתונים",
+	"Chunk Size": "גודל נתונים",
+	"Citation": "ציטוט",
+	"Click here for help.": "לחץ כאן לעזרה.",
+	"Click here to": "לחץ כאן כדי",
+	"Click here to check other modelfiles.": "לחץ כאן לבדיקת קבצי מודלים אחרים.",
+	"Click here to select": "לחץ כאן לבחירה",
+	"Click here to select a csv file.": "לחץ כאן לבחירת קובץ csv.",
+	"Click here to select documents.": "לחץ כאן לבחירת מסמכים.",
+	"click here.": "לחץ כאן.",
+	"Click on the user role button to change a user's role.": "לחץ על כפתור תפקיד המשתמש כדי לשנות את תפקיד המשתמש.",
+	"Close": "סגור",
+	"Collection": "אוסף",
+	"ComfyUI": "ComfyUI",
+	"ComfyUI Base URL": "כתובת URL בסיסית של ComfyUI",
+	"ComfyUI Base URL is required.": "נדרשת כתובת URL בסיסית של ComfyUI",
+	"Command": "פקודה",
+	"Confirm Password": "אשר סיסמה",
+	"Connections": "חיבורים",
+	"Content": "תוכן",
+	"Context Length": "אורך הקשר",
+	"Continue Response": "המשך תגובה",
+	"Conversation Mode": "מצב שיחה",
+	"Copied shared chat URL to clipboard!": "העתקת כתובת URL של צ'אט משותף ללוח!",
+	"Copy": "העתק",
+	"Copy last code block": "העתק את בלוק הקוד האחרון",
+	"Copy last response": "העתק את התגובה האחרונה",
+	"Copy Link": "העתק קישור",
+	"Copying to clipboard was successful!": "ההעתקה ללוח הייתה מוצלחת!",
+	"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "צור ביטוי תמציתי של 3-5 מילים ככותרת לשאילתה הבאה, תוך שמירה מדויקת על מגבלת 3-5 המילים והימנעות משימוש במילה 'כותרת':",
+	"Create a modelfile": "צור קובץ מודל",
+	"Create Account": "צור חשבון",
+	"Create new key": "צור מפתח חדש",
+	"Create new secret key": "צור מפתח סודי חדש",
+	"Created at": "נוצר ב",
+	"Created At": "נוצר ב",
+	"Current Model": "המודל הנוכחי",
+	"Current Password": "הסיסמה הנוכחית",
+	"Custom": "מותאם אישית",
+	"Customize Ollama models for a specific purpose": "התאמה אישית של מודלים של Ollama למטרה מסוימת",
+	"Dark": "כהה",
+	"Dashboard": "לוח בקרה",
+	"Database": "מסד נתונים",
+	"December": "דצמבר",
+	"Default": "ברירת מחדל",
+	"Default (Automatic1111)": "ברירת מחדל (Automatic1111)",
+	"Default (SentenceTransformers)": "ברירת מחדל (SentenceTransformers)",
+	"Default (Web API)": "ברירת מחדל (Web API)",
+	"Default model updated": "המודל המוגדר כברירת מחדל עודכן",
+	"Default Prompt Suggestions": "הצעות ברירת מחדל לפקודות",
+	"Default User Role": "תפקיד משתמש ברירת מחדל",
+	"delete": "מחק",
+	"Delete": "מחק",
+	"Delete a model": "מחק מודל",
+	"Delete chat": "מחק צ'אט",
+	"Delete Chat": "מחק צ'אט",
+	"Delete Chats": "מחק צ'אטים",
+	"delete this link": "מחק את הקישור הזה",
+	"Delete User": "מחק משתמש",
+	"Deleted {{deleteModelTag}}": "נמחק {{deleteModelTag}}",
+	"Deleted {{tagName}}": "נמחק {{tagName}}",
+	"Description": "תיאור",
+	"Didn't fully follow instructions": "לא עקב אחרי ההוראות באופן מלא",
+	"Disabled": "מושבת",
+	"Discover a modelfile": "גלה קובץ מודל",
+	"Discover a prompt": "גלה פקודה",
+	"Discover, download, and explore custom prompts": "גלה, הורד, וחקור פקודות מותאמות אישית",
+	"Discover, download, and explore model presets": "גלה, הורד, וחקור הגדרות מודל מוגדרות מראש",
+	"Display the username instead of You in the Chat": "הצג את שם המשתמש במקום 'אתה' בצ'אט",
+	"Document": "מסמך",
+	"Document Settings": "הגדרות מסמך",
+	"Documents": "מסמכים",
+	"does not make any external connections, and your data stays securely on your locally hosted server.": "לא מבצע חיבורים חיצוניים, והנתונים שלך נשמרים באופן מאובטח בשרת המקומי שלך.",
+	"Don't Allow": "אל תאפשר",
+	"Don't have an account?": "אין לך חשבון?",
+	"Don't like the style": "לא אוהב את הסגנון",
+	"Download": "הורד",
+	"Download canceled": "ההורדה בוטלה",
+	"Download Database": "הורד מסד נתונים",
+	"Drop any files here to add to the conversation": "גרור כל קובץ לכאן כדי להוסיף לשיחה",
+	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "למשל '30s', '10m'. יחידות זמן חוקיות הן 's', 'm', 'h'.",
+	"Edit": "ערוך",
+	"Edit Doc": "ערוך מסמך",
+	"Edit User": "ערוך משתמש",
+	"Email": "דוא\"ל",
+	"Embedding Model": "מודל הטמעה",
+	"Embedding Model Engine": "מנוע מודל הטמעה",
+	"Embedding model set to \"{{embedding_model}}\"": "מודל ההטמעה הוגדר ל-\"{{embedding_model}}\"",
+	"Enable Chat History": "הפעל היסטוריית צ'אט",
+	"Enable New Sign Ups": "אפשר הרשמות חדשות",
+	"Enabled": "מופעל",
+	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "ודא שקובץ ה-CSV שלך כולל 4 עמודות בסדר הבא: שם, דוא\"ל, סיסמה, תפקיד.",
+	"Enter {{role}} message here": "הזן הודעת {{role}} כאן",
+	"Enter a detail about yourself for your LLMs to recall": "",
+	"Enter Chunk Overlap": "הזן חפיפת נתונים",
+	"Enter Chunk Size": "הזן גודל נתונים",
+	"Enter Image Size (e.g. 512x512)": "הזן גודל תמונה (למשל 512x512)",
+	"Enter language codes": "הזן קודי שפה",
+	"Enter LiteLLM API Base URL (litellm_params.api_base)": "הזן כתובת URL בסיסית ל-API של LiteLLM (litellm_params.api_base)",
+	"Enter LiteLLM API Key (litellm_params.api_key)": "הזן מפתח API של LiteLLM (litellm_params.api_key)",
+	"Enter LiteLLM API RPM (litellm_params.rpm)": "הזן RPM של API של LiteLLM (litellm_params.rpm)",
+	"Enter LiteLLM Model (litellm_params.model)": "הזן מודל LiteLLM (litellm_params.model)",
+	"Enter Max Tokens (litellm_params.max_tokens)": "הזן מספר מקסימלי של טוקנים (litellm_params.max_tokens)",
+	"Enter model tag (e.g. {{modelTag}})": "הזן תג מודל (למשל {{modelTag}})",
+	"Enter Number of Steps (e.g. 50)": "הזן מספר שלבים (למשל 50)",
+	"Enter Score": "הזן ציון",
+	"Enter stop sequence": "הזן רצף עצירה",
+	"Enter Top K": "הזן Top K",
+	"Enter URL (e.g. http://127.0.0.1:7860/)": "הזן כתובת URL (למשל http://127.0.0.1:7860/)",
+	"Enter URL (e.g. http://localhost:11434)": "הזן כתובת URL (למשל http://localhost:11434)",
+	"Enter Your Email": "הזן את דוא\"ל שלך",
+	"Enter Your Full Name": "הזן את שמך המלא",
+	"Enter Your Password": "הזן את הסיסמה שלך",
+	"Enter Your Role": "הזן את התפקיד שלך",
+	"Experimental": "ניסיוני",
+	"Export All Chats (All Users)": "ייצוא כל הצ'אטים (כל המשתמשים)",
+	"Export Chats": "ייצוא צ'אטים",
+	"Export Documents Mapping": "ייצוא מיפוי מסמכים",
+	"Export Modelfiles": "ייצוא קבצי מודלים",
+	"Export Prompts": "ייצוא פקודות",
+	"Failed to create API Key.": "יצירת מפתח API נכשלה.",
+	"Failed to read clipboard contents": "קריאת תוכן הלוח נכשלה",
+	"February": "פברואר",
+	"Feel free to add specific details": "נא להוסיף פרטים ספציפיים לפי רצון",
+	"File Mode": "מצב קובץ",
+	"File not found.": "הקובץ לא נמצא.",
+	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "התגלתה הזיית טביעת אצבע: לא ניתן להשתמש בראשי תיבות כאווטאר. משתמש בתמונת פרופיל ברירת מחדל.",
+	"Fluidly stream large external response chunks": "שידור נתונים חיצוניים בקצב רציף",
+	"Focus chat input": "מיקוד הקלט לצ'אט",
+	"Followed instructions perfectly": "עקב אחר ההוראות במושלמות",
+	"Format your variables using square brackets like this:": "עצב את המשתנים שלך באמצעות סוגריים מרובעים כך:",
+	"From (Base Model)": "מ (מודל בסיס)",
+	"Full Screen Mode": "מצב מסך מלא",
+	"General": "כללי",
+	"General Settings": "הגדרות כלליות",
+	"Generation Info": "מידע על היצירה",
+	"Good Response": "תגובה טובה",
+	"h:mm a": "",
+	"has no conversations.": "אין שיחות.",
+	"Hello, {{name}}": "שלום, {{name}}",
+	"Help": "עזרה",
+	"Hide": "הסתר",
+	"Hide Additional Params": "הסתר פרמטרים נוספים",
+	"How can I help you today?": "כיצד אוכל לעזור לך היום?",
+	"Hybrid Search": "חיפוש היברידי",
+	"Image Generation (Experimental)": "יצירת תמונות (ניסיוני)",
+	"Image Generation Engine": "מנוע יצירת תמונות",
+	"Image Settings": "הגדרות תמונה",
+	"Images": "תמונות",
+	"Import Chats": "יבוא צ'אטים",
+	"Import Documents Mapping": "יבוא מיפוי מסמכים",
+	"Import Modelfiles": "יבוא קבצי מודלים",
+	"Import Prompts": "יבוא פקודות",
+	"Include `--api` flag when running stable-diffusion-webui": "כלול את הדגל `--api` בעת הרצת stable-diffusion-webui",
+	"Input commands": "פקודות קלט",
+	"Interface": "ממשק",
+	"Invalid Tag": "תג לא חוקי",
+	"January": "ינואר",
+	"join our Discord for help.": "הצטרף ל-Discord שלנו לעזרה.",
+	"JSON": "JSON",
+	"July": "יולי",
+	"June": "יוני",
+	"JWT Expiration": "תפוגת JWT",
+	"JWT Token": "אסימון JWT",
+	"Keep Alive": "השאר פעיל",
+	"Keyboard shortcuts": "קיצורי מקלדת",
+	"Language": "שפה",
+	"Last Active": "פעיל לאחרונה",
+	"Light": "בהיר",
+	"Listening...": "מאזין...",
+	"LLMs can make mistakes. Verify important information.": "מודלים בשפה טבעית יכולים לטעות. אמת מידע חשוב.",
+	"LTR": "",
+	"Made by OpenWebUI Community": "נוצר על ידי קהילת OpenWebUI",
+	"Make sure to enclose them with": "ודא להקיף אותם עם",
+	"Manage LiteLLM Models": "נהל מודלים של LiteLLM",
+	"Manage Models": "נהל מודלים",
+	"Manage Ollama Models": "נהל מודלים של Ollama",
+	"March": "מרץ",
+	"Max Tokens": "מקסימום טוקנים",
+	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "ניתן להוריד מקסימום 3 מודלים בו זמנית. אנא נסה שוב מאוחר יותר.",
+	"May": "מאי",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
+	"Minimum Score": "ציון מינימלי",
+	"Mirostat": "Mirostat",
+	"Mirostat Eta": "Mirostat Eta",
+	"Mirostat Tau": "Mirostat Tau",
+	"MMMM DD, YYYY": "DD בMMMM, YYYY",
+	"MMMM DD, YYYY HH:mm": "DD בMMMM, YYYY HH:mm",
+	"Model '{{modelName}}' has been successfully downloaded.": "המודל '{{modelName}}' הורד בהצלחה.",
+	"Model '{{modelTag}}' is already in queue for downloading.": "המודל '{{modelTag}}' כבר בתור להורדה.",
+	"Model {{modelId}} not found": "המודל {{modelId}} לא נמצא",
+	"Model {{modelName}} already exists.": "המודל {{modelName}} כבר קיים.",
+	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "נתיב מערכת הקבצים של המודל זוהה. נדרש שם קצר של המודל לעדכון, לא ניתן להמשיך.",
+	"Model Name": "שם המודל",
+	"Model not selected": "לא נבחר מודל",
+	"Model Tag Name": "שם תג המודל",
+	"Model Whitelisting": "רישום לבן של מודלים",
+	"Model(s) Whitelisted": "מודלים שנכללו ברשימה הלבנה",
+	"Modelfile": "קובץ מודל",
+	"Modelfile Advanced Settings": "הגדרות מתקדמות לקובץ מודל",
+	"Modelfile Content": "תוכן קובץ מודל",
+	"Modelfiles": "קבצי מודל",
+	"Models": "מודלים",
+	"More": "עוד",
+	"Name": "שם",
+	"Name Tag": "תג שם",
+	"Name your modelfile": "תן שם לקובץ המודל שלך",
+	"New Chat": "צ'אט חדש",
+	"New Password": "סיסמה חדשה",
+	"No results found": "לא נמצאו תוצאות",
+	"No source available": "אין מקור זמין",
+	"Not factually correct": "לא נכון מבחינה עובדתית",
+	"Not sure what to add?": "לא בטוח מה להוסיף?",
+	"Not sure what to write? Switch to": "לא בטוח מה לכתוב? החלף ל",
+	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "הערה: אם תקבע ציון מינימלי, החיפוש יחזיר רק מסמכים עם ציון שגבוה או שווה לציון המינימלי.",
+	"Notifications": "התראות",
+	"November": "נובמבר",
+	"October": "אוקטובר",
+	"Off": "כבוי",
+	"Okay, Let's Go!": "בסדר, בואו נתחיל!",
+	"OLED Dark": "OLED כהה",
+	"Ollama": "Ollama",
+	"Ollama Base URL": "כתובת URL בסיסית של Ollama",
+	"Ollama Version": "גרסת Ollama",
+	"On": "פועל",
+	"Only": "רק",
+	"Only alphanumeric characters and hyphens are allowed in the command string.": "רק תווים אלפאנומריים ומקפים מותרים במחרוזת הפקודה.",
+	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "אופס! תחזיק מעמד! הקבצים שלך עדיין בתהליך העיבוד. אנו מבשלים אותם לשלמות. נא להתאזר בסבלנות ונודיע לך ברגע שיהיו מוכנים.",
+	"Oops! Looks like the URL is invalid. Please double-check and try again.": "אופס! נראה שהכתובת URL אינה תקינה. אנא בדוק שוב ונסה שנית.",
+	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "אופס! אתה משתמש בשיטה לא נתמכת (רק חזית). אנא שרת את ממשק המשתמש האינטרנטי מהשרת האחורי.",
+	"Open": "פתח",
+	"Open AI": "Open AI",
+	"Open AI (Dall-E)": "Open AI (Dall-E)",
+	"Open new chat": "פתח צ'אט חדש",
+	"OpenAI": "OpenAI",
+	"OpenAI API": "API של OpenAI",
+	"OpenAI API Config": "תצורת API של OpenAI",
+	"OpenAI API Key is required.": "נדרש מפתח API של OpenAI.",
+	"OpenAI URL/Key required.": "נדרשת כתובת URL/מפתח של OpenAI.",
+	"or": "או",
+	"Other": "אחר",
+	"Overview": "סקירה כללית",
+	"Parameters": "פרמטרים",
+	"Password": "סיסמה",
+	"PDF document (.pdf)": "מסמך PDF (.pdf)",
+	"PDF Extract Images (OCR)": "חילוץ תמונות מ-PDF (OCR)",
+	"pending": "ממתין",
+	"Permission denied when accessing microphone: {{error}}": "ההרשאה נדחתה בעת גישה למיקרופון: {{error}}",
+	"Personalization": "",
+	"Plain text (.txt)": "טקסט פשוט (.txt)",
+	"Playground": "אזור משחקים",
+	"Positive attitude": "גישה חיובית",
+	"Previous 30 days": "30 הימים הקודמים",
+	"Previous 7 days": "7 הימים הקודמים",
+	"Profile Image": "תמונת פרופיל",
+	"Prompt": "פקודה",
+	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "פקודה (למשל, ספר לי עובדה מעניינת על האימפריה הרומית)",
+	"Prompt Content": "תוכן הפקודה",
+	"Prompt suggestions": "הצעות לפקודות",
+	"Prompts": "פקודות",
+	"Pull \"{{searchValue}}\" from Ollama.com": "משוך \"{{searchValue}}\" מ-Ollama.com",
+	"Pull a model from Ollama.com": "משוך מודל מ-Ollama.com",
+	"Pull Progress": "משוך התקדמות",
+	"Query Params": "פרמטרי שאילתה",
+	"RAG Template": "תבנית RAG",
+	"Raw Format": "פורמט גולמי",
+	"Read Aloud": "קרא בקול",
+	"Record voice": "הקלט קול",
+	"Redirecting you to OpenWebUI Community": "מפנה אותך לקהילת OpenWebUI",
+	"Refused when it shouldn't have": "נדחה כאשר לא היה צריך",
+	"Regenerate": "הפק מחדש",
+	"Release Notes": "הערות שחרור",
+	"Remove": "הסר",
+	"Remove Model": "הסר מודל",
+	"Rename": "שנה שם",
+	"Repeat Last N": "חזור על ה-N האחרונים",
+	"Repeat Penalty": "עונש חזרה",
+	"Request Mode": "מצב בקשה",
+	"Reranking Model": "מודל דירוג מחדש",
+	"Reranking model disabled": "מודל דירוג מחדש מושבת",
+	"Reranking model set to \"{{reranking_model}}\"": "מודל דירוג מחדש הוגדר ל-\"{{reranking_model}}\"",
+	"Reset Vector Storage": "איפוס אחסון וקטורים",
+	"Response AutoCopy to Clipboard": "העתקה אוטומטית של תגובה ללוח",
+	"Role": "תפקיד",
+	"Rosé Pine": "Rosé Pine",
+	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
+	"Save": "שמור",
+	"Save & Create": "שמור וצור",
+	"Save & Update": "שמור ועדכן",
+	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "שמירת יומני צ'אט ישירות באחסון הדפדפן שלך אינה נתמכת יותר. אנא הקדש רגע להוריד ולמחוק את יומני הצ'אט שלך על ידי לחיצה על הכפתור למטה. אל דאגה, באפשרותך לייבא מחדש בקלות את יומני הצ'אט שלך לשרת האחורי דרך",
+	"Scan": "סרוק",
+	"Scan complete!": "הסריקה הושלמה!",
+	"Scan for documents from {{path}}": "סרוק מסמכים מ-{{path}}",
+	"Search": "חפש",
+	"Search a model": "חפש מודל",
+	"Search Documents": "חפש מסמכים",
+	"Search Prompts": "חפש פקודות",
+	"See readme.md for instructions": "ראה את readme.md להוראות",
+	"See what's new": "ראה מה חדש",
+	"Seed": "זרע",
+	"Select a mode": "בחר מצב",
+	"Select a model": "בחר מודל",
+	"Select an Ollama instance": "בחר מופע של Ollama",
+	"Select model": "בחר מודל",
+	"Send": "",
+	"Send a Message": "שלח הודעה",
+	"Send message": "שלח הודעה",
+	"September": "ספטמבר",
+	"Server connection verified": "החיבור לשרת אומת",
+	"Set as default": "הגדר כברירת מחדל",
+	"Set Default Model": "הגדר מודל ברירת מחדל",
+	"Set embedding model (e.g. {{model}})": "הגדר מודל הטמעה (למשל {{model}})",
+	"Set Image Size": "הגדר גודל תמונה",
+	"Set Model": "הגדר מודל",
+	"Set reranking model (e.g. {{model}})": "הגדר מודל דירוג מחדש (למשל {{model}})",
+	"Set Steps": "הגדר שלבים",
+	"Set Title Auto-Generation Model": "הגדר מודל יצירת כותרת אוטומטית",
+	"Set Voice": "הגדר קול",
+	"Settings": "הגדרות",
+	"Settings saved successfully!": "ההגדרות נשמרו בהצלחה!",
+	"Share": "שתף",
+	"Share Chat": "שתף צ'אט",
+	"Share to OpenWebUI Community": "שתף לקהילת OpenWebUI",
+	"short-summary": "סיכום קצר",
+	"Show": "הצג",
+	"Show Additional Params": "הצג פרמטרים נוספים",
+	"Show shortcuts": "הצג קיצורי דרך",
+	"Showcased creativity": "הצגת יצירתיות",
+	"sidebar": "סרגל צד",
+	"Sign in": "",
+	"Sign Out": "",
+	"Sign up": "",
+	"Signing in": "",
+	"Source": "",
+	"Speech recognition error: {{error}}": "",
+	"Speech-to-Text Engine": "",
+	"SpeechRecognition API is not supported in this browser.": "",
+	"Stop Sequence": "",
+	"STT Settings": "",
+	"Submit": "שלח",
+	"Subtitle (e.g. about the Roman Empire)": "",
+	"Success": "הצלחה",
+	"Successfully updated.": "",
+	"Suggested": "",
+	"Sync All": "",
+	"System": "מערכת",
+	"System Prompt": "",
+	"Tags": "",
+	"Tell us more:": "",
+	"Temperature": "",
+	"Template": "תבנית",
+	"Text Completion": "",
+	"Text-to-Speech Engine": "מנוע טקסט לדיבור",
+	"Tfs Z": "",
+	"Thanks for your feedback!": "תודה על המשוב שלך!",
+	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
+	"Theme": "נושא",
+	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "",
+	"This setting does not sync across browsers or devices.": "",
+	"Thorough explanation": "",
+	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "",
+	"Title": "",
+	"Title (e.g. Tell me a fun fact)": "",
+	"Title Auto-Generation": "",
+	"Title cannot be an empty string.": "",
+	"Title Generation Prompt": "",
+	"to": "",
+	"To access the available model names for downloading,": "",
+	"To access the GGUF models available for downloading,": "",
+	"to chat input.": "",
+	"Today": "",
+	"Toggle settings": "",
+	"Toggle sidebar": "",
+	"Top K": "",
+	"Top P": "",
+	"Trouble accessing Ollama?": "",
+	"TTS Settings": "",
+	"Type Hugging Face Resolve (Download) URL": "",
+	"Uh-oh! There was an issue connecting to {{provider}}.": "",
+	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
+	"Update and Copy Link": "",
+	"Update password": "",
+	"Upload a GGUF model": "",
+	"Upload files": "",
+	"Upload Progress": "",
+	"URL Mode": "",
+	"Use '#' in the prompt input to load and select your documents.": "",
+	"Use Gravatar": "",
+	"Use Initials": "",
+	"user": "",
+	"User Permissions": "",
+	"Users": "משתמשים",
+	"Utilize": "",
+	"Valid time units:": "",
+	"variable": "",
+	"variable to have them replaced with clipboard content.": "",
+	"Version": "גרסה",
+	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
+	"Web": "רשת",
+	"Web Loader Settings": "",
+	"Web Params": "",
+	"Webhook URL": "",
+	"WebUI Add-ons": "",
+	"WebUI Settings": "",
+	"WebUI will make requests to": "",
+	"What’s New in": "",
+	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "",
+	"Whisper (Local)": "",
+	"Workspace": "",
+	"Write a prompt suggestion (e.g. Who are you?)": "",
+	"Write a summary in 50 words that summarizes [topic or keyword].": "",
+	"Yesterday": "אתמול",
+	"You": "",
+	"You have no archived conversations.": "",
+	"You have shared this chat": "",
+	"You're a helpful assistant.": "",
+	"You're now logged in.": "",
+	"Youtube": "",
+	"Youtube Loader Settings": ""
+}

+ 22 - 14
src/lib/i18n/locales/hi-IN/translation.json

@@ -10,14 +10,16 @@
 	"About": "हमारे बारे में",
 	"Account": "खाता",
 	"Accurate information": "सटीक जानकारी",
+	"Add": "",
 	"Add a model": "एक मॉडल जोड़ें",
 	"Add a model tag name": "एक मॉडल टैग नाम जोड़ें",
 	"Add a short description about what this modelfile does": "यह मॉडलफ़ाइल क्या करती है इसके बारे में एक संक्षिप्त विवरण जोड़ें",
 	"Add a short title for this prompt": "इस संकेत के लिए एक संक्षिप्त शीर्षक जोड़ें",
 	"Add a tag": "एक टैग जोड़े",
-	"Add custom prompt": "",
+	"Add custom prompt": "अनुकूल संकेत जोड़ें",
 	"Add Docs": "दस्तावेज़ जोड़ें",
 	"Add Files": "फाइलें जोड़ें",
+	"Add Memory": "",
 	"Add message": "संदेश डालें",
 	"Add Model": "मॉडल जोड़ें",
 	"Add Tags": "टैगों को जोड़ें",
@@ -47,7 +49,7 @@
 	"Archived Chats": "संग्रहीत चैट",
 	"are allowed - Activate this command by typing": "अनुमति है - टाइप करके इस कमांड को सक्रिय करें",
 	"Are you sure?": "क्या आपको यकीन है?",
-	"Attach file": "",
+	"Attach file": "फ़ाइल atta",
 	"Attention to detail": "विस्तार पर ध्यान",
 	"Audio": "ऑडियो",
 	"August": "",
@@ -66,6 +68,8 @@
 	"Categories": "श्रेणियाँ",
 	"Change Password": "पासवर्ड बदलें",
 	"Chat": "चैट करें",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
 	"Chat History": "चैट का इतिहास",
 	"Chat History is off for this browser.": "इस ब्राउज़र के लिए चैट इतिहास बंद है।",
 	"Chats": "सभी चैट",
@@ -76,7 +80,7 @@
 	"Chunk Overlap": "चंक ओवरलैप",
 	"Chunk Params": "चंक पैरामीटर्स",
 	"Chunk Size": "चंक आकार",
-	"Citation": "",
+	"Citation": "उद्धरण",
 	"Click here for help.": "सहायता के लिए यहां क्लिक करें।",
 	"Click here to": "",
 	"Click here to check other modelfiles.": "अन्य मॉडल फ़ाइलों की जांच के लिए यहां क्लिक करें।",
@@ -117,7 +121,6 @@
 	"Dark": "",
 	"Dashboard": "",
 	"Database": "डेटाबेस",
-	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
 	"December": "",
 	"Default": "डिफ़ॉल्ट",
 	"Default (Automatic1111)": "डिफ़ॉल्ट (Automatic1111)",
@@ -168,6 +171,7 @@
 	"Enabled": "सक्रिय",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "सुनिश्चित करें कि आपकी CSV फ़ाइल में इस क्रम में 4 कॉलम शामिल हैं: नाम, ईमेल, पासवर्ड, भूमिका।",
 	"Enter {{role}} message here": "यहां {{role}} संदेश दर्ज करें",
+	"Enter a detail about yourself for your LLMs to recall": "",
 	"Enter Chunk Overlap": "चंक ओवरलैप दर्ज करें",
 	"Enter Chunk Size": "खंड आकार दर्ज करें",
 	"Enter Image Size (e.g. 512x512)": "छवि का आकार दर्ज करें (उदा. 512x512)",
@@ -211,6 +215,7 @@
 	"General Settings": "सामान्य सेटिंग्स",
 	"Generation Info": "जनरेशन की जानकारी",
 	"Good Response": "अच्छी प्रतिक्रिया",
+	"h:mm a": "",
 	"has no conversations.": "कोई बातचीत नहीं है",
 	"Hello, {{name}}": "नमस्ते, {{name}}",
 	"Help": "",
@@ -227,7 +232,7 @@
 	"Import Modelfiles": "मॉडल फ़ाइलें आयात करें",
 	"Import Prompts": "प्रॉम्प्ट आयात करें",
 	"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui चलाते समय `--api` ध्वज शामिल करें",
-	"Input commands": "",
+	"Input commands": "इनपुट क命",
 	"Interface": "इंटरफेस",
 	"Invalid Tag": "",
 	"January": "",
@@ -244,6 +249,7 @@
 	"Light": "",
 	"Listening...": "सुन रहा हूँ...",
 	"LLMs can make mistakes. Verify important information.": "एलएलएम गलतियाँ कर सकते हैं। महत्वपूर्ण जानकारी सत्यापित करें.",
+	"LTR": "",
 	"Made by OpenWebUI Community": "OpenWebUI समुदाय द्वारा निर्मित",
 	"Make sure to enclose them with": "उन्हें संलग्न करना सुनिश्चित करें",
 	"Manage LiteLLM Models": "LiteLLM मॉडल प्रबंधित करें",
@@ -253,7 +259,9 @@
 	"Max Tokens": "अधिकतम टोकन",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "अधिकतम 3 मॉडल एक साथ डाउनलोड किये जा सकते हैं। कृपया बाद में पुन: प्रयास करें।",
 	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
 	"Minimum Score": "न्यूनतम स्कोर",
 	"Mirostat": "",
 	"Mirostat Eta": "",
@@ -276,16 +284,13 @@
 	"Modelfiles": "मॉडल फ़ाइलें",
 	"Models": "सभी मॉडल",
 	"More": "और..",
-	"My Documents": "मेरे दस्तावेज़",
-	"My Modelfiles": "मेरी मॉडल फ़ाइलें",
-	"My Prompts": "मेरे प्रॉम्प्ट",
 	"Name": "नाम",
 	"Name Tag": "नाम टैग",
 	"Name your modelfile": "अपनी मॉडलफ़ाइल को नाम दें",
 	"New Chat": "नई चैट",
 	"New Password": "नया पासवर्ड",
 	"No results found": "",
-	"No source available": "",
+	"No source available": "कोई स्रोत उपलब्ध नहीं है",
 	"Not factually correct": "तथ्यात्मक रूप से सही नहीं है",
 	"Not sure what to add?": "निश्चित नहीं कि क्या जोड़ें?",
 	"Not sure what to write? Switch to": "मैं आश्वस्त नहीं हूं कि क्या लिखना है? स्विच करें",
@@ -323,6 +328,7 @@
 	"PDF Extract Images (OCR)": "PDF छवियाँ निकालें (OCR)",
 	"pending": "लंबित",
 	"Permission denied when accessing microphone: {{error}}": "माइक्रोफ़ोन तक पहुँचने पर अनुमति अस्वीकृत: {{error}}",
+	"Personalization": "",
 	"Plain text (.txt)": "सादा पाठ (.txt)",
 	"Playground": "कार्यक्षेत्र",
 	"Positive attitude": "सकारात्मक रवैया",
@@ -360,9 +366,9 @@
 	"Role": "भूमिका",
 	"Rosé Pine": "",
 	"Rosé Pine Dawn": "",
+	"RTL": "",
 	"Save": "सहेजें",
 	"Save & Create": "सहेजें और बनाएं",
-	"Save & Submit": "सहेजें और सबमिट करें",
 	"Save & Update": "सहेजें और अपडेट करें",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "चैट लॉग को सीधे आपके ब्राउज़र के स्टोरेज में सहेजना अब समर्थित नहीं है। कृपया नीचे दिए गए बटन पर क्लिक करके डाउनलोड करने और अपने चैट लॉग को हटाने के लिए कुछ समय दें। चिंता न करें, आप आसानी से अपने चैट लॉग को बैकएंड पर पुनः आयात कर सकते हैं",
 	"Scan": "स्कैन",
@@ -378,7 +384,8 @@
 	"Select a mode": "एक मोड चुनें",
 	"Select a model": "एक मॉडल चुनें",
 	"Select an Ollama instance": "एक Ollama Instance चुनें",
-	"Select model": "",
+	"Select model": "मॉडल चुनें",
+	"Send": "",
 	"Send a Message": "एक संदेश भेजो",
 	"Send message": "मेसेज भेजें",
 	"September": "",
@@ -407,7 +414,7 @@
 	"Sign Out": "साइन आउट",
 	"Sign up": "साइन अप",
 	"Signing in": "साइन इन हो रहा है",
-	"Source": "",
+	"Source": "स्रोत",
 	"Speech recognition error: {{error}}": "वाक् पहचान त्रुटि: {{error}}",
 	"Speech-to-Text Engine": "वाक्-से-पाठ इंजन",
 	"SpeechRecognition API is not supported in this browser.": "इस ब्राउज़र में SpeechRecognition API समर्थित नहीं है",
@@ -482,10 +489,11 @@
 	"What’s New in": "इसमें नया क्या है",
 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "जब इतिहास बंद हो जाता है, तो इस ब्राउज़र पर नई चैट आपके किसी भी डिवाइस पर इतिहास में दिखाई नहीं देंगी।",
 	"Whisper (Local)": "Whisper (स्थानीय)",
+	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "एक त्वरित सुझाव लिखें (जैसे कि आप कौन हैं?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "50 शब्दों में एक सारांश लिखें जो [विषय या कीवर्ड] का सारांश प्रस्तुत करता हो।",
 	"Yesterday": "",
-	"You": "आप",
+	"You": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",
 	"You're a helpful assistant.": "आप एक सहायक सहायक हैं",

+ 503 - 0
src/lib/i18n/locales/hr-HR/translation.json

@@ -0,0 +1,503 @@
+{
+	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ili '-1' za bez isteka.",
+	"(Beta)": "(Beta)",
+	"(e.g. `sh webui.sh --api`)": "(npr. `sh webui.sh --api`)",
+	"(latest)": "(najnovije)",
+	"{{modelName}} is thinking...": "{{modelName}} razmišlja...",
+	"{{user}}'s Chats": "Razgovori korisnika {{user}}",
+	"{{webUIName}} Backend Required": "{{webUIName}} Backend je potreban",
+	"a user": "korisnik",
+	"About": "O",
+	"Account": "Račun",
+	"Accurate information": "Točne informacije",
+	"Add": "",
+	"Add a model": "Dodaj model",
+	"Add a model tag name": "Dodaj oznaku modela",
+	"Add a short description about what this modelfile does": "Dodajte kratak opis što ova datoteka modela radi",
+	"Add a short title for this prompt": "Dodajte kratki naslov za ovaj prompt",
+	"Add a tag": "Dodaj oznaku",
+	"Add custom prompt": "Dodaj prilagođeni prompt",
+	"Add Docs": "Dodaj dokumente",
+	"Add Files": "Dodaj datoteke",
+	"Add Memory": "",
+	"Add message": "Dodaj poruku",
+	"Add Model": "Dodaj model",
+	"Add Tags": "Dodaj oznake",
+	"Add User": "Dodaj korisnika",
+	"Adjusting these settings will apply changes universally to all users.": "Podešavanje ovih postavki primijenit će promjene univerzalno na sve korisnike.",
+	"admin": "administrator",
+	"Admin Panel": "Administratorska ploča",
+	"Admin Settings": "Administratorske postavke",
+	"Advanced Parameters": "Napredni parametri",
+	"all": "sve",
+	"All Documents": "Svi dokumenti",
+	"All Users": "Svi korisnici",
+	"Allow": "Dopusti",
+	"Allow Chat Deletion": "Dopusti brisanje razgovora",
+	"alphanumeric characters and hyphens": "alfanumerički znakovi i crtice",
+	"Already have an account?": "Već imate račun?",
+	"an assistant": "asistent",
+	"and": "i",
+	"and create a new shared link.": "i stvorite novu dijeljenu vezu.",
+	"API Base URL": "Osnovni URL API-ja",
+	"API Key": "API ključ",
+	"API Key created.": "API ključ je stvoren.",
+	"API keys": "API ključevi",
+	"API RPM": "API RPM",
+	"April": "Travanj",
+	"Archive": "Arhiva",
+	"Archived Chats": "Arhivirani razgovori",
+	"are allowed - Activate this command by typing": "su dopušteni - Aktivirajte ovu naredbu upisivanjem",
+	"Are you sure?": "Jeste li sigurni?",
+	"Attach file": "Priloži datoteku",
+	"Attention to detail": "Pažnja na detalje",
+	"Audio": "Audio",
+	"August": "Kolovoz",
+	"Auto-playback response": "Automatska reprodukcija odgovora",
+	"Auto-send input after 3 sec.": "Automatsko slanje unosa nakon 3 sek.",
+	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 osnovni URL",
+	"AUTOMATIC1111 Base URL is required.": "Potreban je AUTOMATIC1111 osnovni URL.",
+	"available!": "dostupno!",
+	"Back": "Natrag",
+	"Bad Response": "Loš odgovor",
+	"before": "prije",
+	"Being lazy": "Biti lijen",
+	"Builder Mode": "Način graditelja",
+	"Bypass SSL verification for Websites": "Zaobiđi SSL provjeru za web stranice",
+	"Cancel": "Otkaži",
+	"Categories": "Kategorije",
+	"Change Password": "Promijeni lozinku",
+	"Chat": "Razgovor",
+	"Chat Bubble UI": "",
+	"Chat direction": "",
+	"Chat History": "Povijest razgovora",
+	"Chat History is off for this browser.": "Povijest razgovora je isključena za ovaj preglednik.",
+	"Chats": "Razgovori",
+	"Check Again": "Provjeri ponovo",
+	"Check for updates": "Provjeri za ažuriranja",
+	"Checking for updates...": "Provjeravam ažuriranja...",
+	"Choose a model before saving...": "Odaberite model prije spremanja...",
+	"Chunk Overlap": "Preklapanje dijelova",
+	"Chunk Params": "Parametri dijelova",
+	"Chunk Size": "Veličina dijela",
+	"Citation": "Citiranje",
+	"Click here for help.": "Kliknite ovdje za pomoć.",
+	"Click here to": "Kliknite ovdje za",
+	"Click here to check other modelfiles.": "Kliknite ovdje da provjerite druge datoteke modela.",
+	"Click here to select": "Kliknite ovdje za odabir",
+	"Click here to select a csv file.": "Kliknite ovdje da odaberete csv datoteku.",
+	"Click here to select documents.": "Kliknite ovdje da odaberete dokumente.",
+	"click here.": "kliknite ovdje.",
+	"Click on the user role button to change a user's role.": "Kliknite na gumb uloge korisnika za promjenu uloge korisnika.",
+	"Close": "Zatvori",
+	"Collection": "Kolekcija",
+	"ComfyUI": "ComfyUI",
+	"ComfyUI Base URL": "ComfyUI osnovni URL",
+	"ComfyUI Base URL is required.": "Potreban je ComfyUI osnovni URL.",
+	"Command": "Naredba",
+	"Confirm Password": "Potvrdite lozinku",
+	"Connections": "Povezivanja",
+	"Content": "Sadržaj",
+	"Context Length": "Dužina konteksta",
+	"Continue Response": "Nastavi odgovor",
+	"Conversation Mode": "Način razgovora",
+	"Copied shared chat URL to clipboard!": "Kopirana URL dijeljenog razgovora u međuspremnik!",
+	"Copy": "Kopiraj",
+	"Copy last code block": "Kopiraj zadnji blok koda",
+	"Copy last response": "Kopiraj zadnji odgovor",
+	"Copy Link": "Kopiraj vezu",
+	"Copying to clipboard was successful!": "Kopiranje u međuspremnik je bilo uspješno!",
+	"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':": "Stvorite sažetu frazu od 3-5 riječi kao naslov za sljedeći upit, strogo se pridržavajući ograničenja od 3-5 riječi i izbjegavajući upotrebu riječi 'naslov':",
+	"Create a modelfile": "Stvorite datoteku modela",
+	"Create Account": "Stvori račun",
+	"Create new key": "Stvori novi ključ",
+	"Create new secret key": "Stvori novi tajni ključ",
+	"Created at": "Stvoreno",
+	"Created At": "Stvoreno",
+	"Current Model": "Trenutni model",
+	"Current Password": "Trenutna lozinka",
+	"Custom": "Prilagođeno",
+	"Customize Ollama models for a specific purpose": "Prilagodite Ollama modele za specifičnu svrhu",
+	"Dark": "Tamno",
+	"Dashboard": "Nadzorna ploča",
+	"Database": "Baza podataka",
+	"December": "Prosinac",
+	"Default": "Zadano",
+	"Default (Automatic1111)": "Zadano (Automatic1111)",
+	"Default (SentenceTransformers)": "Zadano (SentenceTransformers)",
+	"Default (Web API)": "Zadano (Web API)",
+	"Default model updated": "Zadani model ažuriran",
+	"Default Prompt Suggestions": "Zadani prijedlozi prompta",
+	"Default User Role": "Zadana korisnička uloga",
+	"delete": "izbriši",
+	"Delete": "Izbriši",
+	"Delete a model": "Izbriši model",
+	"Delete chat": "Izbriši razgovor",
+	"Delete Chat": "Izbriši razgovor",
+	"Delete Chats": "Izbriši razgovore",
+	"delete this link": "izbriši ovu vezu",
+	"Delete User": "Izbriši korisnika",
+	"Deleted {{deleteModelTag}}": "Izbrisan {{deleteModelTag}}",
+	"Deleted {{tagName}}": "Izbrisan {{tagName}}",
+	"Description": "Opis",
+	"Didn't fully follow instructions": "Nije u potpunosti slijedio upute",
+	"Disabled": "Onemogućeno",
+	"Discover a modelfile": "Otkrijte datoteku modela",
+	"Discover a prompt": "Otkrijte prompt",
+	"Discover, download, and explore custom prompts": "Otkrijte, preuzmite i istražite prilagođene prompte",
+	"Discover, download, and explore model presets": "Otkrijte, preuzmite i istražite unaprijed postavljene modele",
+	"Display the username instead of You in the Chat": "Prikaži korisničko ime umjesto Vas u razgovoru",
+	"Document": "Dokument",
+	"Document Settings": "Postavke dokumenta",
+	"Documents": "Dokumenti",
+	"does not make any external connections, and your data stays securely on your locally hosted server.": "ne uspostavlja vanjske veze, a vaši podaci ostaju sigurno na vašem lokalno hostiranom poslužitelju.",
+	"Don't Allow": "Ne dopuštaj",
+	"Don't have an account?": "Nemate račun?",
+	"Don't like the style": "Ne sviđa mi se stil",
+	"Download": "Preuzimanje",
+	"Download canceled": "Preuzimanje otkazano",
+	"Download Database": "Preuzmi bazu podataka",
+	"Drop any files here to add to the conversation": "Spustite bilo koje datoteke ovdje za dodavanje u razgovor",
+	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "npr. '30s','10m'. Važeće vremenske jedinice su 's', 'm', 'h'.",
+	"Edit": "Uredi",
+	"Edit Doc": "Uredi dokument",
+	"Edit User": "Uredi korisnika",
+	"Email": "Email",
+	"Embedding Model": "Umetanje modela",
+	"Embedding Model Engine": "Stroj za umetanje modela",
+	"Embedding model set to \"{{embedding_model}}\"": "Model za umetanje postavljen na \"{{embedding_model}}\"",
+	"Enable Chat History": "Omogući povijest razgovora",
+	"Enable New Sign Ups": "Omogući nove prijave",
+	"Enabled": "Omogućeno",
+	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Provjerite da vaša CSV datoteka uključuje 4 stupca u ovom redoslijedu: Ime, Email, Lozinka, Uloga.",
+	"Enter {{role}} message here": "Unesite poruku {{role}} ovdje",
+	"Enter a detail about yourself for your LLMs to recall": "",
+	"Enter Chunk Overlap": "Unesite preklapanje dijelova",
+	"Enter Chunk Size": "Unesite veličinu dijela",
+	"Enter Image Size (e.g. 512x512)": "Unesite veličinu slike (npr. 512x512)",
+	"Enter language codes": "Unesite kodove jezika",
+	"Enter LiteLLM API Base URL (litellm_params.api_base)": "Unesite osnovni URL LiteLLM API-ja (litellm_params.api_base)",
+	"Enter LiteLLM API Key (litellm_params.api_key)": "Unesite ključ LiteLLM API-ja (litellm_params.api_key)",
+	"Enter LiteLLM API RPM (litellm_params.rpm)": "Unesite LiteLLM API RPM (litellm_params.rpm)",
+	"Enter LiteLLM Model (litellm_params.model)": "Unesite LiteLLM model (litellm_params.model)",
+	"Enter Max Tokens (litellm_params.max_tokens)": "Unesite maksimalan broj tokena (litellm_params.max_tokens)",
+	"Enter model tag (e.g. {{modelTag}})": "Unesite oznaku modela (npr. {{modelTag}})",
+	"Enter Number of Steps (e.g. 50)": "Unesite broj koraka (npr. 50)",
+	"Enter Score": "Unesite ocjenu",
+	"Enter stop sequence": "Unesite sekvencu zaustavljanja",
+	"Enter Top K": "Unesite Top K",
+	"Enter URL (e.g. http://127.0.0.1:7860/)": "Unesite URL (npr. http://127.0.0.1:7860/)",
+	"Enter URL (e.g. http://localhost:11434)": "Unesite URL (npr. http://localhost:11434)",
+	"Enter Your Email": "Unesite svoj email",
+	"Enter Your Full Name": "Unesite svoje puno ime",
+	"Enter Your Password": "Unesite svoju lozinku",
+	"Enter Your Role": "Unesite svoju ulogu",
+	"Experimental": "Eksperimentalno",
+	"Export All Chats (All Users)": "Izvoz svih razgovora (svi korisnici)",
+	"Export Chats": "Izvoz razgovora",
+	"Export Documents Mapping": "Izvoz mapiranja dokumenata",
+	"Export Modelfiles": "Izvoz datoteka modela",
+	"Export Prompts": "Izvoz prompta",
+	"Failed to create API Key.": "Neuspješno stvaranje API ključa.",
+	"Failed to read clipboard contents": "Neuspješno čitanje sadržaja međuspremnika",
+	"February": "Veljača",
+	"Feel free to add specific details": "Slobodno dodajte specifične detalje",
+	"File Mode": "Način datoteke",
+	"File not found.": "Datoteka nije pronađena.",
+	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Otkriveno krivotvorenje otisaka prstiju: Nemoguće je koristiti inicijale kao avatar. Postavljanje na zadanu profilnu sliku.",
+	"Fluidly stream large external response chunks": "Glavno strujanje velikih vanjskih dijelova odgovora",
+	"Focus chat input": "Fokusiraj unos razgovora",
+	"Followed instructions perfectly": "Savršeno slijedio upute",
+	"Format your variables using square brackets like this:": "Formatirajte svoje varijable pomoću uglatih zagrada ovako:",
+	"From (Base Model)": "Od (osnovni model)",
+	"Full Screen Mode": "Način cijelog zaslona",
+	"General": "Općenito",
+	"General Settings": "Opće postavke",
+	"Generation Info": "Informacije o generaciji",
+	"Good Response": "Dobar odgovor",
+	"h:mm a": "",
+	"has no conversations.": "nema razgovora.",
+	"Hello, {{name}}": "Bok, {{name}}",
+	"Help": "Pomoć",
+	"Hide": "Sakrij",
+	"Hide Additional Params": "Sakrij dodatne parametre",
+	"How can I help you today?": "Kako vam mogu pomoći danas?",
+	"Hybrid Search": "Hibridna pretraga",
+	"Image Generation (Experimental)": "Generiranje slika (eksperimentalno)",
+	"Image Generation Engine": "Stroj za generiranje slika",
+	"Image Settings": "Postavke slike",
+	"Images": "Slike",
+	"Import Chats": "Uvoz razgovora",
+	"Import Documents Mapping": "Uvoz mapiranja dokumenata",
+	"Import Modelfiles": "Uvoz datoteka modela",
+	"Import Prompts": "Uvoz prompta",
+	"Include `--api` flag when running stable-diffusion-webui": "Uključite zastavicu `--api` prilikom pokretanja stable-diffusion-webui",
+	"Input commands": "Unos naredbi",
+	"Interface": "Sučelje",
+	"Invalid Tag": "Nevažeća oznaka",
+	"January": "Siječanj",
+	"join our Discord for help.": "pridružite se našem Discordu za pomoć.",
+	"JSON": "JSON",
+	"July": "Srpanj",
+	"June": "Lipanj",
+	"JWT Expiration": "Isticanje JWT-a",
+	"JWT Token": "JWT token",
+	"Keep Alive": "Održavanje živim",
+	"Keyboard shortcuts": "Tipkovnički prečaci",
+	"Language": "Jezik",
+	"Last Active": "Zadnja aktivnost",
+	"Light": "Svijetlo",
+	"Listening...": "Slušanje...",
+	"LLMs can make mistakes. Verify important information.": "LLM-ovi mogu pogriješiti. Provjerite važne informacije.",
+	"LTR": "",
+	"Made by OpenWebUI Community": "Izradio OpenWebUI Community",
+	"Make sure to enclose them with": "Provjerite da ih zatvorite s",
+	"Manage LiteLLM Models": "Upravljajte LiteLLM modelima",
+	"Manage Models": "Upravljanje modelima",
+	"Manage Ollama Models": "Upravljanje Ollama modelima",
+	"March": "Ožujak",
+	"Max Tokens": "Maksimalni tokeni",
+	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksimalno 3 modela mogu se preuzeti istovremeno. Pokušajte ponovo kasnije.",
+	"May": "Svibanj",
+	"Memories accessible by LLMs will be shown here.": "",
+	"Memory": "",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
+	"Minimum Score": "Minimalna ocjena",
+	"Mirostat": "Mirostat",
+	"Mirostat Eta": "Mirostat Eta",
+	"Mirostat Tau": "Mirostat Tau",
+	"MMMM DD, YYYY": "MMMM DD, YYYY",
+	"MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm",
+	"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' je uspješno preuzet.",
+	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' je već u redu za preuzimanje.",
+	"Model {{modelId}} not found": "Model {{modelId}} nije pronađen",
+	"Model {{modelName}} already exists.": "Model {{modelName}} već postoji.",
+	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Otkriven put datotečnog sustava modela. Kratko ime modela je potrebno za ažuriranje, nije moguće nastaviti.",
+	"Model Name": "Naziv modela",
+	"Model not selected": "Model nije odabran",
+	"Model Tag Name": "Naziv oznake modela",
+	"Model Whitelisting": "Bijela lista modela",
+	"Model(s) Whitelisted": "Model(i) na bijeloj listi",
+	"Modelfile": "Datoteka modela",
+	"Modelfile Advanced Settings": "Napredne postavke datoteke modela",
+	"Modelfile Content": "Sadržaj datoteke modela",
+	"Modelfiles": "Datoteke modela",
+	"Models": "Modeli",
+	"More": "Više",
+	"Name": "Ime",
+	"Name Tag": "Naziv oznake",
+	"Name your modelfile": "Nazovite svoju datoteku modela",
+	"New Chat": "Novi razgovor",
+	"New Password": "Nova lozinka",
+	"No results found": "Nema rezultata",
+	"No source available": "Nema dostupnog izvora",
+	"Not factually correct": "Nije činjenično točno",
+	"Not sure what to add?": "Niste sigurni što dodati?",
+	"Not sure what to write? Switch to": "Niste sigurni što napisati? Prebacite se na",
+	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Napomena: Ako postavite minimalnu ocjenu, pretraga će vratiti samo dokumente s ocjenom većom ili jednakom minimalnoj ocjeni.",
+	"Notifications": "Obavijesti",
+	"November": "Studeni",
+	"October": "Listopad",
+	"Off": "Isključeno",
+	"Okay, Let's Go!": "U redu, idemo!",
+	"OLED Dark": "OLED Tamno",
+	"Ollama": "Ollama",
+	"Ollama Base URL": "Osnovni URL Ollama",
+	"Ollama Version": "Verzija Ollama",
+	"On": "Uključeno",
+	"Only": "Samo",
+	"Only alphanumeric characters and hyphens are allowed in the command string.": "Samo alfanumerički znakovi i crtice su dopušteni u naredbenom nizu.",
+	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Ups! Držite se! Vaše datoteke su još uvijek u procesu obrade. Pečemo ih do savršenstva. Molimo vas da budete strpljivi i obavijestit ćemo vas kada budu spremne.",
+	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ups! Izgleda da je URL nevažeći. Molimo provjerite ponovno i pokušajte ponovo.",
+	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ups! Koristite nepodržanu metodu (samo frontend). Molimo poslužite WebUI s backend-a.",
+	"Open": "Otvoreno",
+	"Open AI": "Open AI",
+	"Open AI (Dall-E)": "Open AI (Dall-E)",
+	"Open new chat": "Otvorite novi razgovor",
+	"OpenAI": "OpenAI",
+	"OpenAI API": "OpenAI API",
+	"OpenAI API Config": "OpenAI API konfiguracija",
+	"OpenAI API Key is required.": "Potreban je OpenAI API ključ.",
+	"OpenAI URL/Key required.": "Potreban je OpenAI URL/ključ.",
+	"or": "ili",
+	"Other": "Ostalo",
+	"Overview": "Pregled",
+	"Parameters": "Parametri",
+	"Password": "Lozinka",
+	"PDF document (.pdf)": "PDF dokument (.pdf)",
+	"PDF Extract Images (OCR)": "PDF izdvajanje slika (OCR)",
+	"pending": "u tijeku",
+	"Permission denied when accessing microphone: {{error}}": "Dopuštenje odbijeno prilikom pristupa mikrofonu: {{error}}",
+	"Personalization": "",
+	"Plain text (.txt)": "Običan tekst (.txt)",
+	"Playground": "Igralište",
+	"Positive attitude": "Pozitivan stav",
+	"Previous 30 days": "Prethodnih 30 dana",
+	"Previous 7 days": "Prethodnih 7 dana",
+	"Profile Image": "Profilna slika",
+	"Prompt": "Prompt",
+	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (npr. Reci mi zanimljivost o Rimskom carstvu)",
+	"Prompt Content": "Sadržaj prompta",
+	"Prompt suggestions": "Prijedlozi prompta",
+	"Prompts": "Prompti",
+	"Pull \"{{searchValue}}\" from Ollama.com": "Povucite \"{{searchValue}}\" s Ollama.com",
+	"Pull a model from Ollama.com": "Povucite model s Ollama.com",
+	"Pull Progress": "Napredak povlačenja",
+	"Query Params": "Parametri upita",
+	"RAG Template": "RAG predložak",
+	"Raw Format": "Neobrađeni format",
+	"Read Aloud": "Čitaj naglas",
+	"Record voice": "Snimanje glasa",
+	"Redirecting you to OpenWebUI Community": "Preusmjeravanje na OpenWebUI zajednicu",
+	"Refused when it shouldn't have": "Odbijen kada nije trebao biti",
+	"Regenerate": "Regeneriraj",
+	"Release Notes": "Bilješke o izdanju",
+	"Remove": "Ukloni",
+	"Remove Model": "Ukloni model",
+	"Rename": "Preimenuj",
+	"Repeat Last N": "Ponovi zadnjih N",
+	"Repeat Penalty": "Kazna za ponavljanje",
+	"Request Mode": "Način zahtjeva",
+	"Reranking Model": "Model za ponovno rangiranje",
+	"Reranking model disabled": "Model za ponovno rangiranje onemogućen",
+	"Reranking model set to \"{{reranking_model}}\"": "Model za ponovno rangiranje postavljen na \"{{reranking_model}}\"",
+	"Reset Vector Storage": "Resetiraj pohranu vektora",
+	"Response AutoCopy to Clipboard": "Automatsko kopiranje odgovora u međuspremnik",
+	"Role": "Uloga",
+	"Rosé Pine": "Rosé Pine",
+	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "",
+	"Save": "Spremi",
+	"Save & Create": "Spremi i stvori",
+	"Save & Update": "Spremi i ažuriraj",
+	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Spremanje zapisnika razgovora izravno u pohranu vašeg preglednika više nije podržano. Molimo vas da odvojite trenutak za preuzimanje i brisanje zapisnika razgovora klikom na gumb ispod. Ne brinite, možete lako ponovno uvesti zapisnike razgovora u backend putem",
+	"Scan": "Skeniraj",
+	"Scan complete!": "Skeniranje dovršeno!",
+	"Scan for documents from {{path}}": "Skeniraj dokumente s {{path}}",
+	"Search": "Pretraga",
+	"Search a model": "Pretraži model",
+	"Search Documents": "Pretraga dokumenata",
+	"Search Prompts": "Pretraga prompta",
+	"See readme.md for instructions": "Pogledajte readme.md za upute",
+	"See what's new": "Pogledajte što je novo",
+	"Seed": "Sjeme",
+	"Select a mode": "Odaberite način",
+	"Select a model": "Odaberite model",
+	"Select an Ollama instance": "Odaberite Ollama instancu",
+	"Select model": "Odaberite model",
+	"Send": "",
+	"Send a Message": "Pošaljite poruku",
+	"Send message": "Pošalji poruku",
+	"September": "Rujan",
+	"Server connection verified": "Veza s poslužiteljem potvrđena",
+	"Set as default": "Postavi kao zadano",
+	"Set Default Model": "Postavi zadani model",
+	"Set embedding model (e.g. {{model}})": "Postavi model za umetanje (npr. {{model}})",
+	"Set Image Size": "Postavi veličinu slike",
+	"Set Model": "Postavi model",
+	"Set reranking model (e.g. {{model}})": "Postavi model za ponovno rangiranje (npr. {{model}})",
+	"Set Steps": "Postavi korake",
+	"Set Title Auto-Generation Model": "Postavi model za automatsko generiranje naslova",
+	"Set Voice": "Postavi glas",
+	"Settings": "Postavke",
+	"Settings saved successfully!": "Postavke su uspješno spremljene!",
+	"Share": "Podijeli",
+	"Share Chat": "Podijeli razgovor",
+	"Share to OpenWebUI Community": "Podijeli u OpenWebUI zajednici",
+	"short-summary": "kratki sažetak",
+	"Show": "Pokaži",
+	"Show Additional Params": "Pokaži dodatne parametre",
+	"Show shortcuts": "Pokaži prečace",
+	"Showcased creativity": "Prikazana kreativnost",
+	"sidebar": "bočna traka",
+	"Sign in": "Prijava",
+	"Sign Out": "Odjava",
+	"Sign up": "Registracija",
+	"Signing in": "Prijava",
+	"Source": "Izvor",
+	"Speech recognition error: {{error}}": "Pogreška prepoznavanja govora: {{error}}",
+	"Speech-to-Text Engine": "Stroj za prepoznavanje govora",
+	"SpeechRecognition API is not supported in this browser.": "API za prepoznavanje govora nije podržan u ovom pregledniku.",
+	"Stop Sequence": "Zaustavi sekvencu",
+	"STT Settings": "STT postavke",
+	"Submit": "Pošalji",
+	"Subtitle (e.g. about the Roman Empire)": "Podnaslov (npr. o Rimskom carstvu)",
+	"Success": "Uspjeh",
+	"Successfully updated.": "Uspješno ažurirano.",
+	"Suggested": "Predloženo",
+	"Sync All": "Sinkroniziraj sve",
+	"System": "Sustav",
+	"System Prompt": "Sistemski prompt",
+	"Tags": "Oznake",
+	"Tell us more:": "Recite nam više:",
+	"Temperature": "Temperatura",
+	"Template": "Predložak",
+	"Text Completion": "Dovršavanje teksta",
+	"Text-to-Speech Engine": "Stroj za pretvorbu teksta u govor",
+	"Tfs Z": "Tfs Z",
+	"Thanks for your feedback!": "Hvala na povratnim informacijama!",
+	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Ocjena treba biti vrijednost između 0,0 (0%) i 1,0 (100%).",
+	"Theme": "Tema",
+	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ovo osigurava da su vaši vrijedni razgovori sigurno spremljeni u vašu bazu podataka na backendu. Hvala vam!",
+	"This setting does not sync across browsers or devices.": "Ova postavka se ne sinkronizira između preglednika ili uređaja.",
+	"Thorough explanation": "Detaljno objašnjenje",
+	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Savjet: Ažurirajte više mjesta za varijable uzastopno pritiskom na tipku tab u unosu razgovora nakon svake zamjene.",
+	"Title": "Naslov",
+	"Title (e.g. Tell me a fun fact)": "Naslov (npr. Reci mi zanimljivost)",
+	"Title Auto-Generation": "Automatsko generiranje naslova",
+	"Title cannot be an empty string.": "Naslov ne može biti prazni niz.",
+	"Title Generation Prompt": "Prompt za generiranje naslova",
+	"to": "do",
+	"To access the available model names for downloading,": "Za pristup dostupnim nazivima modela za preuzimanje,",
+	"To access the GGUF models available for downloading,": "Za pristup GGUF modelima dostupnim za preuzimanje,",
+	"to chat input.": "u unos razgovora.",
+	"Today": "Danas",
+	"Toggle settings": "Prebaci postavke",
+	"Toggle sidebar": "Prebaci bočnu traku",
+	"Top K": "Top K",
+	"Top P": "Top P",
+	"Trouble accessing Ollama?": "Problemi s pristupom Ollama?",
+	"TTS Settings": "TTS postavke",
+	"Type Hugging Face Resolve (Download) URL": "Upišite Hugging Face Resolve (Download) URL",
+	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Pojavio se problem s povezivanjem na {{provider}}.",
+	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Nepoznata vrsta datoteke '{{file_type}}', ali prihvaćena i obrađuje se kao običan tekst",
+	"Update and Copy Link": "Ažuriraj i kopiraj vezu",
+	"Update password": "Ažuriraj lozinku",
+	"Upload a GGUF model": "Učitaj GGUF model",
+	"Upload files": "Učitaj datoteke",
+	"Upload Progress": "Napredak učitavanja",
+	"URL Mode": "URL način",
+	"Use '#' in the prompt input to load and select your documents.": "Koristite '#' u unosu prompta za učitavanje i odabir vaših dokumenata.",
+	"Use Gravatar": "Koristi Gravatar",
+	"Use Initials": "Koristi inicijale",
+	"user": "korisnik",
+	"User Permissions": "Korisnička dopuštenja",
+	"Users": "Korisnici",
+	"Utilize": "Iskoristi",
+	"Valid time units:": "Važeće vremenske jedinice:",
+	"variable": "varijabla",
+	"variable to have them replaced with clipboard content.": "varijabla za zamjenu sadržajem međuspremnika.",
+	"Version": "Verzija",
+	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Upozorenje: Ako ažurirate ili promijenite svoj model za umetanje, morat ćete ponovno uvesti sve dokumente.",
+	"Web": "Web",
+	"Web Loader Settings": "Postavke web učitavanja",
+	"Web Params": "Web parametri",
+	"Webhook URL": "URL webkuke",
+	"WebUI Add-ons": "Dodaci za WebUI",
+	"WebUI Settings": "WebUI postavke",
+	"WebUI will make requests to": "WebUI će slati zahtjeve na",
+	"What’s New in": "Što je novo u",
+	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kada je povijest isključena, novi razgovori na ovom pregledniku neće se pojaviti u vašoj povijesti na bilo kojem od vaših uređaja.",
+	"Whisper (Local)": "Whisper (lokalno)",
+	"Workspace": "",
+	"Write a prompt suggestion (e.g. Who are you?)": "Napišite prijedlog prompta (npr. Tko si ti?)",
+	"Write a summary in 50 words that summarizes [topic or keyword].": "Napišite sažetak u 50 riječi koji sažima [temu ili ključnu riječ].",
+	"Yesterday": "Jučer",
+	"You": "Vi",
+	"You have no archived conversations.": "Nemate arhiviranih razgovora.",
+	"You have shared this chat": "Podijelili ste ovaj razgovor",
+	"You're a helpful assistant.": "Vi ste korisni asistent.",
+	"You're now logged in.": "Sada ste prijavljeni.",
+	"Youtube": "YouTube",
+	"Youtube Loader Settings": "YouTube postavke učitavanja"
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff