mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
Merge pull request #18402 from open-webui/dev
Some checks are pending
Release / release (push) Waiting to run
Deploy to HuggingFace Spaces / check-secret (push) Waiting to run
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-slim-images (push) Blocked by required conditions
Python CI / Format Backend (push) Waiting to run
Frontend Build / Format & Build Frontend (push) Waiting to run
Frontend Build / Frontend Unit Tests (push) Waiting to run
Release to PyPI / release (push) Waiting to run
Some checks are pending
Release / release (push) Waiting to run
Deploy to HuggingFace Spaces / check-secret (push) Waiting to run
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-slim-images (push) Blocked by required conditions
Python CI / Format Backend (push) Waiting to run
Frontend Build / Format & Build Frontend (push) Waiting to run
Frontend Build / Frontend Unit Tests (push) Waiting to run
Release to PyPI / release (push) Waiting to run
0.6.35
This commit is contained in:
commit
e85c7f7931
183 changed files with 8457 additions and 6982 deletions
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
|
|
@ -11,9 +11,9 @@ body:
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues and discussions.**
|
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues AND discussions. No matter open or closed.**
|
||||||
|
|
||||||
- Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed!**
|
- Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed on the dev branch!**
|
||||||
|
|
||||||
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
|
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
|
||||||
|
|
||||||
|
|
@ -21,6 +21,8 @@ body:
|
||||||
|
|
||||||
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
|
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
|
||||||
|
|
||||||
|
- **Scope**: If you want to report a SECURITY VULNERABILITY, then do so through our [GitHub security page](https://github.com/open-webui/open-webui/security).
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: issue-check
|
id: issue-check
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -31,6 +33,8 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I have searched for any existing and/or related discussions.
|
- label: I have searched for any existing and/or related discussions.
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!).
|
||||||
|
required: true
|
||||||
- label: I am using the latest version of Open WebUI.
|
- label: I am using the latest version of Open WebUI.
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|
|
||||||
27
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
27
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
|
|
@ -8,9 +8,19 @@ body:
|
||||||
value: |
|
value: |
|
||||||
## Important Notes
|
## Important Notes
|
||||||
### Before submitting
|
### Before submitting
|
||||||
Please check the open AND closed [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
|
|
||||||
|
Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
|
||||||
It's likely we're already tracking it! If you’re unsure, start a discussion post first.
|
It's likely we're already tracking it! If you’re unsure, start a discussion post first.
|
||||||
If your feature request might impact others in the community, consider opening a discussion instead and evaluate whether and how to implement it.
|
|
||||||
|
#### Scope
|
||||||
|
|
||||||
|
If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead.
|
||||||
|
**We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.**
|
||||||
|
This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers.
|
||||||
|
Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions).
|
||||||
|
|
||||||
|
If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it.
|
||||||
|
|
||||||
This will help us efficiently focus on improving the project.
|
This will help us efficiently focus on improving the project.
|
||||||
|
|
||||||
### Collaborate respectfully
|
### Collaborate respectfully
|
||||||
|
|
@ -23,7 +33,6 @@ body:
|
||||||
|
|
||||||
We appreciate your time and ask that you **respect ours**.
|
We appreciate your time and ask that you **respect ours**.
|
||||||
|
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
||||||
|
|
||||||
|
|
@ -36,14 +45,22 @@ body:
|
||||||
label: Check Existing Issues
|
label: Check Existing Issues
|
||||||
description: Please confirm that you've checked for existing similar requests
|
description: Please confirm that you've checked for existing similar requests
|
||||||
options:
|
options:
|
||||||
- label: I have searched all existing open AND closed issues and discussions for similar requests. I have found none that is comparable to my request.
|
- label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request.
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: feature-scope
|
||||||
|
attributes:
|
||||||
|
label: Verify Feature Scope
|
||||||
|
description: Please confirm the feature's scope is within the described scope
|
||||||
|
options:
|
||||||
|
- label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions.
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem-description
|
id: problem-description
|
||||||
attributes:
|
attributes:
|
||||||
label: Problem Description
|
label: Problem Description
|
||||||
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
|
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
|
||||||
placeholder: "Ex. I'm always frustrated when..."
|
placeholder: "Ex. I'm always frustrated when... / Not related to a problem"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|
|
||||||
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
|
|
@ -1,16 +1,18 @@
|
||||||
# Pull Request Checklist
|
# Pull Request Checklist
|
||||||
|
|
||||||
### 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.
|
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request.
|
||||||
|
|
||||||
|
This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR.
|
||||||
|
|
||||||
**Before submitting, make sure you've checked the following:**
|
**Before submitting, make sure you've checked the following:**
|
||||||
|
|
||||||
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. Not targeting the `dev` branch may lead to immediate closure of the PR.
|
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **Not targeting the `dev` branch will lead to immediate closure of the PR.**
|
||||||
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
|
- [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
|
||||||
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
- [ ] **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:** If necessary, update relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs) like environment variables, the tutorials, or other documentation sources.
|
- [ ] **Documentation:** If necessary, update relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs) like environment variables, the tutorials, or other documentation sources.
|
||||||
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
||||||
- [ ] **Testing:** Perform manual tests to verify the implemented fix/feature works as intended AND does not break any other functionality. Take this as an opportunity to make screenshots of the feature/fix and include it in the PR description.
|
- [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Take this as an opportunity to **make screenshots of the feature/fix and include it in the PR description**.
|
||||||
- [ ] **Agentic AI Code:**: Confirm this Pull Request is **not written by any AI Agent** or has at least gone through additional human review **and** manual testing. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR.
|
- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR.
|
||||||
- [ ] **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?
|
- [ ] **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?
|
||||||
- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
|
- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
|
||||||
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
||||||
|
|
@ -75,3 +77,6 @@
|
||||||
### Contributor License Agreement
|
### Contributor License Agreement
|
||||||
|
|
||||||
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
|
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
|
||||||
|
|
|
||||||
87
CHANGELOG.md
87
CHANGELOG.md
|
|
@ -5,6 +5,93 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.6.35] - 2025-11-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🖼️ Image generation system received a comprehensive overhaul with major new capabilities including full image editing support allowing users to modify existing images using text prompts with OpenAI, Gemini, or ComfyUI engines, adding Gemini 2.5 Flash Image (Nano Banana) support, Qwen Image Edit integration, resolution of base64-encoded image display issues, streamlined AUTOMATIC1111 configuration by consolidating parameters into a flexible JSON parameters field, and enhanced UI with a code editor modal for ComfyUI workflow management. [#17434](https://github.com/open-webui/open-webui/pull/17434), [#16976](https://github.com/open-webui/open-webui/issues/16976), [Commit](https://github.com/open-webui/open-webui/commit/8e5690aab4f632a57027e2acf880b8f89a8717c0), [Commit](https://github.com/open-webui/open-webui/commit/72f8539fd2e679fec0762945f22f4b8a6920afa0), [Commit](https://github.com/open-webui/open-webui/commit/8d34fcb586eeee1fac6da2f991518b8a68b00b72), [Commit](https://github.com/open-webui/open-webui/commit/72900cd686de1fa6be84b5a8a2fc857cff7b91b8)
|
||||||
|
- 🔒 CORS origin validation was added to WebSocket connections as a defense-in-depth security measure against cross-site WebSocket hijacking attacks. [#18411](https://github.com/open-webui/open-webui/pull/18411), [#18410](https://github.com/open-webui/open-webui/issues/18410)
|
||||||
|
- 🔄 Automatic page refresh now occurs when a version update is detected via WebSocket connection, ensuring users always run the latest version without cache issues. [Commit](https://github.com/open-webui/open-webui/commit/989f192c92d2fe55daa31336e7971e21798b96ae)
|
||||||
|
- 🐍 Experimental initial preparations for Python 3.13 compatibility by updating dependencies with security enhancements and cryptographic improvements. [#18430](https://github.com/open-webui/open-webui/pull/18430), [#18424](https://github.com/open-webui/open-webui/pull/18424)
|
||||||
|
- ⚡ Image compression now preserves the original image format instead of converting to PNG, significantly reducing file sizes and improving chat loading performance. [#18506](https://github.com/open-webui/open-webui/pull/18506)
|
||||||
|
- 🎤 Mistral Voxtral model support was added for text-to-speech, including voxtral-small and voxtral-mini models with both transcription and chat completion API support. [#18934](https://github.com/open-webui/open-webui/pull/18934)
|
||||||
|
- 🔊 Text-to-speech now uses a global audio queue system to prevent overlapping playback, ensuring only one TTS instance plays at a time with proper stop/start controls and automatic cleanup when switching between messages. [#16152](https://github.com/open-webui/open-webui/pull/16152), [#18744](https://github.com/open-webui/open-webui/pull/18744), [#16150](https://github.com/open-webui/open-webui/issues/16150)
|
||||||
|
- 🔊 ELEVENLABS_API_BASE_URL environment variable now allows configuration of custom ElevenLabs API endpoints, enabling support for EU residency API requirements. [#18402](https://github.com/open-webui/open-webui/issues/18402)
|
||||||
|
- 🔐 OAUTH_ROLES_SEPARATOR environment variable now allows custom role separators for OAuth roles that contain commas, useful for roles specified in LDAP syntax. [#18572](https://github.com/open-webui/open-webui/pull/18572)
|
||||||
|
- 📄 External document loaders can now optionally forward user information headers when ENABLE_FORWARD_USER_INFO_HEADERS is enabled, enabling cost tracking, audit logs, and usage analytics for external services. [#18731](https://github.com/open-webui/open-webui/pull/18731)
|
||||||
|
- 📄 MISTRAL_OCR_API_BASE_URL environment variable now allows configuration of custom Mistral OCR API endpoints for flexible deployment options. [Commit](https://github.com/open-webui/open-webui/commit/415b93c7c35c2e2db4425e6da1b88b3750f496b0)
|
||||||
|
- ⌨️ Keyboard shortcut hints are now displayed on sidebar buttons with a refactored shortcuts modal that accurately reflects all available hotkeys across different keyboard layouts. [#18473](https://github.com/open-webui/open-webui/pull/18473)
|
||||||
|
- 🛠️ Tooltips now display tool descriptions when hovering over tool names on the model edit page, improving usability and providing immediate context. [#18707](https://github.com/open-webui/open-webui/pull/18707)
|
||||||
|
- 📝 "Create a new note" from the search modal now immediately creates a new private note and opens it in the editor instead of navigating to the generic notes page. [#18255](https://github.com/open-webui/open-webui/pull/18255)
|
||||||
|
- 🖨️ Code block output now preserves whitespace formatting with monospace font to accurately reflect terminal behavior. [#18352](https://github.com/open-webui/open-webui/pull/18352)
|
||||||
|
- ✏️ Edit button is now available in the three-dot menu of models in the workspace section for quick access to model editing, with the menu reorganized for better user experience and Edit, Clone, Copy Link, and Share options logically grouped. [#18574](https://github.com/open-webui/open-webui/pull/18574)
|
||||||
|
- 📌 Sidebar models section is now collapsible, allowing users to expand and collapse the pinned models list for better sidebar organization. [Commit](https://github.com/open-webui/open-webui/commit/82c08a3b5d189f81c96b6548cc872198771015b0)
|
||||||
|
- 🌙 Dark mode styles for select elements were added using Tailwind CSS classes, improving consistency across the interface. [#18636](https://github.com/open-webui/open-webui/pull/18636)
|
||||||
|
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
|
||||||
|
- 🌐 Translations for Portuguese (Brazil), Greek, German, Traditional Chinese, Simplified Chinese, Spanish, Georgian, Danish, and Estonian were enhanced and expanded.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🔒 Server-Sent Event (SSE) code injection vulnerability in Direct Connections is resolved by blocking event emission from untrusted external model servers; event emitters from direct connected model servers are no longer supported, preventing arbitrary JavaScript execution in user browsers. [Commit](https://github.com/open-webui/open-webui/commit/8af6a4cf21b756a66cd58378a01c60f74c39b7ca)
|
||||||
|
- 🛡️ DOM XSS vulnerability in "Insert Prompt as Rich Text" is resolved by sanitizing HTML content with DOMPurify before rendering. [Commit](https://github.com/open-webui/open-webui/commit/eb9c4c0e358c274aea35f21c2856c0a20051e5f1)
|
||||||
|
- ⚙️ MCP server cancellation scope corruption is prevented by reversing disconnection order to follow LIFO and properly handling exceptions, resolving 100% CPU usage when resuming chats with expired tokens or using multiple streamable MCP servers. [#18537](https://github.com/open-webui/open-webui/pull/18537)
|
||||||
|
- 🔧 UI freeze when querying models with knowledge bases containing inconsistent distance metrics is resolved by properly initializing the distances array in citations. [#18585](https://github.com/open-webui/open-webui/pull/18585)
|
||||||
|
- 🤖 Duplicate model IDs from multiple OpenAI endpoints are now automatically deduplicated server-side, preventing frontend crashes for users with unified gateway proxies that aggregate multiple providers. [Commit](https://github.com/open-webui/open-webui/commit/fdf7ca11d4f3cc8fe63e81c98dc0d1e48e52ba36)
|
||||||
|
- 🔐 Login failures with passwords longer than 72 bytes are resolved by safely truncating oversized passwords for bcrypt compatibility. [#18157](https://github.com/open-webui/open-webui/issues/18157)
|
||||||
|
- 🔐 OAuth 2.1 MCP tool connections now automatically re-register clients when stored client IDs become stale, preventing unauthorized_client errors after editing tool endpoints and providing detailed error messages for callback failures. [#18415](https://github.com/open-webui/open-webui/pull/18415), [#18309](https://github.com/open-webui/open-webui/issues/18309)
|
||||||
|
- 🔓 OAuth 2.1 discovery, metadata fetching, and dynamic client registration now correctly use HTTP proxy environment variables when trust_env is enabled. [Commit](https://github.com/open-webui/open-webui/commit/bafeb76c411483bd6b135f0edbcdce048120f264)
|
||||||
|
- 🔌 MCP server connection failures now display clear error messages in the chat interface instead of silently failing. [#18892](https://github.com/open-webui/open-webui/pull/18892), [#18889](https://github.com/open-webui/open-webui/issues/18889)
|
||||||
|
- 💬 Chat titles are now properly generated even when title auto-generation is disabled in interface settings, fixing an issue where chats would remain labeled as "New chat". [#18761](https://github.com/open-webui/open-webui/pull/18761), [#18717](https://github.com/open-webui/open-webui/issues/18717), [#6478](https://github.com/open-webui/open-webui/issues/6478)
|
||||||
|
- 🔍 Chat query errors are prevented by properly validating and handling the "order_by" parameter to ensure requested columns exist. [#18400](https://github.com/open-webui/open-webui/pull/18400), [#18452](https://github.com/open-webui/open-webui/pull/18452)
|
||||||
|
- 🔧 Root-level max_tokens parameter is no longer dropped when proxying to Ollama, properly converting to num_predict to limit output token length as intended. [#18618](https://github.com/open-webui/open-webui/issues/18618)
|
||||||
|
- 🔑 Self-hosted Marker instances can now be used without requiring an API key, while keeping it optional for datalab Marker service users. [#18617](https://github.com/open-webui/open-webui/issues/18617)
|
||||||
|
- 🔧 OpenAPI specification endpoint conflict between "/api/v1/models" and "/api/v1/models/" is resolved by changing the models router endpoint to "/list", preventing duplicate operationId errors when generating TypeScript API clients. [#18758](https://github.com/open-webui/open-webui/issues/18758)
|
||||||
|
- 🏷️ Model tags are now de-duplicated case-insensitively in both the model selector and workspace models page, preventing duplicate entries with different capitalization from appearing in filter dropdowns. [#18716](https://github.com/open-webui/open-webui/pull/18716), [#18711](https://github.com/open-webui/open-webui/issues/18711)
|
||||||
|
- 📄 Docling RAG parameter configuration is now correctly saved in the admin UI by fixing the typo in the "DOCLING_PARAMS" parameter name. [#18390](https://github.com/open-webui/open-webui/pull/18390)
|
||||||
|
- 📃 Tika document processing now automatically detects content types instead of relying on potentially incorrect browser-provided mime-types, improving file handling accuracy for formats like RTF. [#18765](https://github.com/open-webui/open-webui/pull/18765), [#18683](https://github.com/open-webui/open-webui/issues/18683)
|
||||||
|
- 🖼️ Image and video uploads to knowledge bases now display proper error messages instead of showing an infinite spinner when the content extraction engine does not support these file types. [#18514](https://github.com/open-webui/open-webui/issues/18514)
|
||||||
|
- 📝 Notes PDF export now properly detects and applies dark mode styling consistently across both the notes list and individual note pages, with a shared utility function to eliminate code duplication. [#18526](https://github.com/open-webui/open-webui/issues/18526)
|
||||||
|
- 💭 Details tags for reasoning content are now correctly identified and rendered even when the same tag is present in user messages. [#18840](https://github.com/open-webui/open-webui/pull/18840), [#18294](https://github.com/open-webui/open-webui/issues/18294)
|
||||||
|
- 📊 Mermaid and Vega rendering errors now display inline with the code instead of showing repetitive toast notifications, improving user experience when models generate invalid diagram syntax. [Commit](https://github.com/open-webui/open-webui/commit/fdc0f04a8b7dd0bc9f9dc0e7e30854f7a0eea3e9)
|
||||||
|
- 📈 Mermaid diagram rendering errors no longer cause UI unavailability or display error messages below the input box. [#18493](https://github.com/open-webui/open-webui/pull/18493), [#18340](https://github.com/open-webui/open-webui/issues/18340)
|
||||||
|
- 🔗 Web search SSL verification is now asynchronous, preventing the website from hanging during web search operations. [#18714](https://github.com/open-webui/open-webui/pull/18714), [#18699](https://github.com/open-webui/open-webui/issues/18699)
|
||||||
|
- 🌍 Web search results now correctly use HTTP proxy environment variables when WEB_SEARCH_TRUST_ENV is enabled. [#18667](https://github.com/open-webui/open-webui/pull/18667), [#7008](https://github.com/open-webui/open-webui/discussions/7008)
|
||||||
|
- 🔍 Google Programmable Search Engine now properly includes referer headers, enabling API keys with HTTP referrer restrictions configured in Google Cloud Console. [#18871](https://github.com/open-webui/open-webui/pull/18871), [#18870](https://github.com/open-webui/open-webui/issues/18870)
|
||||||
|
- ⚡ YouTube video transcript fetching now works correctly when using a proxy connection. [#18419](https://github.com/open-webui/open-webui/pull/18419)
|
||||||
|
- 🎙️ Speech-to-text transcription no longer deletes or replaces existing text in the prompt input field, properly preserving any previously entered content. [#18540](https://github.com/open-webui/open-webui/issues/18540)
|
||||||
|
- 🎙️ The "Instant Auto-Send After Voice Transcription" setting now functions correctly and automatically sends transcribed text when enabled. [#18466](https://github.com/open-webui/open-webui/issues/18466)
|
||||||
|
- ⚙️ Chat settings now load properly when reopening a tab or starting a new session by initializing defaults when sessionStorage is empty. [#18438](https://github.com/open-webui/open-webui/pull/18438)
|
||||||
|
- 🔎 Folder tag search in the sidebar now correctly handles folder names with multiple spaces by replacing all spaces with underscores. [Commit](https://github.com/open-webui/open-webui/commit/a8fe979af68e47e4e4bb3eb76e48d93d60cd2a45)
|
||||||
|
- 🛠️ Functions page now updates immediately after deleting a function, removing the need for a manual page reload. [#18912](https://github.com/open-webui/open-webui/pull/18912), [#18908](https://github.com/open-webui/open-webui/issues/18908)
|
||||||
|
- 🛠️ Native tool calling now properly supports sequential tool calls with shared context, allowing tools to access images and data from previous tool executions in the same conversation. [#18664](https://github.com/open-webui/open-webui/pull/18664)
|
||||||
|
- 🎯 Globally enabled actions in the model editor now correctly apply as global instead of being treated as disabled. [#18577](https://github.com/open-webui/open-webui/pull/18577)
|
||||||
|
- 📋 Clipboard images pasted via the "{{CLIPBOARD}}" prompt variable are now correctly converted to base64 format before being sent to the backend, resolving base64 encoding errors. [#18432](https://github.com/open-webui/open-webui/pull/18432), [#18425](https://github.com/open-webui/open-webui/issues/18425)
|
||||||
|
- 📋 File list is now cleared when switching to models that do not support file uploads, preventing files from being sent to incompatible models. [#18496](https://github.com/open-webui/open-webui/pull/18496)
|
||||||
|
- 📂 Move menu no longer displays when folders are empty. [#18484](https://github.com/open-webui/open-webui/pull/18484)
|
||||||
|
- 📁 Folder and channel creation now validates that names are not empty, preventing creation of folders or channels with no name and showing an error toast if attempted. [#18564](https://github.com/open-webui/open-webui/pull/18564)
|
||||||
|
- 🖊️ Rich text input no longer removes text between equals signs when pasting code with comparison operators. [#18551](https://github.com/open-webui/open-webui/issues/18551)
|
||||||
|
- ⌨️ Keyboard shortcuts now display the correct keys for international and non-QWERTY keyboard layouts by detecting the user's layout using the Keyboard API. [#18533](https://github.com/open-webui/open-webui/pull/18533)
|
||||||
|
- 🌐 "Attach Webpage" button now displays with correct disabled styling when a model does not support file uploads. [#18483](https://github.com/open-webui/open-webui/pull/18483)
|
||||||
|
- 🎚️ Divider no longer displays in the integrations menu when no integrations are enabled. [#18487](https://github.com/open-webui/open-webui/pull/18487)
|
||||||
|
- 📱 Chat controls button is now properly hidden on mobile for users without admin or explicit chat control permissions. [#18641](https://github.com/open-webui/open-webui/pull/18641)
|
||||||
|
- 📍 User menu, download submenu, and move submenu are now repositioned to prevent overlap with the Chat Controls sidebar when it is open. [Commit](https://github.com/open-webui/open-webui/commit/414ab51cb6df1ab0d6c85ac6c1f2c5c9a5f8e2aa)
|
||||||
|
- 🎯 Artifacts button no longer appears in the chat menu when there are no artifacts to display. [Commit](https://github.com/open-webui/open-webui/commit/ed6449d35f84f68dc75ee5c6b3f4748a3fda0096)
|
||||||
|
- 🎨 Artifacts view now automatically displays when opening an existing conversation containing artifacts, improving user experience. [#18215](https://github.com/open-webui/open-webui/pull/18215)
|
||||||
|
- 🖌️ Formatting toolbar is no longer hidden under images or code blocks in chat and now displays correctly above all message content.
|
||||||
|
- 🎨 Layout shift near system instructions is prevented by properly rendering the chat component when system prompts are empty. [#18594](https://github.com/open-webui/open-webui/pull/18594)
|
||||||
|
- 📐 Modal layout shift caused by scrollbar appearance is prevented by adding a stable scrollbar gutter. [#18591](https://github.com/open-webui/open-webui/pull/18591)
|
||||||
|
- ✨ Spacing between icon and label in the user menu dropdown items is now consistent. [#18595](https://github.com/open-webui/open-webui/pull/18595)
|
||||||
|
- 💬 Duplicate prompt suggestions no longer cause the webpage to freeze or throw JavaScript errors by implementing proper key management with composite keys. [#18841](https://github.com/open-webui/open-webui/pull/18841), [#18566](https://github.com/open-webui/open-webui/issues/18566)
|
||||||
|
- 🔍 Chat preview loading in the search modal now works correctly for all search results by fixing an index boundary check that previously caused out-of-bounds errors. [#18911](https://github.com/open-webui/open-webui/pull/18911)
|
||||||
|
- ♿ Screen reader support was enhanced by wrapping messages in semantic elements with descriptive aria-labels, adding "Assistant is typing" and "Response complete" announcements for improved accessibility. [#18735](https://github.com/open-webui/open-webui/pull/18735)
|
||||||
|
- 🔒 Incorrect await call in the OAuth 2.1 flow is removed, eliminating a logged exception during authentication. [#18236](https://github.com/open-webui/open-webui/pull/18236)
|
||||||
|
- 🛡️ Duplicate crossorigin attribute in the manifest file was removed. [#18413](https://github.com/open-webui/open-webui/pull/18413)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 🔄 Firecrawl integration was refactored to use the official Firecrawl SDK instead of direct HTTP requests and langchain_community FireCrawlLoader, improving reliability and performance with batch scraping support and enhanced error handling. [#18635](https://github.com/open-webui/open-webui/pull/18635)
|
||||||
|
- 📄 MinerU content extraction engine now only supports PDF files following the upstream removal of LibreOffice document conversion in version 2.0.0; users needing to process office documents should convert them to PDF format first. [#18448](https://github.com/open-webui/open-webui/issues/18448)
|
||||||
|
|
||||||
## [0.6.34] - 2025-10-16
|
## [0.6.34] - 2025-10-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -17,7 +17,7 @@ Passionate about open-source AI? [Join our team →](https://careers.openwebui.c
|
||||||

|

|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:sales@openwebui.com)**
|
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](https://docs.openwebui.com/enterprise)**
|
||||||
>
|
>
|
||||||
> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
|
> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
|
||||||
|
|
||||||
|
|
@ -65,43 +65,6 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
|
||||||
|
|
||||||
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
|
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
|
||||||
|
|
||||||
## Sponsors 🙌
|
|
||||||
|
|
||||||
#### Emerald
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<!-- <tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://n8n.io/" target="_blank">
|
|
||||||
<img src="https://docs.openwebui.com/sponsors/logos/n8n.png" alt="n8n" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://n8n.io/">n8n</a> • Does your interface have a backend yet?<br>Try <a href="https://n8n.io/">n8n</a>
|
|
||||||
</td>
|
|
||||||
</tr> -->
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs" target="_blank">
|
|
||||||
<img src="https://docs.openwebui.com/sponsors/logos/tailscale.png" alt="Tailscale" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs">Tailscale</a> • Connect self-hosted AI to any device with Tailscale
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://warp.dev/open-webui" target="_blank">
|
|
||||||
<img src="https://docs.openwebui.com/sponsors/logos/warp.png" alt="Warp" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://warp.dev/open-webui">Warp</a> • The intelligent terminal for developers
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
|
We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
|
||||||
|
|
|
||||||
|
|
@ -576,19 +576,26 @@ OAUTH_ROLES_CLAIM = PersistentConfig(
|
||||||
os.environ.get("OAUTH_ROLES_CLAIM", "roles"),
|
os.environ.get("OAUTH_ROLES_CLAIM", "roles"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SEP = os.environ.get("OAUTH_ROLES_SEPARATOR", ",")
|
||||||
|
|
||||||
OAUTH_ALLOWED_ROLES = PersistentConfig(
|
OAUTH_ALLOWED_ROLES = PersistentConfig(
|
||||||
"OAUTH_ALLOWED_ROLES",
|
"OAUTH_ALLOWED_ROLES",
|
||||||
"oauth.allowed_roles",
|
"oauth.allowed_roles",
|
||||||
[
|
[
|
||||||
role.strip()
|
role.strip()
|
||||||
for role in os.environ.get("OAUTH_ALLOWED_ROLES", "user,admin").split(",")
|
for role in os.environ.get("OAUTH_ALLOWED_ROLES", f"user{SEP}admin").split(SEP)
|
||||||
|
if role
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
OAUTH_ADMIN_ROLES = PersistentConfig(
|
OAUTH_ADMIN_ROLES = PersistentConfig(
|
||||||
"OAUTH_ADMIN_ROLES",
|
"OAUTH_ADMIN_ROLES",
|
||||||
"oauth.admin_roles",
|
"oauth.admin_roles",
|
||||||
[role.strip() for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(",")],
|
[
|
||||||
|
role.strip()
|
||||||
|
for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(SEP)
|
||||||
|
if role
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
OAUTH_ALLOWED_DOMAINS = PersistentConfig(
|
OAUTH_ALLOWED_DOMAINS = PersistentConfig(
|
||||||
|
|
@ -2457,6 +2464,12 @@ DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
|
||||||
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
|
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MISTRAL_OCR_API_BASE_URL = PersistentConfig(
|
||||||
|
"MISTRAL_OCR_API_BASE_URL",
|
||||||
|
"rag.MISTRAL_OCR_API_BASE_URL",
|
||||||
|
os.getenv("MISTRAL_OCR_API_BASE_URL", "https://api.mistral.ai/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
MISTRAL_OCR_API_KEY = PersistentConfig(
|
MISTRAL_OCR_API_KEY = PersistentConfig(
|
||||||
"MISTRAL_OCR_API_KEY",
|
"MISTRAL_OCR_API_KEY",
|
||||||
"rag.mistral_ocr_api_key",
|
"rag.mistral_ocr_api_key",
|
||||||
|
|
@ -3067,16 +3080,30 @@ EXTERNAL_WEB_LOADER_API_KEY = PersistentConfig(
|
||||||
# Images
|
# Images
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
|
ENABLE_IMAGE_GENERATION = PersistentConfig(
|
||||||
|
"ENABLE_IMAGE_GENERATION",
|
||||||
|
"image_generation.enable",
|
||||||
|
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
IMAGE_GENERATION_ENGINE = PersistentConfig(
|
IMAGE_GENERATION_ENGINE = PersistentConfig(
|
||||||
"IMAGE_GENERATION_ENGINE",
|
"IMAGE_GENERATION_ENGINE",
|
||||||
"image_generation.engine",
|
"image_generation.engine",
|
||||||
os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
|
os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
|
||||||
)
|
)
|
||||||
|
|
||||||
ENABLE_IMAGE_GENERATION = PersistentConfig(
|
IMAGE_GENERATION_MODEL = PersistentConfig(
|
||||||
"ENABLE_IMAGE_GENERATION",
|
"IMAGE_GENERATION_MODEL",
|
||||||
"image_generation.enable",
|
"image_generation.model",
|
||||||
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
|
os.getenv("IMAGE_GENERATION_MODEL", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGE_SIZE = PersistentConfig(
|
||||||
|
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGE_STEPS = PersistentConfig(
|
||||||
|
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
|
||||||
)
|
)
|
||||||
|
|
||||||
ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
|
ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
|
||||||
|
|
@ -3096,35 +3123,17 @@ AUTOMATIC1111_API_AUTH = PersistentConfig(
|
||||||
os.getenv("AUTOMATIC1111_API_AUTH", ""),
|
os.getenv("AUTOMATIC1111_API_AUTH", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
AUTOMATIC1111_CFG_SCALE = PersistentConfig(
|
automatic1111_params = os.getenv("AUTOMATIC1111_PARAMS", "")
|
||||||
"AUTOMATIC1111_CFG_SCALE",
|
try:
|
||||||
"image_generation.automatic1111.cfg_scale",
|
automatic1111_params = json.loads(automatic1111_params)
|
||||||
(
|
except json.JSONDecodeError:
|
||||||
float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
|
automatic1111_params = {}
|
||||||
if os.environ.get("AUTOMATIC1111_CFG_SCALE")
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
AUTOMATIC1111_SAMPLER = PersistentConfig(
|
AUTOMATIC1111_PARAMS = PersistentConfig(
|
||||||
"AUTOMATIC1111_SAMPLER",
|
"AUTOMATIC1111_PARAMS",
|
||||||
"image_generation.automatic1111.sampler",
|
"image_generation.automatic1111.api_auth",
|
||||||
(
|
automatic1111_params,
|
||||||
os.environ.get("AUTOMATIC1111_SAMPLER")
|
|
||||||
if os.environ.get("AUTOMATIC1111_SAMPLER")
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
AUTOMATIC1111_SCHEDULER = PersistentConfig(
|
|
||||||
"AUTOMATIC1111_SCHEDULER",
|
|
||||||
"image_generation.automatic1111.scheduler",
|
|
||||||
(
|
|
||||||
os.environ.get("AUTOMATIC1111_SCHEDULER")
|
|
||||||
if os.environ.get("AUTOMATIC1111_SCHEDULER")
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
COMFYUI_BASE_URL = PersistentConfig(
|
COMFYUI_BASE_URL = PersistentConfig(
|
||||||
|
|
@ -3290,18 +3299,79 @@ IMAGES_GEMINI_API_KEY = PersistentConfig(
|
||||||
os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY),
|
os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY),
|
||||||
)
|
)
|
||||||
|
|
||||||
IMAGE_SIZE = PersistentConfig(
|
IMAGES_GEMINI_ENDPOINT_METHOD = PersistentConfig(
|
||||||
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
|
"IMAGES_GEMINI_ENDPOINT_METHOD",
|
||||||
|
"image_generation.gemini.endpoint_method",
|
||||||
|
os.getenv("IMAGES_GEMINI_ENDPOINT_METHOD", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
IMAGE_STEPS = PersistentConfig(
|
|
||||||
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
|
IMAGE_EDIT_ENGINE = PersistentConfig(
|
||||||
|
"IMAGE_EDIT_ENGINE",
|
||||||
|
"images.edit.engine",
|
||||||
|
os.getenv("IMAGE_EDIT_ENGINE", "openai"),
|
||||||
)
|
)
|
||||||
|
|
||||||
IMAGE_GENERATION_MODEL = PersistentConfig(
|
IMAGE_EDIT_MODEL = PersistentConfig(
|
||||||
"IMAGE_GENERATION_MODEL",
|
"IMAGE_EDIT_MODEL",
|
||||||
"image_generation.model",
|
"images.edit.model",
|
||||||
os.getenv("IMAGE_GENERATION_MODEL", ""),
|
os.getenv("IMAGE_EDIT_MODEL", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGE_EDIT_SIZE = PersistentConfig(
|
||||||
|
"IMAGE_EDIT_SIZE", "images.edit.size", os.getenv("IMAGE_EDIT_SIZE", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_OPENAI_API_BASE_URL",
|
||||||
|
"images.edit.openai.api_base_url",
|
||||||
|
os.getenv("IMAGES_EDIT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||||
|
)
|
||||||
|
IMAGES_EDIT_OPENAI_API_VERSION = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_OPENAI_API_VERSION",
|
||||||
|
"images.edit.openai.api_version",
|
||||||
|
os.getenv("IMAGES_EDIT_OPENAI_API_VERSION", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_OPENAI_API_KEY = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_OPENAI_API_KEY",
|
||||||
|
"images.edit.openai.api_key",
|
||||||
|
os.getenv("IMAGES_EDIT_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_GEMINI_API_BASE_URL = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_GEMINI_API_BASE_URL",
|
||||||
|
"images.edit.gemini.api_base_url",
|
||||||
|
os.getenv("IMAGES_EDIT_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL),
|
||||||
|
)
|
||||||
|
IMAGES_EDIT_GEMINI_API_KEY = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_GEMINI_API_KEY",
|
||||||
|
"images.edit.gemini.api_key",
|
||||||
|
os.getenv("IMAGES_EDIT_GEMINI_API_KEY", GEMINI_API_KEY),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
IMAGES_EDIT_COMFYUI_BASE_URL = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_BASE_URL",
|
||||||
|
"images.edit.comfyui.base_url",
|
||||||
|
os.getenv("IMAGES_EDIT_COMFYUI_BASE_URL", ""),
|
||||||
|
)
|
||||||
|
IMAGES_EDIT_COMFYUI_API_KEY = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_API_KEY",
|
||||||
|
"images.edit.comfyui.api_key",
|
||||||
|
os.getenv("IMAGES_EDIT_COMFYUI_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW",
|
||||||
|
"images.edit.comfyui.workflow",
|
||||||
|
os.getenv("IMAGES_EDIT_COMFYUI_WORKFLOW", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW_NODES",
|
||||||
|
"images.edit.comfyui.nodes",
|
||||||
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
|
|
@ -3336,6 +3406,10 @@ DEEPGRAM_API_KEY = PersistentConfig(
|
||||||
os.getenv("DEEPGRAM_API_KEY", ""),
|
os.getenv("DEEPGRAM_API_KEY", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ElevenLabs configuration
|
||||||
|
ELEVENLABS_API_BASE_URL = os.getenv(
|
||||||
|
"ELEVENLABS_API_BASE_URL", "https://api.elevenlabs.io"
|
||||||
|
)
|
||||||
|
|
||||||
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
"AUDIO_STT_OPENAI_API_BASE_URL",
|
"AUDIO_STT_OPENAI_API_BASE_URL",
|
||||||
|
|
@ -3403,6 +3477,24 @@ AUDIO_STT_AZURE_MAX_SPEAKERS = PersistentConfig(
|
||||||
os.getenv("AUDIO_STT_AZURE_MAX_SPEAKERS", ""),
|
os.getenv("AUDIO_STT_AZURE_MAX_SPEAKERS", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUDIO_STT_MISTRAL_API_KEY = PersistentConfig(
|
||||||
|
"AUDIO_STT_MISTRAL_API_KEY",
|
||||||
|
"audio.stt.mistral.api_key",
|
||||||
|
os.getenv("AUDIO_STT_MISTRAL_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
AUDIO_STT_MISTRAL_API_BASE_URL = PersistentConfig(
|
||||||
|
"AUDIO_STT_MISTRAL_API_BASE_URL",
|
||||||
|
"audio.stt.mistral.api_base_url",
|
||||||
|
os.getenv("AUDIO_STT_MISTRAL_API_BASE_URL", "https://api.mistral.ai/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = PersistentConfig(
|
||||||
|
"AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS",
|
||||||
|
"audio.stt.mistral.use_chat_completions",
|
||||||
|
os.getenv("AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS", "false").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
"AUDIO_TTS_OPENAI_API_BASE_URL",
|
"AUDIO_TTS_OPENAI_API_BASE_URL",
|
||||||
"audio.tts.openai.api_base_url",
|
"audio.tts.openai.api_base_url",
|
||||||
|
|
|
||||||
|
|
@ -146,9 +146,7 @@ from open_webui.config import (
|
||||||
# Image
|
# Image
|
||||||
AUTOMATIC1111_API_AUTH,
|
AUTOMATIC1111_API_AUTH,
|
||||||
AUTOMATIC1111_BASE_URL,
|
AUTOMATIC1111_BASE_URL,
|
||||||
AUTOMATIC1111_CFG_SCALE,
|
AUTOMATIC1111_PARAMS,
|
||||||
AUTOMATIC1111_SAMPLER,
|
|
||||||
AUTOMATIC1111_SCHEDULER,
|
|
||||||
COMFYUI_BASE_URL,
|
COMFYUI_BASE_URL,
|
||||||
COMFYUI_API_KEY,
|
COMFYUI_API_KEY,
|
||||||
COMFYUI_WORKFLOW,
|
COMFYUI_WORKFLOW,
|
||||||
|
|
@ -164,6 +162,19 @@ from open_webui.config import (
|
||||||
IMAGES_OPENAI_API_KEY,
|
IMAGES_OPENAI_API_KEY,
|
||||||
IMAGES_GEMINI_API_BASE_URL,
|
IMAGES_GEMINI_API_BASE_URL,
|
||||||
IMAGES_GEMINI_API_KEY,
|
IMAGES_GEMINI_API_KEY,
|
||||||
|
IMAGES_GEMINI_ENDPOINT_METHOD,
|
||||||
|
IMAGE_EDIT_ENGINE,
|
||||||
|
IMAGE_EDIT_MODEL,
|
||||||
|
IMAGE_EDIT_SIZE,
|
||||||
|
IMAGES_EDIT_OPENAI_API_BASE_URL,
|
||||||
|
IMAGES_EDIT_OPENAI_API_KEY,
|
||||||
|
IMAGES_EDIT_OPENAI_API_VERSION,
|
||||||
|
IMAGES_EDIT_GEMINI_API_BASE_URL,
|
||||||
|
IMAGES_EDIT_GEMINI_API_KEY,
|
||||||
|
IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||||
|
IMAGES_EDIT_COMFYUI_API_KEY,
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||||
# Audio
|
# Audio
|
||||||
AUDIO_STT_ENGINE,
|
AUDIO_STT_ENGINE,
|
||||||
AUDIO_STT_MODEL,
|
AUDIO_STT_MODEL,
|
||||||
|
|
@ -175,6 +186,9 @@ from open_webui.config import (
|
||||||
AUDIO_STT_AZURE_LOCALES,
|
AUDIO_STT_AZURE_LOCALES,
|
||||||
AUDIO_STT_AZURE_BASE_URL,
|
AUDIO_STT_AZURE_BASE_URL,
|
||||||
AUDIO_STT_AZURE_MAX_SPEAKERS,
|
AUDIO_STT_AZURE_MAX_SPEAKERS,
|
||||||
|
AUDIO_STT_MISTRAL_API_KEY,
|
||||||
|
AUDIO_STT_MISTRAL_API_BASE_URL,
|
||||||
|
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
|
||||||
AUDIO_TTS_ENGINE,
|
AUDIO_TTS_ENGINE,
|
||||||
AUDIO_TTS_MODEL,
|
AUDIO_TTS_MODEL,
|
||||||
AUDIO_TTS_VOICE,
|
AUDIO_TTS_VOICE,
|
||||||
|
|
@ -266,6 +280,7 @@ from open_webui.config import (
|
||||||
DOCLING_PICTURE_DESCRIPTION_API,
|
DOCLING_PICTURE_DESCRIPTION_API,
|
||||||
DOCUMENT_INTELLIGENCE_ENDPOINT,
|
DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
DOCUMENT_INTELLIGENCE_KEY,
|
DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
MISTRAL_OCR_API_BASE_URL,
|
||||||
MISTRAL_OCR_API_KEY,
|
MISTRAL_OCR_API_KEY,
|
||||||
RAG_TEXT_SPLITTER,
|
RAG_TEXT_SPLITTER,
|
||||||
TIKTOKEN_ENCODING_NAME,
|
TIKTOKEN_ENCODING_NAME,
|
||||||
|
|
@ -482,9 +497,11 @@ from open_webui.utils.auth import (
|
||||||
)
|
)
|
||||||
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
||||||
from open_webui.utils.oauth import (
|
from open_webui.utils.oauth import (
|
||||||
|
get_oauth_client_info_with_dynamic_client_registration,
|
||||||
|
encrypt_data,
|
||||||
|
decrypt_data,
|
||||||
OAuthManager,
|
OAuthManager,
|
||||||
OAuthClientManager,
|
OAuthClientManager,
|
||||||
decrypt_data,
|
|
||||||
OAuthClientInformationFull,
|
OAuthClientInformationFull,
|
||||||
)
|
)
|
||||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||||
|
|
@ -856,6 +873,7 @@ app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL = DOCLING_PICTURE_DESCRIPTION
|
||||||
app.state.config.DOCLING_PICTURE_DESCRIPTION_API = DOCLING_PICTURE_DESCRIPTION_API
|
app.state.config.DOCLING_PICTURE_DESCRIPTION_API = DOCLING_PICTURE_DESCRIPTION_API
|
||||||
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
|
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
|
||||||
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
|
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
|
||||||
|
app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL
|
||||||
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
|
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
|
||||||
app.state.config.MINERU_API_MODE = MINERU_API_MODE
|
app.state.config.MINERU_API_MODE = MINERU_API_MODE
|
||||||
app.state.config.MINERU_API_URL = MINERU_API_URL
|
app.state.config.MINERU_API_URL = MINERU_API_URL
|
||||||
|
|
@ -1062,27 +1080,40 @@ app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
|
||||||
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
||||||
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
||||||
|
|
||||||
|
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
|
||||||
|
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
||||||
|
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
||||||
|
|
||||||
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||||
app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION
|
app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION
|
||||||
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||||
|
|
||||||
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
|
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
|
||||||
app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
|
app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
|
||||||
|
app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = IMAGES_GEMINI_ENDPOINT_METHOD
|
||||||
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
|
|
||||||
|
|
||||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||||
app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
|
app.state.config.AUTOMATIC1111_PARAMS = AUTOMATIC1111_PARAMS
|
||||||
app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
|
|
||||||
app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
|
|
||||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY
|
app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY
|
||||||
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
||||||
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
||||||
|
|
||||||
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
|
||||||
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
app.state.config.IMAGE_EDIT_ENGINE = IMAGE_EDIT_ENGINE
|
||||||
|
app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL
|
||||||
|
app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE
|
||||||
|
app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = IMAGES_EDIT_OPENAI_API_BASE_URL
|
||||||
|
app.state.config.IMAGES_EDIT_OPENAI_API_KEY = IMAGES_EDIT_OPENAI_API_KEY
|
||||||
|
app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION
|
||||||
|
app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL
|
||||||
|
app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = IMAGES_EDIT_COMFYUI_BASE_URL
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = IMAGES_EDIT_COMFYUI_API_KEY
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = IMAGES_EDIT_COMFYUI_WORKFLOW
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFLOW_NODES
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
|
|
@ -1108,6 +1139,12 @@ app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
|
||||||
app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
|
app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
|
||||||
app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
|
app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
|
||||||
|
|
||||||
|
app.state.config.AUDIO_STT_MISTRAL_API_KEY = AUDIO_STT_MISTRAL_API_KEY
|
||||||
|
app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = AUDIO_STT_MISTRAL_API_BASE_URL
|
||||||
|
app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = (
|
||||||
|
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS
|
||||||
|
)
|
||||||
|
|
||||||
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||||
|
|
||||||
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||||
|
|
@ -1556,11 +1593,15 @@ async def chat_completion(
|
||||||
log.info("Chat processing was cancelled")
|
log.info("Chat processing was cancelled")
|
||||||
try:
|
try:
|
||||||
event_emitter = get_event_emitter(metadata)
|
event_emitter = get_event_emitter(metadata)
|
||||||
await event_emitter(
|
await asyncio.shield(
|
||||||
|
event_emitter(
|
||||||
{"type": "chat:tasks:cancel"},
|
{"type": "chat:tasks:cancel"},
|
||||||
)
|
)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
raise # re-raise to ensure proper task cancellation handling
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"Error processing chat payload: {e}")
|
log.debug(f"Error processing chat payload: {e}")
|
||||||
if metadata.get("chat_id") and metadata.get("message_id"):
|
if metadata.get("chat_id") and metadata.get("message_id"):
|
||||||
|
|
@ -1591,7 +1632,7 @@ async def chat_completion(
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
if mcp_clients := metadata.get("mcp_clients"):
|
if mcp_clients := metadata.get("mcp_clients"):
|
||||||
for client in mcp_clients.values():
|
for client in reversed(mcp_clients.values()):
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"Error cleaning up: {e}")
|
log.debug(f"Error cleaning up: {e}")
|
||||||
|
|
@ -1937,6 +1978,7 @@ if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
|
||||||
if tool_server_connection.get("type", "openapi") == "mcp":
|
if tool_server_connection.get("type", "openapi") == "mcp":
|
||||||
server_id = tool_server_connection.get("info", {}).get("id")
|
server_id = tool_server_connection.get("info", {}).get("id")
|
||||||
auth_type = tool_server_connection.get("auth_type", "none")
|
auth_type = tool_server_connection.get("auth_type", "none")
|
||||||
|
|
||||||
if server_id and auth_type == "oauth_2.1":
|
if server_id and auth_type == "oauth_2.1":
|
||||||
oauth_client_info = tool_server_connection.get("info", {}).get(
|
oauth_client_info = tool_server_connection.get("info", {}).get(
|
||||||
"oauth_client_info", ""
|
"oauth_client_info", ""
|
||||||
|
|
@ -1982,6 +2024,64 @@ except Exception as e:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def register_client(self, request, client_id: str) -> bool:
|
||||||
|
server_type, server_id = client_id.split(":", 1)
|
||||||
|
|
||||||
|
connection = None
|
||||||
|
connection_idx = None
|
||||||
|
|
||||||
|
for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []):
|
||||||
|
if conn.get("type", "openapi") == server_type:
|
||||||
|
info = conn.get("info", {})
|
||||||
|
if info.get("id") == server_id:
|
||||||
|
connection = conn
|
||||||
|
connection_idx = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
if connection is None or connection_idx is None:
|
||||||
|
log.warning(
|
||||||
|
f"Unable to locate MCP tool server configuration for client {client_id} during re-registration"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
server_url = connection.get("url")
|
||||||
|
oauth_server_key = (connection.get("config") or {}).get("oauth_server_key")
|
||||||
|
|
||||||
|
try:
|
||||||
|
oauth_client_info = (
|
||||||
|
await get_oauth_client_info_with_dynamic_client_registration(
|
||||||
|
request,
|
||||||
|
client_id,
|
||||||
|
server_url,
|
||||||
|
oauth_server_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Dynamic client re-registration failed for {client_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
request.app.state.config.TOOL_SERVER_CONNECTIONS[connection_idx] = {
|
||||||
|
**connection,
|
||||||
|
"info": {
|
||||||
|
**connection.get("info", {}),
|
||||||
|
"oauth_client_info": encrypt_data(
|
||||||
|
oauth_client_info.model_dump(mode="json")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.error(
|
||||||
|
f"Failed to persist updated OAuth client info for tool server {client_id}: {e}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
oauth_client_manager.remove_client(client_id)
|
||||||
|
oauth_client_manager.add_client(client_id, oauth_client_info)
|
||||||
|
log.info(f"Re-registered OAuth client {client_id} for tool server")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@app.get("/oauth/clients/{client_id}/authorize")
|
@app.get("/oauth/clients/{client_id}/authorize")
|
||||||
async def oauth_client_authorize(
|
async def oauth_client_authorize(
|
||||||
client_id: str,
|
client_id: str,
|
||||||
|
|
@ -1989,6 +2089,41 @@ async def oauth_client_authorize(
|
||||||
response: Response,
|
response: Response,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
|
# ensure_valid_client_registration
|
||||||
|
client = oauth_client_manager.get_client(client_id)
|
||||||
|
client_info = oauth_client_manager.get_client_info(client_id)
|
||||||
|
if client is None or client_info is None:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if not await oauth_client_manager._preflight_authorization_url(client, client_info):
|
||||||
|
log.info(
|
||||||
|
"Detected invalid OAuth client %s; attempting re-registration",
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
registered = await register_client(request, client_id)
|
||||||
|
if not registered:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to re-register OAuth client",
|
||||||
|
)
|
||||||
|
|
||||||
|
client = oauth_client_manager.get_client(client_id)
|
||||||
|
client_info = oauth_client_manager.get_client_info(client_id)
|
||||||
|
if client is None or client_info is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="OAuth client unavailable after re-registration",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not await oauth_client_manager._preflight_authorization_url(
|
||||||
|
client, client_info
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="OAuth client registration is still invalid after re-registration",
|
||||||
|
)
|
||||||
|
|
||||||
return await oauth_client_manager.handle_authorize(request, client_id=client_id)
|
return await oauth_client_manager.handle_authorize(request, client_id=client_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,10 @@ class ChatTable:
|
||||||
order_by = filter.get("order_by")
|
order_by = filter.get("order_by")
|
||||||
direction = filter.get("direction")
|
direction = filter.get("direction")
|
||||||
|
|
||||||
if order_by and direction and getattr(Chat, order_by):
|
if order_by and direction:
|
||||||
|
if not getattr(Chat, order_by, None):
|
||||||
|
raise ValueError("Invalid order_by field")
|
||||||
|
|
||||||
if direction.lower() == "asc":
|
if direction.lower() == "asc":
|
||||||
query = query.order_by(getattr(Chat, order_by).asc())
|
query = query.order_by(getattr(Chat, order_by).asc())
|
||||||
elif direction.lower() == "desc":
|
elif direction.lower() == "desc":
|
||||||
|
|
@ -762,15 +765,20 @@ class ChatTable:
|
||||||
)
|
)
|
||||||
|
|
||||||
elif dialect_name == "postgresql":
|
elif dialect_name == "postgresql":
|
||||||
# PostgreSQL relies on proper JSON query for search
|
# PostgreSQL doesn't allow null bytes in text. We filter those out by checking
|
||||||
|
# the JSON representation for \u0000 before attempting text extraction
|
||||||
postgres_content_sql = (
|
postgres_content_sql = (
|
||||||
"EXISTS ("
|
"EXISTS ("
|
||||||
" SELECT 1 "
|
" SELECT 1 "
|
||||||
" FROM json_array_elements(Chat.chat->'messages') AS message "
|
" FROM json_array_elements(Chat.chat->'messages') AS message "
|
||||||
" WHERE LOWER(message->>'content') LIKE '%' || :content_key || '%'"
|
" WHERE message->'content' IS NOT NULL "
|
||||||
|
" AND (message->'content')::text NOT LIKE '%\\u0000%' "
|
||||||
|
" AND LOWER(message->>'content') LIKE '%' || :content_key || '%'"
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
postgres_content_clause = text(postgres_content_sql)
|
postgres_content_clause = text(postgres_content_sql)
|
||||||
|
# Also filter out chats with null bytes in title
|
||||||
|
query = query.filter(text("Chat.title::text NOT LIKE '%\\x00%'"))
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
Chat.title.ilike(bindparam("title_key")),
|
Chat.title.ilike(bindparam("title_key")),
|
||||||
|
|
|
||||||
|
|
@ -262,5 +262,16 @@ class OAuthSessionTable:
|
||||||
log.error(f"Error deleting OAuth sessions by user ID: {e}")
|
log.error(f"Error deleting OAuth sessions by user ID: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def delete_sessions_by_provider(self, provider: str) -> bool:
|
||||||
|
"""Delete all OAuth sessions for a provider"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
db.query(OAuthSession).filter_by(provider=provider).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error deleting OAuth sessions by provider {provider}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
OAuthSessions = OAuthSessionTable()
|
OAuthSessions = OAuthSessionTable()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from urllib.parse import quote
|
||||||
|
|
||||||
from langchain_core.document_loaders import BaseLoader
|
from langchain_core.document_loaders import BaseLoader
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
|
from open_webui.utils.headers import include_user_info_headers
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -18,6 +19,7 @@ class ExternalDocumentLoader(BaseLoader):
|
||||||
url: str,
|
url: str,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
mime_type=None,
|
mime_type=None,
|
||||||
|
user=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
@ -26,6 +28,8 @@ class ExternalDocumentLoader(BaseLoader):
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.mime_type = mime_type
|
self.mime_type = mime_type
|
||||||
|
|
||||||
|
self.user = user
|
||||||
|
|
||||||
def load(self) -> List[Document]:
|
def load(self) -> List[Document]:
|
||||||
with open(self.file_path, "rb") as f:
|
with open(self.file_path, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
@ -42,6 +46,9 @@ class ExternalDocumentLoader(BaseLoader):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if self.user is not None:
|
||||||
|
headers = include_user_info_headers(headers, self.user)
|
||||||
|
|
||||||
url = self.url
|
url = self.url
|
||||||
if url.endswith("/"):
|
if url.endswith("/"):
|
||||||
url = url[:-1]
|
url = url[:-1]
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,7 @@ class DoclingLoader:
|
||||||
class Loader:
|
class Loader:
|
||||||
def __init__(self, engine: str = "", **kwargs):
|
def __init__(self, engine: str = "", **kwargs):
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
|
self.user = kwargs.get("user", None)
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def load(
|
def load(
|
||||||
|
|
@ -264,6 +265,7 @@ class Loader:
|
||||||
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
|
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
|
||||||
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
|
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
|
||||||
mime_type=file_content_type,
|
mime_type=file_content_type,
|
||||||
|
user=self.user,
|
||||||
)
|
)
|
||||||
elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
|
elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
|
||||||
if self._is_text_file(file_ext, file_content_type):
|
if self._is_text_file(file_ext, file_content_type):
|
||||||
|
|
@ -272,7 +274,6 @@ class Loader:
|
||||||
loader = TikaLoader(
|
loader = TikaLoader(
|
||||||
url=self.kwargs.get("TIKA_SERVER_URL"),
|
url=self.kwargs.get("TIKA_SERVER_URL"),
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
mime_type=file_content_type,
|
|
||||||
extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
|
extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
|
||||||
)
|
)
|
||||||
elif (
|
elif (
|
||||||
|
|
@ -369,14 +370,8 @@ class Loader:
|
||||||
azure_credential=DefaultAzureCredential(),
|
azure_credential=DefaultAzureCredential(),
|
||||||
)
|
)
|
||||||
elif self.engine == "mineru" and file_ext in [
|
elif self.engine == "mineru" and file_ext in [
|
||||||
"pdf",
|
"pdf"
|
||||||
"doc",
|
]: # MinerU currently only supports PDF
|
||||||
"docx",
|
|
||||||
"ppt",
|
|
||||||
"pptx",
|
|
||||||
"xls",
|
|
||||||
"xlsx",
|
|
||||||
]:
|
|
||||||
loader = MinerULoader(
|
loader = MinerULoader(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
api_mode=self.kwargs.get("MINERU_API_MODE", "local"),
|
api_mode=self.kwargs.get("MINERU_API_MODE", "local"),
|
||||||
|
|
@ -391,16 +386,9 @@ class Loader:
|
||||||
in ["pdf"] # Mistral OCR currently only supports PDF and images
|
in ["pdf"] # Mistral OCR currently only supports PDF and images
|
||||||
):
|
):
|
||||||
loader = MistralLoader(
|
loader = MistralLoader(
|
||||||
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
|
base_url=self.kwargs.get("MISTRAL_OCR_API_BASE_URL"),
|
||||||
)
|
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"),
|
||||||
elif (
|
file_path=file_path,
|
||||||
self.engine == "external"
|
|
||||||
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
|
|
||||||
and file_ext
|
|
||||||
in ["pdf"] # Mistral OCR currently only supports PDF and images
|
|
||||||
):
|
|
||||||
loader = MistralLoader(
|
|
||||||
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if file_ext == "pdf":
|
if file_ext == "pdf":
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,9 @@ class MistralLoader:
|
||||||
- Enhanced error handling with retryable error classification
|
- Enhanced error handling with retryable error classification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
BASE_API_URL = "https://api.mistral.ai/v1"
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
base_url: str,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
timeout: int = 300, # 5 minutes default
|
timeout: int = 300, # 5 minutes default
|
||||||
|
|
@ -55,6 +54,9 @@ class MistralLoader:
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
raise FileNotFoundError(f"File not found at {file_path}")
|
raise FileNotFoundError(f"File not found at {file_path}")
|
||||||
|
|
||||||
|
self.base_url = (
|
||||||
|
base_url.rstrip("/") if base_url else "https://api.mistral.ai/v1"
|
||||||
|
)
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
@ -240,7 +242,7 @@ class MistralLoader:
|
||||||
in a context manager to minimize memory usage duration.
|
in a context manager to minimize memory usage duration.
|
||||||
"""
|
"""
|
||||||
log.info("Uploading file to Mistral API")
|
log.info("Uploading file to Mistral API")
|
||||||
url = f"{self.BASE_API_URL}/files"
|
url = f"{self.base_url}/files"
|
||||||
|
|
||||||
def upload_request():
|
def upload_request():
|
||||||
# MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime
|
# MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime
|
||||||
|
|
@ -275,7 +277,7 @@ class MistralLoader:
|
||||||
|
|
||||||
async def _upload_file_async(self, session: aiohttp.ClientSession) -> str:
|
async def _upload_file_async(self, session: aiohttp.ClientSession) -> str:
|
||||||
"""Async file upload with streaming for better memory efficiency."""
|
"""Async file upload with streaming for better memory efficiency."""
|
||||||
url = f"{self.BASE_API_URL}/files"
|
url = f"{self.base_url}/files"
|
||||||
|
|
||||||
async def upload_request():
|
async def upload_request():
|
||||||
# Create multipart writer for streaming upload
|
# Create multipart writer for streaming upload
|
||||||
|
|
@ -321,7 +323,7 @@ class MistralLoader:
|
||||||
def _get_signed_url(self, file_id: str) -> str:
|
def _get_signed_url(self, file_id: str) -> str:
|
||||||
"""Retrieves a temporary signed URL for the uploaded file (sync version)."""
|
"""Retrieves a temporary signed URL for the uploaded file (sync version)."""
|
||||||
log.info(f"Getting signed URL for file ID: {file_id}")
|
log.info(f"Getting signed URL for file ID: {file_id}")
|
||||||
url = f"{self.BASE_API_URL}/files/{file_id}/url"
|
url = f"{self.base_url}/files/{file_id}/url"
|
||||||
params = {"expiry": 1}
|
params = {"expiry": 1}
|
||||||
signed_url_headers = {**self.headers, "Accept": "application/json"}
|
signed_url_headers = {**self.headers, "Accept": "application/json"}
|
||||||
|
|
||||||
|
|
@ -346,7 +348,7 @@ class MistralLoader:
|
||||||
self, session: aiohttp.ClientSession, file_id: str
|
self, session: aiohttp.ClientSession, file_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Async signed URL retrieval."""
|
"""Async signed URL retrieval."""
|
||||||
url = f"{self.BASE_API_URL}/files/{file_id}/url"
|
url = f"{self.base_url}/files/{file_id}/url"
|
||||||
params = {"expiry": 1}
|
params = {"expiry": 1}
|
||||||
|
|
||||||
headers = {**self.headers, "Accept": "application/json"}
|
headers = {**self.headers, "Accept": "application/json"}
|
||||||
|
|
@ -373,7 +375,7 @@ class MistralLoader:
|
||||||
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
|
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
|
||||||
"""Sends the signed URL to the OCR endpoint for processing (sync version)."""
|
"""Sends the signed URL to the OCR endpoint for processing (sync version)."""
|
||||||
log.info("Processing OCR via Mistral API")
|
log.info("Processing OCR via Mistral API")
|
||||||
url = f"{self.BASE_API_URL}/ocr"
|
url = f"{self.base_url}/ocr"
|
||||||
ocr_headers = {
|
ocr_headers = {
|
||||||
**self.headers,
|
**self.headers,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -407,7 +409,7 @@ class MistralLoader:
|
||||||
self, session: aiohttp.ClientSession, signed_url: str
|
self, session: aiohttp.ClientSession, signed_url: str
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Async OCR processing with timing metrics."""
|
"""Async OCR processing with timing metrics."""
|
||||||
url = f"{self.BASE_API_URL}/ocr"
|
url = f"{self.base_url}/ocr"
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
**self.headers,
|
**self.headers,
|
||||||
|
|
@ -446,7 +448,7 @@ class MistralLoader:
|
||||||
def _delete_file(self, file_id: str) -> None:
|
def _delete_file(self, file_id: str) -> None:
|
||||||
"""Deletes the file from Mistral storage (sync version)."""
|
"""Deletes the file from Mistral storage (sync version)."""
|
||||||
log.info(f"Deleting uploaded file ID: {file_id}")
|
log.info(f"Deleting uploaded file ID: {file_id}")
|
||||||
url = f"{self.BASE_API_URL}/files/{file_id}"
|
url = f"{self.base_url}/files/{file_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.delete(
|
response = requests.delete(
|
||||||
|
|
@ -467,7 +469,7 @@ class MistralLoader:
|
||||||
async def delete_request():
|
async def delete_request():
|
||||||
self._debug_log(f"Deleting file ID: {file_id}")
|
self._debug_log(f"Deleting file ID: {file_id}")
|
||||||
async with session.delete(
|
async with session.delete(
|
||||||
url=f"{self.BASE_API_URL}/files/{file_id}",
|
url=f"{self.base_url}/files/{file_id}",
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
timeout=aiohttp.ClientTimeout(
|
timeout=aiohttp.ClientTimeout(
|
||||||
total=self.cleanup_timeout
|
total=self.cleanup_timeout
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ class YoutubeLoader:
|
||||||
TranscriptsDisabled,
|
TranscriptsDisabled,
|
||||||
YouTubeTranscriptApi,
|
YouTubeTranscriptApi,
|
||||||
)
|
)
|
||||||
|
from youtube_transcript_api.proxies import GenericProxyConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
'Could not import "youtube_transcript_api" Python package. '
|
'Could not import "youtube_transcript_api" Python package. '
|
||||||
|
|
@ -90,10 +91,9 @@ class YoutubeLoader:
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.proxy_url:
|
if self.proxy_url:
|
||||||
youtube_proxies = {
|
youtube_proxies = GenericProxyConfig(
|
||||||
"http": self.proxy_url,
|
http_url=self.proxy_url, https_url=self.proxy_url
|
||||||
"https": self.proxy_url,
|
)
|
||||||
}
|
|
||||||
log.debug(f"Using proxy URL: {self.proxy_url[:14]}...")
|
log.debug(f"Using proxy URL: {self.proxy_url[:14]}...")
|
||||||
else:
|
else:
|
||||||
youtube_proxies = None
|
youtube_proxies = None
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ def get_loader(request, url: str):
|
||||||
url,
|
url,
|
||||||
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
|
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
|
||||||
requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS,
|
requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS,
|
||||||
|
trust_env=request.app.state.config.WEB_SEARCH_TRUST_ENV,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -668,13 +669,18 @@ def get_sources_from_items(
|
||||||
collection_names.append(f"file-{item['id']}")
|
collection_names.append(f"file-{item['id']}")
|
||||||
|
|
||||||
elif item.get("type") == "collection":
|
elif item.get("type") == "collection":
|
||||||
|
# Manual Full Mode Toggle for Collection
|
||||||
|
knowledge_base = Knowledges.get_knowledge_by_id(item.get("id"))
|
||||||
|
|
||||||
|
if knowledge_base and (
|
||||||
|
user.role == "admin"
|
||||||
|
or knowledge_base.user_id == user.id
|
||||||
|
or has_access(user.id, "read", knowledge_base.access_control)
|
||||||
|
):
|
||||||
if (
|
if (
|
||||||
item.get("context") == "full"
|
item.get("context") == "full"
|
||||||
or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
|
or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||||
):
|
):
|
||||||
# Manual Full Mode Toggle for Collection
|
|
||||||
knowledge_base = Knowledges.get_knowledge_by_id(item.get("id"))
|
|
||||||
|
|
||||||
if knowledge_base and (
|
if knowledge_base and (
|
||||||
user.role == "admin"
|
user.role == "admin"
|
||||||
or knowledge_base.user_id == user.id
|
or knowledge_base.user_id == user.id
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
|
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
from firecrawl import Firecrawl
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
@ -18,27 +18,18 @@ def search_firecrawl(
|
||||||
filter_list: Optional[List[str]] = None,
|
filter_list: Optional[List[str]] = None,
|
||||||
) -> List[SearchResult]:
|
) -> List[SearchResult]:
|
||||||
try:
|
try:
|
||||||
firecrawl_search_url = urljoin(firecrawl_url, "/v1/search")
|
firecrawl = Firecrawl(api_key=firecrawl_api_key, api_url=firecrawl_url)
|
||||||
response = requests.post(
|
response = firecrawl.search(
|
||||||
firecrawl_search_url,
|
query=query, limit=count, ignore_invalid_urls=True, timeout=count * 3
|
||||||
headers={
|
|
||||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
|
||||||
"Authorization": f"Bearer {firecrawl_api_key}",
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"query": query,
|
|
||||||
"limit": count,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
results = response.web
|
||||||
results = response.json().get("data", [])
|
|
||||||
if filter_list:
|
if filter_list:
|
||||||
results = get_filtered_results(results, filter_list)
|
results = get_filtered_results(results, filter_list)
|
||||||
results = [
|
results = [
|
||||||
SearchResult(
|
SearchResult(
|
||||||
link=result.get("url"),
|
link=result.url,
|
||||||
title=result.get("title"),
|
title=result.title,
|
||||||
snippet=result.get("description"),
|
snippet=result.description,
|
||||||
)
|
)
|
||||||
for result in results[:count]
|
for result in results[:count]
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ def search_google_pse(
|
||||||
query: str,
|
query: str,
|
||||||
count: int,
|
count: int,
|
||||||
filter_list: Optional[list[str]] = None,
|
filter_list: Optional[list[str]] = None,
|
||||||
|
referer: Optional[str] = None,
|
||||||
) -> list[SearchResult]:
|
) -> list[SearchResult]:
|
||||||
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
||||||
Handles pagination for counts greater than 10.
|
Handles pagination for counts greater than 10.
|
||||||
|
|
@ -30,7 +31,11 @@ def search_google_pse(
|
||||||
list[SearchResult]: A list of SearchResult objects.
|
list[SearchResult]: A list of SearchResult objects.
|
||||||
"""
|
"""
|
||||||
url = "https://www.googleapis.com/customsearch/v1"
|
url = "https://www.googleapis.com/customsearch/v1"
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if referer:
|
||||||
|
headers["Referer"] = referer
|
||||||
|
|
||||||
all_results = []
|
all_results = []
|
||||||
start_index = 1 # Google PSE start parameter is 1-based
|
start_index = 1 # Google PSE start parameter is 1-based
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import socket
|
||||||
import ssl
|
import ssl
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
|
@ -17,11 +16,12 @@ from typing import (
|
||||||
Union,
|
Union,
|
||||||
Literal,
|
Literal,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import certifi
|
import certifi
|
||||||
import validators
|
import validators
|
||||||
from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader
|
from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader
|
||||||
from langchain_community.document_loaders.firecrawl import FireCrawlLoader
|
|
||||||
from langchain_community.document_loaders.base import BaseLoader
|
from langchain_community.document_loaders.base import BaseLoader
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
from open_webui.retrieval.loaders.tavily import TavilyLoader
|
from open_webui.retrieval.loaders.tavily import TavilyLoader
|
||||||
|
|
@ -39,7 +39,9 @@ from open_webui.config import (
|
||||||
EXTERNAL_WEB_LOADER_URL,
|
EXTERNAL_WEB_LOADER_URL,
|
||||||
EXTERNAL_WEB_LOADER_API_KEY,
|
EXTERNAL_WEB_LOADER_API_KEY,
|
||||||
)
|
)
|
||||||
from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
from firecrawl import Firecrawl
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
@ -142,13 +144,13 @@ class RateLimitMixin:
|
||||||
|
|
||||||
|
|
||||||
class URLProcessingMixin:
|
class URLProcessingMixin:
|
||||||
def _verify_ssl_cert(self, url: str) -> bool:
|
async def _verify_ssl_cert(self, url: str) -> bool:
|
||||||
"""Verify SSL certificate for a URL."""
|
"""Verify SSL certificate for a URL."""
|
||||||
return verify_ssl_cert(url)
|
return await run_in_threadpool(verify_ssl_cert, url)
|
||||||
|
|
||||||
async def _safe_process_url(self, url: str) -> bool:
|
async def _safe_process_url(self, url: str) -> bool:
|
||||||
"""Perform safety checks before processing a URL."""
|
"""Perform safety checks before processing a URL."""
|
||||||
if self.verify_ssl and not self._verify_ssl_cert(url):
|
if self.verify_ssl and not await self._verify_ssl_cert(url):
|
||||||
raise ValueError(f"SSL certificate verification failed for {url}")
|
raise ValueError(f"SSL certificate verification failed for {url}")
|
||||||
await self._wait_for_rate_limit()
|
await self._wait_for_rate_limit()
|
||||||
return True
|
return True
|
||||||
|
|
@ -189,13 +191,12 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
|
||||||
(uses FIRE_CRAWL_API_KEY environment variable if not provided).
|
(uses FIRE_CRAWL_API_KEY environment variable if not provided).
|
||||||
api_url: Base URL for FireCrawl API. Defaults to official API endpoint.
|
api_url: Base URL for FireCrawl API. Defaults to official API endpoint.
|
||||||
mode: Operation mode selection:
|
mode: Operation mode selection:
|
||||||
- 'crawl': Website crawling mode (default)
|
- 'crawl': Website crawling mode
|
||||||
- 'scrape': Direct page scraping
|
- 'scrape': Direct page scraping (default)
|
||||||
- 'map': Site map generation
|
- 'map': Site map generation
|
||||||
proxy: Proxy override settings for the FireCrawl API.
|
proxy: Proxy override settings for the FireCrawl API.
|
||||||
params: The parameters to pass to the Firecrawl API.
|
params: The parameters to pass to the Firecrawl API.
|
||||||
Examples include crawlerOptions.
|
For more details, visit: https://docs.firecrawl.dev/sdks/python#batch-scrape
|
||||||
For more details, visit: https://github.com/mendableai/firecrawl-py
|
|
||||||
"""
|
"""
|
||||||
proxy_server = proxy.get("server") if proxy else None
|
proxy_server = proxy.get("server") if proxy else None
|
||||||
if trust_env and not proxy_server:
|
if trust_env and not proxy_server:
|
||||||
|
|
@ -215,50 +216,84 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.api_url = api_url
|
self.api_url = api_url
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.params = params
|
self.params = params or {}
|
||||||
|
|
||||||
def lazy_load(self) -> Iterator[Document]:
|
def lazy_load(self) -> Iterator[Document]:
|
||||||
"""Load documents concurrently using FireCrawl."""
|
"""Load documents using FireCrawl batch_scrape."""
|
||||||
for url in self.web_paths:
|
log.debug(
|
||||||
try:
|
"Starting FireCrawl batch scrape for %d URLs, mode: %s, params: %s",
|
||||||
self._safe_process_url_sync(url)
|
len(self.web_paths),
|
||||||
loader = FireCrawlLoader(
|
self.mode,
|
||||||
url=url,
|
self.params,
|
||||||
api_key=self.api_key,
|
|
||||||
api_url=self.api_url,
|
|
||||||
mode=self.mode,
|
|
||||||
params=self.params,
|
|
||||||
)
|
)
|
||||||
for document in loader.lazy_load():
|
try:
|
||||||
if not document.metadata.get("source"):
|
firecrawl = Firecrawl(api_key=self.api_key, api_url=self.api_url)
|
||||||
document.metadata["source"] = document.metadata.get("sourceURL")
|
result = firecrawl.batch_scrape(
|
||||||
yield document
|
self.web_paths,
|
||||||
|
formats=["markdown"],
|
||||||
|
skip_tls_verification=not self.verify_ssl,
|
||||||
|
ignore_invalid_urls=True,
|
||||||
|
remove_base64_images=True,
|
||||||
|
max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values
|
||||||
|
wait_timeout=len(self.web_paths) * 3,
|
||||||
|
**self.params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status != "completed":
|
||||||
|
raise RuntimeError(
|
||||||
|
f"FireCrawl batch scrape did not complete successfully. result: {result}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for data in result.data:
|
||||||
|
metadata = data.metadata or {}
|
||||||
|
yield Document(
|
||||||
|
page_content=data.markdown or "",
|
||||||
|
metadata={"source": metadata.url or metadata.source_url or ""},
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.continue_on_failure:
|
if self.continue_on_failure:
|
||||||
log.exception(f"Error loading {url}: {e}")
|
log.exception(f"Error extracting content from URLs: {e}")
|
||||||
continue
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def alazy_load(self):
|
async def alazy_load(self):
|
||||||
"""Async version of lazy_load."""
|
"""Async version of lazy_load."""
|
||||||
for url in self.web_paths:
|
log.debug(
|
||||||
try:
|
"Starting FireCrawl batch scrape for %d URLs, mode: %s, params: %s",
|
||||||
await self._safe_process_url(url)
|
len(self.web_paths),
|
||||||
loader = FireCrawlLoader(
|
self.mode,
|
||||||
url=url,
|
self.params,
|
||||||
api_key=self.api_key,
|
|
||||||
api_url=self.api_url,
|
|
||||||
mode=self.mode,
|
|
||||||
params=self.params,
|
|
||||||
)
|
)
|
||||||
async for document in loader.alazy_load():
|
try:
|
||||||
if not document.metadata.get("source"):
|
firecrawl = Firecrawl(api_key=self.api_key, api_url=self.api_url)
|
||||||
document.metadata["source"] = document.metadata.get("sourceURL")
|
result = firecrawl.batch_scrape(
|
||||||
yield document
|
self.web_paths,
|
||||||
|
formats=["markdown"],
|
||||||
|
skip_tls_verification=not self.verify_ssl,
|
||||||
|
ignore_invalid_urls=True,
|
||||||
|
remove_base64_images=True,
|
||||||
|
max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values
|
||||||
|
wait_timeout=len(self.web_paths) * 3,
|
||||||
|
**self.params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status != "completed":
|
||||||
|
raise RuntimeError(
|
||||||
|
f"FireCrawl batch scrape did not complete successfully. result: {result}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for data in result.data:
|
||||||
|
metadata = data.metadata or {}
|
||||||
|
yield Document(
|
||||||
|
page_content=data.markdown or "",
|
||||||
|
metadata={"source": metadata.url or metadata.source_url or ""},
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.continue_on_failure:
|
if self.continue_on_failure:
|
||||||
log.exception(f"Error loading {url}: {e}")
|
log.exception(f"Error extracting content from URLs: {e}")
|
||||||
continue
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import html
|
import html
|
||||||
|
import base64
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from pydub.silence import split_on_silence
|
from pydub.silence import split_on_silence
|
||||||
|
|
@ -39,13 +40,14 @@ from open_webui.config import (
|
||||||
WHISPER_MODEL_DIR,
|
WHISPER_MODEL_DIR,
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
WHISPER_LANGUAGE,
|
WHISPER_LANGUAGE,
|
||||||
|
ELEVENLABS_API_BASE_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
|
ENV,
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
ENV,
|
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
DEVICE_TYPE,
|
DEVICE_TYPE,
|
||||||
ENABLE_FORWARD_USER_INFO_HEADERS,
|
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||||
|
|
@ -178,6 +180,9 @@ class STTConfigForm(BaseModel):
|
||||||
AZURE_LOCALES: str
|
AZURE_LOCALES: str
|
||||||
AZURE_BASE_URL: str
|
AZURE_BASE_URL: str
|
||||||
AZURE_MAX_SPEAKERS: str
|
AZURE_MAX_SPEAKERS: str
|
||||||
|
MISTRAL_API_KEY: str
|
||||||
|
MISTRAL_API_BASE_URL: str
|
||||||
|
MISTRAL_USE_CHAT_COMPLETIONS: bool
|
||||||
|
|
||||||
|
|
||||||
class AudioConfigUpdateForm(BaseModel):
|
class AudioConfigUpdateForm(BaseModel):
|
||||||
|
|
@ -214,6 +219,9 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
|
||||||
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
|
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
|
||||||
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
|
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
|
||||||
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
|
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
|
||||||
|
"MISTRAL_API_KEY": request.app.state.config.AUDIO_STT_MISTRAL_API_KEY,
|
||||||
|
"MISTRAL_API_BASE_URL": request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL,
|
||||||
|
"MISTRAL_USE_CHAT_COMPLETIONS": request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,6 +263,13 @@ async def update_audio_config(
|
||||||
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = (
|
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = (
|
||||||
form_data.stt.AZURE_MAX_SPEAKERS
|
form_data.stt.AZURE_MAX_SPEAKERS
|
||||||
)
|
)
|
||||||
|
request.app.state.config.AUDIO_STT_MISTRAL_API_KEY = form_data.stt.MISTRAL_API_KEY
|
||||||
|
request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = (
|
||||||
|
form_data.stt.MISTRAL_API_BASE_URL
|
||||||
|
)
|
||||||
|
request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = (
|
||||||
|
form_data.stt.MISTRAL_USE_CHAT_COMPLETIONS
|
||||||
|
)
|
||||||
|
|
||||||
if request.app.state.config.STT_ENGINE == "":
|
if request.app.state.config.STT_ENGINE == "":
|
||||||
request.app.state.faster_whisper_model = set_faster_whisper_model(
|
request.app.state.faster_whisper_model = set_faster_whisper_model(
|
||||||
|
|
@ -290,6 +305,9 @@ async def update_audio_config(
|
||||||
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
|
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
|
||||||
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
|
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
|
||||||
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
|
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
|
||||||
|
"MISTRAL_API_KEY": request.app.state.config.AUDIO_STT_MISTRAL_API_KEY,
|
||||||
|
"MISTRAL_API_BASE_URL": request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL,
|
||||||
|
"MISTRAL_USE_CHAT_COMPLETIONS": request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +431,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
timeout=timeout, trust_env=True
|
timeout=timeout, trust_env=True
|
||||||
) as session:
|
) as session:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
|
f"{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}",
|
||||||
json={
|
json={
|
||||||
"text": payload["input"],
|
"text": payload["input"],
|
||||||
"model_id": request.app.state.config.TTS_MODEL,
|
"model_id": request.app.state.config.TTS_MODEL,
|
||||||
|
|
@ -828,6 +846,186 @@ def transcription_handler(request, file_path, metadata):
|
||||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif request.app.state.config.STT_ENGINE == "mistral":
|
||||||
|
# Check file exists
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise HTTPException(status_code=400, detail="Audio file not found")
|
||||||
|
|
||||||
|
# Check file size
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
if file_size > MAX_FILE_SIZE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"File size exceeds limit of {MAX_FILE_SIZE_MB}MB",
|
||||||
|
)
|
||||||
|
|
||||||
|
api_key = request.app.state.config.AUDIO_STT_MISTRAL_API_KEY
|
||||||
|
api_base_url = (
|
||||||
|
request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL
|
||||||
|
or "https://api.mistral.ai/v1"
|
||||||
|
)
|
||||||
|
use_chat_completions = (
|
||||||
|
request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Mistral API key is required for Mistral STT",
|
||||||
|
)
|
||||||
|
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
# Use voxtral-mini-latest as the default model for transcription
|
||||||
|
model = request.app.state.config.STT_MODEL or "voxtral-mini-latest"
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f"Mistral STT - model: {model}, "
|
||||||
|
f"method: {'chat_completions' if use_chat_completions else 'transcriptions'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if use_chat_completions:
|
||||||
|
# Use chat completions API with audio input
|
||||||
|
# This method requires mp3 or wav format
|
||||||
|
audio_file_to_use = file_path
|
||||||
|
|
||||||
|
if is_audio_conversion_required(file_path):
|
||||||
|
log.debug("Converting audio to mp3 for chat completions API")
|
||||||
|
converted_path = convert_audio_to_mp3(file_path)
|
||||||
|
if converted_path:
|
||||||
|
audio_file_to_use = converted_path
|
||||||
|
else:
|
||||||
|
log.error("Audio conversion failed")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Audio conversion failed. Chat completions API requires mp3 or wav format.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read and encode audio file as base64
|
||||||
|
with open(audio_file_to_use, "rb") as audio_file:
|
||||||
|
audio_base64 = base64.b64encode(audio_file.read()).decode("utf-8")
|
||||||
|
|
||||||
|
# Prepare chat completions request
|
||||||
|
url = f"{api_base_url}/chat/completions"
|
||||||
|
|
||||||
|
# Add language instruction if specified
|
||||||
|
language = metadata.get("language", None) if metadata else None
|
||||||
|
if language:
|
||||||
|
text_instruction = f"Transcribe this audio exactly as spoken in {language}. Do not translate it."
|
||||||
|
else:
|
||||||
|
text_instruction = "Transcribe this audio exactly as spoken in its original language. Do not translate it to another language."
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "input_audio",
|
||||||
|
"input_audio": audio_base64,
|
||||||
|
},
|
||||||
|
{"type": "text", "text": text_instruction},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
url=url,
|
||||||
|
json=payload,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
response = r.json()
|
||||||
|
|
||||||
|
# Extract transcript from chat completion response
|
||||||
|
transcript = (
|
||||||
|
response.get("choices", [{}])[0]
|
||||||
|
.get("message", {})
|
||||||
|
.get("content", "")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
if not transcript:
|
||||||
|
raise ValueError("Empty transcript in response")
|
||||||
|
|
||||||
|
data = {"text": transcript}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Use dedicated transcriptions API
|
||||||
|
url = f"{api_base_url}/audio/transcriptions"
|
||||||
|
|
||||||
|
# Determine the MIME type
|
||||||
|
mime_type, _ = mimetypes.guess_type(file_path)
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = "audio/webm"
|
||||||
|
|
||||||
|
# Use context manager to ensure file is properly closed
|
||||||
|
with open(file_path, "rb") as audio_file:
|
||||||
|
files = {"file": (filename, audio_file, mime_type)}
|
||||||
|
data_form = {"model": model}
|
||||||
|
|
||||||
|
# Add language if specified in metadata
|
||||||
|
language = metadata.get("language", None) if metadata else None
|
||||||
|
if language:
|
||||||
|
data_form["language"] = language
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
url=url,
|
||||||
|
files=files,
|
||||||
|
data=data_form,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
response = r.json()
|
||||||
|
|
||||||
|
# Extract transcript from response
|
||||||
|
transcript = response.get("text", "").strip()
|
||||||
|
if not transcript:
|
||||||
|
raise ValueError("Empty transcript in response")
|
||||||
|
|
||||||
|
data = {"text": transcript}
|
||||||
|
|
||||||
|
# Save transcript to json file (consistent with other providers)
|
||||||
|
transcript_file = f"{file_dir}/{id}.json"
|
||||||
|
with open(transcript_file, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
log.debug(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
log.exception("Error parsing Mistral response")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Failed to parse Mistral response: {str(e)}",
|
||||||
|
)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
log.exception(e)
|
||||||
|
detail = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if r is not None and r.status_code != 200:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
detail = f"External: {res['error'].get('message', '')}"
|
||||||
|
else:
|
||||||
|
detail = f"External: {r.text}"
|
||||||
|
except Exception:
|
||||||
|
detail = f"External: {e}"
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=getattr(r, "status_code", 500) if r else 500,
|
||||||
|
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None):
|
def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None):
|
||||||
log.info(f"transcribe: {file_path} {metadata}")
|
log.info(f"transcribe: {file_path} {metadata}")
|
||||||
|
|
@ -1037,7 +1235,7 @@ def get_available_models(request: Request) -> list[dict]:
|
||||||
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
|
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"https://api.elevenlabs.io/v1/models",
|
f"{ELEVENLABS_API_BASE_URL}/v1/models",
|
||||||
headers={
|
headers={
|
||||||
"xi-api-key": request.app.state.config.TTS_API_KEY,
|
"xi-api-key": request.app.state.config.TTS_API_KEY,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -1141,7 +1339,7 @@ def get_elevenlabs_voices(api_key: str) -> dict:
|
||||||
try:
|
try:
|
||||||
# TODO: Add retries
|
# TODO: Add retries
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"https://api.elevenlabs.io/v1/voices",
|
f"{ELEVENLABS_API_BASE_URL}/v1/voices",
|
||||||
headers={
|
headers={
|
||||||
"xi-api-key": api_key,
|
"xi-api-key": api_key,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,15 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||||
|
|
||||||
user = Auths.authenticate_user(admin_email.lower(), admin_password)
|
user = Auths.authenticate_user(admin_email.lower(), admin_password)
|
||||||
else:
|
else:
|
||||||
|
password_bytes = form_data.password.encode("utf-8")
|
||||||
|
if len(password_bytes) > 72:
|
||||||
|
# TODO: Implement other hashing algorithms that support longer passwords
|
||||||
|
log.info("Password too long, truncating to 72 bytes for bcrypt")
|
||||||
|
password_bytes = password_bytes[:72]
|
||||||
|
|
||||||
|
# decode safely — ignore incomplete UTF-8 sequences
|
||||||
|
form_data.password = password_bytes.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import copy
|
||||||
from fastapi import APIRouter, Depends, Request, HTTPException
|
from fastapi import APIRouter, Depends, Request, HTTPException
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
@ -15,6 +16,7 @@ from open_webui.utils.tools import (
|
||||||
set_tool_servers,
|
set_tool_servers,
|
||||||
)
|
)
|
||||||
from open_webui.utils.mcp.client import MCPClient
|
from open_webui.utils.mcp.client import MCPClient
|
||||||
|
from open_webui.models.oauth_sessions import OAuthSessions
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
@ -165,6 +167,21 @@ async def set_tool_servers_config(
|
||||||
form_data: ToolServersConfigForm,
|
form_data: ToolServersConfigForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
|
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||||
|
server_type = connection.get("type", "openapi")
|
||||||
|
auth_type = connection.get("auth_type", "none")
|
||||||
|
|
||||||
|
if auth_type == "oauth_2.1":
|
||||||
|
# Remove existing OAuth clients for tool servers
|
||||||
|
server_id = connection.get("info", {}).get("id")
|
||||||
|
client_key = f"{server_type}:{server_id}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
request.app.state.oauth_client_manager.remove_client(client_key)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Set new tool server connections
|
||||||
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
|
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
|
||||||
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
|
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
|
||||||
]
|
]
|
||||||
|
|
@ -176,6 +193,7 @@ async def set_tool_servers_config(
|
||||||
if server_type == "mcp":
|
if server_type == "mcp":
|
||||||
server_id = connection.get("info", {}).get("id")
|
server_id = connection.get("info", {}).get("id")
|
||||||
auth_type = connection.get("auth_type", "none")
|
auth_type = connection.get("auth_type", "none")
|
||||||
|
|
||||||
if auth_type == "oauth_2.1" and server_id:
|
if auth_type == "oauth_2.1" and server_id:
|
||||||
try:
|
try:
|
||||||
oauth_client_info = connection.get("info", {}).get(
|
oauth_client_info = connection.get("info", {}).get(
|
||||||
|
|
@ -211,7 +229,7 @@ async def verify_tool_servers_config(
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
|
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
|
||||||
)
|
)
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
discovery_url
|
discovery_url
|
||||||
) as oauth_server_metadata_response:
|
) as oauth_server_metadata_response:
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us
|
||||||
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
|
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
|
||||||
):
|
):
|
||||||
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
|
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"File type {file.content_type} is not supported for processing"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
log.info(
|
log.info(
|
||||||
f"File type {file.content_type} is not provided, but trying to process anyway"
|
f"File type {file.content_type} is not provided, but trying to process anyway"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -44,7 +44,9 @@ def validate_model_id(model_id: str) -> bool:
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[ModelUserResponse])
|
@router.get(
|
||||||
|
"/list", response_model=list[ModelUserResponse]
|
||||||
|
) # do NOT use "/" as path, conflicts with main.py
|
||||||
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
|
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
|
||||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||||
return Models.get_models()
|
return Models.get_models()
|
||||||
|
|
|
||||||
|
|
@ -501,30 +501,9 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
||||||
return response
|
return response
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def merge_models_lists(model_lists):
|
def is_supported_openai_models(model_id):
|
||||||
log.debug(f"merge_models_lists {model_lists}")
|
if any(
|
||||||
merged_list = []
|
name in model_id
|
||||||
|
|
||||||
for idx, models in enumerate(model_lists):
|
|
||||||
if models is not None and "error" not in models:
|
|
||||||
|
|
||||||
merged_list.extend(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
**model,
|
|
||||||
"name": model.get("name", model["id"]),
|
|
||||||
"owned_by": "openai",
|
|
||||||
"openai": model,
|
|
||||||
"connection_type": model.get("connection_type", "external"),
|
|
||||||
"urlIdx": idx,
|
|
||||||
}
|
|
||||||
for model in models
|
|
||||||
if (model.get("id") or model.get("name"))
|
|
||||||
and (
|
|
||||||
"api.openai.com"
|
|
||||||
not in request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
|
||||||
or not any(
|
|
||||||
name in model["id"]
|
|
||||||
for name in [
|
for name in [
|
||||||
"babbage",
|
"babbage",
|
||||||
"dall-e",
|
"dall-e",
|
||||||
|
|
@ -533,18 +512,44 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
||||||
"tts",
|
"tts",
|
||||||
"whisper",
|
"whisper",
|
||||||
]
|
]
|
||||||
)
|
):
|
||||||
)
|
return False
|
||||||
]
|
return True
|
||||||
)
|
|
||||||
|
|
||||||
return merged_list
|
def get_merged_models(model_lists):
|
||||||
|
log.debug(f"merge_models_lists {model_lists}")
|
||||||
|
models = {}
|
||||||
|
|
||||||
models = {"data": merge_models_lists(map(extract_data, responses))}
|
for idx, model_list in enumerate(model_lists):
|
||||||
|
if model_list is not None and "error" not in model_list:
|
||||||
|
for model in model_list:
|
||||||
|
model_id = model.get("id") or model.get("name")
|
||||||
|
|
||||||
|
if (
|
||||||
|
"api.openai.com"
|
||||||
|
in request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
|
and not is_supported_openai_models(model_id)
|
||||||
|
):
|
||||||
|
# Skip unwanted OpenAI models
|
||||||
|
continue
|
||||||
|
|
||||||
|
if model_id and model_id not in models:
|
||||||
|
models[model_id] = {
|
||||||
|
**model,
|
||||||
|
"name": model.get("name", model_id),
|
||||||
|
"owned_by": "openai",
|
||||||
|
"openai": model,
|
||||||
|
"connection_type": model.get("connection_type", "external"),
|
||||||
|
"urlIdx": idx,
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
|
||||||
|
models = get_merged_models(map(extract_data, responses))
|
||||||
log.debug(f"models: {models}")
|
log.debug(f"models: {models}")
|
||||||
|
|
||||||
request.app.state.OPENAI_MODELS = {model["id"]: model for model in models["data"]}
|
request.app.state.OPENAI_MODELS = models
|
||||||
return models
|
return {"data": list(models.values())}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/models")
|
@router.get("/models")
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||||
"DOCLING_PICTURE_DESCRIPTION_API": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
|
"DOCLING_PICTURE_DESCRIPTION_API": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
|
||||||
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
"MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL,
|
||||||
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
|
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
|
||||||
# MinerU settings
|
# MinerU settings
|
||||||
"MINERU_API_MODE": request.app.state.config.MINERU_API_MODE,
|
"MINERU_API_MODE": request.app.state.config.MINERU_API_MODE,
|
||||||
|
|
@ -650,6 +651,7 @@ class ConfigForm(BaseModel):
|
||||||
DOCLING_PICTURE_DESCRIPTION_API: Optional[dict] = None
|
DOCLING_PICTURE_DESCRIPTION_API: Optional[dict] = None
|
||||||
DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
|
DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
|
||||||
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
|
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
|
||||||
|
MISTRAL_OCR_API_BASE_URL: Optional[str] = None
|
||||||
MISTRAL_OCR_API_KEY: Optional[str] = None
|
MISTRAL_OCR_API_KEY: Optional[str] = None
|
||||||
|
|
||||||
# MinerU settings
|
# MinerU settings
|
||||||
|
|
@ -891,6 +893,12 @@ async def update_rag_config(
|
||||||
if form_data.DOCUMENT_INTELLIGENCE_KEY is not None
|
if form_data.DOCUMENT_INTELLIGENCE_KEY is not None
|
||||||
else request.app.state.config.DOCUMENT_INTELLIGENCE_KEY
|
else request.app.state.config.DOCUMENT_INTELLIGENCE_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.app.state.config.MISTRAL_OCR_API_BASE_URL = (
|
||||||
|
form_data.MISTRAL_OCR_API_BASE_URL
|
||||||
|
if form_data.MISTRAL_OCR_API_BASE_URL is not None
|
||||||
|
else request.app.state.config.MISTRAL_OCR_API_BASE_URL
|
||||||
|
)
|
||||||
request.app.state.config.MISTRAL_OCR_API_KEY = (
|
request.app.state.config.MISTRAL_OCR_API_KEY = (
|
||||||
form_data.MISTRAL_OCR_API_KEY
|
form_data.MISTRAL_OCR_API_KEY
|
||||||
if form_data.MISTRAL_OCR_API_KEY is not None
|
if form_data.MISTRAL_OCR_API_KEY is not None
|
||||||
|
|
@ -1182,6 +1190,7 @@ async def update_rag_config(
|
||||||
"DOCLING_PICTURE_DESCRIPTION_API": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
|
"DOCLING_PICTURE_DESCRIPTION_API": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
|
||||||
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
"MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL,
|
||||||
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
|
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
|
||||||
# MinerU settings
|
# MinerU settings
|
||||||
"MINERU_API_MODE": request.app.state.config.MINERU_API_MODE,
|
"MINERU_API_MODE": request.app.state.config.MINERU_API_MODE,
|
||||||
|
|
@ -1565,6 +1574,7 @@ def process_file(
|
||||||
file_path = Storage.get_file(file_path)
|
file_path = Storage.get_file(file_path)
|
||||||
loader = Loader(
|
loader = Loader(
|
||||||
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
|
user=user,
|
||||||
DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY,
|
DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY,
|
||||||
DATALAB_MARKER_API_BASE_URL=request.app.state.config.DATALAB_MARKER_API_BASE_URL,
|
DATALAB_MARKER_API_BASE_URL=request.app.state.config.DATALAB_MARKER_API_BASE_URL,
|
||||||
DATALAB_MARKER_ADDITIONAL_CONFIG=request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG,
|
DATALAB_MARKER_ADDITIONAL_CONFIG=request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG,
|
||||||
|
|
@ -1597,6 +1607,7 @@ def process_file(
|
||||||
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
|
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
MISTRAL_OCR_API_BASE_URL=request.app.state.config.MISTRAL_OCR_API_BASE_URL,
|
||||||
MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY,
|
MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY,
|
||||||
MINERU_API_MODE=request.app.state.config.MINERU_API_MODE,
|
MINERU_API_MODE=request.app.state.config.MINERU_API_MODE,
|
||||||
MINERU_API_URL=request.app.state.config.MINERU_API_URL,
|
MINERU_API_URL=request.app.state.config.MINERU_API_URL,
|
||||||
|
|
@ -1875,6 +1886,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
||||||
query,
|
query,
|
||||||
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||||
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
|
referer=request.app.state.config.WEBUI_URL,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,7 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{user_id}/oauth/sessions", response_model=Optional[dict])
|
@router.get("/{user_id}/oauth/sessions")
|
||||||
async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_user)):
|
async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_user)):
|
||||||
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
|
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
|
||||||
if sessions and len(sessions) > 0:
|
if sessions and len(sessions) > 0:
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,12 @@ from open_webui.utils.redis import (
|
||||||
get_sentinel_url_from_env,
|
get_sentinel_url_from_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from open_webui.config import (
|
||||||
|
CORS_ALLOW_ORIGIN,
|
||||||
|
)
|
||||||
|
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
|
VERSION,
|
||||||
ENABLE_WEBSOCKET_SUPPORT,
|
ENABLE_WEBSOCKET_SUPPORT,
|
||||||
WEBSOCKET_MANAGER,
|
WEBSOCKET_MANAGER,
|
||||||
WEBSOCKET_REDIS_URL,
|
WEBSOCKET_REDIS_URL,
|
||||||
|
|
@ -58,7 +63,7 @@ if WEBSOCKET_MANAGER == "redis":
|
||||||
else:
|
else:
|
||||||
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
|
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
|
||||||
sio = socketio.AsyncServer(
|
sio = socketio.AsyncServer(
|
||||||
cors_allowed_origins=[],
|
cors_allowed_origins=CORS_ALLOW_ORIGIN,
|
||||||
async_mode="asgi",
|
async_mode="asgi",
|
||||||
transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]),
|
transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]),
|
||||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||||
|
|
@ -67,7 +72,7 @@ if WEBSOCKET_MANAGER == "redis":
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sio = socketio.AsyncServer(
|
sio = socketio.AsyncServer(
|
||||||
cors_allowed_origins=[],
|
cors_allowed_origins=CORS_ALLOW_ORIGIN,
|
||||||
async_mode="asgi",
|
async_mode="asgi",
|
||||||
transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]),
|
transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]),
|
||||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from open_webui.routers.images import (
|
from open_webui.routers.images import (
|
||||||
load_b64_image_data,
|
get_image_data,
|
||||||
upload_image,
|
upload_image,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ def get_image_url_from_base64(request, base64_image_string, metadata, user):
|
||||||
if "data:image/png;base64" in base64_image_string:
|
if "data:image/png;base64" in base64_image_string:
|
||||||
image_url = ""
|
image_url = ""
|
||||||
# Extract base64 image data from the line
|
# Extract base64 image data from the line
|
||||||
image_data, content_type = load_b64_image_data(base64_image_string)
|
image_data, content_type = get_image_data(base64_image_string)
|
||||||
if image_data is not None:
|
if image_data is not None:
|
||||||
image_url = upload_image(
|
image_url = upload_image(
|
||||||
request,
|
request,
|
||||||
|
|
|
||||||
11
backend/open_webui/utils/headers.py
Normal file
11
backend/open_webui/utils/headers.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
||||||
|
def include_user_info_headers(headers, user):
|
||||||
|
return {
|
||||||
|
**headers,
|
||||||
|
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
import requests
|
||||||
|
import aiohttp
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
@ -91,6 +93,25 @@ def get_images(ws, prompt, client_id, base_url, api_key):
|
||||||
return {"data": output_images}
|
return {"data": output_images}
|
||||||
|
|
||||||
|
|
||||||
|
async def comfyui_upload_image(image_file_item, base_url, api_key):
|
||||||
|
url = f"{base_url}/api/upload/image"
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
if api_key:
|
||||||
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
|
|
||||||
|
_, (filename, file_bytes, mime_type) = image_file_item
|
||||||
|
|
||||||
|
form = aiohttp.FormData()
|
||||||
|
form.add_field("image", file_bytes, filename=filename, content_type=mime_type)
|
||||||
|
form.add_field("type", "input") # required by ComfyUI
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(url, data=form, headers=headers) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
return await resp.json()
|
||||||
|
|
||||||
|
|
||||||
class ComfyUINodeInput(BaseModel):
|
class ComfyUINodeInput(BaseModel):
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
node_ids: list[str] = []
|
node_ids: list[str] = []
|
||||||
|
|
@ -103,7 +124,7 @@ class ComfyUIWorkflow(BaseModel):
|
||||||
nodes: list[ComfyUINodeInput]
|
nodes: list[ComfyUINodeInput]
|
||||||
|
|
||||||
|
|
||||||
class ComfyUIGenerateImageForm(BaseModel):
|
class ComfyUICreateImageForm(BaseModel):
|
||||||
workflow: ComfyUIWorkflow
|
workflow: ComfyUIWorkflow
|
||||||
|
|
||||||
prompt: str
|
prompt: str
|
||||||
|
|
@ -116,8 +137,8 @@ class ComfyUIGenerateImageForm(BaseModel):
|
||||||
seed: Optional[int] = None
|
seed: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
async def comfyui_generate_image(
|
async def comfyui_create_image(
|
||||||
model: str, payload: ComfyUIGenerateImageForm, client_id, base_url, api_key
|
model: str, payload: ComfyUICreateImageForm, client_id, base_url, api_key
|
||||||
):
|
):
|
||||||
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
workflow = json.loads(payload.workflow.workflow)
|
workflow = json.loads(payload.workflow.workflow)
|
||||||
|
|
@ -191,3 +212,102 @@ async def comfyui_generate_image(
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
class ComfyUIEditImageForm(BaseModel):
|
||||||
|
workflow: ComfyUIWorkflow
|
||||||
|
|
||||||
|
image: str | list[str]
|
||||||
|
prompt: str
|
||||||
|
width: Optional[int] = None
|
||||||
|
height: Optional[int] = None
|
||||||
|
n: Optional[int] = None
|
||||||
|
|
||||||
|
steps: Optional[int] = None
|
||||||
|
seed: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def comfyui_edit_image(
|
||||||
|
model: str, payload: ComfyUIEditImageForm, client_id, base_url, api_key
|
||||||
|
):
|
||||||
|
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
|
workflow = json.loads(payload.workflow.workflow)
|
||||||
|
|
||||||
|
for node in payload.workflow.nodes:
|
||||||
|
if node.type:
|
||||||
|
if node.type == "model":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = model
|
||||||
|
elif node.type == "image":
|
||||||
|
if isinstance(payload.image, list):
|
||||||
|
# check if multiple images are provided
|
||||||
|
for idx, node_id in enumerate(node.node_ids):
|
||||||
|
if idx < len(payload.image):
|
||||||
|
workflow[node_id]["inputs"][node.key] = payload.image[idx]
|
||||||
|
else:
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = payload.image
|
||||||
|
elif node.type == "prompt":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "text"
|
||||||
|
] = payload.prompt
|
||||||
|
elif node.type == "negative_prompt":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "text"
|
||||||
|
] = payload.negative_prompt
|
||||||
|
elif node.type == "width":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "width"
|
||||||
|
] = payload.width
|
||||||
|
elif node.type == "height":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "height"
|
||||||
|
] = payload.height
|
||||||
|
elif node.type == "n":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "batch_size"
|
||||||
|
] = payload.n
|
||||||
|
elif node.type == "steps":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "steps"
|
||||||
|
] = payload.steps
|
||||||
|
elif node.type == "seed":
|
||||||
|
seed = (
|
||||||
|
payload.seed
|
||||||
|
if payload.seed
|
||||||
|
else random.randint(0, 1125899906842624)
|
||||||
|
)
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = seed
|
||||||
|
else:
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = node.value
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
|
ws.connect(f"{ws_url}/ws?clientId={client_id}", header=headers)
|
||||||
|
log.info("WebSocket connection established.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Failed to connect to WebSocket server: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.info("Sending workflow to WebSocket server.")
|
||||||
|
log.info(f"Workflow: {workflow}")
|
||||||
|
images = await asyncio.to_thread(
|
||||||
|
get_images, ws, workflow, client_id, base_url, api_key
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error while receiving images: {e}")
|
||||||
|
images = None
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import asyncio
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
|
||||||
from mcp import ClientSession
|
from mcp import ClientSession
|
||||||
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
||||||
from mcp.client.streamable_http import streamablehttp_client
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
|
|
@ -11,25 +13,28 @@ from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAu
|
||||||
class MCPClient:
|
class MCPClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.session: Optional[ClientSession] = None
|
self.session: Optional[ClientSession] = None
|
||||||
self.exit_stack = AsyncExitStack()
|
self.exit_stack = None
|
||||||
|
|
||||||
async def connect(self, url: str, headers: Optional[dict] = None):
|
async def connect(self, url: str, headers: Optional[dict] = None):
|
||||||
|
async with AsyncExitStack() as exit_stack:
|
||||||
try:
|
try:
|
||||||
self._streams_context = streamablehttp_client(url, headers=headers)
|
self._streams_context = streamablehttp_client(url, headers=headers)
|
||||||
|
|
||||||
transport = await self.exit_stack.enter_async_context(self._streams_context)
|
transport = await exit_stack.enter_async_context(self._streams_context)
|
||||||
read_stream, write_stream, _ = transport
|
read_stream, write_stream, _ = transport
|
||||||
|
|
||||||
self._session_context = ClientSession(
|
self._session_context = ClientSession(
|
||||||
read_stream, write_stream
|
read_stream, write_stream
|
||||||
) # pylint: disable=W0201
|
) # pylint: disable=W0201
|
||||||
|
|
||||||
self.session = await self.exit_stack.enter_async_context(
|
self.session = await exit_stack.enter_async_context(
|
||||||
self._session_context
|
self._session_context
|
||||||
)
|
)
|
||||||
|
with anyio.fail_after(10):
|
||||||
await self.session.initialize()
|
await self.session.initialize()
|
||||||
|
self.exit_stack = exit_stack.pop_all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.disconnect()
|
await asyncio.shield(self.disconnect())
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def list_tool_specs(self) -> Optional[dict]:
|
async def list_tool_specs(self) -> Optional[dict]:
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,10 @@ from open_webui.routers.retrieval import (
|
||||||
SearchForm,
|
SearchForm,
|
||||||
)
|
)
|
||||||
from open_webui.routers.images import (
|
from open_webui.routers.images import (
|
||||||
load_b64_image_data,
|
|
||||||
image_generations,
|
image_generations,
|
||||||
GenerateImageForm,
|
CreateImageForm,
|
||||||
upload_image,
|
image_edits,
|
||||||
|
EditImageForm,
|
||||||
)
|
)
|
||||||
from open_webui.routers.pipelines import (
|
from open_webui.routers.pipelines import (
|
||||||
process_pipeline_inlet_filter,
|
process_pipeline_inlet_filter,
|
||||||
|
|
@ -91,7 +91,7 @@ from open_webui.utils.misc import (
|
||||||
convert_logit_bias_input_to_json,
|
convert_logit_bias_input_to_json,
|
||||||
get_content_from_message,
|
get_content_from_message,
|
||||||
)
|
)
|
||||||
from open_webui.utils.tools import get_tools
|
from open_webui.utils.tools import get_tools, get_updated_tool_function
|
||||||
from open_webui.utils.plugin import load_function_module_by_id
|
from open_webui.utils.plugin import load_function_module_by_id
|
||||||
from open_webui.utils.filter import (
|
from open_webui.utils.filter import (
|
||||||
get_sorted_filter_ids,
|
get_sorted_filter_ids,
|
||||||
|
|
@ -718,9 +718,31 @@ async def chat_web_search_handler(
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_images(message_list):
|
||||||
|
images = []
|
||||||
|
for message in reversed(message_list):
|
||||||
|
images_flag = False
|
||||||
|
for file in message.get("files", []):
|
||||||
|
if file.get("type") == "image":
|
||||||
|
images.append(file.get("url"))
|
||||||
|
images_flag = True
|
||||||
|
|
||||||
|
if images_flag:
|
||||||
|
break
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
async def chat_image_generation_handler(
|
async def chat_image_generation_handler(
|
||||||
request: Request, form_data: dict, extra_params: dict, user
|
request: Request, form_data: dict, extra_params: dict, user
|
||||||
):
|
):
|
||||||
|
metadata = extra_params.get("__metadata__", {})
|
||||||
|
chat_id = metadata.get("chat_id", None)
|
||||||
|
if not chat_id:
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
||||||
|
|
||||||
__event_emitter__ = extra_params["__event_emitter__"]
|
__event_emitter__ = extra_params["__event_emitter__"]
|
||||||
await __event_emitter__(
|
await __event_emitter__(
|
||||||
{
|
{
|
||||||
|
|
@ -729,19 +751,24 @@ async def chat_image_generation_handler(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
messages = form_data["messages"]
|
messages_map = chat.chat.get("history", {}).get("messages", {})
|
||||||
user_message = get_last_user_message(messages)
|
message_id = chat.chat.get("history", {}).get("currentId")
|
||||||
|
message_list = get_message_list(messages_map, message_id)
|
||||||
|
user_message = get_last_user_message(message_list)
|
||||||
|
|
||||||
prompt = user_message
|
prompt = user_message
|
||||||
negative_prompt = ""
|
input_images = get_last_images(message_list)
|
||||||
|
|
||||||
|
system_message_content = ""
|
||||||
|
if len(input_images) == 0:
|
||||||
|
# Create image(s)
|
||||||
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
|
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
|
||||||
try:
|
try:
|
||||||
res = await generate_image_prompt(
|
res = await generate_image_prompt(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
"model": form_data["model"],
|
"model": form_data["model"],
|
||||||
"messages": messages,
|
"messages": form_data["messages"],
|
||||||
},
|
},
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
|
|
@ -765,12 +792,10 @@ async def chat_image_generation_handler(
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
prompt = user_message
|
prompt = user_message
|
||||||
|
|
||||||
system_message_content = ""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
images = await image_generations(
|
images = await image_generations(
|
||||||
request=request,
|
request=request,
|
||||||
form_data=GenerateImageForm(**{"prompt": prompt}),
|
form_data=CreateImageForm(**{"prompt": prompt}),
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -796,9 +821,17 @@ async def chat_image_generation_handler(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
system_message_content = "<context>User is shown the generated image, tell the user that the image has been generated</context>"
|
system_message_content = "<context>The requested image has been created and is now being shown to the user. Let them know that it has been generated.</context>"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug(e)
|
||||||
|
|
||||||
|
error_message = ""
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
if e.detail and isinstance(e.detail, dict):
|
||||||
|
error_message = e.detail.get("message", str(e.detail))
|
||||||
|
else:
|
||||||
|
error_message = str(e.detail)
|
||||||
|
|
||||||
await __event_emitter__(
|
await __event_emitter__(
|
||||||
{
|
{
|
||||||
"type": "status",
|
"type": "status",
|
||||||
|
|
@ -809,7 +842,60 @@ async def chat_image_generation_handler(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
system_message_content = "<context>Unable to generate an image, tell the user that an error occurred</context>"
|
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
|
||||||
|
else:
|
||||||
|
# Edit image(s)
|
||||||
|
try:
|
||||||
|
images = await image_edits(
|
||||||
|
request=request,
|
||||||
|
form_data=EditImageForm(**{"prompt": prompt, "image": input_images}),
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {"description": "Image created", "done": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "files",
|
||||||
|
"data": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"url": image["url"],
|
||||||
|
}
|
||||||
|
for image in images
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
system_message_content = "<context>The requested image has been created and is now being shown to the user. Let them know that it has been generated.</context>"
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(e)
|
||||||
|
|
||||||
|
error_message = ""
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
if e.detail and isinstance(e.detail, dict):
|
||||||
|
error_message = e.detail.get("message", str(e.detail))
|
||||||
|
else:
|
||||||
|
error_message = str(e.detail)
|
||||||
|
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {
|
||||||
|
"description": f"An error occurred while generating an image",
|
||||||
|
"done": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
|
||||||
|
|
||||||
if system_message_content:
|
if system_message_content:
|
||||||
form_data["messages"] = add_or_update_system_message(
|
form_data["messages"] = add_or_update_system_message(
|
||||||
|
|
@ -1307,6 +1393,17 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
|
if event_emitter:
|
||||||
|
await event_emitter(
|
||||||
|
{
|
||||||
|
"type": "chat:message:error",
|
||||||
|
"data": {
|
||||||
|
"error": {
|
||||||
|
"content": f"Failed to connect to MCP server '{server_id}'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tools_dict = await get_tools(
|
tools_dict = await get_tools(
|
||||||
|
|
@ -1543,16 +1640,13 @@ async def process_chat_response(
|
||||||
if not metadata.get("chat_id", "").startswith(
|
if not metadata.get("chat_id", "").startswith(
|
||||||
"local:"
|
"local:"
|
||||||
): # Only update titles and tags for non-temp chats
|
): # Only update titles and tags for non-temp chats
|
||||||
if (
|
if TASKS.TITLE_GENERATION in tasks:
|
||||||
TASKS.TITLE_GENERATION in tasks
|
|
||||||
and tasks[TASKS.TITLE_GENERATION]
|
|
||||||
):
|
|
||||||
user_message = get_last_user_message(messages)
|
user_message = get_last_user_message(messages)
|
||||||
if user_message and len(user_message) > 100:
|
if user_message and len(user_message) > 100:
|
||||||
user_message = user_message[:100] + "..."
|
user_message = user_message[:100] + "..."
|
||||||
|
|
||||||
|
title = None
|
||||||
if tasks[TASKS.TITLE_GENERATION]:
|
if tasks[TASKS.TITLE_GENERATION]:
|
||||||
|
|
||||||
res = await generate_title(
|
res = await generate_title(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
|
|
@ -1603,7 +1697,8 @@ async def process_chat_response(
|
||||||
"data": title,
|
"data": title,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif len(messages) == 2:
|
|
||||||
|
if title == None and len(messages) == 2:
|
||||||
title = messages[0].get("content", user_message)
|
title = messages[0].get("content", user_message)
|
||||||
|
|
||||||
Chats.update_chat_title_by_id(metadata["chat_id"], title)
|
Chats.update_chat_title_by_id(metadata["chat_id"], title)
|
||||||
|
|
@ -1939,10 +2034,12 @@ async def process_chat_response(
|
||||||
content = f"{content}{tool_calls_display_content}"
|
content = f"{content}{tool_calls_display_content}"
|
||||||
|
|
||||||
elif block["type"] == "reasoning":
|
elif block["type"] == "reasoning":
|
||||||
reasoning_display_content = "\n".join(
|
reasoning_display_content = html.escape(
|
||||||
|
"\n".join(
|
||||||
(f"> {line}" if not line.startswith(">") else line)
|
(f"> {line}" if not line.startswith(">") else line)
|
||||||
for line in block["content"].splitlines()
|
for line in block["content"].splitlines()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
reasoning_duration = block.get("duration", None)
|
reasoning_duration = block.get("duration", None)
|
||||||
|
|
||||||
|
|
@ -2349,7 +2446,9 @@ async def process_chat_response(
|
||||||
)
|
)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
if "event" in data:
|
if "event" in data and not getattr(
|
||||||
|
request.state, "direct", False
|
||||||
|
):
|
||||||
await event_emitter(data.get("event", {}))
|
await event_emitter(data.get("event", {}))
|
||||||
|
|
||||||
if "selected_model_id" in data:
|
if "selected_model_id" in data:
|
||||||
|
|
@ -2740,7 +2839,16 @@ async def process_chat_response(
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
tool_function = tool["callable"]
|
tool_function = await get_updated_tool_function(
|
||||||
|
function=tool["callable"],
|
||||||
|
extra_params={
|
||||||
|
"__messages__": form_data.get(
|
||||||
|
"messages", []
|
||||||
|
),
|
||||||
|
"__files__": metadata.get("files", []),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
tool_result = await tool_function(
|
tool_result = await tool_function(
|
||||||
**tool_function_params
|
**tool_function_params
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,8 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||||
action_ids = []
|
action_ids = []
|
||||||
filter_ids = []
|
filter_ids = []
|
||||||
|
|
||||||
if "info" in model and "meta" in model["info"]:
|
if "info" in model:
|
||||||
|
if "meta" in model["info"]:
|
||||||
action_ids.extend(
|
action_ids.extend(
|
||||||
model["info"]["meta"].get("actionIds", [])
|
model["info"]["meta"].get("actionIds", [])
|
||||||
)
|
)
|
||||||
|
|
@ -174,6 +175,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||||
model["info"]["meta"].get("filterIds", [])
|
model["info"]["meta"].get("filterIds", [])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "params" in model["info"]:
|
||||||
|
# Remove params to avoid exposing sensitive info
|
||||||
|
del model["info"]["params"]
|
||||||
|
|
||||||
model["action_ids"] = action_ids
|
model["action_ids"] = action_ids
|
||||||
model["filter_ids"] = filter_ids
|
model["filter_ids"] = filter_ids
|
||||||
else:
|
else:
|
||||||
|
|
@ -182,22 +187,40 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||||
elif custom_model.is_active and (
|
elif custom_model.is_active and (
|
||||||
custom_model.id not in [model["id"] for model in models]
|
custom_model.id not in [model["id"] for model in models]
|
||||||
):
|
):
|
||||||
|
# Custom model based on a base model
|
||||||
owned_by = "openai"
|
owned_by = "openai"
|
||||||
pipe = None
|
pipe = None
|
||||||
|
|
||||||
|
for m in models:
|
||||||
|
if (
|
||||||
|
custom_model.base_model_id == m["id"]
|
||||||
|
or custom_model.base_model_id == m["id"].split(":")[0]
|
||||||
|
):
|
||||||
|
owned_by = m.get("owned_by", "unknown")
|
||||||
|
if "pipe" in m:
|
||||||
|
pipe = m["pipe"]
|
||||||
|
break
|
||||||
|
|
||||||
|
model = {
|
||||||
|
"id": f"{custom_model.id}",
|
||||||
|
"name": custom_model.name,
|
||||||
|
"object": "model",
|
||||||
|
"created": custom_model.created_at,
|
||||||
|
"owned_by": owned_by,
|
||||||
|
"preset": True,
|
||||||
|
**({"pipe": pipe} if pipe is not None else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
info = custom_model.model_dump()
|
||||||
|
if "params" in info:
|
||||||
|
# Remove params to avoid exposing sensitive info
|
||||||
|
del info["params"]
|
||||||
|
|
||||||
|
model["info"] = info
|
||||||
|
|
||||||
action_ids = []
|
action_ids = []
|
||||||
filter_ids = []
|
filter_ids = []
|
||||||
|
|
||||||
for model in models:
|
|
||||||
if (
|
|
||||||
custom_model.base_model_id == model["id"]
|
|
||||||
or custom_model.base_model_id == model["id"].split(":")[0]
|
|
||||||
):
|
|
||||||
owned_by = model.get("owned_by", "unknown owner")
|
|
||||||
if "pipe" in model:
|
|
||||||
pipe = model["pipe"]
|
|
||||||
break
|
|
||||||
|
|
||||||
if custom_model.meta:
|
if custom_model.meta:
|
||||||
meta = custom_model.meta.model_dump()
|
meta = custom_model.meta.model_dump()
|
||||||
|
|
||||||
|
|
@ -207,20 +230,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||||
if "filterIds" in meta:
|
if "filterIds" in meta:
|
||||||
filter_ids.extend(meta["filterIds"])
|
filter_ids.extend(meta["filterIds"])
|
||||||
|
|
||||||
models.append(
|
model["action_ids"] = action_ids
|
||||||
{
|
model["filter_ids"] = filter_ids
|
||||||
"id": f"{custom_model.id}",
|
|
||||||
"name": custom_model.name,
|
models.append(model)
|
||||||
"object": "model",
|
|
||||||
"created": custom_model.created_at,
|
|
||||||
"owned_by": owned_by,
|
|
||||||
"info": custom_model.model_dump(),
|
|
||||||
"preset": True,
|
|
||||||
**({"pipe": pipe} if pipe is not None else {}),
|
|
||||||
"action_ids": action_ids,
|
|
||||||
"filter_ids": filter_ids,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process action_ids to get the actions
|
# Process action_ids to get the actions
|
||||||
def get_action_items_from_module(function, module):
|
def get_action_items_from_module(function, module):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
@ -74,6 +75,8 @@ from mcp.shared.auth import (
|
||||||
OAuthMetadata,
|
OAuthMetadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from authlib.oauth2.rfc6749.errors import OAuth2Error
|
||||||
|
|
||||||
|
|
||||||
class OAuthClientInformationFull(OAuthClientMetadata):
|
class OAuthClientInformationFull(OAuthClientMetadata):
|
||||||
issuer: Optional[str] = None # URL of the OAuth server that issued this client
|
issuer: Optional[str] = None # URL of the OAuth server that issued this client
|
||||||
|
|
@ -150,6 +153,37 @@ def decrypt_data(data: str):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _build_oauth_callback_error_message(e: Exception) -> str:
|
||||||
|
"""
|
||||||
|
Produce a user-facing callback error string with actionable context.
|
||||||
|
Keeps the message short and strips newlines for safe redirect usage.
|
||||||
|
"""
|
||||||
|
if isinstance(e, OAuth2Error):
|
||||||
|
parts = [p for p in [e.error, e.description] if p]
|
||||||
|
detail = " - ".join(parts)
|
||||||
|
elif isinstance(e, HTTPException):
|
||||||
|
detail = e.detail if isinstance(e.detail, str) else str(e.detail)
|
||||||
|
elif isinstance(e, aiohttp.ClientResponseError):
|
||||||
|
detail = f"Upstream provider returned {e.status}: {e.message}"
|
||||||
|
elif isinstance(e, aiohttp.ClientError):
|
||||||
|
detail = str(e)
|
||||||
|
elif isinstance(e, KeyError):
|
||||||
|
missing = str(e).strip("'")
|
||||||
|
if missing.lower() == "state":
|
||||||
|
detail = "Missing state parameter in callback (session may have expired)"
|
||||||
|
else:
|
||||||
|
detail = f"Missing expected key '{missing}' in OAuth response"
|
||||||
|
else:
|
||||||
|
detail = str(e)
|
||||||
|
|
||||||
|
detail = detail.replace("\n", " ").strip()
|
||||||
|
if not detail:
|
||||||
|
detail = e.__class__.__name__
|
||||||
|
|
||||||
|
message = f"OAuth callback failed: {detail}"
|
||||||
|
return message[:197] + "..." if len(message) > 200 else message
|
||||||
|
|
||||||
|
|
||||||
def is_in_blocked_groups(group_name: str, groups: list) -> bool:
|
def is_in_blocked_groups(group_name: str, groups: list) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a group name matches any blocked pattern.
|
Check if a group name matches any blocked pattern.
|
||||||
|
|
@ -251,7 +285,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
|
||||||
# Attempt to fetch OAuth server metadata to get registration endpoint & scopes
|
# Attempt to fetch OAuth server metadata to get registration endpoint & scopes
|
||||||
discovery_urls = get_discovery_urls(oauth_server_url)
|
discovery_urls = get_discovery_urls(oauth_server_url)
|
||||||
for url in discovery_urls:
|
for url in discovery_urls:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
url, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||||
) as oauth_server_metadata_response:
|
) as oauth_server_metadata_response:
|
||||||
|
|
@ -287,7 +321,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Perform dynamic client registration and return client info
|
# Perform dynamic client registration and return client info
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
registration_url, json=registration_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
registration_url, json=registration_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||||
) as oauth_client_registration_response:
|
) as oauth_client_registration_response:
|
||||||
|
|
@ -371,6 +405,82 @@ class OAuthClientManager:
|
||||||
if client_id in self.clients:
|
if client_id in self.clients:
|
||||||
del self.clients[client_id]
|
del self.clients[client_id]
|
||||||
log.info(f"Removed OAuth client {client_id}")
|
log.info(f"Removed OAuth client {client_id}")
|
||||||
|
|
||||||
|
if hasattr(self.oauth, "_clients"):
|
||||||
|
if client_id in self.oauth._clients:
|
||||||
|
self.oauth._clients.pop(client_id, None)
|
||||||
|
|
||||||
|
if hasattr(self.oauth, "_registry"):
|
||||||
|
if client_id in self.oauth._registry:
|
||||||
|
self.oauth._registry.pop(client_id, None)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _preflight_authorization_url(
|
||||||
|
self, client, client_info: OAuthClientInformationFull
|
||||||
|
) -> bool:
|
||||||
|
# TODO: Replace this logic with a more robust OAuth client registration validation
|
||||||
|
# Only perform preflight checks for Starlette OAuth clients
|
||||||
|
if not hasattr(client, "create_authorization_url"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
redirect_uri = None
|
||||||
|
if client_info.redirect_uris:
|
||||||
|
redirect_uri = str(client_info.redirect_uris[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_data = await client.create_authorization_url(redirect_uri=redirect_uri)
|
||||||
|
authorization_url = auth_data.get("url")
|
||||||
|
|
||||||
|
if not authorization_url:
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
f"Skipping OAuth preflight for client {client_info.client_id}: {e}",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
|
async with session.get(
|
||||||
|
authorization_url,
|
||||||
|
allow_redirects=False,
|
||||||
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
) as resp:
|
||||||
|
if resp.status < 400:
|
||||||
|
return True
|
||||||
|
response_text = await resp.text()
|
||||||
|
|
||||||
|
error = None
|
||||||
|
error_description = ""
|
||||||
|
|
||||||
|
content_type = resp.headers.get("content-type", "")
|
||||||
|
if "application/json" in content_type:
|
||||||
|
try:
|
||||||
|
payload = json.loads(response_text)
|
||||||
|
error = payload.get("error")
|
||||||
|
error_description = payload.get("error_description", "")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
error_description = response_text
|
||||||
|
|
||||||
|
error_message = f"{error or ''} {error_description or ''}".lower()
|
||||||
|
|
||||||
|
if any(
|
||||||
|
keyword in error_message
|
||||||
|
for keyword in ("invalid_client", "invalid client", "client id")
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
f"OAuth client preflight detected invalid registration for {client_info.client_id}: {error} {error_description}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
f"Skipping OAuth preflight network check for client {client_info.client_id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_client(self, client_id):
|
def get_client(self, client_id):
|
||||||
|
|
@ -561,7 +671,6 @@ class OAuthClientManager:
|
||||||
client = self.get_client(client_id)
|
client = self.get_client(client_id)
|
||||||
if client is None:
|
if client is None:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
|
|
||||||
client_info = self.get_client_info(client_id)
|
client_info = self.get_client_info(client_id)
|
||||||
if client_info is None:
|
if client_info is None:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
|
|
@ -569,7 +678,8 @@ class OAuthClientManager:
|
||||||
redirect_uri = (
|
redirect_uri = (
|
||||||
client_info.redirect_uris[0] if client_info.redirect_uris else None
|
client_info.redirect_uris[0] if client_info.redirect_uris else None
|
||||||
)
|
)
|
||||||
return await client.authorize_redirect(request, str(redirect_uri))
|
redirect_uri_str = str(redirect_uri) if redirect_uri else None
|
||||||
|
return await client.authorize_redirect(request, redirect_uri_str)
|
||||||
|
|
||||||
async def handle_callback(self, request, client_id: str, user_id: str, response):
|
async def handle_callback(self, request, client_id: str, user_id: str, response):
|
||||||
client = self.get_client(client_id)
|
client = self.get_client(client_id)
|
||||||
|
|
@ -621,8 +731,14 @@ class OAuthClientManager:
|
||||||
error_message = "Failed to obtain OAuth token"
|
error_message = "Failed to obtain OAuth token"
|
||||||
log.warning(error_message)
|
log.warning(error_message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = "OAuth callback error"
|
error_message = _build_oauth_callback_error_message(e)
|
||||||
log.warning(f"OAuth callback error: {e}")
|
log.warning(
|
||||||
|
"OAuth callback error for user_id=%s client_id=%s: %s",
|
||||||
|
user_id,
|
||||||
|
client_id,
|
||||||
|
error_message,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
redirect_url = (
|
redirect_url = (
|
||||||
str(request.app.state.config.WEBUI_URL or request.base_url)
|
str(request.app.state.config.WEBUI_URL or request.base_url)
|
||||||
|
|
@ -630,7 +746,9 @@ class OAuthClientManager:
|
||||||
|
|
||||||
if error_message:
|
if error_message:
|
||||||
log.debug(error_message)
|
log.debug(error_message)
|
||||||
redirect_url = f"{redirect_url}/?error={error_message}"
|
redirect_url = (
|
||||||
|
f"{redirect_url}/?error={urllib.parse.quote_plus(error_message)}"
|
||||||
|
)
|
||||||
return RedirectResponse(url=redirect_url, headers=response.headers)
|
return RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
|
||||||
response = RedirectResponse(url=redirect_url, headers=response.headers)
|
response = RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
|
@ -1104,7 +1222,13 @@ class OAuthManager:
|
||||||
try:
|
try:
|
||||||
token = await client.authorize_access_token(request)
|
token = await client.authorize_access_token(request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"OAuth callback error: {e}")
|
detailed_error = _build_oauth_callback_error_message(e)
|
||||||
|
log.warning(
|
||||||
|
"OAuth callback error during authorize_access_token for provider %s: %s",
|
||||||
|
provider,
|
||||||
|
detailed_error,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
|
|
||||||
# Try to get userinfo from the token first, some providers include it there
|
# Try to get userinfo from the token first, some providers include it there
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,10 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
|
||||||
if "tools" in openai_payload:
|
if "tools" in openai_payload:
|
||||||
ollama_payload["tools"] = openai_payload["tools"]
|
ollama_payload["tools"] = openai_payload["tools"]
|
||||||
|
|
||||||
|
if "max_tokens" in openai_payload:
|
||||||
|
ollama_payload["num_predict"] = openai_payload["max_tokens"]
|
||||||
|
del openai_payload["max_tokens"]
|
||||||
|
|
||||||
# If there are advanced parameters in the payload, format them in Ollama's options field
|
# If there are advanced parameters in the payload, format them in Ollama's options field
|
||||||
if openai_payload.get("options"):
|
if openai_payload.get("options"):
|
||||||
ollama_payload["options"] = openai_payload["options"]
|
ollama_payload["options"] = openai_payload["options"]
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,26 @@ def get_async_tool_function_and_apply_extra_params(
|
||||||
update_wrapper(new_function, function)
|
update_wrapper(new_function, function)
|
||||||
new_function.__signature__ = new_sig
|
new_function.__signature__ = new_sig
|
||||||
|
|
||||||
|
new_function.__function__ = function # type: ignore
|
||||||
|
new_function.__extra_params__ = extra_params # type: ignore
|
||||||
|
|
||||||
return new_function
|
return new_function
|
||||||
|
|
||||||
|
|
||||||
|
async def get_updated_tool_function(function: Callable, extra_params: dict):
|
||||||
|
# Get the original function and merge updated params
|
||||||
|
__function__ = getattr(function, "__function__", None)
|
||||||
|
__extra_params__ = getattr(function, "__extra_params__", None)
|
||||||
|
|
||||||
|
if __function__ is not None and __extra_params__ is not None:
|
||||||
|
return await get_async_tool_function_and_apply_extra_params(
|
||||||
|
__function__,
|
||||||
|
{**__extra_params__, **extra_params},
|
||||||
|
)
|
||||||
|
|
||||||
|
return function
|
||||||
|
|
||||||
|
|
||||||
async def get_tools(
|
async def get_tools(
|
||||||
request: Request, tool_ids: list[str], user: UserModel, extra_params: dict
|
request: Request, tool_ids: list[str], user: UserModel, extra_params: dict
|
||||||
) -> dict[str, dict]:
|
) -> dict[str, dict]:
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict) -> b
|
||||||
payload = {**event_data}
|
payload = {**event_data}
|
||||||
|
|
||||||
log.debug(f"payload: {payload}")
|
log.debug(f"payload: {payload}")
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.post(url, json=payload) as r:
|
async with session.post(url, json=payload) as r:
|
||||||
r_text = await r.text()
|
r_text = await r.text()
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ python-multipart==0.0.20
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
|
|
||||||
python-socketio==5.13.0
|
python-socketio==5.13.0
|
||||||
python-jose==3.4.0
|
python-jose==3.5.0
|
||||||
cryptography
|
cryptography
|
||||||
bcrypt==5.0.0
|
bcrypt==5.0.0
|
||||||
argon2-cffi==25.1.0
|
argon2-cffi==25.1.0
|
||||||
|
|
@ -63,7 +63,7 @@ fpdf2==2.8.2
|
||||||
pymdown-extensions==10.14.2
|
pymdown-extensions==10.14.2
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
python-pptx==1.0.2
|
python-pptx==1.0.2
|
||||||
unstructured==0.16.17
|
unstructured==0.18.15
|
||||||
nltk==3.9.1
|
nltk==3.9.1
|
||||||
Markdown==3.9
|
Markdown==3.9
|
||||||
pypandoc==1.15
|
pypandoc==1.15
|
||||||
|
|
@ -133,7 +133,7 @@ pytest-docker~=3.1.1
|
||||||
ldap3==2.9.1
|
ldap3==2.9.1
|
||||||
|
|
||||||
## Firecrawl
|
## Firecrawl
|
||||||
firecrawl-py==1.12.0
|
firecrawl-py==4.5.0
|
||||||
|
|
||||||
## Trace
|
## Trace
|
||||||
opentelemetry-api==1.37.0
|
opentelemetry-api==1.37.0
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ services:
|
||||||
open-webui:
|
open-webui:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
|
||||||
OLLAMA_BASE_URL: '/ollama'
|
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
|
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
|
||||||
container_name: open-webui
|
container_name: open-webui
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ Noticed something off? Have an idea? Check our [Issues tab](https://github.com/o
|
||||||
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
|
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
|
||||||
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
|
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Reporting vulnerabilities is not wanted through Issues!
|
||||||
|
> Instead, [use the security reporting functionality](https://github.com/open-webui/open-webui/security) and ensure you comply with the outlined requirements.
|
||||||
|
|
||||||
### 🧭 Scope of Support
|
### 🧭 Scope of Support
|
||||||
|
|
||||||
We've noticed an uptick in issues not directly related to Open WebUI but rather to the environment it's run in, especially Docker setups. While we strive to support Docker deployment, understanding Docker fundamentals is crucial for a smooth experience.
|
We've noticed an uptick in issues not directly related to Open WebUI but rather to the environment it's run in, especially Docker setups. While we strive to support Docker deployment, understanding Docker fundamentals is crucial for a smooth experience.
|
||||||
|
|
@ -32,6 +36,8 @@ We've noticed an uptick in issues not directly related to Open WebUI but rather
|
||||||
|
|
||||||
- **Advanced Configurations**: Setting up reverse proxies for HTTPS and managing Docker deployments requires foundational knowledge. There are numerous online resources available to learn these skills. Ensuring you have this knowledge will greatly enhance your experience with Open WebUI and similar projects.
|
- **Advanced Configurations**: Setting up reverse proxies for HTTPS and managing Docker deployments requires foundational knowledge. There are numerous online resources available to learn these skills. Ensuring you have this knowledge will greatly enhance your experience with Open WebUI and similar projects.
|
||||||
|
|
||||||
|
- **Check the documentation and help improve it**: [Our documentation](https://docs.openwebui.com) has ever growing troubleshooting guides and detailed installation tutorials. Please verify if it is of help to your issue and help expand it by submitting issues and PRs on our [Docs Repository](https://github.com/open-webui/docs).
|
||||||
|
|
||||||
## 💡 Contributing
|
## 💡 Contributing
|
||||||
|
|
||||||
Looking to contribute? Great! Here's how you can help:
|
Looking to contribute? Great! Here's how you can help:
|
||||||
|
|
@ -46,9 +52,15 @@ We welcome pull requests. Before submitting one, please:
|
||||||
4. Write clear, descriptive commit messages.
|
4. Write clear, descriptive commit messages.
|
||||||
5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward.
|
5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The Pull Request Template has various requirements outlined. Go through the PR-checklist one by one and ensure you completed all steps before submitting your PR for review (you can open it as draft otherwise!).
|
||||||
|
|
||||||
### 📚 Documentation & Tutorials
|
### 📚 Documentation & Tutorials
|
||||||
|
|
||||||
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
|
Help us make Open WebUI more accessible by improving the documentation, writing tutorials, or creating guides on setting up and optimizing the Web UI.
|
||||||
|
|
||||||
|
Help expand our documentation by submitting issues and PRs on our [Docs Repository](https://github.com/open-webui/docs).
|
||||||
|
We welcome tutorials, guides and other documentation improvements!
|
||||||
|
|
||||||
### 🌐 Translations and Internationalization
|
### 🌐 Translations and Internationalization
|
||||||
|
|
||||||
|
|
@ -62,9 +74,12 @@ To add a new language:
|
||||||
- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object.
|
- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object.
|
||||||
- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`.
|
- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> When adding new translations, do so in a standalone PR! Feature PRs or PRs fixing a bug should not contain translation updates. Always keep the scope of a PR narrow.
|
||||||
|
|
||||||
### 🤔 Questions & Feedback
|
### 🤔 Questions & Feedback
|
||||||
|
|
||||||
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
|
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue or discussion. We're here to help!
|
||||||
|
|
||||||
## 🙏 Thank You!
|
## 🙏 Thank You!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,9 @@ We appreciate the community's interest in identifying potential vulnerabilities.
|
||||||
> - Screenshots/videos demonstrating the exploit (supplementary to written steps)
|
> - Screenshots/videos demonstrating the exploit (supplementary to written steps)
|
||||||
>
|
>
|
||||||
> **Failure to provide a reproducible PoC may lead to closure of the report**
|
> **Failure to provide a reproducible PoC may lead to closure of the report**
|
||||||
> We will notify you, if we struggle to reproduce the exploit using your PoC to allow you to improve your PoC
|
>
|
||||||
> However, if we repeatedly cannot reproduce the exploit using the PoC, the report may be closed
|
> We will notify you, if we struggle to reproduce the exploit using your PoC to allow you to improve your PoC.
|
||||||
|
> However, if we repeatedly cannot reproduce the exploit using the PoC, the report may be closed.
|
||||||
|
|
||||||
5. **Required Patch or Actionable Remediation Plan Submission**: Along with the PoC, reporters must provide a patch or some actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
|
5. **Required Patch or Actionable Remediation Plan Submission**: Along with the PoC, reporters must provide a patch or some actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
|
||||||
|
|
||||||
|
|
@ -56,9 +57,10 @@ We appreciate the community's interest in identifying potential vulnerabilities.
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Note**: If you believe you have found a security issue that
|
> **Note**: If you believe you have found a security issue that
|
||||||
>
|
>
|
||||||
> 1. affects default configurations **or**
|
> 1. affects default configurations, **or**
|
||||||
> 2. represents a genuine bypass of intended security controls **or**
|
> 2. represents a genuine bypass of intended security controls, **or**
|
||||||
> 3. works only with non-default configurations **but the configuration in question is likely to be used by production deployments** > **then we absolutely want to hear about it.** This policy is intended to filter configuration issues and deployment problems, not to discourage legitimate security research.
|
> 3. works only with non-default configurations, **but the configuration in question is likely to be used by production deployments**,
|
||||||
|
> **then we absolutely want to hear about it.** This policy is intended to filter configuration issues and deployment problems, not to discourage legitimate security research.
|
||||||
|
|
||||||
8. **Threat Model Understanding Required**: Reports must demonstrate understanding of Open WebUI's self-hosted, authenticated, role-based access control architecture. Comparing Open WebUI to services with fundamentally different security models without acknowledging the architectural differences may result in report rejection.
|
8. **Threat Model Understanding Required**: Reports must demonstrate understanding of Open WebUI's self-hosted, authenticated, role-based access control architecture. Comparing Open WebUI to services with fundamentally different security models without acknowledging the architectural differences may result in report rejection.
|
||||||
|
|
||||||
|
|
@ -66,12 +68,16 @@ We appreciate the community's interest in identifying potential vulnerabilities.
|
||||||
|
|
||||||
> [!WARNING] > **Using CVE Precedents:** If you cite other CVEs to support your report, ensure they are **genuinely comparable** in vulnerability type, threat model, and attack vector. Citing CVEs from different product categories, different vulnerability classes or different deployment models will lead us to suspect the use of AI in your report.
|
> [!WARNING] > **Using CVE Precedents:** If you cite other CVEs to support your report, ensure they are **genuinely comparable** in vulnerability type, threat model, and attack vector. Citing CVEs from different product categories, different vulnerability classes or different deployment models will lead us to suspect the use of AI in your report.
|
||||||
|
|
||||||
11. **Admin Actions Are Out of Scope:** Vulnerabilities that require an administrator to actively perform unsafe actions are **not considered valid vulnerabilities**. Admins have full system control and are expected to understand the security implications of their actions and configurations. This includes but is not limited to: adding malicious external servers (models, tools, webhooks), pasting untrusted code into Functions/Tools, or intentionally weakening security settings. **Reports requiring admin negligence or social engineering of admins may be rejected.**
|
10. **Admin Actions Are Out of Scope:** Vulnerabilities that require an administrator to actively perform unsafe actions are **not considered valid vulnerabilities**. Admins have full system control and are expected to understand the security implications of their actions and configurations. This includes but is not limited to: adding malicious external servers (models, tools, webhooks), pasting untrusted code into Functions/Tools, or intentionally weakening security settings. **Reports requiring admin negligence or social engineering of admins may be rejected.**
|
||||||
|
|
||||||
12. **AI report transparency:** Due to an extreme spike in AI-aided vulnerability reports **YOU MUST DISCLOSE if AI was used in any capacity** - whether for writing the report, generating the PoC, or identifying the vulnerability. If AI helped you in any way shape or form in the creation of the report, PoC or finding the vulnerability, you MUST disclose it.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> AI-aided vulnerability reports **will not be rejected by us by default.** But:
|
> Similar to rule "Default Configuration Testing": If you believe you have found a vulnerability that affects admins and is NOT caused by admin negligence or intentionally malicious actions,
|
||||||
|
> **then we absolutely want to hear about it.** This policy is intended to filter social engineering attacks on admins, malicious plugins being deployed by admins and similar malicious actions, not to discourage legitimate security research.
|
||||||
|
|
||||||
|
11. **AI report transparency:** Due to an extreme spike in AI-aided vulnerability reports **YOU MUST DISCLOSE if AI was used in any capacity** - whether for writing the report, generating the PoC, or identifying the vulnerability. If AI helped you in any way shape or form in the creation of the report, PoC or finding the vulnerability, you MUST disclose it.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> AI-aided vulnerability reports **will not be rejected by us by default**. But:
|
||||||
>
|
>
|
||||||
> - If we suspect you used AI (but you did not disclose it to us), we will be asking tough follow-up questions to validate your understanding of the reported vulnerability and Open WebUI itself.
|
> - If we suspect you used AI (but you did not disclose it to us), we will be asking tough follow-up questions to validate your understanding of the reported vulnerability and Open WebUI itself.
|
||||||
> - If we suspect you used AI (but you did not disclose it to us) **and** your report ends up being invalid/not a vulnerability/not reproducible, then you **may be banned** from reporting future vulnerabilities.
|
> - If we suspect you used AI (but you did not disclose it to us) **and** your report ends up being invalid/not a vulnerability/not reproducible, then you **may be banned** from reporting future vulnerabilities.
|
||||||
|
|
@ -88,11 +94,21 @@ We appreciate the community's interest in identifying potential vulnerabilities.
|
||||||
|
|
||||||
**Non-compliant submissions will be closed, and repeat extreme violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users.
|
**Non-compliant submissions will be closed, and repeat extreme violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users.
|
||||||
|
|
||||||
|
## Where to report the vulnerability
|
||||||
|
|
||||||
If you want to report a vulnerability and can meet the outlined requirements, [open a vulnerability report here](https://github.com/open-webui/open-webui/security/advisories/new).
|
If you want to report a vulnerability and can meet the outlined requirements, [open a vulnerability report here](https://github.com/open-webui/open-webui/security/advisories/new).
|
||||||
|
If you feel like you are not able to follow ALL outlined requirements for vulnerability-specific reasons, still do report it, we will check every report either way.
|
||||||
|
|
||||||
## Product Security And For Non-Vulnerability Security Concerns:
|
## Product Security And For Non-Vulnerability Related Security Concerns:
|
||||||
|
|
||||||
If your concern does not meet the vulnerability requirements outlined above, such as:
|
If your concern does not meet the vulnerability requirements outlined above, is not a vulnerability, **but is still related to security concerns**, then use the following channels instead:
|
||||||
|
|
||||||
|
- **Documentation issues/improvement ideas:** Open an issue on our [Documentation Repository](https://github.com/open-webui/docs)
|
||||||
|
- **Feature requests:** Create a discussion in [GitHub Discussions - Ideas](https://github.com/open-webui/open-webui/discussions/) to discuss with the community if this feature request is wanted by multiple people
|
||||||
|
- **Configuration help:** Ask the community for help and guidance on our [Discord Server](https://discord.gg/5rJgQTnV4s) or on [Reddit](https://www.reddit.com/r/OpenWebUI/)
|
||||||
|
- **General issues:** Use our [Issue Tracker](https://github.com/open-webui/open-webui/issues)
|
||||||
|
|
||||||
|
**Examples of non-vulnerability, still security related concerns:**
|
||||||
|
|
||||||
- Suggestions for better default configuration values
|
- Suggestions for better default configuration values
|
||||||
- Security hardening recommendations
|
- Security hardening recommendations
|
||||||
|
|
@ -102,12 +118,7 @@ If your concern does not meet the vulnerability requirements outlined above, suc
|
||||||
- Feature requests for optional security enhancements (2FA, audit logging, etc.)
|
- Feature requests for optional security enhancements (2FA, audit logging, etc.)
|
||||||
- General security questions about production deployment
|
- General security questions about production deployment
|
||||||
|
|
||||||
**then use one of the following channels instead:**
|
Please use the adequate channel for your specific issue - e.g. best-practice guidance or additional documentation needs into the Documentation Repository, and feature requests into the Main Repository as an issue or discussion.
|
||||||
|
|
||||||
- **Documentation issues/improvement ideas:** Open an issue on our [Documentation Repository](https://github.com/open-webui/docs)
|
|
||||||
- **Feature requests:** Create a discussion in [GitHub Discussions - Ideas](https://github.com/open-webui/open-webui/discussions/) to discuss with the community if this feature request is wanted by multiple people
|
|
||||||
- **Configuration help:** Ask the community for help and guidance on our [Discord Server](https://discord.gg/5rJgQTnV4s) or on [Reddit](https://www.reddit.com/r/OpenWebUI/)
|
|
||||||
- **General issues:** Use our [Issue Tracker](https://github.com/open-webui/open-webui/issues)
|
|
||||||
|
|
||||||
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
|
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
|
||||||
|
|
||||||
|
|
@ -115,4 +126,4 @@ For any other immediate concerns, please create an issue in our [issue tracker](
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_Last updated on **2025-10-12**._
|
_Last updated on **2025-11-06**._
|
||||||
|
|
|
||||||
688
package-lock.json
generated
688
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.34",
|
"version": "0.6.35",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.34",
|
"version": "0.6.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.5.0",
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
|
@ -103,8 +103,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "3.2.2",
|
"@sveltejs/adapter-auto": "3.2.2",
|
||||||
"@sveltejs/adapter-static": "^3.0.2",
|
"@sveltejs/adapter-static": "^3.0.2",
|
||||||
"@sveltejs/kit": "^2.5.20",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
|
|
@ -114,14 +114,14 @@
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-cypress": "^3.4.0",
|
"eslint-plugin-cypress": "^3.4.0",
|
||||||
"eslint-plugin-svelte": "^2.43.0",
|
"eslint-plugin-svelte": "^2.45.1",
|
||||||
"i18next-parser": "^9.0.1",
|
"i18next-parser": "^9.0.1",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"sass-embedded": "^1.81.0",
|
"sass-embedded": "^1.81.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^3.8.5",
|
"svelte-check": "^4.0.0",
|
||||||
"svelte-confetti": "^1.3.2",
|
"svelte-confetti": "^1.3.2",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
|
|
@ -155,18 +155,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@antfu/install-pkg": {
|
"node_modules/@antfu/install-pkg": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
|
||||||
|
|
@ -1997,16 +1985,23 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"node_modules/@jridgewell/remapping": {
|
||||||
"node": ">=6.0.0"
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
|
@ -2017,18 +2012,11 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/set-array": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
|
|
@ -2210,23 +2198,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||||
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="
|
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="
|
||||||
},
|
},
|
||||||
"node_modules/@melt-ui/svelte": {
|
|
||||||
"version": "0.76.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.2.tgz",
|
|
||||||
"integrity": "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/core": "^1.3.1",
|
|
||||||
"@floating-ui/dom": "^1.4.5",
|
|
||||||
"@internationalized/date": "^3.5.0",
|
|
||||||
"dequal": "^2.0.3",
|
|
||||||
"focus-trap": "^7.5.2",
|
|
||||||
"nanoid": "^5.0.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"svelte": ">=3 <5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mermaid-js/parser": {
|
"node_modules/@mermaid-js/parser": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
|
||||||
|
|
@ -2948,42 +2919,89 @@
|
||||||
"license": "LIL"
|
"license": "LIL"
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||||
"version": "3.1.1",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz",
|
||||||
"integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==",
|
"integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
|
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
"magic-string": "^0.30.10",
|
"magic-string": "^0.30.12",
|
||||||
"svelte-hmr": "^0.16.0",
|
"vitefu": "^1.0.3"
|
||||||
"vitefu": "^0.2.5"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || >=20"
|
"node": "^18.0.0 || ^20.0.0 || >=22"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
"svelte": "^5.0.0-next.96 || ^5.0.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
||||||
"version": "2.1.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
|
||||||
"integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==",
|
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || >=20"
|
"node": "^18.0.0 || ^20.0.0 || >=22"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0",
|
||||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
"svelte": "^5.0.0-next.96 || ^5.0.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.17",
|
"version": "0.5.17",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||||
|
|
@ -4198,12 +4216,6 @@
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/pug": {
|
|
||||||
"version": "2.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
|
||||||
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/raf": {
|
"node_modules/@types/raf": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
|
@ -4803,11 +4815,12 @@
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
},
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||||
"dependencies": {
|
"license": "Apache-2.0",
|
||||||
"dequal": "^2.0.3"
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asn1": {
|
"node_modules/asn1": {
|
||||||
|
|
@ -4895,11 +4908,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.0.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
|
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||||
"dependencies": {
|
"license": "Apache-2.0",
|
||||||
"dequal": "^2.0.3"
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
|
|
@ -4990,6 +5004,23 @@
|
||||||
"svelte": "^4.0.0 || ^5.0.0-next.118"
|
"svelte": "^4.0.0 || ^5.0.0-next.118"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bits-ui/node_modules/@melt-ui/svelte": {
|
||||||
|
"version": "0.76.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.2.tgz",
|
||||||
|
"integrity": "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.3.1",
|
||||||
|
"@floating-ui/dom": "^1.4.5",
|
||||||
|
"@internationalized/date": "^3.5.0",
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"focus-trap": "^7.5.2",
|
||||||
|
"nanoid": "^5.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": ">=3 <5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
|
||||||
|
|
@ -5702,24 +5733,13 @@
|
||||||
"integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==",
|
"integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/code-red": {
|
"node_modules/clsx": {
|
||||||
"version": "1.0.4",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
"dependencies": {
|
"license": "MIT",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"engines": {
|
||||||
"@types/estree": "^1.0.1",
|
"node": ">=6"
|
||||||
"acorn": "^8.10.0",
|
|
||||||
"estree-walker": "^3.0.3",
|
|
||||||
"periscopic": "^3.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/code-red/node_modules/estree-walker": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "^1.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/codedent": {
|
"node_modules/codedent": {
|
||||||
|
|
@ -5981,18 +6001,6 @@
|
||||||
"url": "https://github.com/sponsors/fb55"
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-tree": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
|
||||||
"dependencies": {
|
|
||||||
"mdn-data": "2.0.30",
|
|
||||||
"source-map-js": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/css-what": {
|
"node_modules/css-what": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||||
|
|
@ -6815,15 +6823,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-indent": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
|
|
@ -7116,12 +7115,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es6-promise": {
|
|
||||||
"version": "3.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
|
|
||||||
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
||||||
|
|
@ -7278,22 +7271,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-svelte": {
|
"node_modules/eslint-plugin-svelte": {
|
||||||
"version": "2.43.0",
|
"version": "2.46.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz",
|
||||||
"integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==",
|
"integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
"eslint-compat-utils": "^0.5.1",
|
"eslint-compat-utils": "^0.5.1",
|
||||||
"esutils": "^2.0.3",
|
"esutils": "^2.0.3",
|
||||||
"known-css-properties": "^0.34.0",
|
"known-css-properties": "^0.35.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-load-config": "^3.1.4",
|
"postcss-load-config": "^3.1.4",
|
||||||
"postcss-safe-parser": "^6.0.0",
|
"postcss-safe-parser": "^6.0.0",
|
||||||
"postcss-selector-parser": "^6.1.0",
|
"postcss-selector-parser": "^6.1.0",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.2",
|
||||||
"svelte-eslint-parser": "^0.41.0"
|
"svelte-eslint-parser": "^0.43.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.17.0 || >=16.0.0"
|
"node": "^14.17.0 || >=16.0.0"
|
||||||
|
|
@ -7303,7 +7297,7 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0",
|
"eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0",
|
||||||
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191"
|
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"svelte": {
|
"svelte": {
|
||||||
|
|
@ -7410,6 +7404,15 @@
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esrap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esrecurse": {
|
"node_modules/esrecurse": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||||
|
|
@ -9021,10 +9024,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/known-css-properties": {
|
"node_modules/known-css-properties": {
|
||||||
"version": "0.34.0",
|
"version": "0.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz",
|
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
|
||||||
"integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==",
|
"integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/kokoro-js": {
|
"node_modules/kokoro-js": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
|
@ -9658,11 +9662,12 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.11",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/markdown-it": {
|
"node_modules/markdown-it": {
|
||||||
|
|
@ -9738,11 +9743,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdn-data": {
|
|
||||||
"version": "2.0.30",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
|
||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
|
||||||
},
|
|
||||||
"node_modules/mdurl": {
|
"node_modules/mdurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
|
@ -9857,15 +9857,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/min-indent": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
|
@ -9961,18 +9952,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
|
||||||
"version": "0.5.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
|
||||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"minimist": "^1.2.6"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"mkdirp": "bin/cmd.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mktemp": {
|
"node_modules/mktemp": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz",
|
||||||
|
|
@ -10461,32 +10440,6 @@
|
||||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/periscopic": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "^1.0.0",
|
|
||||||
"estree-walker": "^3.0.0",
|
|
||||||
"is-reference": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/periscopic/node_modules/estree-walker": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/periscopic/node_modules/is-reference": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/phonemizer": {
|
"node_modules/phonemizer": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz",
|
||||||
|
|
@ -10684,6 +10637,7 @@
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0"
|
"node": ">=12.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11581,73 +11535,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
"node_modules/sander": {
|
|
||||||
"version": "0.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
|
|
||||||
"integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"es6-promise": "^3.1.2",
|
|
||||||
"graceful-fs": "^4.1.3",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"rimraf": "^2.5.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sander/node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sander/node_modules/glob": {
|
|
||||||
"version": "7.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"fs.realpath": "^1.0.0",
|
|
||||||
"inflight": "^1.0.4",
|
|
||||||
"inherits": "2",
|
|
||||||
"minimatch": "^3.1.1",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sander/node_modules/minimatch": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sander/node_modules/rimraf": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rimraf": "bin.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sass-embedded": {
|
"node_modules/sass-embedded": {
|
||||||
"version": "1.81.0",
|
"version": "1.81.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.81.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.81.0.tgz",
|
||||||
|
|
@ -12231,21 +12118,6 @@
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sorcery": {
|
|
||||||
"version": "0.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
|
||||||
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
|
||||||
"buffer-crc32": "^0.2.5",
|
|
||||||
"minimist": "^1.2.0",
|
|
||||||
"sander": "^0.5.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"sorcery": "bin/sorcery"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sort-keys": {
|
"node_modules/sort-keys": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz",
|
||||||
|
|
@ -12456,18 +12328,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-indent": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"min-indent": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
|
|
@ -12527,47 +12387,115 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "4.2.19",
|
"version": "5.42.2",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz",
|
||||||
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
|
"integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.1",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
"@jridgewell/trace-mapping": "^0.3.18",
|
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||||
"@types/estree": "^1.0.1",
|
"@types/estree": "^1.0.5",
|
||||||
"acorn": "^8.9.0",
|
"acorn": "^8.12.1",
|
||||||
"aria-query": "^5.3.0",
|
"aria-query": "^5.3.1",
|
||||||
"axobject-query": "^4.0.0",
|
"axobject-query": "^4.1.0",
|
||||||
"code-red": "^1.0.3",
|
"clsx": "^2.1.1",
|
||||||
"css-tree": "^2.3.1",
|
"esm-env": "^1.2.1",
|
||||||
"estree-walker": "^3.0.3",
|
"esrap": "^2.1.0",
|
||||||
"is-reference": "^3.0.1",
|
"is-reference": "^3.0.3",
|
||||||
"locate-character": "^3.0.0",
|
"locate-character": "^3.0.0",
|
||||||
"magic-string": "^0.30.4",
|
"magic-string": "^0.30.11",
|
||||||
"periscopic": "^3.1.0"
|
"zimmerframe": "^1.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-check": {
|
"node_modules/svelte-check": {
|
||||||
"version": "3.8.5",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz",
|
||||||
"integrity": "sha512-3OGGgr9+bJ/+1nbPgsvulkLC48xBsqsgtc8Wam281H4G9F5v3mYGa2bHRsPuwHC5brKl4AxJH95QF73kmfihGQ==",
|
"integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.17",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"chokidar": "^3.4.1",
|
"chokidar": "^4.0.1",
|
||||||
|
"fdir": "^6.2.0",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"sade": "^1.7.4",
|
"sade": "^1.7.4"
|
||||||
"svelte-preprocess": "^5.1.3",
|
|
||||||
"typescript": "^5.0.3"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"svelte-check": "bin/svelte-check"
|
"svelte-check": "bin/svelte-check"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-check/node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-check/node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-check/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-check/node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-confetti": {
|
"node_modules/svelte-confetti": {
|
||||||
|
|
@ -12580,10 +12508,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-eslint-parser": {
|
"node_modules/svelte-eslint-parser": {
|
||||||
"version": "0.41.0",
|
"version": "0.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||||
"integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==",
|
"integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-scope": "^7.2.2",
|
"eslint-scope": "^7.2.2",
|
||||||
"eslint-visitor-keys": "^3.4.3",
|
"eslint-visitor-keys": "^3.4.3",
|
||||||
|
|
@ -12598,7 +12527,7 @@
|
||||||
"url": "https://github.com/sponsors/ota-meshi"
|
"url": "https://github.com/sponsors/ota-meshi"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191"
|
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"svelte": {
|
"svelte": {
|
||||||
|
|
@ -12606,80 +12535,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-hmr": {
|
|
||||||
"version": "0.16.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz",
|
|
||||||
"integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20 || ^14.13.1 || >= 16"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"svelte": "^3.19.0 || ^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svelte-preprocess": {
|
|
||||||
"version": "5.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
|
|
||||||
"integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/pug": "^2.0.6",
|
|
||||||
"detect-indent": "^6.1.0",
|
|
||||||
"magic-string": "^0.30.5",
|
|
||||||
"sorcery": "^0.11.0",
|
|
||||||
"strip-indent": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 16.0.0",
|
|
||||||
"pnpm": "^8.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@babel/core": "^7.10.2",
|
|
||||||
"coffeescript": "^2.5.1",
|
|
||||||
"less": "^3.11.3 || ^4.0.0",
|
|
||||||
"postcss": "^7 || ^8",
|
|
||||||
"postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
|
|
||||||
"pug": "^3.0.0",
|
|
||||||
"sass": "^1.26.8",
|
|
||||||
"stylus": "^0.55.0",
|
|
||||||
"sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
|
||||||
"svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0",
|
|
||||||
"typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@babel/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"coffeescript": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"less": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"postcss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"postcss-load-config": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"pug": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"stylus": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sugarss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svelte-sonner": {
|
"node_modules/svelte-sonner": {
|
||||||
"version": "0.3.28",
|
"version": "0.3.28",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz",
|
||||||
|
|
@ -12688,20 +12543,19 @@
|
||||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
|
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte/node_modules/estree-walker": {
|
"node_modules/svelte/node_modules/@types/estree": {
|
||||||
"version": "3.0.3",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"dependencies": {
|
"license": "MIT"
|
||||||
"@types/estree": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/svelte/node_modules/is-reference": {
|
"node_modules/svelte/node_modules/is-reference": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "*"
|
"@types/estree": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svg-pathdata": {
|
"node_modules/svg-pathdata": {
|
||||||
|
|
@ -14213,11 +14067,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitefu": {
|
"node_modules/vitefu": {
|
||||||
"version": "0.2.5",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
|
||||||
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
|
"integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"tests/deps/*",
|
||||||
|
"tests/projects/*",
|
||||||
|
"tests/projects/workspace/packages/*"
|
||||||
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"vite": {
|
"vite": {
|
||||||
|
|
@ -14945,6 +14805,12 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zimmerframe": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.34",
|
"version": "0.6.35",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
|
@ -24,8 +24,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "3.2.2",
|
"@sveltejs/adapter-auto": "3.2.2",
|
||||||
"@sveltejs/adapter-static": "^3.0.2",
|
"@sveltejs/adapter-static": "^3.0.2",
|
||||||
"@sveltejs/kit": "^2.5.20",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
|
|
@ -35,14 +35,14 @@
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-cypress": "^3.4.0",
|
"eslint-plugin-cypress": "^3.4.0",
|
||||||
"eslint-plugin-svelte": "^2.43.0",
|
"eslint-plugin-svelte": "^2.45.1",
|
||||||
"i18next-parser": "^9.0.1",
|
"i18next-parser": "^9.0.1",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"sass-embedded": "^1.81.0",
|
"sass-embedded": "^1.81.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^3.8.5",
|
"svelte-check": "^4.0.0",
|
||||||
"svelte-confetti": "^1.3.2",
|
"svelte-confetti": "^1.3.2",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ dependencies = [
|
||||||
"itsdangerous==2.2.0",
|
"itsdangerous==2.2.0",
|
||||||
|
|
||||||
"python-socketio==5.13.0",
|
"python-socketio==5.13.0",
|
||||||
"python-jose==3.4.0",
|
"python-jose==3.5.0",
|
||||||
"cryptography",
|
"cryptography",
|
||||||
"bcrypt==5.0.0",
|
"bcrypt==5.0.0",
|
||||||
"argon2-cffi==25.1.0",
|
"argon2-cffi==25.1.0",
|
||||||
|
|
@ -73,7 +73,7 @@ dependencies = [
|
||||||
"pymdown-extensions==10.14.2",
|
"pymdown-extensions==10.14.2",
|
||||||
"docx2txt==0.8",
|
"docx2txt==0.8",
|
||||||
"python-pptx==1.0.2",
|
"python-pptx==1.0.2",
|
||||||
"unstructured==0.16.17",
|
"unstructured==0.18.15",
|
||||||
"nltk==3.9.1",
|
"nltk==3.9.1",
|
||||||
"Markdown==3.9",
|
"Markdown==3.9",
|
||||||
"pypandoc==1.15",
|
"pypandoc==1.15",
|
||||||
|
|
@ -151,9 +151,7 @@ all = [
|
||||||
"oracledb==3.2.0",
|
"oracledb==3.2.0",
|
||||||
|
|
||||||
"colbert-ai==0.2.21",
|
"colbert-ai==0.2.21",
|
||||||
|
"firecrawl-py==4.5.0",
|
||||||
"firecrawl-py==1.12.0",
|
|
||||||
"tencentcloud-sdk-python==3.0.1336",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
12
src/app.css
12
src/app.css
|
|
@ -129,8 +129,8 @@ li p {
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
height: 0.8rem;
|
height: 0.45rem;
|
||||||
width: 0.8rem;
|
width: 0.45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
|
|
@ -152,6 +152,14 @@ select {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark select:not([class*='bg-transparent']) {
|
||||||
|
@apply bg-gray-900 text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark select option {
|
||||||
|
@apply bg-gray-850 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
background-position: 200% 0;
|
background-position: 200% 0;
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,7 @@
|
||||||
href="/static/apple-touch-icon.png"
|
href="/static/apple-touch-icon.png"
|
||||||
crossorigin="use-credentials"
|
crossorigin="use-credentials"
|
||||||
/>
|
/>
|
||||||
<link
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
rel="manifest"
|
|
||||||
href="/manifest.json"
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
/>
|
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ export const uploadFile = async (token: string, file: File, metadata?: object |
|
||||||
console.error(data.error);
|
console.error(data.error);
|
||||||
res.error = data.error;
|
res.error = data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (res?.data) {
|
||||||
|
res.data = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1401,6 +1401,33 @@ export const getChangelog = async () => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVersion = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/version`, {
|
||||||
|
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.error(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res?.version ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
export const getVersionUpdates = async (token: string) => {
|
export const getVersionUpdates = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getModels = async (token: string = '') => {
|
export const getModelItems = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/models/`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/models/list`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Function deleted successfully'));
|
toast.success($i18n.t('Function deleted successfully'));
|
||||||
|
functions = functions.filter((f) => f.id !== func.id);
|
||||||
|
|
||||||
_functions.set(await getFunctions(localStorage.token));
|
_functions.set(await getFunctions(localStorage.token));
|
||||||
models.set(
|
models.set(
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@
|
||||||
let STT_AZURE_BASE_URL = '';
|
let STT_AZURE_BASE_URL = '';
|
||||||
let STT_AZURE_MAX_SPEAKERS = '';
|
let STT_AZURE_MAX_SPEAKERS = '';
|
||||||
let STT_DEEPGRAM_API_KEY = '';
|
let STT_DEEPGRAM_API_KEY = '';
|
||||||
|
let STT_MISTRAL_API_KEY = '';
|
||||||
|
let STT_MISTRAL_API_BASE_URL = '';
|
||||||
|
let STT_MISTRAL_USE_CHAT_COMPLETIONS = false;
|
||||||
|
|
||||||
let STT_WHISPER_MODEL_LOADING = false;
|
let STT_WHISPER_MODEL_LOADING = false;
|
||||||
|
|
||||||
|
|
@ -135,7 +138,10 @@
|
||||||
AZURE_REGION: STT_AZURE_REGION,
|
AZURE_REGION: STT_AZURE_REGION,
|
||||||
AZURE_LOCALES: STT_AZURE_LOCALES,
|
AZURE_LOCALES: STT_AZURE_LOCALES,
|
||||||
AZURE_BASE_URL: STT_AZURE_BASE_URL,
|
AZURE_BASE_URL: STT_AZURE_BASE_URL,
|
||||||
AZURE_MAX_SPEAKERS: STT_AZURE_MAX_SPEAKERS
|
AZURE_MAX_SPEAKERS: STT_AZURE_MAX_SPEAKERS,
|
||||||
|
MISTRAL_API_KEY: STT_MISTRAL_API_KEY,
|
||||||
|
MISTRAL_API_BASE_URL: STT_MISTRAL_API_BASE_URL,
|
||||||
|
MISTRAL_USE_CHAT_COMPLETIONS: STT_MISTRAL_USE_CHAT_COMPLETIONS
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -184,6 +190,9 @@
|
||||||
STT_AZURE_BASE_URL = res.stt.AZURE_BASE_URL;
|
STT_AZURE_BASE_URL = res.stt.AZURE_BASE_URL;
|
||||||
STT_AZURE_MAX_SPEAKERS = res.stt.AZURE_MAX_SPEAKERS;
|
STT_AZURE_MAX_SPEAKERS = res.stt.AZURE_MAX_SPEAKERS;
|
||||||
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
|
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
|
||||||
|
STT_MISTRAL_API_KEY = res.stt.MISTRAL_API_KEY;
|
||||||
|
STT_MISTRAL_API_BASE_URL = res.stt.MISTRAL_API_BASE_URL;
|
||||||
|
STT_MISTRAL_USE_CHAT_COMPLETIONS = res.stt.MISTRAL_USE_CHAT_COMPLETIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
await getVoices();
|
await getVoices();
|
||||||
|
|
@ -201,7 +210,7 @@
|
||||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -235,6 +244,7 @@
|
||||||
<option value="web">{$i18n.t('Web API')}</option>
|
<option value="web">{$i18n.t('Web API')}</option>
|
||||||
<option value="deepgram">{$i18n.t('Deepgram')}</option>
|
<option value="deepgram">{$i18n.t('Deepgram')}</option>
|
||||||
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
|
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
|
||||||
|
<option value="mistral">{$i18n.t('MistralAI')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -367,6 +377,67 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if STT_ENGINE === 'mistral'}
|
||||||
|
<div>
|
||||||
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
class="flex-1 w-full bg-transparent outline-hidden"
|
||||||
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={STT_MISTRAL_API_BASE_URL}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_MISTRAL_API_KEY} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
|
bind:value={STT_MODEL}
|
||||||
|
placeholder="voxtral-mini-latest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t('Leave empty to use the default model (voxtral-mini-latest).')}
|
||||||
|
<a
|
||||||
|
class=" hover:underline dark:text-gray-200 text-gray-800"
|
||||||
|
href="https://docs.mistral.ai/capabilities/audio_transcription"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{$i18n.t('Learn more about Voxtral transcription.')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="text-xs font-medium">{$i18n.t('Use Chat Completions API')}</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={STT_MISTRAL_USE_CHAT_COMPLETIONS}
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
|
||||||
|
></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if STT_ENGINE === ''}
|
{:else if STT_ENGINE === ''}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
||||||
|
|
@ -427,7 +498,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
{#if config}
|
{#if config}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@
|
||||||
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
|
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,14 +171,6 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
|
|
||||||
!RAGConfig.DATALAB_MARKER_API_KEY
|
|
||||||
) {
|
|
||||||
toast.error($i18n.t('Datalab Marker API Key required.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
|
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
|
||||||
RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG &&
|
RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG &&
|
||||||
|
|
@ -220,6 +212,15 @@
|
||||||
await embeddingModelUpdateHandler();
|
await embeddingModelUpdateHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RAGConfig.MINERU_PARAMS) {
|
||||||
|
try {
|
||||||
|
JSON.parse(RAGConfig.MINERU_PARAMS);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error($i18n.t('Invalid JSON format in MinerU Parameters'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await updateRAGConfig(localStorage.token, {
|
const res = await updateRAGConfig(localStorage.token, {
|
||||||
...RAGConfig,
|
...RAGConfig,
|
||||||
ALLOWED_FILE_EXTENSIONS: RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
|
ALLOWED_FILE_EXTENSIONS: RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
|
||||||
|
|
@ -228,7 +229,13 @@
|
||||||
DOCLING_PICTURE_DESCRIPTION_LOCAL: JSON.parse(
|
DOCLING_PICTURE_DESCRIPTION_LOCAL: JSON.parse(
|
||||||
RAGConfig.DOCLING_PICTURE_DESCRIPTION_LOCAL || '{}'
|
RAGConfig.DOCLING_PICTURE_DESCRIPTION_LOCAL || '{}'
|
||||||
),
|
),
|
||||||
DOCLING_PICTURE_DESCRIPTION_API: JSON.parse(RAGConfig.DOCLING_PICTURE_DESCRIPTION_API || '{}')
|
DOCLING_PICTURE_DESCRIPTION_API: JSON.parse(
|
||||||
|
RAGConfig.DOCLING_PICTURE_DESCRIPTION_API || '{}'
|
||||||
|
),
|
||||||
|
MINERU_PARAMS:
|
||||||
|
typeof RAGConfig.MINERU_PARAMS === 'string' && RAGConfig.MINERU_PARAMS.trim() !== ''
|
||||||
|
? JSON.parse(RAGConfig.MINERU_PARAMS)
|
||||||
|
: {}
|
||||||
});
|
});
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
};
|
};
|
||||||
|
|
@ -269,6 +276,11 @@
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
|
config.MINERU_PARAMS =
|
||||||
|
typeof config.MINERU_PARAMS === 'object'
|
||||||
|
? JSON.stringify(config.MINERU_PARAMS ?? {}, null, 2)
|
||||||
|
: config.MINERU_PARAMS;
|
||||||
|
|
||||||
RAGConfig = config;
|
RAGConfig = config;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -325,7 +337,7 @@
|
||||||
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -733,7 +745,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<Textarea
|
<Textarea
|
||||||
bind:value={RAGConfig.DOCLING_PARAMETERS}
|
bind:value={RAGConfig.DOCLING_PARAMS}
|
||||||
placeholder={$i18n.t('Enter additional parameters in JSON format')}
|
placeholder={$i18n.t('Enter additional parameters in JSON format')}
|
||||||
minSize={100}
|
minSize={100}
|
||||||
/>
|
/>
|
||||||
|
|
@ -754,6 +766,11 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
|
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
|
||||||
<div class="my-0.5 flex gap-2 pr-2">
|
<div class="my-0.5 flex gap-2 pr-2">
|
||||||
|
<input
|
||||||
|
class="flex-1 w-full text-sm bg-transparent outline-hidden"
|
||||||
|
placeholder={$i18n.t('Enter Mistral API Base URL')}
|
||||||
|
bind:value={RAGConfig.MISTRAL_OCR_API_BASE_URL}
|
||||||
|
/>
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Mistral API Key')}
|
placeholder={$i18n.t('Enter Mistral API Key')}
|
||||||
bind:value={RAGConfig.MISTRAL_OCR_API_KEY}
|
bind:value={RAGConfig.MISTRAL_OCR_API_KEY}
|
||||||
|
|
@ -785,8 +802,8 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="local">{$i18n.t('Self-Hosted')}</option>
|
<option value="local">{$i18n.t('local')}</option>
|
||||||
<option value="cloud">{$i18n.t('minerU managed (Cloud API)')}</option>
|
<option value="cloud">{$i18n.t('cloud')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -802,19 +819,16 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Key (Cloud only) -->
|
|
||||||
{#if RAGConfig.MINERU_API_MODE === 'cloud'}
|
|
||||||
<div class="flex w-full mt-2">
|
<div class="flex w-full mt-2">
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter MinerU API Key')}
|
placeholder={$i18n.t('Enter MinerU API Key')}
|
||||||
bind:value={RAGConfig.MINERU_API_KEY}
|
bind:value={RAGConfig.MINERU_API_KEY}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Parameters -->
|
<!-- Parameters -->
|
||||||
<div class="flex justify-between w-full mt-2">
|
<div class="flex flex-col justify-between w-full mt-2">
|
||||||
<div class="self-center text-xs font-medium">
|
<div class="text-xs font-medium">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
'Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)'
|
'Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)'
|
||||||
|
|
@ -824,22 +838,9 @@
|
||||||
{$i18n.t('Parameters')}
|
{$i18n.t('Parameters')}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="mt-1.5">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={typeof RAGConfig.MINERU_PARAMS === 'object' &&
|
bind:value={RAGConfig.MINERU_PARAMS}
|
||||||
RAGConfig.MINERU_PARAMS !== null &&
|
|
||||||
Object.keys(RAGConfig.MINERU_PARAMS).length > 0
|
|
||||||
? JSON.stringify(RAGConfig.MINERU_PARAMS, null, 2)
|
|
||||||
: ''}
|
|
||||||
on:input={(e) => {
|
|
||||||
try {
|
|
||||||
const value = e.target.value.trim();
|
|
||||||
RAGConfig.MINERU_PARAMS = value ? JSON.parse(value) : {};
|
|
||||||
} catch (err) {
|
|
||||||
// Keep the string value if JSON is invalid (user is still typing)
|
|
||||||
RAGConfig.MINERU_PARAMS = e.target.value;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={`{\n "enable_ocr": false,\n "enable_formula": true,\n "enable_table": true,\n "language": "en",\n "model_version": "pipeline",\n "page_ranges": ""\n}`}
|
placeholder={`{\n "enable_ocr": false,\n "enable_formula": true,\n "enable_table": true,\n "language": "en",\n "model_version": "pipeline",\n "page_ranges": ""\n}`}
|
||||||
minSize={100}
|
minSize={100}
|
||||||
/>
|
/>
|
||||||
|
|
@ -925,7 +926,7 @@
|
||||||
|
|
||||||
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
|
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Embedding')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Embedding')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -1100,7 +1101,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Retrieval')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Retrieval')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -1343,7 +1344,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -1455,7 +1456,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -1475,7 +1476,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
{#if evaluationConfig !== null}
|
{#if evaluationConfig !== null}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
|
|
||||||
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
|
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium flex justify-between items-center">
|
<div class=" mt-0.5 mb-2.5 text-base font-medium flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
{$i18n.t('Manage')}
|
{$i18n.t('Manage')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -118,11 +118,11 @@
|
||||||
updateHandler();
|
updateHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="mt-0.5 space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
<div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if adminConfig !== null}
|
{#if adminConfig !== null}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -637,7 +637,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -111,7 +111,7 @@
|
||||||
>
|
>
|
||||||
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -384,7 +384,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@
|
||||||
|
|
||||||
<div class=" my-2 mb-5" id="model-list">
|
<div class=" my-2 mb-5" id="model-list">
|
||||||
{#if models.length > 0}
|
{#if models.length > 0}
|
||||||
{#each filteredModels as model, modelIdx (model.id)}
|
{#each filteredModels as model, modelIdx (`${model.id}-${modelIdx}`)}
|
||||||
<div
|
<div
|
||||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition {model
|
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition {model
|
||||||
?.meta?.hidden
|
?.meta?.hidden
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
{#if servers !== null}
|
{#if servers !== null}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
{#if webConfig}
|
{#if webConfig}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
@ -724,7 +724,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,19 +112,17 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#key selectedUser}
|
<AddUserModal
|
||||||
<EditUserModal
|
bind:show={showAddUserModal}
|
||||||
bind:show={showEditUserModal}
|
|
||||||
{selectedUser}
|
|
||||||
sessionUser={$user}
|
|
||||||
on:save={async () => {
|
on:save={async () => {
|
||||||
getUserList();
|
getUserList();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/key}
|
|
||||||
|
|
||||||
<AddUserModal
|
<EditUserModal
|
||||||
bind:show={showAddUserModal}
|
bind:show={showEditUserModal}
|
||||||
|
{selectedUser}
|
||||||
|
sessionUser={$user}
|
||||||
on:save={async () => {
|
on:save={async () => {
|
||||||
getUserList();
|
getUserList();
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,18 @@
|
||||||
export let selectedUser;
|
export let selectedUser;
|
||||||
export let sessionUser;
|
export let sessionUser;
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
if (selectedUser) {
|
||||||
|
_user = selectedUser;
|
||||||
|
_user.password = '';
|
||||||
|
loadUserGroups();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _user = {
|
let _user = {
|
||||||
profile_image_url: '',
|
profile_image_url: '',
|
||||||
role: 'pending',
|
role: 'pending',
|
||||||
|
|
@ -52,14 +64,6 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (selectedUser) {
|
|
||||||
_user = selectedUser;
|
|
||||||
_user.password = '';
|
|
||||||
loadUserGroups();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
||||||
<div
|
<div
|
||||||
|
id="navbar-bg-gradient-to-b"
|
||||||
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { artifactCode, chatId, settings, showArtifacts, showControls } from '$lib/stores';
|
import {
|
||||||
|
artifactCode,
|
||||||
|
chatId,
|
||||||
|
settings,
|
||||||
|
showArtifacts,
|
||||||
|
showControls,
|
||||||
|
artifactContents
|
||||||
|
} from '$lib/stores';
|
||||||
import { copyToClipboard, createMessagesList } from '$lib/utils';
|
import { copyToClipboard, createMessagesList } from '$lib/utils';
|
||||||
|
|
||||||
import XMark from '../icons/XMark.svelte';
|
import XMark from '../icons/XMark.svelte';
|
||||||
|
|
@ -15,8 +22,6 @@
|
||||||
import Download from '../icons/Download.svelte';
|
import Download from '../icons/Download.svelte';
|
||||||
|
|
||||||
export let overlay = false;
|
export let overlay = false;
|
||||||
export let history;
|
|
||||||
let messages = [];
|
|
||||||
|
|
||||||
let contents: Array<{ type: string; content: string }> = [];
|
let contents: Array<{ type: string; content: string }> = [];
|
||||||
let selectedContentIdx = 0;
|
let selectedContentIdx = 0;
|
||||||
|
|
@ -24,121 +29,11 @@
|
||||||
let copied = false;
|
let copied = false;
|
||||||
let iframeElement: HTMLIFrameElement;
|
let iframeElement: HTMLIFrameElement;
|
||||||
|
|
||||||
$: if (history) {
|
|
||||||
messages = createMessagesList(history, history.currentId);
|
|
||||||
getContents();
|
|
||||||
} else {
|
|
||||||
messages = [];
|
|
||||||
getContents();
|
|
||||||
}
|
|
||||||
|
|
||||||
const getContents = () => {
|
|
||||||
contents = [];
|
|
||||||
messages.forEach((message) => {
|
|
||||||
if (message?.role !== 'user' && message?.content) {
|
|
||||||
const codeBlockContents = message.content.match(/```[\s\S]*?```/g);
|
|
||||||
let codeBlocks = [];
|
|
||||||
|
|
||||||
let htmlContent = '';
|
|
||||||
let cssContent = '';
|
|
||||||
let jsContent = '';
|
|
||||||
|
|
||||||
if (codeBlockContents) {
|
|
||||||
codeBlockContents.forEach((block) => {
|
|
||||||
const lang = block.split('\n')[0].replace('```', '').trim().toLowerCase();
|
|
||||||
const code = block.replace(/```[\s\S]*?\n/, '').replace(/```$/, '');
|
|
||||||
codeBlocks.push({ lang, code });
|
|
||||||
});
|
|
||||||
|
|
||||||
codeBlocks.forEach((block) => {
|
|
||||||
const { lang, code } = block;
|
|
||||||
|
|
||||||
if (lang === 'html') {
|
|
||||||
htmlContent += code + '\n';
|
|
||||||
} else if (lang === 'css') {
|
|
||||||
cssContent += code + '\n';
|
|
||||||
} else if (lang === 'javascript' || lang === 'js') {
|
|
||||||
jsContent += code + '\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const inlineHtml = message.content.match(/<html>[\s\S]*?<\/html>/gi);
|
|
||||||
const inlineCss = message.content.match(/<style>[\s\S]*?<\/style>/gi);
|
|
||||||
const inlineJs = message.content.match(/<script>[\s\S]*?<\/script>/gi);
|
|
||||||
|
|
||||||
if (inlineHtml) {
|
|
||||||
inlineHtml.forEach((block) => {
|
|
||||||
const content = block.replace(/<\/?html>/gi, ''); // Remove <html> tags
|
|
||||||
htmlContent += content + '\n';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (inlineCss) {
|
|
||||||
inlineCss.forEach((block) => {
|
|
||||||
const content = block.replace(/<\/?style>/gi, ''); // Remove <style> tags
|
|
||||||
cssContent += content + '\n';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (inlineJs) {
|
|
||||||
inlineJs.forEach((block) => {
|
|
||||||
const content = block.replace(/<\/?script>/gi, ''); // Remove <script> tags
|
|
||||||
jsContent += content + '\n';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (htmlContent || cssContent || jsContent) {
|
|
||||||
const renderedContent = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<${''}style>
|
|
||||||
body {
|
|
||||||
background-color: white; /* Ensure the iframe has a white background */
|
|
||||||
}
|
|
||||||
|
|
||||||
${cssContent}
|
|
||||||
</${''}style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${htmlContent}
|
|
||||||
|
|
||||||
<${''}script>
|
|
||||||
${jsContent}
|
|
||||||
</${''}script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
contents = [...contents, { type: 'iframe', content: renderedContent }];
|
|
||||||
} else {
|
|
||||||
// Check for SVG content
|
|
||||||
for (const block of codeBlocks) {
|
|
||||||
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
|
|
||||||
contents = [...contents, { type: 'svg', content: block.code }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (contents.length === 0) {
|
|
||||||
showControls.set(false);
|
|
||||||
showArtifacts.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedContentIdx = contents ? contents.length - 1 : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
function navigateContent(direction: 'prev' | 'next') {
|
function navigateContent(direction: 'prev' | 'next') {
|
||||||
console.log(selectedContentIdx);
|
|
||||||
|
|
||||||
selectedContentIdx =
|
selectedContentIdx =
|
||||||
direction === 'prev'
|
direction === 'prev'
|
||||||
? Math.max(selectedContentIdx - 1, 0)
|
? Math.max(selectedContentIdx - 1, 0)
|
||||||
: Math.min(selectedContentIdx + 1, contents.length - 1);
|
: Math.min(selectedContentIdx + 1, contents.length - 1);
|
||||||
|
|
||||||
console.log(selectedContentIdx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframeLoadHandler = () => {
|
const iframeLoadHandler = () => {
|
||||||
|
|
@ -201,6 +96,18 @@
|
||||||
selectedContentIdx = codeIdx !== -1 ? codeIdx : 0;
|
selectedContentIdx = codeIdx !== -1 ? codeIdx : 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
artifactContents.subscribe((value) => {
|
||||||
|
contents = value;
|
||||||
|
console.log('Artifact contents updated:', contents);
|
||||||
|
|
||||||
|
if (contents.length === 0) {
|
||||||
|
showControls.set(false);
|
||||||
|
showArtifacts.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedContentIdx = contents ? contents.length - 1 : 0;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||||
|
|
||||||
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
const i18n: Writable<i18nType> = getContext('i18n');
|
const i18n: Writable<i18nType> = getContext('i18n');
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -26,6 +27,7 @@
|
||||||
banners,
|
banners,
|
||||||
user,
|
user,
|
||||||
socket,
|
socket,
|
||||||
|
audioQueue,
|
||||||
showControls,
|
showControls,
|
||||||
showCallOverlay,
|
showCallOverlay,
|
||||||
currentChatPage,
|
currentChatPage,
|
||||||
|
|
@ -34,6 +36,7 @@
|
||||||
showOverview,
|
showOverview,
|
||||||
chatTitle,
|
chatTitle,
|
||||||
showArtifacts,
|
showArtifacts,
|
||||||
|
artifactContents,
|
||||||
tools,
|
tools,
|
||||||
toolServers,
|
toolServers,
|
||||||
functions,
|
functions,
|
||||||
|
|
@ -41,6 +44,7 @@
|
||||||
pinnedChats,
|
pinnedChats,
|
||||||
showEmbeds
|
showEmbeds
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
|
@ -48,8 +52,10 @@
|
||||||
createMessagesList,
|
createMessagesList,
|
||||||
getPromptVariables,
|
getPromptVariables,
|
||||||
processDetails,
|
processDetails,
|
||||||
removeAllDetails
|
removeAllDetails,
|
||||||
|
getCodeBlockContents
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
import { AudioQueue } from '$lib/utils/audio';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createNewChat,
|
createNewChat,
|
||||||
|
|
@ -75,8 +81,8 @@
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
import { uploadFile } from '$lib/apis/files';
|
import { uploadFile } from '$lib/apis/files';
|
||||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||||
|
import { getFunctions } from '$lib/apis/functions';
|
||||||
import { fade } from 'svelte/transition';
|
import { updateFolderById } from '$lib/apis/folders';
|
||||||
|
|
||||||
import Banner from '../common/Banner.svelte';
|
import Banner from '../common/Banner.svelte';
|
||||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||||
|
|
@ -89,9 +95,7 @@
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import Sidebar from '../icons/Sidebar.svelte';
|
import Sidebar from '../icons/Sidebar.svelte';
|
||||||
import { getFunctions } from '$lib/apis/functions';
|
|
||||||
import Image from '../common/Image.svelte';
|
import Image from '../common/Image.svelte';
|
||||||
import { updateFolderById } from '$lib/apis/folders';
|
|
||||||
|
|
||||||
export let chatIdProp = '';
|
export let chatIdProp = '';
|
||||||
|
|
||||||
|
|
@ -192,6 +196,8 @@
|
||||||
codeInterpreterEnabled = input.codeInterpreterEnabled;
|
codeInterpreterEnabled = input.codeInterpreterEnabled;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
} else {
|
||||||
|
await setDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
@ -527,17 +533,28 @@
|
||||||
let showControlsSubscribe = null;
|
let showControlsSubscribe = null;
|
||||||
let selectedFolderSubscribe = null;
|
let selectedFolderSubscribe = null;
|
||||||
|
|
||||||
|
const stopAudio = () => {
|
||||||
|
try {
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
$audioQueue.stop();
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
console.log('mounted');
|
console.log('mounted');
|
||||||
window.addEventListener('message', onMessageHandler);
|
window.addEventListener('message', onMessageHandler);
|
||||||
$socket?.on('events', chatEventHandler);
|
$socket?.on('events', chatEventHandler);
|
||||||
|
|
||||||
|
audioQueue.set(new AudioQueue(document.getElementById('audioElement')));
|
||||||
|
|
||||||
pageSubscribe = page.subscribe(async (p) => {
|
pageSubscribe = page.subscribe(async (p) => {
|
||||||
if (p.url.pathname === '/') {
|
if (p.url.pathname === '/') {
|
||||||
await tick();
|
await tick();
|
||||||
initNewChat();
|
initNewChat();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopAudio();
|
||||||
});
|
});
|
||||||
|
|
||||||
const storageChatInput = sessionStorage.getItem(
|
const storageChatInput = sessionStorage.getItem(
|
||||||
|
|
@ -619,6 +636,7 @@
|
||||||
chatIdUnsubscriber?.();
|
chatIdUnsubscriber?.();
|
||||||
window.removeEventListener('message', onMessageHandler);
|
window.removeEventListener('message', onMessageHandler);
|
||||||
$socket?.off('events', chatEventHandler);
|
$socket?.off('events', chatEventHandler);
|
||||||
|
$audioQueue?.destroy();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
@ -817,6 +835,63 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$: if (history) {
|
||||||
|
getContents();
|
||||||
|
} else {
|
||||||
|
artifactContents.set([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContents = () => {
|
||||||
|
const messages = history ? createMessagesList(history, history.currentId) : [];
|
||||||
|
let contents = [];
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if (message?.role !== 'user' && message?.content) {
|
||||||
|
const {
|
||||||
|
codeBlocks: codeBlocks,
|
||||||
|
html: htmlContent,
|
||||||
|
css: cssContent,
|
||||||
|
js: jsContent
|
||||||
|
} = getCodeBlockContents(message.content);
|
||||||
|
|
||||||
|
if (htmlContent || cssContent || jsContent) {
|
||||||
|
const renderedContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<${''}style>
|
||||||
|
body {
|
||||||
|
background-color: white; /* Ensure the iframe has a white background */
|
||||||
|
}
|
||||||
|
|
||||||
|
${cssContent}
|
||||||
|
</${''}style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${htmlContent}
|
||||||
|
|
||||||
|
<${''}script>
|
||||||
|
${jsContent}
|
||||||
|
</${''}script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
contents = [...contents, { type: 'iframe', content: renderedContent }];
|
||||||
|
} else {
|
||||||
|
// Check for SVG content
|
||||||
|
for (const block of codeBlocks) {
|
||||||
|
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
|
||||||
|
contents = [...contents, { type: 'svg', content: block.code }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
artifactContents.set(contents);
|
||||||
|
};
|
||||||
|
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// Web functions
|
// Web functions
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
@ -1080,7 +1155,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
|
const chatCompletedHandler = async (_chatId, modelId, responseMessageId, messages) => {
|
||||||
const res = await chatCompleted(localStorage.token, {
|
const res = await chatCompleted(localStorage.token, {
|
||||||
model: modelId,
|
model: modelId,
|
||||||
messages: messages.map((m) => ({
|
messages: messages.map((m) => ({
|
||||||
|
|
@ -1094,7 +1169,7 @@
|
||||||
})),
|
})),
|
||||||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||||
model_item: $models.find((m) => m.id === modelId),
|
model_item: $models.find((m) => m.id === modelId),
|
||||||
chat_id: chatId,
|
chat_id: _chatId,
|
||||||
session_id: $socket?.id,
|
session_id: $socket?.id,
|
||||||
id: responseMessageId
|
id: responseMessageId
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|
@ -1122,9 +1197,9 @@
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
if ($chatId == chatId) {
|
if ($chatId == _chatId) {
|
||||||
if (!$temporaryChatEnabled) {
|
if (!$temporaryChatEnabled) {
|
||||||
chat = await updateChatById(localStorage.token, chatId, {
|
chat = await updateChatById(localStorage.token, _chatId, {
|
||||||
models: selectedModels,
|
models: selectedModels,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history,
|
history: history,
|
||||||
|
|
@ -1140,7 +1215,7 @@
|
||||||
taskIds = null;
|
taskIds = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
|
const chatActionHandler = async (_chatId, actionId, modelId, responseMessageId, event = null) => {
|
||||||
const messages = createMessagesList(history, responseMessageId);
|
const messages = createMessagesList(history, responseMessageId);
|
||||||
|
|
||||||
const res = await chatAction(localStorage.token, actionId, {
|
const res = await chatAction(localStorage.token, actionId, {
|
||||||
|
|
@ -1155,7 +1230,7 @@
|
||||||
})),
|
})),
|
||||||
...(event ? { event: event } : {}),
|
...(event ? { event: event } : {}),
|
||||||
model_item: $models.find((m) => m.id === modelId),
|
model_item: $models.find((m) => m.id === modelId),
|
||||||
chat_id: chatId,
|
chat_id: _chatId,
|
||||||
session_id: $socket?.id,
|
session_id: $socket?.id,
|
||||||
id: responseMessageId
|
id: responseMessageId
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|
@ -1177,9 +1252,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($chatId == chatId) {
|
if ($chatId == _chatId) {
|
||||||
if (!$temporaryChatEnabled) {
|
if (!$temporaryChatEnabled) {
|
||||||
chat = await updateChatById(localStorage.token, chatId, {
|
chat = await updateChatById(localStorage.token, _chatId, {
|
||||||
models: selectedModels,
|
models: selectedModels,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history,
|
history: history,
|
||||||
|
|
@ -2288,7 +2363,7 @@
|
||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<audio id="audioElement" src="" style="display: none;" />
|
<audio id="audioElement" src="" style="display: none;"></audio>
|
||||||
|
|
||||||
<EventConfirmDialog
|
<EventConfirmDialog
|
||||||
bind:show={showEventConfirmation}
|
bind:show={showEventConfirmation}
|
||||||
|
|
@ -2440,7 +2515,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" pb-2">
|
<div class=" pb-2 z-10">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
bind:this={messageInput}
|
bind:this={messageInput}
|
||||||
{history}
|
{history}
|
||||||
|
|
@ -2570,3 +2645,10 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
height: 0.5rem;
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -168,27 +168,28 @@
|
||||||
return '{{CLIPBOARD}}';
|
return '{{CLIPBOARD}}';
|
||||||
});
|
});
|
||||||
|
|
||||||
const clipboardItems = await navigator.clipboard.read();
|
const clipboardItems = await navigator.clipboard.read().catch((err) => {
|
||||||
|
console.error('Failed to read clipboard items:', err);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
let imageUrl = null;
|
|
||||||
for (const item of clipboardItems) {
|
for (const item of clipboardItems) {
|
||||||
// Check for known image types
|
|
||||||
for (const type of item.types) {
|
for (const type of item.types) {
|
||||||
if (type.startsWith('image/')) {
|
if (type.startsWith('image/')) {
|
||||||
const blob = await item.getType(type);
|
const blob = await item.getType(type);
|
||||||
imageUrl = URL.createObjectURL(blob);
|
const reader = new FileReader();
|
||||||
}
|
reader.onload = (event) => {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrl) {
|
|
||||||
files = [
|
files = [
|
||||||
...files,
|
...files,
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
url: imageUrl
|
url: event.target.result as string
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||||
|
|
@ -1018,7 +1019,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if recording}
|
<div class={recording ? '' : 'hidden'}>
|
||||||
<VoiceRecording
|
<VoiceRecording
|
||||||
bind:recording
|
bind:recording
|
||||||
onCancel={async () => {
|
onCancel={async () => {
|
||||||
|
|
@ -1033,8 +1034,7 @@
|
||||||
recording = false;
|
recording = false;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
insertTextAtCursor(text);
|
await insertTextAtCursor(`${text}`);
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
document.getElementById('chat-input')?.focus();
|
document.getElementById('chat-input')?.focus();
|
||||||
|
|
||||||
|
|
@ -1043,14 +1043,20 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
</div>
|
||||||
<form
|
<form
|
||||||
class="w-full flex flex-col gap-1.5"
|
class="w-full flex flex-col gap-1.5 {recording ? 'hidden' : ''}"
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
// check if selectedModels support image input
|
// check if selectedModels support image input
|
||||||
dispatch('submit', prompt);
|
dispatch('submit', prompt);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
id="generate-message-pair-button"
|
||||||
|
class="hidden"
|
||||||
|
on:click={() => createMessagePair(prompt)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="message-input-container"
|
id="message-input-container"
|
||||||
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
|
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
|
||||||
|
|
@ -1105,9 +1111,7 @@
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className=" absolute top-1 left-1"
|
className=" absolute top-1 left-1"
|
||||||
content={$i18n.t('{{ models }}', {
|
content={$i18n.t('{{ models }}', {
|
||||||
models: [
|
models: [...(atSelectedModel ? [atSelectedModel] : selectedModels)]
|
||||||
...(atSelectedModel ? [atSelectedModel] : selectedModels)
|
|
||||||
]
|
|
||||||
.filter((id) => !visionCapableModels.includes(id))
|
.filter((id) => !visionCapableModels.includes(id))
|
||||||
.join(', ')
|
.join(', ')
|
||||||
})}
|
})}
|
||||||
|
|
@ -1255,24 +1259,6 @@
|
||||||
stopResponse();
|
stopResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
|
||||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
createMessagePair(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Ctrl + R is pressed
|
|
||||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log('regenerate');
|
|
||||||
|
|
||||||
const regenerateButton = [
|
|
||||||
...document.getElementsByClassName('regenerate-response-button')
|
|
||||||
]?.at(-1);
|
|
||||||
|
|
||||||
regenerateButton?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prompt === '' && e.key == 'ArrowUp') {
|
if (prompt === '' && e.key == 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -1455,11 +1441,11 @@
|
||||||
</div>
|
</div>
|
||||||
</InputMenu>
|
</InputMenu>
|
||||||
|
|
||||||
|
{#if showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showToolsButton || (toggleFilters && toggleFilters.length > 0)}
|
||||||
<div
|
<div
|
||||||
class="flex self-center w-[1px] h-4 mx-1 bg-gray-200/50 dark:bg-gray-800/50"
|
class="flex self-center w-[1px] h-4 mx-1 bg-gray-200/50 dark:bg-gray-800/50"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showToolsButton || (toggleFilters && toggleFilters.length > 0)}
|
|
||||||
<IntegrationsMenu
|
<IntegrationsMenu
|
||||||
selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
|
selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
|
||||||
{toggleFilters}
|
{toggleFilters}
|
||||||
|
|
@ -1543,9 +1529,7 @@
|
||||||
<Tooltip content={filter?.name} placement="top">
|
<Tooltip content={filter?.name} placement="top">
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
selectedFilterIds = selectedFilterIds.filter(
|
selectedFilterIds = selectedFilterIds.filter((id) => id !== filterId);
|
||||||
(id) => id !== filterId
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {selectedFilterIds.includes(
|
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {selectedFilterIds.includes(
|
||||||
|
|
@ -1817,7 +1801,6 @@
|
||||||
<div class="mb-1" />
|
<div class="mb-1" />
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@
|
||||||
fileUploadCapableModels.length === selectedModels.length &&
|
fileUploadCapableModels.length === selectedModels.length &&
|
||||||
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
|
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
|
||||||
|
|
||||||
|
$: if (!fileUploadEnabled && files.length > 0) {
|
||||||
|
files = [];
|
||||||
|
}
|
||||||
|
|
||||||
const detectMobile = () => {
|
const detectMobile = () => {
|
||||||
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||||
return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
|
return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
|
||||||
|
|
@ -199,7 +203,9 @@
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
||||||
|
? 'opacity-50'
|
||||||
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (fileUploadEnabled) {
|
if (fileUploadEnabled) {
|
||||||
showAttachWebpageModal = true;
|
showAttachWebpageModal = true;
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,6 @@
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
bind:show
|
bind:show
|
||||||
{closeOnOutsideClick}
|
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
if (e.detail === false) {
|
if (e.detail === false) {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,7 @@
|
||||||
|
|
||||||
stopDurationCounter();
|
stopDurationCounter();
|
||||||
audioChunks = [];
|
audioChunks = [];
|
||||||
|
visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
const tracks = stream.getTracks();
|
const tracks = stream.getTracks();
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@
|
||||||
source: _source,
|
source: _source,
|
||||||
document: [document],
|
document: [document],
|
||||||
metadata: metadata ? [metadata] : [],
|
metadata: metadata ? [metadata] : [],
|
||||||
distances: distance !== undefined ? [distance] : undefined
|
distances: distance !== undefined ? [distance] : []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@
|
||||||
|
|
||||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||||
import { executeCode } from '$lib/apis/utils';
|
import { executeCode } from '$lib/apis/utils';
|
||||||
import { copyToClipboard, renderMermaidDiagram, renderVegaVisualization } from '$lib/utils';
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
initMermaid,
|
||||||
|
renderMermaidDiagram,
|
||||||
|
renderVegaVisualization
|
||||||
|
} from '$lib/utils';
|
||||||
|
|
||||||
import 'highlight.js/styles/github-dark.min.css';
|
import 'highlight.js/styles/github-dark.min.css';
|
||||||
|
|
||||||
|
|
@ -54,8 +59,8 @@
|
||||||
|
|
||||||
let _token = null;
|
let _token = null;
|
||||||
|
|
||||||
let mermaidHtml = null;
|
let renderHTML = null;
|
||||||
let vegaHtml = null;
|
let renderError = null;
|
||||||
|
|
||||||
let highlightedCode = null;
|
let highlightedCode = null;
|
||||||
let executing = false;
|
let executing = false;
|
||||||
|
|
@ -323,28 +328,36 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mermaid = null;
|
||||||
|
const renderMermaid = async (code) => {
|
||||||
|
if (!mermaid) {
|
||||||
|
mermaid = await initMermaid();
|
||||||
|
}
|
||||||
|
return await renderMermaidDiagram(mermaid, code);
|
||||||
|
};
|
||||||
|
|
||||||
const render = async () => {
|
const render = async () => {
|
||||||
onUpdate(token);
|
onUpdate(token);
|
||||||
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
||||||
try {
|
try {
|
||||||
mermaidHtml = await renderMermaidDiagram(code);
|
renderHTML = await renderMermaid(code);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to render mermaid diagram:', error);
|
console.error('Failed to render mermaid diagram:', error);
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
toast.error($i18n.t('Failed to render diagram') + `: ${errorMsg}`);
|
renderError = $i18n.t('Failed to render diagram') + `: ${errorMsg}`;
|
||||||
mermaidHtml = null;
|
renderHTML = null;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(lang === 'vega' || lang === 'vega-lite') &&
|
(lang === 'vega' || lang === 'vega-lite') &&
|
||||||
(token?.raw ?? '').slice(-4).includes('```')
|
(token?.raw ?? '').slice(-4).includes('```')
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
vegaHtml = await renderVegaVisualization(code);
|
renderHTML = await renderVegaVisualization(code);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to render Vega visualization:', error);
|
console.error('Failed to render Vega visualization:', error);
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
toast.error($i18n.t('Failed to render diagram') + `: ${errorMsg}`);
|
renderError = $i18n.t('Failed to render visualization') + `: ${errorMsg}`;
|
||||||
vegaHtml = null;
|
renderHTML = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -407,25 +420,24 @@
|
||||||
class="relative {className} flex flex-col rounded-3xl border border-gray-100 dark:border-gray-850 my-0.5"
|
class="relative {className} flex flex-col rounded-3xl border border-gray-100 dark:border-gray-850 my-0.5"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
>
|
>
|
||||||
{#if lang === 'mermaid'}
|
{#if ['mermaid', 'vega', 'vega-lite'].includes(lang)}
|
||||||
{#if mermaidHtml}
|
{#if renderHTML}
|
||||||
<SvgPanZoom
|
<SvgPanZoom
|
||||||
className=" rounded-3xl max-h-fit overflow-hidden"
|
className=" rounded-3xl max-h-fit overflow-hidden"
|
||||||
svg={mermaidHtml}
|
svg={renderHTML}
|
||||||
content={_token.text}
|
content={_token.text}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<pre class="mermaid">{code}</pre>
|
<div class="p-3">
|
||||||
|
{#if renderError}
|
||||||
|
<div
|
||||||
|
class="flex gap-2.5 border px-4 py-3 border-red-600/10 bg-red-600/10 rounded-2xl mb-2"
|
||||||
|
>
|
||||||
|
{renderError}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if lang === 'vega' || lang === 'vega-lite'}
|
<pre>{code}</pre>
|
||||||
{#if vegaHtml}
|
</div>
|
||||||
<SvgPanZoom
|
|
||||||
className="rounded-3xl max-h-fit overflow-hidden"
|
|
||||||
svg={vegaHtml}
|
|
||||||
content={_token.text}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<pre class="vega">{code}</pre>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
|
|
@ -561,15 +573,15 @@
|
||||||
>
|
>
|
||||||
{#if executing}
|
{#if executing}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-gray-500 text-xs mb-1">{$i18n.t('STDOUT/STDERR')}</div>
|
<div class=" text-gray-500 text-sm mb-1">{$i18n.t('STDOUT/STDERR')}</div>
|
||||||
<div class="text-sm">{$i18n.t('Running...')}</div>
|
<div class="text-sm">{$i18n.t('Running...')}</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if stdout || stderr}
|
{#if stdout || stderr}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-gray-500 text-xs mb-1">{$i18n.t('STDOUT/STDERR')}</div>
|
<div class=" text-gray-500 text-sm mb-1">{$i18n.t('STDOUT/STDERR')}</div>
|
||||||
<div
|
<div
|
||||||
class="text-sm {stdout?.split('\n')?.length > 100
|
class="text-sm font-mono whitespace-pre-wrap {stdout?.split('\n')?.length > 100
|
||||||
? `max-h-96`
|
? `max-h-96`
|
||||||
: ''} overflow-y-auto"
|
: ''} overflow-y-auto"
|
||||||
>
|
>
|
||||||
|
|
@ -579,7 +591,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if result || files}
|
{#if result || files}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-gray-500 text-xs mb-1">{$i18n.t('RESULT')}</div>
|
<div class=" text-gray-500 text-sm mb-1">{$i18n.t('RESULT')}</div>
|
||||||
{#if result}
|
{#if result}
|
||||||
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
|
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSave}
|
{onSave}
|
||||||
onUpdate={(token) => {
|
onUpdate={async (token) => {
|
||||||
const { lang, text: code } = token;
|
const { lang, text: code } = token;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -185,6 +185,7 @@
|
||||||
!$mobile &&
|
!$mobile &&
|
||||||
$chatId
|
$chatId
|
||||||
) {
|
) {
|
||||||
|
await tick();
|
||||||
showArtifacts.set(true);
|
showArtifacts.set(true);
|
||||||
showControls.set(true);
|
showControls.set(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,12 @@
|
||||||
title="Embedded content"
|
title="Embedded content"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
sandbox
|
sandbox
|
||||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
on:load={(e) => {
|
||||||
|
try {
|
||||||
|
e.currentTarget.style.height =
|
||||||
|
e.currentTarget.contentWindow.document.body.scrollHeight + 20 + 'px';
|
||||||
|
} catch {}
|
||||||
|
}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{:else}
|
{:else}
|
||||||
{token.text}
|
{token.text}
|
||||||
|
|
@ -116,17 +121,19 @@
|
||||||
referrerpolicy="strict-origin-when-cross-origin"
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
allowfullscreen
|
allowfullscreen
|
||||||
width="100%"
|
width="100%"
|
||||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
on:load={(e) => {
|
||||||
|
try {
|
||||||
|
e.currentTarget.style.height =
|
||||||
|
e.currentTarget.contentWindow.document.body.scrollHeight + 20 + 'px';
|
||||||
|
} catch {}
|
||||||
|
}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if token.text.includes(`<source_id`)}
|
{:else if token.text.includes(`<source_id`)}
|
||||||
<Source {id} {token} onClick={onSourceClick} />
|
<Source {id} {token} onClick={onSourceClick} />
|
||||||
{:else}
|
{:else if token.text.trim().match(/^<br\s*\/?>$/i)}
|
||||||
{@const br = token.text.match(/<br\s*\/?>/)}
|
|
||||||
{#if br}
|
|
||||||
<br />
|
<br />
|
||||||
{:else}
|
{:else}
|
||||||
{token.text}
|
{token.text}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
export let onSourceClick: Function = () => {};
|
export let onSourceClick: Function = () => {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each tokens as token}
|
{#each tokens as token, tokenIdx (tokenIdx)}
|
||||||
{#if token.type === 'escape'}
|
{#if token.type === 'escape'}
|
||||||
{unescapeHtml(token.text)}
|
{unescapeHtml(token.text)}
|
||||||
{:else if token.type === 'html'}
|
{:else if token.type === 'html'}
|
||||||
|
|
@ -59,7 +59,12 @@
|
||||||
title={token.fileId}
|
title={token.fileId}
|
||||||
width="100%"
|
width="100%"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
on:load={(e) => {
|
||||||
|
try {
|
||||||
|
e.currentTarget.style.height =
|
||||||
|
e.currentTarget.contentWindow.document.body.scrollHeight + 20 + 'px';
|
||||||
|
} catch {}
|
||||||
|
}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{:else if token.type === 'mention'}
|
{:else if token.type === 'mention'}
|
||||||
<MentionToken {token} />
|
<MentionToken {token} />
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
{#each texts as text}
|
{#each texts as text}
|
||||||
<span class="" transition:fade={{ duration: 100 }}>
|
<span class="" transition:fade={{ duration: 100 }}>
|
||||||
{text}
|
{text}{' '}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { decode } from 'html-entities';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
import { unescapeHtml } from '$lib/utils';
|
import { unescapeHtml } from '$lib/utils';
|
||||||
|
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
||||||
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
||||||
|
|
@ -20,7 +22,6 @@
|
||||||
import Download from '$lib/components/icons/Download.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
|
|
||||||
import Source from './Source.svelte';
|
import Source from './Source.svelte';
|
||||||
import { settings } from '$lib/stores';
|
|
||||||
import HtmlToken from './HTMLToken.svelte';
|
import HtmlToken from './HTMLToken.svelte';
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
|
|
@ -304,7 +305,7 @@
|
||||||
<div class=" mb-1.5" slot="content">
|
<div class=" mb-1.5" slot="content">
|
||||||
<svelte:self
|
<svelte:self
|
||||||
id={`${id}-${tokenIdx}-d`}
|
id={`${id}-${tokenIdx}-d`}
|
||||||
tokens={marked.lexer(token.text)}
|
tokens={marked.lexer(decode(token.text))}
|
||||||
attributes={token?.attributes}
|
attributes={token?.attributes}
|
||||||
{done}
|
{done}
|
||||||
{editCodeBlock}
|
{editCodeBlock}
|
||||||
|
|
@ -321,7 +322,12 @@
|
||||||
title={token.fileId}
|
title={token.fileId}
|
||||||
width="100%"
|
width="100%"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
on:load={(e) => {
|
||||||
|
try {
|
||||||
|
e.currentTarget.style.height =
|
||||||
|
e.currentTarget.contentWindow.document.body.scrollHeight + 20 + 'px';
|
||||||
|
} catch {}
|
||||||
|
}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{:else if token.type === 'paragraph'}
|
{:else if token.type === 'paragraph'}
|
||||||
<p dir="auto">
|
<p dir="auto">
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
role="listitem"
|
||||||
class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
|
class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
|
||||||
? 'max-w-full'
|
? 'max-w-full'
|
||||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,15 @@
|
||||||
import { getChatById } from '$lib/apis/chats';
|
import { getChatById } from '$lib/apis/chats';
|
||||||
import { generateTags } from '$lib/apis';
|
import { generateTags } from '$lib/apis';
|
||||||
|
|
||||||
import { config, models, settings, temporaryChatEnabled, TTSWorker, user } from '$lib/stores';
|
import {
|
||||||
|
audioQueue,
|
||||||
|
config,
|
||||||
|
models,
|
||||||
|
settings,
|
||||||
|
temporaryChatEnabled,
|
||||||
|
TTSWorker,
|
||||||
|
user
|
||||||
|
} from '$lib/stores';
|
||||||
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
import {
|
import {
|
||||||
|
|
@ -156,7 +164,6 @@
|
||||||
|
|
||||||
let messageIndexEdit = false;
|
let messageIndexEdit = false;
|
||||||
|
|
||||||
let audioParts: Record<number, HTMLAudioElement | null> = {};
|
|
||||||
let speaking = false;
|
let speaking = false;
|
||||||
let speakingIdx: number | undefined;
|
let speakingIdx: number | undefined;
|
||||||
|
|
||||||
|
|
@ -178,51 +185,25 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const playAudio = (idx: number) => {
|
const stopAudio = () => {
|
||||||
return new Promise<void>((res) => {
|
|
||||||
speakingIdx = idx;
|
|
||||||
const audio = audioParts[idx];
|
|
||||||
|
|
||||||
if (!audio) {
|
|
||||||
return res();
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.play();
|
|
||||||
audio.onended = async () => {
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
|
|
||||||
if (Object.keys(audioParts).length - 1 === idx) {
|
|
||||||
speaking = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
res();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSpeakMessage = async () => {
|
|
||||||
if (speaking) {
|
|
||||||
try {
|
try {
|
||||||
speechSynthesis.cancel();
|
speechSynthesis.cancel();
|
||||||
|
$audioQueue.stop();
|
||||||
if (speakingIdx !== undefined && audioParts[speakingIdx]) {
|
|
||||||
audioParts[speakingIdx]!.pause();
|
|
||||||
audioParts[speakingIdx]!.currentTime = 0;
|
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
if (speaking) {
|
||||||
speaking = false;
|
speaking = false;
|
||||||
speakingIdx = undefined;
|
speakingIdx = undefined;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const speak = async () => {
|
||||||
if (!(message?.content ?? '').trim().length) {
|
if (!(message?.content ?? '').trim().length) {
|
||||||
toast.info($i18n.t('No content to speak'));
|
toast.info($i18n.t('No content to speak'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
speaking = true;
|
speaking = true;
|
||||||
|
|
||||||
const content = removeAllDetails(message.content);
|
const content = removeAllDetails(message.content);
|
||||||
|
|
||||||
if ($config.audio.tts.engine === '') {
|
if ($config.audio.tts.engine === '') {
|
||||||
|
|
@ -241,12 +222,12 @@
|
||||||
|
|
||||||
console.log(voice);
|
console.log(voice);
|
||||||
|
|
||||||
const speak = new SpeechSynthesisUtterance(content);
|
const speech = new SpeechSynthesisUtterance(content);
|
||||||
speak.rate = $settings.audio?.tts?.playbackRate ?? 1;
|
speech.rate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||||
|
|
||||||
console.log(speak);
|
console.log(speech);
|
||||||
|
|
||||||
speak.onend = () => {
|
speech.onend = () => {
|
||||||
speaking = false;
|
speaking = false;
|
||||||
if ($settings.conversationMode) {
|
if ($settings.conversationMode) {
|
||||||
document.getElementById('voice-input-button')?.click();
|
document.getElementById('voice-input-button')?.click();
|
||||||
|
|
@ -254,15 +235,21 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
if (voice) {
|
if (voice) {
|
||||||
speak.voice = voice;
|
speech.voice = voice;
|
||||||
}
|
}
|
||||||
|
|
||||||
speechSynthesis.speak(speak);
|
speechSynthesis.speak(speech);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
loadingSpeech = true;
|
$audioQueue.setId(`${message.id}`);
|
||||||
|
$audioQueue.setPlaybackRate($settings.audio?.tts?.playbackRate ?? 1);
|
||||||
|
$audioQueue.onStopped = () => {
|
||||||
|
speaking = false;
|
||||||
|
speakingIdx = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
loadingSpeech = true;
|
||||||
const messageContentParts: string[] = getMessageContentParts(
|
const messageContentParts: string[] = getMessageContentParts(
|
||||||
content,
|
content,
|
||||||
$config?.audio?.tts?.split_on ?? 'punctuation'
|
$config?.audio?.tts?.split_on ?? 'punctuation'
|
||||||
|
|
@ -278,17 +265,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Prepared message content for TTS', messageContentParts);
|
console.debug('Prepared message content for TTS', messageContentParts);
|
||||||
|
|
||||||
audioParts = messageContentParts.reduce(
|
|
||||||
(acc, _sentence, idx) => {
|
|
||||||
acc[idx] = null;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as typeof audioParts
|
|
||||||
);
|
|
||||||
|
|
||||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
|
||||||
|
|
||||||
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
|
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
|
||||||
if (!$TTSWorker) {
|
if (!$TTSWorker) {
|
||||||
await TTSWorker.set(
|
await TTSWorker.set(
|
||||||
|
|
@ -315,12 +291,9 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blob) {
|
if (blob) {
|
||||||
const audio = new Audio(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
$audioQueue.enqueue(url);
|
||||||
|
|
||||||
audioParts[idx] = audio;
|
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -341,13 +314,10 @@
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const audio = new Audio(blobUrl);
|
|
||||||
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
|
||||||
|
|
||||||
audioParts[idx] = audio;
|
$audioQueue.enqueue(url);
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -620,7 +590,7 @@
|
||||||
<div class="flex-auto w-0 pl-1 relative">
|
<div class="flex-auto w-0 pl-1 relative">
|
||||||
<Name>
|
<Name>
|
||||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||||
<span class="line-clamp-1 text-black dark:text-white">
|
<span id="response-message-model-name" class="line-clamp-1 text-black dark:text-white">
|
||||||
{model?.name ?? message.model}
|
{model?.name ?? message.model}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -648,10 +618,7 @@
|
||||||
<div class="chat-{message.role} w-full min-w-full markdown-prose">
|
<div class="chat-{message.role} w-full min-w-full markdown-prose">
|
||||||
<div>
|
<div>
|
||||||
{#if model?.info?.meta?.capabilities?.status_updates ?? true}
|
{#if model?.info?.meta?.capabilities?.status_updates ?? true}
|
||||||
<StatusHistory
|
<StatusHistory statusHistory={message?.statusHistory} />
|
||||||
statusHistory={message?.statusHistory}
|
|
||||||
expand={message?.content === ''}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message?.files && message.files?.filter((f) => f.type === 'image').length > 0}
|
{#if message?.files && message.files?.filter((f) => f.type === 'image').length > 0}
|
||||||
|
|
@ -995,7 +962,11 @@
|
||||||
: '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"
|
: '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={() => {
|
on:click={() => {
|
||||||
if (!loadingSpeech) {
|
if (!loadingSpeech) {
|
||||||
toggleSpeakMessage();
|
if (speaking) {
|
||||||
|
stopAudio();
|
||||||
|
} else {
|
||||||
|
speak();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -29,38 +29,6 @@
|
||||||
{#if history && history.length > 0}
|
{#if history && history.length > 0}
|
||||||
{#if status?.hidden !== true}
|
{#if status?.hidden !== true}
|
||||||
<div class="text-sm flex flex-col w-full">
|
<div class="text-sm flex flex-col w-full">
|
||||||
{#if showHistory}
|
|
||||||
<div class="flex flex-row">
|
|
||||||
{#if history.length > 1}
|
|
||||||
<div class="w-full">
|
|
||||||
{#each history as status, idx}
|
|
||||||
{#if idx !== history.length - 1}
|
|
||||||
<div class="flex items-stretch gap-2 mb-1">
|
|
||||||
<div class=" ">
|
|
||||||
<div class="pt-3 px-1 mb-1.5">
|
|
||||||
<span
|
|
||||||
class="relative flex size-1.5 rounded-full justify-center items-center"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="w-[0.5px] ml-[6.5px] h-[calc(100%-14px)] bg-gray-300 dark:bg-gray-700"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<StatusItem {status} done={true} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -68,23 +36,38 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
{#if history.length > 1}
|
|
||||||
<div class="pt-3 px-1">
|
|
||||||
<span class="relative flex size-1.5 rounded-full justify-center items-center">
|
|
||||||
{#if status?.done === false}
|
|
||||||
<span
|
|
||||||
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-500 dark:bg-gray-300 opacity-75"
|
|
||||||
></span>
|
|
||||||
{/if}
|
|
||||||
<span
|
|
||||||
class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<StatusItem {status} />
|
<StatusItem {status} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if showHistory}
|
||||||
|
<div class="flex flex-row">
|
||||||
|
{#if history.length > 1}
|
||||||
|
<div class="w-full">
|
||||||
|
{#each history as status, idx}
|
||||||
|
<div class="flex items-stretch gap-2 mb-1">
|
||||||
|
<div class=" ">
|
||||||
|
<div class="pt-3 px-1 mb-1.5">
|
||||||
|
<span class="relative flex size-1.5 rounded-full justify-center items-center">
|
||||||
|
<span
|
||||||
|
class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-400"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if idx !== history.length - 1}
|
||||||
|
<div
|
||||||
|
class="w-[0.5px] ml-[6.5px] h-[calc(100%-14px)] bg-gray-300 dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusItem {status} done={true} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,10 @@
|
||||||
if (selectedTag === '') {
|
if (selectedTag === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
|
|
||||||
|
return (item.model?.tags ?? [])
|
||||||
|
.map((tag) => tag.name.toLowerCase())
|
||||||
|
.includes(selectedTag.toLowerCase());
|
||||||
})
|
})
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (selectedConnectionType === '') {
|
if (selectedConnectionType === '') {
|
||||||
|
|
@ -139,7 +142,9 @@
|
||||||
if (selectedTag === '') {
|
if (selectedTag === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
|
return (item.model?.tags ?? [])
|
||||||
|
.map((tag) => tag.name.toLowerCase())
|
||||||
|
.includes(selectedTag.toLowerCase());
|
||||||
})
|
})
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (selectedConnectionType === '') {
|
if (selectedConnectionType === '') {
|
||||||
|
|
@ -315,8 +320,7 @@
|
||||||
tags = items
|
tags = items
|
||||||
.filter((item) => !(item.model?.info?.meta?.hidden ?? false))
|
.filter((item) => !(item.model?.info?.meta?.hidden ?? false))
|
||||||
.flatMap((item) => item.model?.tags ?? [])
|
.flatMap((item) => item.model?.tags ?? [])
|
||||||
.map((tag) => tag.name);
|
.map((tag) => tag.name.toLowerCase());
|
||||||
|
|
||||||
// Remove duplicates and sort
|
// Remove duplicates and sort
|
||||||
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
|
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
|
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
|
||||||
<div class="flex items-center w-full pl-1.5 pr-1">
|
<div class="flex items-center w-full pl-1.5 pr-1">
|
||||||
<div
|
<div
|
||||||
|
id="navbar-bg-gradient-to-b"
|
||||||
class=" bg-linear-to-b via-40% to-97% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
class=" bg-linear-to-b via-40% to-97% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
|
|
||||||
96
src/lib/components/chat/ShortcutItem.svelte
Normal file
96
src/lib/components/chat/ShortcutItem.svelte
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
import type { Shortcut } from '$lib/shortcuts';
|
||||||
|
|
||||||
|
export let shortcut: Shortcut;
|
||||||
|
export let isMac: boolean;
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
let keyboardLayoutMap: Map<string, string> | undefined;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (navigator.keyboard && 'getLayoutMap' in navigator.keyboard) {
|
||||||
|
try {
|
||||||
|
keyboardLayoutMap = await navigator.keyboard.getLayoutMap();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get keyboard layout map:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatKey(key: string): string {
|
||||||
|
// First, handle special modifier keys which are defined in lowercase
|
||||||
|
switch (key) {
|
||||||
|
case 'mod':
|
||||||
|
return isMac ? '⌘' : 'Ctrl';
|
||||||
|
case 'shift':
|
||||||
|
return isMac ? '⇧' : 'Shift';
|
||||||
|
case 'alt':
|
||||||
|
return isMac ? '⌥' : 'Alt';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, try to use the layout map with the raw KeyboardEvent.code (e.g., "Slash")
|
||||||
|
if (keyboardLayoutMap && keyboardLayoutMap.has(key)) {
|
||||||
|
const mappedKey = keyboardLayoutMap.get(key) ?? key;
|
||||||
|
// For single characters, make them uppercase. For others (like 'CapsLock'), leave as is.
|
||||||
|
return mappedKey.length === 1 ? mappedKey.toUpperCase() : mappedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, provide a fallback for browsers without getLayoutMap or for keys not in the map
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
switch (lowerKey) {
|
||||||
|
case 'backspace':
|
||||||
|
case 'delete':
|
||||||
|
return isMac ? '⌫' : 'Delete';
|
||||||
|
case 'escape':
|
||||||
|
return 'Esc';
|
||||||
|
case 'enter':
|
||||||
|
return isMac ? '↩' : 'Enter';
|
||||||
|
case 'tab':
|
||||||
|
return isMac ? '⇥' : 'Tab';
|
||||||
|
case 'arrowup':
|
||||||
|
return '↑';
|
||||||
|
case 'arrowdown':
|
||||||
|
return '↓';
|
||||||
|
case 'quote':
|
||||||
|
return "'";
|
||||||
|
case 'period':
|
||||||
|
return '.';
|
||||||
|
case 'slash':
|
||||||
|
return '/';
|
||||||
|
case 'semicolon':
|
||||||
|
return ';';
|
||||||
|
default:
|
||||||
|
// For 'KeyA', 'Digit1', etc., extract the last character.
|
||||||
|
if (lowerKey.startsWith('key') || lowerKey.startsWith('digit')) {
|
||||||
|
return key.slice(-1).toUpperCase();
|
||||||
|
}
|
||||||
|
// For anything else, just uppercase it.
|
||||||
|
return key.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full flex justify-between">
|
||||||
|
<div class="text-sm whitespace-pre-line">
|
||||||
|
{#if shortcut.tooltip}
|
||||||
|
<Tooltip content={$i18n.t(shortcut.tooltip)}>
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
{$i18n.t(shortcut.name)}<span class="text-xs"> *</span>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
{$i18n.t(shortcut.name)}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 flex justify-end self-start h-full space-x-1 text-xs">
|
||||||
|
{#each shortcut.keys.filter((key) => !(key.toLowerCase() === 'delete' && shortcut.keys.includes('Backspace'))) as key}
|
||||||
|
<div
|
||||||
|
class="h-fit px-1 py-0.5 flex items-start justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
{formatKey(key)}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,392 +1,99 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import { shortcuts } from '$lib/shortcuts';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import { settings } from '$lib/stores';
|
||||||
const i18n = getContext('i18n');
|
import ShortcutItem from './ShortcutItem.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
|
type CategorizedShortcuts = {
|
||||||
|
[category: string]: {
|
||||||
|
left: Shortcut[];
|
||||||
|
right: Shortcut[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
let categorizedShortcuts: CategorizedShortcuts = {};
|
||||||
|
let isMac = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
isMac = /Mac/i.test(navigator.userAgent);
|
||||||
|
});
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const allShortcuts = Object.values(shortcuts).filter((shortcut) => {
|
||||||
|
if (!shortcut.setting) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $settings[shortcut.setting.id] === shortcut.setting.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
categorizedShortcuts = allShortcuts.reduce((acc, shortcut) => {
|
||||||
|
const category = shortcut.category;
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = [];
|
||||||
|
}
|
||||||
|
acc[category].push(shortcut);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div class="text-gray-700 dark:text-gray-100">
|
<div class="text-gray-700 dark:text-gray-100 px-5 py-4">
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
<div class="flex justify-between dark:text-gray-300 pb-2">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
|
<div class="text-lg font-medium self-center">{$i18n.t('Keyboard Shortcuts')}</div>
|
||||||
<button
|
<button class="self-center" on:click={() => (show = false)}>
|
||||||
class="self-center"
|
|
||||||
on:click={() => {
|
|
||||||
show = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XMark className={'size-5'} />
|
<XMark className={'size-5'} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
|
{#each Object.entries(categorizedShortcuts) as [category, items], categoryIndex}
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
{#if categoryIndex > 0}
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
<div class="py-3">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full border-t dark:border-gray-850 border-gray-50" />
|
||||||
<div class=" text-sm">{$i18n.t('Open new chat')}</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex justify-between dark:text-gray-300 pb-2">
|
||||||
<div
|
<div class="text-base self-center">{$i18n.t(category)}</div>
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
O
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Focus chat input')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Esc
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t(
|
|
||||||
'Only active when the chat input is in focus and an LLM is generating a response.'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{$i18n.t('Stop Generating')}<span class="text-xs"> *</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Esc
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Copy last code block')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
;
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Copy last response')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
C
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t(
|
|
||||||
'Only active when "Paste Large Text as File" setting is toggled on.'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{$i18n.t('Prevent file creation')}<span class="text-s"> *</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
V
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Generate prompt pair')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Enter
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Toggle search')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
K
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Toggle settings')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Toggle sidebar')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
S
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Delete chat')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
⌫/Delete
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">{$i18n.t('Show shortcuts')}</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Ctrl/⌘
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
/
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-5 pb-4 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{$i18n.t(
|
|
||||||
'Shortcuts with an asterisk (*) are situational and only active under specific conditions.'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5">
|
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Input commands')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-5 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">
|
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">
|
|
||||||
{$i18n.t('Attach file from knowledge')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
#
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">
|
|
||||||
{$i18n.t('Add custom prompt')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
/
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">
|
|
||||||
{$i18n.t('Talk to model')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
@
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
|
||||||
<div class=" text-sm">
|
|
||||||
{$i18n.t('Accept autocomplete generation / Jump to prompt variable')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
|
||||||
<div
|
|
||||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
TAB
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row w-full md:space-x-2 dark:text-gray-200">
|
||||||
|
<div class="flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<div class=" grid grid-cols-1 sm:grid-cols-2 gap-2 gap-x-4 w-full">
|
||||||
|
{#each items as shortcut}
|
||||||
|
<div class="col-span-1 flex items-start">
|
||||||
|
<ShortcutItem {shortcut} {isMac} />
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
input::-webkit-outer-spin-button,
|
input::-webkit-outer-spin-button,
|
||||||
input::-webkit-inner-spin-button {
|
input::-webkit-inner-spin-button {
|
||||||
/* display: none; <- Crashes Chrome on hover */
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs::-webkit-scrollbar {
|
.tabs::-webkit-scrollbar {
|
||||||
display: none; /* for Chrome, Safari and Opera */
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='number'] {
|
input[type='number'] {
|
||||||
-moz-appearance: textfield; /* Firefox */
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
<div class="h-40 w-full">
|
<div class="h-40 w-full">
|
||||||
{#if filteredPrompts.length > 0}
|
{#if filteredPrompts.length > 0}
|
||||||
<div role="list" class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
<div role="list" class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
||||||
{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
|
{#each filteredPrompts as prompt, idx (prompt.id || `${prompt.content}-${idx}`)}
|
||||||
<!-- svelte-ignore a11y-no-interactive-element-to-noninteractive-role -->
|
<!-- svelte-ignore a11y-no-interactive-element-to-noninteractive-role -->
|
||||||
<button
|
<button
|
||||||
role="listitem"
|
role="listitem"
|
||||||
|
|
|
||||||
65
src/lib/components/common/CodeEditorModal.svelte
Normal file
65
src/lib/components/common/CodeEditorModal.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import CodeEditor from './CodeEditor.svelte';
|
||||||
|
import Drawer from './Drawer.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let {
|
||||||
|
show = $bindable(),
|
||||||
|
value = $bindable(),
|
||||||
|
lang = 'python',
|
||||||
|
onChange = () => {},
|
||||||
|
onSave = () => {}
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let boilerplate = ``;
|
||||||
|
|
||||||
|
let codeEditor = $state(null);
|
||||||
|
let _content = $state(value);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (_content) {
|
||||||
|
value = _content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Drawer bind:show>
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<div
|
||||||
|
class=" sticky top-0 z-30 flex justify-between bg-white px-4.5 pt-3 pb-3 dark:bg-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<div class=" font-primary self-center text-lg font-medium">
|
||||||
|
{$i18n.t('Code Editor')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
aria-label="Close"
|
||||||
|
onclick={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="h-5 w-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 h-full w-full flex-1 flex-col md:flex-row md:space-x-4 dark:text-gray-200 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div class=" flex h-full w-full flex-col sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<CodeEditor bind:this={codeEditor} {value} {boilerplate} {lang} {onChange} {onSave} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { fade, fly, slide } from 'svelte/transition';
|
import { fade, fly, slide } from 'svelte/transition';
|
||||||
import { isApp } from '$lib/stores';
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let className = '';
|
export let className = '';
|
||||||
|
|
@ -54,26 +53,25 @@
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
{#if show}
|
||||||
<div
|
<div
|
||||||
bind:this={modalElement}
|
bind:this={modalElement}
|
||||||
class="modal fixed right-0 {$isApp
|
class="modal fixed right-0 bottom-0 left-0 z-999 flex h-screen max-h-[100dvh] w-full justify-center overflow-hidden overscroll-contain bg-black/60"
|
||||||
? ' ml-[4.5rem] max-w-[calc(100%-4.5rem)]'
|
|
||||||
: ''} left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-999 overflow-hidden overscroll-contain"
|
|
||||||
in:fly={{ y: 100, duration: 100 }}
|
in:fly={{ y: 100, duration: 100 }}
|
||||||
on:mousedown={() => {
|
on:mousedown={() => {
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=" mt-auto w-full bg-gray-50 dark:bg-gray-900 dark:text-gray-100 {className} max-h-[100dvh] overflow-y-auto scrollbar-hidden"
|
class=" mt-auto w-full bg-gray-50 dark:bg-gray-900 dark:text-gray-100 {className} scrollbar-hidden max-h-[100dvh] overflow-y-auto"
|
||||||
on:mousedown={(e) => {
|
on:mousedown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.modal-content {
|
.modal-content {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if edit}
|
{#if edit}
|
||||||
<div>
|
<div class=" self-end">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={enableFullContent
|
content={enableFullContent
|
||||||
? $i18n.t(
|
? $i18n.t(
|
||||||
|
|
@ -205,7 +205,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if isPDF}
|
{:else if isPDF}
|
||||||
<div
|
<div
|
||||||
class="flex mb-2.5 scrollbar-none overflow-x-auto w-full border-b border-gray-100 dark:border-gray-800 text-center text-sm font-medium bg-transparent dark:text-gray-200"
|
class="flex mb-2.5 scrollbar-none overflow-x-auto w-full border-b border-gray-50 dark:border-gray-850 text-center text-sm font-medium bg-transparent dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="min-w-fit py-1.5 px-4 border-b {selectedTab === ''
|
class="min-w-fit py-1.5 px-4 border-b {selectedTab === ''
|
||||||
|
|
|
||||||
37
src/lib/components/common/HotkeyHint.svelte
Normal file
37
src/lib/components/common/HotkeyHint.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { shortcuts } from '$lib/shortcuts';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
|
export let name: string;
|
||||||
|
export let className = '';
|
||||||
|
|
||||||
|
let isMac = false;
|
||||||
|
let mounted = false;
|
||||||
|
let keys: string[] = [];
|
||||||
|
let isVisible = true;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
isMac = /Mac/i.test(navigator.userAgent);
|
||||||
|
keys = shortcuts[name]?.keys ?? [];
|
||||||
|
mounted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatKey(key: string): string {
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerKey === 'mod') return isMac ? '⌘' : 'Ctrl';
|
||||||
|
if (lowerKey === 'shift') return isMac ? '⇧' : 'Shift';
|
||||||
|
if (lowerKey.startsWith('key')) return key.slice(-1);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if mounted && isVisible}
|
||||||
|
<div
|
||||||
|
class="hidden md:flex items-center self-center text-xs text-gray-400 dark:text-gray-600 {className}"
|
||||||
|
>
|
||||||
|
<span>{keys.map(formatKey).join(isMac ? '' : '+')}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -92,6 +92,7 @@
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/30 dark:bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain"
|
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/30 dark:bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain"
|
||||||
|
style="scrollbar-gutter: stable;"
|
||||||
in:fade={{ duration: 10 }}
|
in:fade={{ duration: 10 }}
|
||||||
on:mousedown={() => {
|
on:mousedown={() => {
|
||||||
show = false;
|
show = false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
marked.use({
|
marked.use({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
|
|
@ -336,12 +338,14 @@
|
||||||
let tr = state.tr;
|
let tr = state.tr;
|
||||||
|
|
||||||
if (insertPromptAsRichText) {
|
if (insertPromptAsRichText) {
|
||||||
const htmlContent = marked
|
const htmlContent = DOMPurify.sanitize(
|
||||||
|
marked
|
||||||
.parse(text, {
|
.parse(text, {
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true
|
gfm: true
|
||||||
})
|
})
|
||||||
.trim();
|
.trim()
|
||||||
|
);
|
||||||
|
|
||||||
// Create a temporary div to parse HTML
|
// Create a temporary div to parse HTML
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
|
|
@ -691,7 +695,6 @@
|
||||||
CodeBlockLowlight.configure({
|
CodeBlockLowlight.configure({
|
||||||
lowlight
|
lowlight
|
||||||
}),
|
}),
|
||||||
Highlight,
|
|
||||||
Typography,
|
Typography,
|
||||||
TableKit.configure({
|
TableKit.configure({
|
||||||
table: { resizable: true }
|
table: { resizable: true }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { mount, unmount } from 'svelte';
|
||||||
|
import { createClassComponent } from 'svelte/legacy';
|
||||||
|
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
|
|
||||||
export function getSuggestionRenderer(Component: any, ComponentProps = {}) {
|
export function getSuggestionRenderer(Component: any, ComponentProps = {}) {
|
||||||
|
|
@ -15,7 +18,8 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) {
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
// mount Svelte component
|
// mount Svelte component
|
||||||
component = new Component({
|
component = createClassComponent({
|
||||||
|
component: Component,
|
||||||
target: container,
|
target: container,
|
||||||
props: {
|
props: {
|
||||||
char: props?.text,
|
char: props?.text,
|
||||||
|
|
@ -104,7 +108,12 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) {
|
||||||
popup?.destroy();
|
popup?.destroy();
|
||||||
popup = null;
|
popup = null;
|
||||||
|
|
||||||
component?.$destroy();
|
try {
|
||||||
|
component.$destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error unmounting component:', e);
|
||||||
|
}
|
||||||
|
|
||||||
component = null;
|
component = null;
|
||||||
|
|
||||||
if (container?.parentNode) container.parentNode.removeChild(container);
|
if (container?.parentNode) container.parentNode.removeChild(container);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={tooltip ? (state ? $i18n.t('Enabled') : $i18n.t('Disabled')) : ''}
|
content={typeof tooltip === 'string'
|
||||||
|
? tooltip
|
||||||
|
: typeof tooltip === 'boolean' && tooltip
|
||||||
|
? state
|
||||||
|
? $i18n.t('Enabled')
|
||||||
|
: $i18n.t('Disabled')
|
||||||
|
: ''}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<Switch.Root
|
<Switch.Root
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
export let className =
|
export let className =
|
||||||
'w-full rounded-lg px-3.5 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full';
|
'w-full rounded-lg px-3.5 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full';
|
||||||
|
|
||||||
|
export let onInput = () => {};
|
||||||
export let onBlur = () => {};
|
export let onBlur = () => {};
|
||||||
|
|
||||||
let textareaElement;
|
let textareaElement;
|
||||||
|
|
||||||
// Adjust height on mount and after setting the element.
|
// Adjust height on mount and after setting the element.
|
||||||
|
|
@ -58,6 +60,8 @@
|
||||||
{readonly}
|
{readonly}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
|
onInput(e);
|
||||||
}}
|
}}
|
||||||
on:focus={() => {
|
on:focus={() => {
|
||||||
resize();
|
resize();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import Switch from './Switch.svelte';
|
import Switch from './Switch.svelte';
|
||||||
import MapSelector from './Valves/MapSelector.svelte';
|
import MapSelector from './Valves/MapSelector.svelte';
|
||||||
import { split } from 'postcss/lib/list';
|
|
||||||
|
|
||||||
export let valvesSpec = null;
|
export let valvesSpec = null;
|
||||||
export let valves = {};
|
export let valves = {};
|
||||||
|
|
@ -168,7 +167,7 @@
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
dispatch('change');
|
dispatch('change');
|
||||||
}}
|
}}
|
||||||
/>
|
></textarea>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
user,
|
user,
|
||||||
settings,
|
settings,
|
||||||
folders,
|
folders,
|
||||||
showEmbeds
|
showEmbeds,
|
||||||
|
artifactContents
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { getChatById } from '$lib/apis/chats';
|
import { getChatById } from '$lib/apis/chats';
|
||||||
|
|
@ -312,7 +313,7 @@
|
||||||
<div class="flex items-center">{$i18n.t('Settings')}</div>
|
<div class="flex items-center">{$i18n.t('Settings')}</div>
|
||||||
</DropdownMenu.Item> -->
|
</DropdownMenu.Item> -->
|
||||||
|
|
||||||
{#if $mobile}
|
{#if $mobile && ($user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true))}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
||||||
id="chat-controls-button"
|
id="chat-controls-button"
|
||||||
|
|
@ -342,6 +343,7 @@
|
||||||
<div class="flex items-center">{$i18n.t('Overview')}</div>
|
<div class="flex items-center">{$i18n.t('Overview')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
{#if ($artifactContents ?? []).length > 0}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
||||||
id="chat-overview-button"
|
id="chat-overview-button"
|
||||||
|
|
@ -355,6 +357,7 @@
|
||||||
<Cube className=" size-4" strokeWidth="1.5" />
|
<Cube className=" size-4" strokeWidth="1.5" />
|
||||||
<div class="flex items-center">{$i18n.t('Artifacts')}</div>
|
<div class="flex items-center">{$i18n.t('Artifacts')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1" />
|
<hr class="border-gray-50 dark:border-gray-800 my-1" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadChatPreview = async (selectedIdx) => {
|
const loadChatPreview = async (selectedIdx) => {
|
||||||
if (
|
if (!chatList || chatList.length === 0 || selectedIdx === null) {
|
||||||
!chatList ||
|
|
||||||
chatList.length === 0 ||
|
|
||||||
selectedIdx === null ||
|
|
||||||
chatList[selectedIdx] === undefined
|
|
||||||
) {
|
|
||||||
selectedChat = null;
|
selectedChat = null;
|
||||||
messages = null;
|
messages = null;
|
||||||
history = null;
|
history = null;
|
||||||
|
|
@ -70,8 +65,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedChatIdx = selectedIdx - actions.length;
|
const selectedChatIdx = selectedIdx - actions.length;
|
||||||
if (selectedChatIdx < 0) {
|
if (selectedChatIdx < 0 || selectedChatIdx >= chatList.length) {
|
||||||
selectedChat = null;
|
selectedChat = null;
|
||||||
|
messages = null;
|
||||||
|
history = null;
|
||||||
|
selectedModels = [''];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +231,7 @@
|
||||||
{
|
{
|
||||||
label: $i18n.t('Create a new note'),
|
label: $i18n.t('Create a new note'),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await goto(`/notes${query ? `?content=${query}` : ''}`);
|
await goto(`/notes?content=${query}`);
|
||||||
show = false;
|
show = false;
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
import PinnedModelList from './Sidebar/PinnedModelList.svelte';
|
import PinnedModelList from './Sidebar/PinnedModelList.svelte';
|
||||||
import Note from '../icons/Note.svelte';
|
import Note from '../icons/Note.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
|
import HotkeyHint from '../common/HotkeyHint.svelte';
|
||||||
|
|
||||||
const BREAKPOINT = 768;
|
const BREAKPOINT = 768;
|
||||||
|
|
||||||
|
|
@ -128,7 +129,8 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFolder = async ({ name, data }) => {
|
const createFolder = async ({ name, data }) => {
|
||||||
if (name === '') {
|
name = name?.trim();
|
||||||
|
if (!name) {
|
||||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +183,7 @@
|
||||||
console.log('initChatList');
|
console.log('initChatList');
|
||||||
currentChatPage.set(1);
|
currentChatPage.set(1);
|
||||||
allChatsLoaded = false;
|
allChatsLoaded = false;
|
||||||
|
scrollPaginationEnabled.set(false);
|
||||||
|
|
||||||
initFolders();
|
initFolders();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
@ -366,10 +369,6 @@
|
||||||
navElement.style['-webkit-app-region'] = 'drag';
|
navElement.style['-webkit-app-region'] = 'drag';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$showSidebar && !value) {
|
|
||||||
showSidebar.set(true);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
showSidebar.subscribe(async (value) => {
|
showSidebar.subscribe(async (value) => {
|
||||||
localStorage.sidebar = value;
|
localStorage.sidebar = value;
|
||||||
|
|
@ -478,6 +477,12 @@
|
||||||
<ChannelModal
|
<ChannelModal
|
||||||
bind:show={showCreateChannel}
|
bind:show={showCreateChannel}
|
||||||
onSubmit={async ({ name, access_control }) => {
|
onSubmit={async ({ name, access_control }) => {
|
||||||
|
name = name?.trim();
|
||||||
|
if (!name) {
|
||||||
|
toast.error($i18n.t('Channel name cannot be empty.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await createNewChannel(localStorage.token, {
|
const res = await createNewChannel(localStorage.token, {
|
||||||
name: name,
|
name: name,
|
||||||
access_control: access_control
|
access_control: access_control
|
||||||
|
|
@ -743,7 +748,10 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/" class="flex flex-1 px-1.5" on:click={newChatHandler}>
|
<a href="/" class="flex flex-1 px-1.5" on:click={newChatHandler}>
|
||||||
<div class=" self-center font-medium text-gray-850 dark:text-white font-primary">
|
<div
|
||||||
|
id="sidebar-webui-name"
|
||||||
|
class=" self-center font-medium text-gray-850 dark:text-white font-primary"
|
||||||
|
>
|
||||||
{$WEBUI_NAME}
|
{$WEBUI_NAME}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -787,7 +795,7 @@
|
||||||
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<a
|
<a
|
||||||
id="sidebar-new-chat-button"
|
id="sidebar-new-chat-button"
|
||||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||||
href="/"
|
href="/"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
on:click={newChatHandler}
|
on:click={newChatHandler}
|
||||||
|
|
@ -797,16 +805,18 @@
|
||||||
<PencilSquare className=" size-4.5" strokeWidth="2" />
|
<PencilSquare className=" size-4.5" strokeWidth="2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex self-center translate-y-[0.5px]">
|
<div class="flex flex-1 self-center translate-y-[0.5px]">
|
||||||
<div class=" self-center text-sm font-primary">{$i18n.t('New Chat')}</div>
|
<div class=" self-center text-sm font-primary">{$i18n.t('New Chat')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HotkeyHint name="newChat" className=" group-hover:visible invisible" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<button
|
<button
|
||||||
id="sidebar-search-button"
|
id="sidebar-search-button"
|
||||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showSearch.set(true);
|
showSearch.set(true);
|
||||||
}}
|
}}
|
||||||
|
|
@ -817,9 +827,10 @@
|
||||||
<Search strokeWidth="2" className="size-4.5" />
|
<Search strokeWidth="2" className="size-4.5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex self-center translate-y-[0.5px]">
|
<div class="flex flex-1 self-center translate-y-[0.5px]">
|
||||||
<div class=" self-center text-sm font-primary">{$i18n.t('Search')}</div>
|
<div class=" self-center text-sm font-primary">{$i18n.t('Search')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<HotkeyHint name="search" className=" group-hover:visible invisible" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -880,7 +891,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
|
{#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
|
||||||
|
<Folder
|
||||||
|
className="px-2 mt-0.5"
|
||||||
|
name={$i18n.t('Models')}
|
||||||
|
chevron={false}
|
||||||
|
dragAndDrop={false}
|
||||||
|
>
|
||||||
<PinnedModelList bind:selectedChatId {shiftKey} />
|
<PinnedModelList bind:selectedChatId {shiftKey} />
|
||||||
|
</Folder>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
|
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin'}
|
||||||
<button
|
<div
|
||||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -88,6 +88,6 @@
|
||||||
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
|
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
|
||||||
<Cog6 className="size-3.5" />
|
<Cog6 className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -393,7 +393,7 @@
|
||||||
<div class="flex items-center">{$i18n.t('Clone')}</div>
|
<div class="flex items-center">{$i18n.t('Clone')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
{#if chatId}
|
{#if chatId && $folders.length > 0}
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger
|
<DropdownMenu.SubTrigger
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue