* feat: improve ollama model management experience

This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.

* fix

* i18n: improve Chinese translation

* fix: handle non‑UTF8 chars in third‑party responses without error

* German translation of new strings in i18n

* log web search queries only with level 'debug' instead of 'info'

* Tool calls now only include text and dont inlcude other content like image b64

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

* refac/fix: do not process xlsx files with azure doc intelligence

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

* feat(onedrive): Enable search and "My Organization" pivot

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations.

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.

* build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend

Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump black from 25.1.0 to 25.9.0 in /backend

Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump markdown from 3.8.2 to 3.9 in /backend

Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend

Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0

Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: opentelemetry-api
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation

Co-Authored-By: gjveld <19951982+gjveld@users.noreply.github.com>

* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

* feat: load data in parallel to accelerate page loading speed

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Feature required in https://github.com/open-webui/open-webui/discussions/18022

Added npm vega lib to package.json
Added function for visualization renderer to src/libs/utils/index.ts
Added logic to src/lib/components/chat/Messages/CodeBlock.svelte

The treatment is similar as for mermaid diagrams.

Reference: https://vega.github.io/vega/

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Add suport for Vega-Lite Specifications.
Vega-Lite is a "compiled" version of Vega Char Visualizer.
For be rendered with Vega it have to be compiled.
This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added.

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

Note:
Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

* Fixed: Cannot handle batch sizes > 1 if no padding token is defined

Fixes Cannot handle batch sizes > 1 if no padding token is defined

For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id.

* refac: fallback to reasoning content

* fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"

Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

- Incorrect: "Eriquetas de Razonamiento"
- Correct:   "Etiquetas de Razonamiento"

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* refac

* chore: dep bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>
This commit is contained in:
Tim Jaeryang Baek 2025-10-07 16:20:27 -05:00 committed by GitHub
parent 4d7fddaf7e
commit 8d7d79d54b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 6119 additions and 3198 deletions

View file

@ -13,6 +13,8 @@ body:
- **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.**
- Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed!**
- **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.
- **Contributing**: If you encounter an issue, consider submitting a pull request or forking the project. We prioritize preventing contributor burnout to maintain Open WebUI's quality.

View file

@ -4,14 +4,15 @@
**Before submitting, make sure you've checked the following:**
- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. Not targeting the `dev` branch may lead to immediate closure of the PR.
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **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?
- [ ] **Testing:** Have you written and run sufficient tests to validate the changes?
- [ ] **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.
- [ ] **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?
- [ ] **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
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows

View file

@ -5,6 +5,75 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.33] - 2025-10-08
### Added
- 🎨 Workspace interface received a comprehensive redesign across Models, Knowledge, Prompts, and Tools sections, featuring reorganized controls, view filters for created vs shared items, tag selectors, improved visual hierarchy, and streamlined import/export functionality. [Commit](https://github.com/open-webui/open-webui/commit/2c59a288603d8c5f004f223ee00fef37cc763a8e), [Commit](https://github.com/open-webui/open-webui/commit/6050c86ab6ef6b8c96dd3f99c62a6867011b67a4), [Commit](https://github.com/open-webui/open-webui/commit/96ecb47bc71c072aa34ef2be10781b042bef4e8c), [Commit](https://github.com/open-webui/open-webui/commit/2250d102b28075a9611696e911536547abb8b38a), [Commit](https://github.com/open-webui/open-webui/commit/23c8f6d507bfee75ab0015a3e2972d5c26f7e9bf), [Commit](https://github.com/open-webui/open-webui/commit/a743b16728c6ae24b8befbc2d7f24eb9e20c4ad5)
- 🛠️ Functions admin interface received a comprehensive redesign with creator attribution display, ownership filters for created vs shared items, improved organization, and refined styling. [Commit](https://github.com/open-webui/open-webui/commit/f5e1a42f51acc0b9d5b63a33c1ca2e42470239c1)
- ⚡ Page initialization performance is significantly improved through parallel data loading and optimized folder API calls, reducing initial page load time. [#17559](https://github.com/open-webui/open-webui/pull/17559), [#17889](https://github.com/open-webui/open-webui/pull/17889)
- ⚡ Chat overview component is now dynamically loaded on demand, reducing initial page bundle size by approximately 470KB and improving first-screen loading speed. [#17595](https://github.com/open-webui/open-webui/pull/17595)
- 📁 Folders can now be attached to chats using the "#" command, automatically expanding to include all files within the folder for streamlined knowledge base integration. [Commit](https://github.com/open-webui/open-webui/commit/d2cb78179d66dc85188172a08622d4c97a2ea1ee)
- 📱 Progressive Web App now supports Android share target functionality, allowing users to share web pages, YouTube videos, and text directly to Open WebUI from the system share menu. [#17633](https://github.com/open-webui/open-webui/pull/17633), [#17125](https://github.com/open-webui/open-webui/issues/17125)
- 🗄️ Redis session storage is now available as an experimental option for OAuth authentication flows via the ENABLE_STAR_SESSIONS_MIDDLEWARE environment variable, providing shared session state across multi-replica deployments to address CSRF errors, though currently only basic Redis setups are supported. [#17223](https://github.com/open-webui/open-webui/pull/17223), [#15373](https://github.com/open-webui/open-webui/issues/15373), [Docs:Commit](https://github.com/open-webui/docs/commit/14052347f165d1b597615370373d7289ce44c7f9)
- 📊 Vega and Vega-Lite chart visualization renderers are now supported in code blocks, enabling inline rendering of data visualizations with automatic compilation of Vega-Lite specifications. [#18033](https://github.com/open-webui/open-webui/pull/18033), [#18040](https://github.com/open-webui/open-webui/pull/18040), [#18022](https://github.com/open-webui/open-webui/issues/18022)
- 🔗 OpenAI connections now support custom HTTP headers, enabling users to configure authentication and routing headers for specific deployment requirements. [#18021](https://github.com/open-webui/open-webui/pull/18021), [#9732](https://github.com/open-webui/open-webui/discussions/9732)
- 🔐 OpenID Connect authentication now supports OIDC providers without email scope via the ENABLE_OAUTH_WITHOUT_EMAIL environment variable, enabling compatibility with identity providers that don't expose email addresses. [#18047](https://github.com/open-webui/open-webui/pull/18047), [#18045](https://github.com/open-webui/open-webui/issues/18045)
- 🤖 Ollama model management modal now features individual model update cancellation, comprehensive tooltips for all buttons, and streamlined notification behavior to reduce toast spam. [#16863](https://github.com/open-webui/open-webui/pull/16863)
- ☁️ OneDrive file picker now includes search functionality and "My Organization" pivot for business accounts, enabling easier file discovery across organizational content. [#17930](https://github.com/open-webui/open-webui/pull/17930), [#17929](https://github.com/open-webui/open-webui/issues/17929)
- 📊 Chat overview flow diagram now supports toggling between vertical and horizontal layout orientations for improved visualization flexibility. [#17941](https://github.com/open-webui/open-webui/pull/17941)
- 🔊 OpenAI Text-to-Speech engine now supports additional parameters, allowing users to customize TTS behavior with provider-specific options via JSON configuration. [#17985](https://github.com/open-webui/open-webui/issues/17985), [#17188](https://github.com/open-webui/open-webui/pull/17188)
- 🛠️ Tool server list now displays server name, URL, and type (OpenAPI or MCP) for easier identification and management. [#18062](https://github.com/open-webui/open-webui/issues/18062)
- 📁 Folders now remember the last selected model, automatically applying it when starting new chats within that folder. [#17836](https://github.com/open-webui/open-webui/issues/17836)
- 🔢 Ollama embedding endpoint now supports the optional dimensions parameter for controlling embedding output size, compatible with Ollama v0.11.11 and later. [#17942](https://github.com/open-webui/open-webui/pull/17942)
- ⚡ Workspace knowledge page load time is improved by removing redundant API calls, enhancing overall responsiveness. [#18057](https://github.com/open-webui/open-webui/pull/18057)
- ⚡ File metadata query performance is enhanced by selecting only relevant columns instead of retrieving entire records, reducing database overhead. [#18013](https://github.com/open-webui/open-webui/pull/18013)
- 📄 Note PDF exports now include titles and properly render in dark mode with appropriate background colors. [Commit](https://github.com/open-webui/open-webui/commit/216fb5c3db1a223ffe6e72d97aa9551fe0e2d028)
- 📄 Docling document extraction now supports additional parameters for VLM pipeline configuration, enabling customized vision model settings. [#17363](https://github.com/open-webui/open-webui/pull/17363)
- ⚙️ Server startup script now supports passing arbitrary arguments to uvicorn, enabling custom server configuration options. [#17919](https://github.com/open-webui/open-webui/pull/17919), [#17918](https://github.com/open-webui/open-webui/issues/17918)
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
- 🌐 Translations for German, Danish, Spanish, Korean, Portuguese (Brazil), Simplified Chinese, and Traditional Chinese were enhanced and expanded.
### Fixed
- 💬 System prompts are no longer duplicated in chat requests, eliminating confusion and excessive token usage caused by repeated instructions being sent to models. [#17198](https://github.com/open-webui/open-webui/issues/17198), [#16855](https://github.com/open-webui/open-webui/issues/16855)
- 🔐 MCP OAuth 2.1 authentication now complies with the standard by implementing PKCE with S256 code challenge method and explicitly passing client credentials during token authorization, resolving "code_challenge: Field required" and "client_id: Field required" errors when connecting to OAuth-secured MCP servers. [Commit](https://github.com/open-webui/open-webui/commit/911a114ad459f5deebd97543c13c2b90196efb54), [#18010](https://github.com/open-webui/open-webui/issues/18010), [#18087](https://github.com/open-webui/open-webui/pull/18087)
- 🔐 OAuth signup flow now handles password hashing correctly by migrating from passlib to native bcrypt, preventing failures when passwords exceed 72 bytes. [#17917](https://github.com/open-webui/open-webui/issues/17917)
- 🔐 OAuth token refresh errors are resolved by properly registering and storing OAuth clients, fixing "Constructor parameter should be str" exceptions for Google, Microsoft, and OIDC providers. [#17829](https://github.com/open-webui/open-webui/issues/17829)
- 🔐 OAuth server metadata URL is now correctly accessed via the proper attribute, fixing automatic token refresh and logout functionality for Microsoft OAuth provider when OPENID_PROVIDER_URL is not set. [#18065](https://github.com/open-webui/open-webui/pull/18065)
- 🔐 OAuth credential decryption failures now allow the application to start gracefully with clear error messages instead of crashing, preventing complete service outages when WEBUI_SECRET_KEY mismatches occur during database migrations or environment changes. [#18094](https://github.com/open-webui/open-webui/pull/18094), [#18092](https://github.com/open-webui/open-webui/issues/18092)
- 🔐 OAuth 2.1 server discovery now correctly attempts all configured discovery URLs in sequence instead of only trying the first URL. [#17906](https://github.com/open-webui/open-webui/pull/17906), [#17904](https://github.com/open-webui/open-webui/issues/17904), [#18026](https://github.com/open-webui/open-webui/pull/18026)
- 🔐 Login redirect now correctly honors the redirect query parameter after authentication, ensuring users are returned to their intended destination with query parameters intact instead of defaulting to the homepage. [#18071](https://github.com/open-webui/open-webui/issues/18071)
- ☁️ OneDrive Business integration authentication regression is resolved, ensuring the popup now properly triggers when connecting to OneDrive accounts. [#17902](https://github.com/open-webui/open-webui/pull/17902), [#17825](https://github.com/open-webui/open-webui/discussions/17825), [#17816](https://github.com/open-webui/open-webui/issues/17816)
- 👥 Default group settings now persist correctly after page navigation, ensuring configuration changes are properly saved and retained. [#17899](https://github.com/open-webui/open-webui/issues/17899), [#18003](https://github.com/open-webui/open-webui/issues/18003)
- 📁 Folder data integrity is now verified on retrieval, automatically fixing orphaned folders with invalid parent references and ensuring proper cascading deletion of nested folder structures. [Commit](https://github.com/open-webui/open-webui/commit/5448618dd5ea181b9635b77040cef60926a902ff)
- 🗄️ Redis Sentinel and Redis Cluster configurations with the experimental ENABLE_STAR_SESSIONS_MIDDLEWARE feature are now properly isolated by making the feature opt-in only, preventing ReadOnlyError failures when connecting to read replicas in multi-node Redis deployments. [#18073](https://github.com/open-webui/open-webui/issues/18073)
- 📊 Mermaid and Vega diagram rendering now displays error toast notifications when syntax errors are detected, helping users identify and fix diagram issues instead of silently failing. [#18068](https://github.com/open-webui/open-webui/pull/18068)
- 🤖 Reasoning models that return reasoning_content instead of content no longer cause NoneType errors during chat title generation, follow-up suggestions, and tag generation. [#18080](https://github.com/open-webui/open-webui/pull/18080)
- 📚 Citation rendering now correctly handles multiple source references in a single bracket, parsing formats like [1,2] and [1, 2] into separate clickable citation links. [#18120](https://github.com/open-webui/open-webui/pull/18120)
- 🔍 Web search now handles individual source failures gracefully, continuing to process remaining sources instead of failing entirely when a single URL is unreachable or returns an error. [Commit](https://github.com/open-webui/open-webui/commit/e000494e488090c5f66989a2b3f89d3eaeb7946b), [Commit](https://github.com/open-webui/open-webui/commit/53e98620bff38ab9280aee5165af0a704bdd99b9)
- 🔍 Hybrid search with reranking now handles empty result sets gracefully instead of crashing with ValueError when all results are filtered out due to relevance thresholds. [#18096](https://github.com/open-webui/open-webui/issues/18096)
- 🔍 Reranking models without defined padding tokens now work correctly by automatically falling back to eos_token_id as pad_token_id, fixing "Cannot handle batch sizes > 1" errors for models like Qwen3-Reranker. [#18108](https://github.com/open-webui/open-webui/pull/18108), [#16027](https://github.com/open-webui/open-webui/discussions/16027)
- 🔍 Model selector search now correctly returns results for non-admin users by dynamically updating the search index when the model list changes, fixing a race condition that caused empty search results. [#17996](https://github.com/open-webui/open-webui/pull/17996), [#17960](https://github.com/open-webui/open-webui/pull/17960)
- ⚡ Task model function calling performance is improved by excluding base64 image data from payloads, significantly reducing token count and memory usage when images are present in conversations. [#17897](https://github.com/open-webui/open-webui/pull/17897)
- 🤖 Text selection "Ask" action now correctly recognizes and uses local models configured via direct connections instead of only showing external provider models. [#17896](https://github.com/open-webui/open-webui/issues/17896)
- 🛑 Task cancellation API now returns accurate response status, correctly reporting successful cancellations instead of incorrectly indicating failures. [#17920](https://github.com/open-webui/open-webui/issues/17920)
- 💬 Follow-up query suggestions are now generated and displayed in temporary chats, matching the behavior of saved chats. [#14987](https://github.com/open-webui/open-webui/issues/14987)
- 🔊 Azure Text-to-Speech now properly escapes special characters like ampersands in SSML, preventing HTTP 400 errors and ensuring audio generation succeeds for all text content. [#17962](https://github.com/open-webui/open-webui/issues/17962)
- 🛠️ OpenAPI tool server calls with optional parameters now execute successfully even when no arguments are provided, removing the incorrect requirement for a request body. [#18036](https://github.com/open-webui/open-webui/issues/18036)
- 🛠️ MCP mode tool server connections no longer incorrectly validate the OpenAPI path field, allowing seamless switching between OpenAPI and MCP connection types. [#17989](https://github.com/open-webui/open-webui/pull/17989), [#17988](https://github.com/open-webui/open-webui/issues/17988)
- 🛠️ Third-party tool responses containing non-UTF8 or invalid byte sequences are now handled gracefully without causing request failures. [#17882](https://github.com/open-webui/open-webui/pull/17882)
- 🎨 Workspace filter dropdown now correctly renders model tags as strings instead of displaying individual characters, fixing broken filtering interface when models have multiple tags. [#18034](https://github.com/open-webui/open-webui/issues/18034)
- ⌨️ Ctrl+Enter keyboard shortcut now correctly sends messages in mobile and narrow browser views on Chrome instead of inserting newlines. [#17975](https://github.com/open-webui/open-webui/issues/17975)
- ⌨️ Tab characters are now preserved when pasting code or formatted text into the chat input box in plain text mode. [#17958](https://github.com/open-webui/open-webui/issues/17958)
- 📋 Text selection copying from the chat input box now correctly copies only the selected text instead of the entire textbox content. [#17911](https://github.com/open-webui/open-webui/issues/17911)
- 🔍 Web search query logging now uses debug level instead of info level, preventing user search queries from appearing in production logs. [#17888](https://github.com/open-webui/open-webui/pull/17888)
- 📝 Debug print statements in middleware were removed to prevent excessive log pollution and respect configured logging levels. [#17943](https://github.com/open-webui/open-webui/issues/17943)
### Changed
- 🗄️ Milvus vector database dependency is updated from pymilvus 2.5.0 to 2.6.2, ensuring compatibility with newer Milvus versions but requiring users on older Milvus instances to either upgrade their database or manually downgrade the pymilvus package. [#18066](https://github.com/open-webui/open-webui/pull/18066)
## [0.6.32] - 2025-09-29
### Added

View file

@ -605,8 +605,8 @@ def load_oauth_providers():
OAUTH_PROVIDERS.clear()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
def google_oauth_register(client: OAuth):
client.register(
def google_oauth_register(oauth: OAuth):
client = oauth.register(
name="google",
client_id=GOOGLE_CLIENT_ID.value,
client_secret=GOOGLE_CLIENT_SECRET.value,
@ -621,6 +621,7 @@ def load_oauth_providers():
},
redirect_uri=GOOGLE_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["google"] = {
"redirect_uri": GOOGLE_REDIRECT_URI.value,
@ -633,8 +634,8 @@ def load_oauth_providers():
and MICROSOFT_CLIENT_TENANT_ID.value
):
def microsoft_oauth_register(client: OAuth):
client.register(
def microsoft_oauth_register(oauth: OAuth):
client = oauth.register(
name="microsoft",
client_id=MICROSOFT_CLIENT_ID.value,
client_secret=MICROSOFT_CLIENT_SECRET.value,
@ -649,6 +650,7 @@ def load_oauth_providers():
},
redirect_uri=MICROSOFT_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["microsoft"] = {
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
@ -658,8 +660,8 @@ def load_oauth_providers():
if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value:
def github_oauth_register(client: OAuth):
client.register(
def github_oauth_register(oauth: OAuth):
client = oauth.register(
name="github",
client_id=GITHUB_CLIENT_ID.value,
client_secret=GITHUB_CLIENT_SECRET.value,
@ -677,6 +679,7 @@ def load_oauth_providers():
},
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["github"] = {
"redirect_uri": GITHUB_CLIENT_REDIRECT_URI.value,
@ -690,7 +693,7 @@ def load_oauth_providers():
and OPENID_PROVIDER_URL.value
):
def oidc_oauth_register(client: OAuth):
def oidc_oauth_register(oauth: OAuth):
client_kwargs = {
"scope": OAUTH_SCOPES.value,
**(
@ -716,7 +719,7 @@ def load_oauth_providers():
% ("S256", OAUTH_CODE_CHALLENGE_METHOD.value)
)
client.register(
client = oauth.register(
name="oidc",
client_id=OAUTH_CLIENT_ID.value,
client_secret=OAUTH_CLIENT_SECRET.value,
@ -724,6 +727,7 @@ def load_oauth_providers():
client_kwargs=client_kwargs,
redirect_uri=OPENID_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["oidc"] = {
"name": OAUTH_PROVIDER_NAME.value,
@ -733,8 +737,8 @@ def load_oauth_providers():
if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value:
def feishu_oauth_register(client: OAuth):
client.register(
def feishu_oauth_register(oauth: OAuth):
client = oauth.register(
name="feishu",
client_id=FEISHU_CLIENT_ID.value,
client_secret=FEISHU_CLIENT_SECRET.value,
@ -752,6 +756,7 @@ def load_oauth_providers():
},
redirect_uri=FEISHU_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["feishu"] = {
"register": feishu_oauth_register,
@ -2310,6 +2315,18 @@ DOCLING_SERVER_URL = PersistentConfig(
os.getenv("DOCLING_SERVER_URL", "http://docling:5001"),
)
docling_params = os.getenv("DOCLING_PARAMS", "")
try:
docling_params = json.loads(docling_params)
except json.JSONDecodeError:
docling_params = {}
DOCLING_PARAMS = PersistentConfig(
"DOCLING_PARAMS",
"rag.docling_params",
docling_params,
)
DOCLING_DO_OCR = PersistentConfig(
"DOCLING_DO_OCR",
"rag.docling_do_ocr",
@ -3361,6 +3378,19 @@ AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
)
audio_tts_openai_params = os.getenv("AUDIO_TTS_OPENAI_PARAMS", "")
try:
audio_tts_openai_params = json.loads(audio_tts_openai_params)
except json.JSONDecodeError:
audio_tts_openai_params = {}
AUDIO_TTS_OPENAI_PARAMS = PersistentConfig(
"AUDIO_TTS_OPENAI_PARAMS",
"audio.tts.openai.params",
audio_tts_openai_params,
)
AUDIO_TTS_API_KEY = PersistentConfig(
"AUDIO_TTS_API_KEY",
"audio.tts.api_key",

View file

@ -212,6 +212,11 @@ ENABLE_FORWARD_USER_INFO_HEADERS = (
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
)
# Experimental feature, may be removed in future
ENABLE_STAR_SESSIONS_MIDDLEWARE = (
os.environ.get("ENABLE_STAR_SESSIONS_MIDDLEWARE", "False").lower() == "true"
)
####################################
# WEBUI_BUILD_HASH
####################################
@ -468,7 +473,9 @@ ENABLE_COMPRESSION_MIDDLEWARE = (
####################################
# OAUTH Configuration
####################################
ENABLE_OAUTH_EMAIL_FALLBACK = (
os.environ.get("ENABLE_OAUTH_EMAIL_FALLBACK", "False").lower() == "true"
)
ENABLE_OAUTH_ID_TOKEN_COOKIE = (
os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
@ -482,7 +489,6 @@ OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
)
####################################
# SCIM Configuration
####################################

View file

@ -8,6 +8,7 @@ import shutil
import sys
import time
import random
import re
from uuid import uuid4
@ -174,13 +175,14 @@ from open_webui.config import (
AUDIO_STT_AZURE_LOCALES,
AUDIO_STT_AZURE_BASE_URL,
AUDIO_STT_AZURE_MAX_SPEAKERS,
AUDIO_TTS_API_KEY,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
AUDIO_TTS_VOICE,
AUDIO_TTS_OPENAI_API_BASE_URL,
AUDIO_TTS_OPENAI_API_KEY,
AUDIO_TTS_OPENAI_PARAMS,
AUDIO_TTS_API_KEY,
AUDIO_TTS_SPLIT_ON,
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_BASE_URL,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
@ -246,6 +248,7 @@ from open_webui.config import (
EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL,
DOCLING_SERVER_URL,
DOCLING_PARAMS,
DOCLING_DO_OCR,
DOCLING_FORCE_OCR,
DOCLING_OCR_ENGINE,
@ -447,6 +450,7 @@ from open_webui.env import (
ENABLE_OTEL,
EXTERNAL_PWA_MANIFEST_URL,
AIOHTTP_CLIENT_SESSION_SSL,
ENABLE_STAR_SESSIONS_MIDDLEWARE,
)
@ -834,6 +838,7 @@ app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_PARAMS = DOCLING_PARAMS
app.state.config.DOCLING_DO_OCR = DOCLING_DO_OCR
app.state.config.DOCLING_FORCE_OCR = DOCLING_FORCE_OCR
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
@ -1095,11 +1100,15 @@ 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_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
app.state.config.TTS_OPENAI_PARAMS = AUDIO_TTS_OPENAI_PARAMS
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
@ -1170,12 +1179,32 @@ class RedirectMiddleware(BaseHTTPMiddleware):
path = request.url.path
query_params = dict(parse_qs(urlparse(str(request.url)).query))
redirect_params = {}
# Check for the specific watch path and the presence of 'v' parameter
if path.endswith("/watch") and "v" in query_params:
# Extract the first 'v' parameter
video_id = query_params["v"][0]
encoded_video_id = urlencode({"youtube": video_id})
redirect_url = f"/?{encoded_video_id}"
youtube_video_id = query_params["v"][0]
redirect_params["youtube"] = youtube_video_id
if "shared" in query_params and len(query_params["shared"]) > 0:
# PWA share_target support
text = query_params["shared"][0]
if text:
urls = re.match(r"https://\S+", text)
if urls:
from open_webui.retrieval.loaders.youtube import _parse_video_id
if youtube_video_id := _parse_video_id(urls[0]):
redirect_params["youtube"] = youtube_video_id
else:
redirect_params["load-url"] = urls[0]
else:
redirect_params["q"] = text
if redirect_params:
redirect_url = f"/?{urlencode(redirect_params)}"
return RedirectResponse(url=redirect_url)
# Proceed with the normal flow of other requests
@ -1474,7 +1503,7 @@ async def chat_completion(
}
if metadata.get("chat_id") and (user and user.role != "admin"):
if metadata["chat_id"] != "local":
if not metadata["chat_id"].startswith("local:"):
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
if chat is None:
raise HTTPException(
@ -1501,6 +1530,7 @@ async def chat_completion(
response = await chat_completion_handler(request, form_data, user)
if metadata.get("chat_id") and metadata.get("message_id"):
try:
if not metadata["chat_id"].startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
@ -1528,6 +1558,7 @@ async def chat_completion(
if metadata.get("chat_id") and metadata.get("message_id"):
# Update the chat message with the error
try:
if not metadata["chat_id"].startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
@ -1903,13 +1934,20 @@ if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
"oauth_client_info", ""
)
try:
oauth_client_info = decrypt_data(oauth_client_info)
app.state.oauth_client_manager.add_client(
f"mcp:{server_id}", OAuthClientInformationFull(**oauth_client_info)
f"mcp:{server_id}",
OAuthClientInformationFull(**oauth_client_info),
)
except Exception as e:
log.error(
f"Error adding OAuth client for MCP tool server {server_id}: {e}"
)
pass
try:
if REDIS_URL:
if ENABLE_STAR_SESSIONS_MIDDLEWARE:
redis_session_store = RedisStore(
url=REDIS_URL,
prefix=(f"{REDIS_KEY_PREFIX}:session:" if REDIS_KEY_PREFIX else "session:"),
@ -2004,6 +2042,11 @@ async def get_manifest_json():
"purpose": "maskable",
},
],
"share_target": {
"action": "/",
"method": "GET",
"params": {"text": "shared"},
},
}

View file

@ -186,7 +186,9 @@ class FilesTable:
created_at=file.created_at,
updated_at=file.updated_at,
)
for file in db.query(File)
for file in db.query(
File.id, File.meta, File.created_at, File.updated_at
)
.filter(File.id.in_(ids))
.order_by(File.updated_at.desc())
.all()

View file

@ -3,7 +3,7 @@ import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from open_webui.models.users import Users
from open_webui.models.users import Users, UserModel
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index
@ -76,6 +76,10 @@ class FunctionWithValvesModel(BaseModel):
####################
class FunctionUserResponse(FunctionModel):
user: Optional[UserModel] = None
class FunctionResponse(BaseModel):
id: str
user_id: str
@ -203,6 +207,28 @@ class FunctionsTable:
FunctionModel.model_validate(function) for function in functions
]
def get_function_list(self) -> list[FunctionUserResponse]:
with get_db() as db:
functions = db.query(Function).order_by(Function.updated_at.desc()).all()
user_ids = list(set(func.user_id for func in functions))
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users_dict = {user.id: user for user in users}
return [
FunctionUserResponse.model_validate(
{
**FunctionModel.model_validate(func).model_dump(),
"user": (
users_dict.get(func.user_id).model_dump()
if func.user_id in users_dict
else None
),
}
)
for func in functions
]
def get_functions_by_type(
self, type: str, active_only=False
) -> list[FunctionModel]:

View file

@ -346,11 +346,9 @@ class Loader:
self.engine == "document_intelligence"
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
and (
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
file_ext in ["pdf", "docx", "ppt", "pptx"]
or file_content_type
in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",

View file

@ -157,3 +157,10 @@ class YoutubeLoader:
f"No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed."
)
raise NoTranscriptFound(self.video_id, self.language, list(transcript_list))
async def aload(self) -> Generator[Document, None, None]:
"""Asynchronously load YouTube transcripts into `Document` objects."""
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, self.load)

View file

@ -6,6 +6,7 @@ import requests
import hashlib
from concurrent.futures import ThreadPoolExecutor
import time
import re
from urllib.parse import quote
from huggingface_hub import snapshot_download
@ -16,6 +17,7 @@ from langchain_core.documents import Document
from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import UserModel
from open_webui.models.files import Files
from open_webui.models.knowledge import Knowledges
@ -27,6 +29,9 @@ from open_webui.retrieval.vector.main import GetResult
from open_webui.utils.access_control import has_access
from open_webui.utils.misc import get_message_list
from open_webui.retrieval.web.utils import get_web_loader
from open_webui.retrieval.loaders.youtube import YoutubeLoader
from open_webui.env import (
SRC_LOG_LEVELS,
@ -49,6 +54,33 @@ from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.retrievers import BaseRetriever
def is_youtube_url(url: str) -> bool:
youtube_regex = r"^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+$"
return re.match(youtube_regex, url) is not None
def get_loader(request, url: str):
if is_youtube_url(url):
return YoutubeLoader(
url,
language=request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
proxy_url=request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
)
else:
return get_web_loader(
url,
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS,
)
def get_content_from_url(request, url: str) -> str:
loader = get_loader(request, url)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
return content, docs
class VectorSearchRetriever(BaseRetriever):
collection_name: Any
embedding_function: Any
@ -188,7 +220,11 @@ def query_doc_with_hybrid_search(
zip(distances, metadatas, documents), key=lambda x: x[0], reverse=True
)
sorted_items = sorted_items[:k]
if sorted_items:
distances, documents, metadatas = map(list, zip(*sorted_items))
else:
distances, documents, metadatas = [], [], []
result = {
"distances": [distances],
@ -571,6 +607,13 @@ def get_sources_from_items(
"metadatas": [[{"file_id": chat.id, "name": chat.title}]],
}
elif item.get("type") == "url":
content, docs = get_content_from_url(request, item.get("url"))
if docs:
query_result = {
"documents": [[content]],
"metadatas": [[{"url": item.get("url"), "name": item.get("url")}]],
}
elif item.get("type") == "file":
if (
item.get("context") == "full"
@ -736,7 +779,6 @@ def get_sources_from_items(
sources.append(source)
except Exception as e:
log.exception(e)
return sources

View file

@ -75,7 +75,8 @@ def safe_validate_urls(url: Sequence[str]) -> Sequence[str]:
try:
if validate_url(u):
valid_urls.append(u)
except ValueError:
except Exception as e:
log.debug(f"Invalid URL {u}: {str(e)}")
continue
return valid_urls

View file

@ -3,6 +3,7 @@ import json
import logging
import os
import uuid
import html
from functools import lru_cache
from pydub import AudioSegment
from pydub.silence import split_on_silence
@ -153,6 +154,7 @@ def set_faster_whisper_model(model: str, auto_update: bool = False):
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
OPENAI_PARAMS: Optional[dict] = None
API_KEY: str
ENGINE: str
MODEL: str
@ -189,6 +191,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"tts": {
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"OPENAI_PARAMS": request.app.state.config.TTS_OPENAI_PARAMS,
"API_KEY": request.app.state.config.TTS_API_KEY,
"ENGINE": request.app.state.config.TTS_ENGINE,
"MODEL": request.app.state.config.TTS_MODEL,
@ -221,6 +224,7 @@ async def update_audio_config(
):
request.app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
request.app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
request.app.state.config.TTS_OPENAI_PARAMS = form_data.tts.OPENAI_PARAMS
request.app.state.config.TTS_API_KEY = form_data.tts.API_KEY
request.app.state.config.TTS_ENGINE = form_data.tts.ENGINE
request.app.state.config.TTS_MODEL = form_data.tts.MODEL
@ -261,12 +265,13 @@ async def update_audio_config(
return {
"tts": {
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": request.app.state.config.TTS_API_KEY,
"ENGINE": request.app.state.config.TTS_ENGINE,
"MODEL": request.app.state.config.TTS_MODEL,
"VOICE": request.app.state.config.TTS_VOICE,
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"OPENAI_PARAMS": request.app.state.config.TTS_OPENAI_PARAMS,
"API_KEY": request.app.state.config.TTS_API_KEY,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
@ -336,6 +341,11 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
) as session:
payload = {
**payload,
**(request.app.state.config.TTS_OPENAI_PARAMS or {}),
}
r = await session.post(
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
json=payload,
@ -458,7 +468,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
try:
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
<voice name="{language}">{payload["input"]}</voice>
<voice name="{language}">{html.escape(payload["input"])}</voice>
</speak>"""
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
async with aiohttp.ClientSession(

View file

@ -340,11 +340,12 @@ async def model_response_handler(request, channel, message, user):
if file.get("type", "") == "image":
images.append(file.get("url", ""))
thread_history_string = "\n\n".join(thread_history)
system_message = {
"role": "system",
"content": f"You are {model.get('name', model_id)}, participating in a threaded conversation. Be concise and conversational."
+ (
f"Here's the thread history:\n\n{''.join([f'{msg}' for msg in thread_history])}\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context."
f"Here's the thread history:\n\n\n{thread_history_string}\n\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context."
if thread_history
else ""
),
@ -384,6 +385,7 @@ async def model_response_handler(request, channel, message, user):
)
if res:
if res.get("choices", []) and len(res["choices"]) > 0:
await update_message_by_id(
channel.id,
response_message.id,
@ -397,6 +399,20 @@ async def model_response_handler(request, channel, message, user):
),
user,
)
elif res.get("error", None):
await update_message_by_id(
channel.id,
response_message.id,
MessageForm(
**{
"content": f"Error: {res['error']}",
"meta": {
"done": True,
},
}
),
user,
)
except Exception as e:
log.info(e)
pass
@ -436,7 +452,7 @@ async def new_message_handler(
}
await sio.emit(
"channel-events",
"events:channel",
event_data,
to=f"channel:{channel.id}",
)
@ -447,7 +463,7 @@ async def new_message_handler(
if parent_message:
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": parent_message.id,
@ -644,7 +660,7 @@ async def update_message_by_id(
if message:
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@ -708,7 +724,7 @@ async def add_reaction_to_message(
message = Messages.get_message_by_id(message_id)
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@ -774,7 +790,7 @@ async def remove_reaction_by_id_and_user_id_and_name(
message = Messages.get_message_by_id(message_id)
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@ -839,7 +855,7 @@ async def delete_message_by_id(
try:
Messages.delete_message_by_id(message_id)
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@ -862,7 +878,7 @@ async def delete_message_by_id(
if parent_message:
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": parent_message.id,

View file

@ -213,7 +213,7 @@ async def verify_tool_servers_config(
)
async with aiohttp.ClientSession() as session:
async with session.get(
discovery_urls[0]
discovery_url
) as oauth_server_metadata_response:
if oauth_server_metadata_response.status == 200:
try:
@ -234,7 +234,7 @@ async def verify_tool_servers_config(
)
raise HTTPException(
status_code=400,
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_urls[0]}",
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_url}",
)
raise HTTPException(

View file

@ -50,7 +50,15 @@ async def get_folders(user=Depends(get_verified_user)):
folders = Folders.get_folders_by_user_id(user.id)
# Verify folder data integrity
folder_list = []
for folder in folders:
if folder.parent_id and not Folders.get_folder_by_id_and_user_id(
folder.parent_id, user.id
):
folder = Folders.update_folder_parent_id_by_id_and_user_id(
folder.id, user.id, None
)
if folder.data:
if "files" in folder.data:
valid_files = []
@ -74,12 +82,9 @@ async def get_folders(user=Depends(get_verified_user)):
folder.id, user.id, FolderUpdateForm(data=folder.data)
)
return [
{
**folder.model_dump(),
}
for folder in folders
]
folder_list.append(FolderNameIdResponse(**folder.model_dump()))
return folder_list
############################
@ -265,7 +270,10 @@ async def delete_folder_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
folders = []
folders.append(Folders.get_folder_by_id_and_user_id(id, user.id))
while folders:
folder = folders.pop()
if folder:
try:
folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id)
@ -280,6 +288,13 @@ async def delete_folder_by_id(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
)
finally:
# Get all subfolders
subfolders = Folders.get_folders_by_parent_id_and_user_id(
folder.id, user.id
)
folders.extend(subfolders)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,

View file

@ -10,6 +10,7 @@ from open_webui.models.functions import (
FunctionForm,
FunctionModel,
FunctionResponse,
FunctionUserResponse,
FunctionWithValvesModel,
Functions,
)
@ -42,6 +43,11 @@ async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions()
@router.get("/list", response_model=list[FunctionUserResponse])
async def get_function_list(user=Depends(get_admin_user)):
return Functions.get_function_list()
############################
# ExportFunctions
############################

View file

@ -1020,6 +1020,10 @@ class GenerateEmbedForm(BaseModel):
options: Optional[dict] = None
keep_alive: Optional[Union[int, str]] = None
model_config = ConfigDict(
extra="allow",
)
@router.post("/api/embed")
@router.post("/api/embed/{url_idx}")

View file

@ -190,6 +190,9 @@ async def get_headers_and_cookies(
if token:
headers["Authorization"] = f"Bearer {token}"
if config.get("headers") and isinstance(config.get("headers"), dict):
headers = {**headers, **config.get("headers")}
return headers, cookies

View file

@ -5,6 +5,7 @@ import os
import shutil
import asyncio
import re
import uuid
from datetime import datetime
from pathlib import Path
@ -70,6 +71,7 @@ from open_webui.retrieval.web.firecrawl import search_firecrawl
from open_webui.retrieval.web.external import search_external
from open_webui.retrieval.utils import (
get_content_from_url,
get_embedding_function,
get_reranking_function,
get_model_path,
@ -189,6 +191,26 @@ def get_rf(
log.error(f"CrossEncoder: {e}")
raise Exception(ERROR_MESSAGES.DEFAULT("CrossEncoder error"))
# Safely adjust pad_token_id if missing as some models do not have this in config
try:
model_cfg = getattr(rf, "model", None)
if model_cfg and hasattr(model_cfg, "config"):
cfg = model_cfg.config
if getattr(cfg, "pad_token_id", None) is None:
# Fallback to eos_token_id when available
eos = getattr(cfg, "eos_token_id", None)
if eos is not None:
cfg.pad_token_id = eos
log.debug(
f"Missing pad_token_id detected; set to eos_token_id={eos}"
)
else:
log.warning(
"Neither pad_token_id nor eos_token_id present in model config"
)
except Exception as e2:
log.warning(f"Failed to adjust pad_token_id on CrossEncoder: {e2}")
return rf
@ -429,6 +451,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS,
"DOCLING_DO_OCR": request.app.state.config.DOCLING_DO_OCR,
"DOCLING_FORCE_OCR": request.app.state.config.DOCLING_FORCE_OCR,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
@ -590,6 +613,7 @@ class ConfigForm(BaseModel):
# Content extraction settings
CONTENT_EXTRACTION_ENGINE: Optional[str] = None
PDF_EXTRACT_IMAGES: Optional[bool] = None
DATALAB_MARKER_API_KEY: Optional[str] = None
DATALAB_MARKER_API_BASE_URL: Optional[str] = None
DATALAB_MARKER_ADDITIONAL_CONFIG: Optional[str] = None
@ -601,11 +625,13 @@ class ConfigForm(BaseModel):
DATALAB_MARKER_FORMAT_LINES: Optional[bool] = None
DATALAB_MARKER_USE_LLM: Optional[bool] = None
DATALAB_MARKER_OUTPUT_FORMAT: Optional[str] = None
EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None
EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None
TIKA_SERVER_URL: Optional[str] = None
DOCLING_SERVER_URL: Optional[str] = None
DOCLING_PARAMS: Optional[dict] = None
DOCLING_DO_OCR: Optional[bool] = None
DOCLING_FORCE_OCR: Optional[bool] = None
DOCLING_OCR_ENGINE: Optional[str] = None
@ -782,6 +808,11 @@ async def update_rag_config(
if form_data.DOCLING_SERVER_URL is not None
else request.app.state.config.DOCLING_SERVER_URL
)
request.app.state.config.DOCLING_PARAMS = (
form_data.DOCLING_PARAMS
if form_data.DOCLING_PARAMS is not None
else request.app.state.config.DOCLING_PARAMS
)
request.app.state.config.DOCLING_DO_OCR = (
form_data.DOCLING_DO_OCR
if form_data.DOCLING_DO_OCR is not None
@ -1104,6 +1135,7 @@ async def update_rag_config(
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS,
"DOCLING_DO_OCR": request.app.state.config.DOCLING_DO_OCR,
"DOCLING_FORCE_OCR": request.app.state.config.DOCLING_FORCE_OCR,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
@ -1522,6 +1554,7 @@ def process_file(
"picture_description_mode": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
"picture_description_local": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
"picture_description_api": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
**request.app.state.config.DOCLING_PARAMS,
},
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
@ -1680,49 +1713,6 @@ def process_text(
@router.post("/process/youtube")
def process_youtube_video(
request: Request, form_data: ProcessUrlForm, user=Depends(get_verified_user)
):
try:
collection_name = form_data.collection_name
if not collection_name:
collection_name = calculate_sha256_string(form_data.url)[:63]
loader = YoutubeLoader(
form_data.url,
language=request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
proxy_url=request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
log.debug(f"text_content: {content}")
save_docs_to_vector_db(
request, docs, collection_name, overwrite=True, user=user
)
return {
"status": True,
"collection_name": collection_name,
"filename": form_data.url,
"file": {
"data": {
"content": content,
},
"meta": {
"name": form_data.url,
},
},
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@router.post("/process/web")
def process_web(
request: Request, form_data: ProcessUrlForm, user=Depends(get_verified_user)
@ -1732,19 +1722,16 @@ def process_web(
if not collection_name:
collection_name = calculate_sha256_string(form_data.url)[:63]
loader = get_web_loader(
form_data.url,
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS,
)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
content, docs = get_content_from_url(request, form_data.url)
log.debug(f"text_content: {content}")
if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
save_docs_to_vector_db(
request, docs, collection_name, overwrite=True, user=user
request,
docs,
collection_name,
overwrite=True,
user=user,
)
else:
collection_name = None
@ -2047,7 +2034,7 @@ async def process_web_search(
result_items = []
try:
logging.info(
logging.debug(
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}"
)
@ -2081,6 +2068,12 @@ async def process_web_search(
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
)
if len(urls) == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.DEFAULT("No results found from web search"),
)
try:
if request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER:
search_results = [

View file

@ -356,7 +356,7 @@ async def join_note(sid, data):
await sio.enter_room(sid, f"note:{note.id}")
@sio.on("channel-events")
@sio.on("events:channel")
async def channel_events(sid, data):
room = f"channel:{data['channel_id']}"
participants = sio.manager.get_participants(
@ -373,7 +373,7 @@ async def channel_events(sid, data):
if event_type == "typing":
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": data["channel_id"],
"message_id": data.get("message_id", None),
@ -653,12 +653,15 @@ def get_event_emitter(request_info, update_db=True):
)
)
chat_id = request_info.get("chat_id", None)
message_id = request_info.get("message_id", None)
emit_tasks = [
sio.emit(
"chat-events",
"events",
{
"chat_id": request_info.get("chat_id", None),
"message_id": request_info.get("message_id", None),
"chat_id": chat_id,
"message_id": message_id,
"data": event_data,
},
to=session_id,
@ -667,8 +670,11 @@ def get_event_emitter(request_info, update_db=True):
]
await asyncio.gather(*emit_tasks)
if update_db:
if (
update_db
and message_id
and not request_info.get("chat_id", "").startswith("local:")
):
if "type" in event_data and event_data["type"] == "status":
Chats.add_message_status_to_chat_by_id_and_message_id(
request_info["chat_id"],
@ -764,7 +770,7 @@ def get_event_emitter(request_info, update_db=True):
def get_event_call(request_info):
async def __event_caller__(event_data):
response = await sio.call(
"chat-events",
"events",
{
"chat_id": request_info.get("chat_id", None),
"message_id": request_info.get("message_id", None),

View file

@ -164,7 +164,10 @@ async def stop_task(redis, task_id: str):
# Task successfully canceled
return {"status": True, "message": f"Task {task_id} successfully stopped."}
return {"status": False, "message": f"Failed to stop task {task_id}."}
if task.cancelled() or task.done():
return {"status": True, "message": f"Task {task_id} successfully cancelled."}
return {"status": True, "message": f"Cancellation requested for {task_id}."}
async def stop_item_tasks(redis: Redis, item_id: str):

View file

@ -6,7 +6,7 @@ import hmac
import hashlib
import requests
import os
import bcrypt
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import ed25519
@ -38,11 +38,8 @@ from open_webui.env import (
from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from passlib.context import CryptContext
logging.getLogger("passlib").setLevel(logging.ERROR)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OAUTH"])
@ -155,17 +152,23 @@ def get_license_data(app, key):
bearer_security = HTTPBearer(auto_error=False)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
def get_password_hash(password: str) -> str:
"""Hash a password using bcrypt"""
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
return (
pwd_context.verify(plain_password, hashed_password) if hashed_password else None
bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8"),
)
if hashed_password
else None
)
def get_password_hash(password):
return pwd_context.hash(password)
def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str:

View file

@ -80,6 +80,7 @@ async def generate_direct_chat_completion(
event_caller = get_event_call(metadata)
channel = f"{user_id}:{session_id}:{request_id}"
logging.info(f"WebSocket channel: {channel}")
if form_data.get("stream"):
q = asyncio.Queue()
@ -121,7 +122,10 @@ async def generate_direct_chat_completion(
yield f"data: {json.dumps(data)}\n\n"
elif isinstance(data, str):
yield data
if "data:" in data:
yield f"{data}\n\n"
else:
yield f"data: {data}\n\n"
except Exception as e:
log.debug(f"Error in event generator: {e}")
pass

View file

@ -40,7 +40,10 @@ from open_webui.routers.tasks import (
generate_image_prompt,
generate_chat_tags,
)
from open_webui.routers.retrieval import process_web_search, SearchForm
from open_webui.routers.retrieval import (
process_web_search,
SearchForm,
)
from open_webui.routers.images import (
load_b64_image_data,
image_generations,
@ -76,14 +79,17 @@ from open_webui.utils.task import (
)
from open_webui.utils.misc import (
deep_update,
extract_urls,
get_message_list,
add_or_update_system_message,
add_or_update_user_message,
get_last_user_message,
get_last_user_message_item,
get_last_assistant_message,
get_system_message,
prepend_to_first_user_message_content,
convert_logit_bias_input_to_json,
get_content_from_message,
)
from open_webui.utils.tools import get_tools
from open_webui.utils.plugin import load_function_module_by_id
@ -147,7 +153,7 @@ def process_tool_result(
if isinstance(tool_result, HTMLResponse):
content_disposition = tool_result.headers.get("Content-Disposition", "")
if "inline" in content_disposition:
content = tool_result.body.decode("utf-8")
content = tool_result.body.decode("utf-8", "replace")
tool_result_embeds.append(content)
if 200 <= tool_result.status_code < 300:
@ -175,7 +181,7 @@ def process_tool_result(
"message": f"{tool_function_name}: Unexpected status code {tool_result.status_code} from embedded UI result.",
}
else:
tool_result = tool_result.body.decode("utf-8")
tool_result = tool_result.body.decode("utf-8", "replace")
elif (tool_type == "external" and isinstance(tool_result, tuple)) or (
direct_tool and isinstance(tool_result, list) and len(tool_result) == 2
@ -283,7 +289,7 @@ async def chat_completion_tools_handler(
content = None
if hasattr(response, "body_iterator"):
async for chunk in response.body_iterator:
data = json.loads(chunk.decode("utf-8"))
data = json.loads(chunk.decode("utf-8", "replace"))
content = data["choices"][0]["message"]["content"]
# Cleanup any remaining background tasks if necessary
@ -298,7 +304,7 @@ async def chat_completion_tools_handler(
recent_messages = messages[-4:] if len(messages) > 4 else messages
chat_history = "\n".join(
f"{message['role'].upper()}: \"\"\"{message['content']}\"\"\""
f"{message['role'].upper()}: \"\"\"{get_content_from_message(message)}\"\"\""
for message in recent_messages
)
@ -821,7 +827,11 @@ async def chat_completion_files_handler(
if files := body.get("metadata", {}).get("files", None):
# Check if all files are in full context mode
all_full_context = all(item.get("context") == "full" for item in files)
all_full_context = all(
item.get("context") == "full"
for item in files
if item.get("type") == "file"
)
queries = []
if not all_full_context:
@ -853,10 +863,6 @@ async def chat_completion_files_handler(
except:
pass
if len(queries) == 0:
queries = [get_last_user_message(body["messages"])]
if not all_full_context:
await __event_emitter__(
{
"type": "status",
@ -868,6 +874,9 @@ async def chat_completion_files_handler(
}
)
if len(queries) == 0:
queries = [get_last_user_message(body["messages"])]
try:
# Offload get_sources_from_items to a separate thread
loop = asyncio.get_running_loop()
@ -906,7 +915,6 @@ async def chat_completion_files_handler(
log.debug(f"rag_contexts:sources: {sources}")
unique_ids = set()
for source in sources or []:
if not source or len(source.keys()) == 0:
continue
@ -925,7 +933,6 @@ async def chat_completion_files_handler(
unique_ids.add(_id)
sources_count = len(unique_ids)
await __event_emitter__(
{
"type": "status",
@ -999,11 +1006,11 @@ async def process_chat_payload(request, form_data, user, metadata, model):
log.debug(f"form_data: {form_data}")
system_message = get_system_message(form_data.get("messages", []))
if system_message:
if system_message: # Chat Controls/User Settings
try:
form_data = apply_system_prompt_to_body(
system_message.get("content"), form_data, metadata, user
)
system_message.get("content"), form_data, metadata, user, replace=True
) # Required to handle system prompt variables
except:
pass
@ -1168,8 +1175,28 @@ async def process_chat_payload(request, form_data, user, metadata, model):
tool_ids = form_data.pop("tool_ids", None)
files = form_data.pop("files", None)
# Remove files duplicates
prompt = get_last_user_message(form_data["messages"])
# TODO: re-enable URL extraction from prompt
# urls = []
# if prompt and len(prompt or "") < 500 and (not files or len(files) == 0):
# urls = extract_urls(prompt)
if files:
if not files:
files = []
for file_item in files:
if file_item.get("type", "file") == "folder":
# Get folder files
folder_id = file_item.get("id", None)
if folder_id:
folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id)
if folder and folder.data and "files" in folder.data:
files = [f for f in files if f.get("id", None) != folder_id]
files = [*files, *folder.data["files"]]
# files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]]
# Remove duplicate files based on their content
files = list({json.dumps(f, sort_keys=True): f for f in files}.values())
metadata = {
@ -1261,9 +1288,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
def make_tool_function(client, function_name):
async def tool_function(**kwargs):
print(kwargs)
print(client)
print(await client.list_tool_specs())
return await client.call_tool(
function_name,
function_args=kwargs,
@ -1370,8 +1394,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
)
context_string = context_string.strip()
prompt = get_last_user_message(form_data["messages"])
if prompt is None:
raise Exception("No user message found")
@ -1410,10 +1432,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
}
)
print("Final form_data:", form_data)
print("Final metadata:", metadata)
print("Final events:", events)
return form_data, metadata, events
@ -1421,10 +1439,13 @@ async def process_chat_response(
request, response, form_data, user, metadata, model, events, tasks
):
async def background_tasks_handler():
message = None
messages = []
if "chat_id" in metadata and not metadata["chat_id"].startswith("local:"):
messages_map = Chats.get_messages_map_by_chat_id(metadata["chat_id"])
message = messages_map.get(metadata["message_id"]) if messages_map else None
if message:
message_list = get_message_list(messages_map, metadata["message_id"])
# Remove details tags and files from the messages.
@ -1457,7 +1478,14 @@ async def process_chat_response(
"content": content,
}
)
else:
# Local temp chat, get the model and message from the form_data
message = get_last_user_message_item(form_data.get("messages", []))
messages = form_data.get("messages", [])
if message:
message["model"] = form_data.get("model")
if message and "model" in message:
if tasks and messages:
if (
TASKS.FOLLOW_UP_GENERATION in tasks
@ -1476,10 +1504,12 @@ async def process_chat_response(
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
follow_ups_string = (
res.get("choices", [])[0]
.get("message", {})
.get("content", "")
response_message = res.get("choices", [])[0].get(
"message", {}
)
follow_ups_string = response_message.get(
"content", response_message.get("reasoning_content", "")
)
else:
follow_ups_string = ""
@ -1493,15 +1523,6 @@ async def process_chat_response(
follow_ups = json.loads(follow_ups_string).get(
"follow_ups", []
)
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"followUps": follow_ups,
},
)
await event_emitter(
{
"type": "chat:message:follow_ups",
@ -1510,10 +1531,26 @@ async def process_chat_response(
},
}
)
if not metadata.get("chat_id", "").startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"followUps": follow_ups,
},
)
except Exception as e:
pass
if TASKS.TITLE_GENERATION in tasks:
if not metadata.get("chat_id", "").startswith(
"local:"
): # Only update titles and tags for non-temp chats
if (
TASKS.TITLE_GENERATION in tasks
and tasks[TASKS.TITLE_GENERATION]
):
user_message = get_last_user_message(messages)
if user_message and len(user_message) > 100:
user_message = user_message[:100] + "..."
@ -1532,12 +1569,16 @@ async def process_chat_response(
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
title_string = (
res.get("choices", [])[0]
.get("message", {})
.get(
"content", message.get("content", user_message)
response_message = res.get("choices", [])[0].get(
"message", {}
)
title_string = response_message.get(
"content",
response_message.get(
"reasoning_content",
message.get("content", user_message),
),
)
else:
title_string = ""
@ -1556,7 +1597,9 @@ async def process_chat_response(
if not title:
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
)
await event_emitter(
{
@ -1589,10 +1632,13 @@ async def process_chat_response(
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
tags_string = (
res.get("choices", [])[0]
.get("message", {})
.get("content", "")
response_message = res.get("choices", [])[0].get(
"message", {}
)
tags_string = response_message.get(
"content",
response_message.get("reasoning_content", ""),
)
else:
tags_string = ""
@ -1642,7 +1688,9 @@ async def process_chat_response(
response.body, bytes
):
try:
response_data = json.loads(response.body.decode("utf-8"))
response_data = json.loads(
response.body.decode("utf-8", "replace")
)
except json.JSONDecodeError:
response_data = {
"error": {"detail": "Invalid JSON response"}
@ -2276,7 +2324,11 @@ async def process_chat_response(
last_delta_data = None
async for line in response.body_iterator:
line = line.decode("utf-8") if isinstance(line, bytes) else line
line = (
line.decode("utf-8", "replace")
if isinstance(line, bytes)
else line
)
data = line
# Skip empty lines

View file

@ -136,6 +136,14 @@ def update_message_content(message: dict, content: str, append: bool = True) ->
return message
def replace_system_message_content(content: str, messages: list[dict]) -> dict:
for message in messages:
if message["role"] == "system":
message["content"] = content
break
return messages
def add_or_update_system_message(
content: str, messages: list[dict], append: bool = False
):
@ -523,3 +531,11 @@ def throttle(interval: float = 10.0):
return wrapper
return decorator
def extract_urls(text: str) -> list[str]:
# Regex pattern to match URLs
url_pattern = re.compile(
r"(https?://[^\s]+)", re.IGNORECASE
) # Matches http and https URLs
return url_pattern.findall(text)

View file

@ -62,6 +62,7 @@ from open_webui.env import (
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
ENABLE_OAUTH_ID_TOKEN_COOKIE,
ENABLE_OAUTH_EMAIL_FALLBACK,
OAUTH_CLIENT_INFO_ENCRYPTION_KEY,
)
from open_webui.utils.misc import parse_duration
@ -82,6 +83,8 @@ class OAuthClientInformationFull(OAuthClientMetadata):
client_id_issued_at: int | None = None
client_secret_expires_at: int | None = None
server_metadata: Optional[OAuthMetadata] = None # Fetched from the OAuth server
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@ -296,6 +299,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
{
**registration_response_json,
**{"issuer": oauth_server_metadata_url},
**{"server_metadata": oauth_server_metadata},
}
)
log.info(
@ -331,20 +335,34 @@ class OAuthClientManager:
self.clients = {}
def add_client(self, client_id, oauth_client_info: OAuthClientInformationFull):
self.clients[client_id] = {
"client": self.oauth.register(
name=client_id,
client_id=oauth_client_info.client_id,
client_secret=oauth_client_info.client_secret,
client_kwargs=(
{"scope": oauth_client_info.scope}
if oauth_client_info.scope
else {}
kwargs = {
"name": client_id,
"client_id": oauth_client_info.client_id,
"client_secret": oauth_client_info.client_secret,
"client_kwargs": (
{"scope": oauth_client_info.scope} if oauth_client_info.scope else {}
),
server_metadata_url=(
"server_metadata_url": (
oauth_client_info.issuer if oauth_client_info.issuer else None
),
),
}
if (
oauth_client_info.server_metadata
and oauth_client_info.server_metadata.code_challenge_methods_supported
):
if (
isinstance(
oauth_client_info.server_metadata.code_challenge_methods_supported,
list,
)
and "S256"
in oauth_client_info.server_metadata.code_challenge_methods_supported
):
kwargs["code_challenge_method"] = "S256"
self.clients[client_id] = {
"client": self.oauth.register(**kwargs),
"client_info": oauth_client_info,
}
return self.clients[client_id]
@ -367,8 +385,8 @@ class OAuthClientManager:
if client_id in self.clients:
client = self.clients[client_id]
return (
client.server_metadata_url
if hasattr(client, "server_metadata_url")
client._server_metadata_url
if hasattr(client, "_server_metadata_url")
else None
)
return None
@ -560,7 +578,17 @@ class OAuthClientManager:
error_message = None
try:
token = await client.authorize_access_token(request)
client_info = self.get_client_info(client_id)
token_params = {}
if (
client_info
and hasattr(client_info, "client_id")
and hasattr(client_info, "client_secret")
):
token_params["client_id"] = client_info.client_id
token_params["client_secret"] = client_info.client_secret
token = await client.authorize_access_token(request, **token_params)
if token:
try:
# Add timestamp for tracking
@ -615,8 +643,14 @@ class OAuthManager:
self.app = app
self._clients = {}
for _, provider_config in OAUTH_PROVIDERS.items():
provider_config["register"](self.oauth)
for name, provider_config in OAUTH_PROVIDERS.items():
if "register" not in provider_config:
log.error(f"OAuth provider {name} missing register function")
continue
client = provider_config["register"](self.oauth)
self._clients[name] = client
def get_client(self, provider_name):
if provider_name not in self._clients:
@ -627,8 +661,8 @@ class OAuthManager:
if provider_name in self._clients:
client = self._clients[provider_name]
return (
client.server_metadata_url
if hasattr(client, "server_metadata_url")
client._server_metadata_url
if hasattr(client, "_server_metadata_url")
else None
)
return None
@ -1147,6 +1181,8 @@ class OAuthManager:
except Exception as e:
log.warning(f"Error fetching GitHub email: {e}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
elif ENABLE_OAUTH_EMAIL_FALLBACK:
email = f"{provider_sub}.local"
else:
log.warning(f"OAuth callback failed, email is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)

View file

@ -2,6 +2,7 @@ from open_webui.utils.task import prompt_template, prompt_variables_template
from open_webui.utils.misc import (
deep_update,
add_or_update_system_message,
replace_system_message_content,
)
from typing import Callable, Optional
@ -10,7 +11,11 @@ import json
# inplace function: form_data is modified
def apply_system_prompt_to_body(
system: Optional[str], form_data: dict, metadata: Optional[dict] = None, user=None
system: Optional[str],
form_data: dict,
metadata: Optional[dict] = None,
user=None,
replace: bool = False,
) -> dict:
if not system:
return form_data
@ -24,9 +29,15 @@ def apply_system_prompt_to_body(
# Legacy (API Usage)
system = prompt_template(system, user)
if replace:
form_data["messages"] = replace_system_message_content(
system, form_data.get("messages", [])
)
else:
form_data["messages"] = add_or_update_system_message(
system, form_data.get("messages", [])
)
return form_data

View file

@ -748,10 +748,6 @@ async def execute_tool_server(
if operation.get("requestBody", {}).get("content"):
if params:
body_params = params
else:
raise Exception(
f"Request body expected for operation '{name}' but none found."
)
async with aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)

View file

@ -1,14 +1,13 @@
fastapi==0.115.7
uvicorn[standard]==0.35.0
pydantic==2.11.7
fastapi==0.118.0
uvicorn[standard]==0.37.0
pydantic==2.11.9
python-multipart==0.0.20
itsdangerous==2.2.0
python-socketio==5.13.0
python-jose==3.4.0
passlib[bcrypt]==1.7.4
cryptography
bcrypt==4.3.0
bcrypt==5.0.0
argon2-cffi==25.1.0
PyJWT[crypto]==2.10.1
authlib==1.6.3
@ -30,14 +29,6 @@ peewee-migrate==1.12.2
pycrdt==0.12.25
redis
pymongo
psycopg2-binary==2.9.10
pgvector==0.4.1
PyMySQL==1.1.1
boto3==1.40.5
APScheduler==3.10.4
RestrictedPython==8.0
@ -57,25 +48,15 @@ langchain==0.3.27
langchain-community==0.3.29
fake-useragent==2.2.0
chromadb==1.0.20
chromadb==1.1.0
opensearch-py==2.8.0
pymilvus==2.5.0
qdrant-client==1.14.3
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
elasticsearch==9.1.0
pinecone==6.0.2
oracledb==3.2.0
av==14.0.1 # Caution: Set due to FATAL FIPS SELFTEST FAILURE, see discussion https://github.com/open-webui/open-webui/discussions/15720
transformers
sentence-transformers==5.1.1
accelerate
pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897
einops==0.8.1
colbert-ai==0.2.21
ftfy==6.2.3
pypdf==6.0.0
fpdf2==2.8.2
@ -84,7 +65,7 @@ docx2txt==0.8
python-pptx==1.0.2
unstructured==0.16.17
nltk==3.9.1
Markdown==3.8.2
Markdown==3.9
pypandoc==1.15
pandas==2.2.3
openpyxl==3.1.5
@ -105,7 +86,7 @@ onnxruntime==1.20.1
faster-whisper==1.1.1
black==25.1.0
black==25.9.0
youtube-transcript-api==1.2.2
pytube==15.0.0
@ -117,11 +98,6 @@ google-api-python-client
google-auth-httplib2
google-auth-oauthlib
## Tests
docker~=7.1.0
pytest~=8.4.1
pytest-docker~=3.1.1
googleapis-common-protos==1.70.0
google-cloud-storage==2.19.0
@ -129,24 +105,45 @@ azure-identity==1.25.0
azure-storage-blob==12.24.1
pymongo
psycopg2-binary==2.9.10
pgvector==0.4.1
PyMySQL==1.1.1
boto3==1.40.5
pymilvus==2.6.2
qdrant-client==1.14.3
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
elasticsearch==9.1.0
pinecone==6.0.2
oracledb==3.2.0
av==14.0.1 # Caution: Set due to FATAL FIPS SELFTEST FAILURE, see discussion https://github.com/open-webui/open-webui/discussions/15720
colbert-ai==0.2.21
## Tests
docker~=7.1.0
pytest~=8.4.1
pytest-docker~=3.1.1
## LDAP
ldap3==2.9.1
## Firecrawl
firecrawl-py==1.12.0
# Sougou API SDK(Tencentcloud SDK)
tencentcloud-sdk-python==3.0.1336
## Trace
opentelemetry-api==1.36.0
opentelemetry-sdk==1.36.0
opentelemetry-exporter-otlp==1.36.0
opentelemetry-instrumentation==0.57b0
opentelemetry-instrumentation-fastapi==0.57b0
opentelemetry-instrumentation-sqlalchemy==0.57b0
opentelemetry-instrumentation-redis==0.57b0
opentelemetry-instrumentation-requests==0.57b0
opentelemetry-instrumentation-logging==0.57b0
opentelemetry-instrumentation-httpx==0.57b0
opentelemetry-instrumentation-aiohttp-client==0.57b0
opentelemetry-api==1.37.0
opentelemetry-sdk==1.37.0
opentelemetry-exporter-otlp==1.37.0
opentelemetry-instrumentation==0.58b0
opentelemetry-instrumentation-fastapi==0.58b0
opentelemetry-instrumentation-sqlalchemy==0.58b0
opentelemetry-instrumentation-redis==0.58b0
opentelemetry-instrumentation-requests==0.58b0
opentelemetry-instrumentation-logging==0.58b0
opentelemetry-instrumentation-httpx==0.58b0
opentelemetry-instrumentation-aiohttp-client==0.58b0

View file

@ -70,5 +70,18 @@ if [ -n "$SPACE_ID" ]; then
fi
PYTHON_CMD=$(command -v python3 || command -v python)
UVICORN_WORKERS="${UVICORN_WORKERS:-1}"
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' --workers "${UVICORN_WORKERS:-1}"
# If script is called with arguments, use them; otherwise use default workers
if [ "$#" -gt 0 ]; then
ARGS=("$@")
else
ARGS=(--workers "$UVICORN_WORKERS")
fi
# Run uvicorn
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app \
--host "$HOST" \
--port "$PORT" \
--forwarded-allow-ips '*' \
"${ARGS[@]}"

681
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.6.32",
"version": "0.6.33",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.6.32",
"version": "0.6.33",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
@ -93,6 +93,8 @@
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.3.0",
"uuid": "^9.0.1",
"vega": "^6.2.0",
"vega-lite": "^6.4.1",
"vite-plugin-static-copy": "^2.2.0",
"y-prosemirror": "^1.3.7",
"yaml": "^2.7.1",
@ -5592,6 +5594,99 @@
"node": ">=8"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@ -6346,6 +6441,36 @@
"node": ">=12"
}
},
"node_modules/d3-geo-projection": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz",
"integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==",
"license": "ISC",
"dependencies": {
"commander": "7",
"d3-array": "1 - 3",
"d3-geo": "1.12.0 - 3"
},
"bin": {
"geo2svg": "bin/geo2svg.js",
"geograticule": "bin/geograticule.js",
"geoproject": "bin/geoproject.js",
"geoquantize": "bin/geoquantize.js",
"geostitch": "bin/geostitch.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-geo-projection/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/d3-hierarchy": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
@ -7038,6 +7163,15 @@
"@esbuild/win32-x64": "0.25.1"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -7750,6 +7884,27 @@
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz",
"integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A=="
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
@ -8763,6 +8918,12 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@ -12770,6 +12931,26 @@
"node": ">=10.13.0"
}
},
"node_modules/topojson-client": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
"license": "ISC",
"dependencies": {
"commander": "2"
},
"bin": {
"topo2geo": "bin/topo2geo",
"topomerge": "bin/topomerge",
"topoquantize": "bin/topoquantize"
}
},
"node_modules/topojson-client/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@ -13047,6 +13228,417 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/vega": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vega/-/vega-6.2.0.tgz",
"integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-crossfilter": "~5.1.0",
"vega-dataflow": "~6.1.0",
"vega-encode": "~5.1.0",
"vega-event-selector": "~4.0.0",
"vega-expression": "~6.1.0",
"vega-force": "~5.1.0",
"vega-format": "~2.1.0",
"vega-functions": "~6.1.0",
"vega-geo": "~5.1.0",
"vega-hierarchy": "~5.1.0",
"vega-label": "~2.1.0",
"vega-loader": "~5.1.0",
"vega-parser": "~7.1.0",
"vega-projection": "~2.1.0",
"vega-regression": "~2.1.0",
"vega-runtime": "~7.1.0",
"vega-scale": "~8.1.0",
"vega-scenegraph": "~5.1.0",
"vega-statistics": "~2.0.0",
"vega-time": "~3.1.0",
"vega-transforms": "~5.1.0",
"vega-typings": "~2.1.0",
"vega-util": "~2.1.0",
"vega-view": "~6.1.0",
"vega-view-transforms": "~5.1.0",
"vega-voronoi": "~5.1.0",
"vega-wordcloud": "~5.1.0"
},
"funding": {
"url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
}
},
"node_modules/vega-canvas": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz",
"integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==",
"license": "BSD-3-Clause"
},
"node_modules/vega-crossfilter": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz",
"integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"vega-dataflow": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-dataflow": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.0.tgz",
"integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-format": "^2.1.0",
"vega-loader": "^5.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-encode": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.1.0.tgz",
"integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-interpolate": "^3.0.1",
"vega-dataflow": "^6.1.0",
"vega-scale": "^8.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-event-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz",
"integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==",
"license": "BSD-3-Clause"
},
"node_modules/vega-expression": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz",
"integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/estree": "^1.0.8",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-expression/node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/vega-force": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.0.tgz",
"integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-force": "^3.0.0",
"vega-dataflow": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.0.tgz",
"integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-format": "^3.1.0",
"d3-time-format": "^4.1.0",
"vega-time": "^3.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-functions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.1.0.tgz",
"integrity": "sha512-yooEbWt0FWMBNoohwLsl25lEh08WsWabTXbbS+q0IXZzWSpX4Cyi45+q7IFyy/2L4oaIfGIIV14dgn3srQQcGA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-color": "^3.1.0",
"d3-geo": "^3.1.1",
"vega-dataflow": "^6.1.0",
"vega-expression": "^6.1.0",
"vega-scale": "^8.1.0",
"vega-scenegraph": "^5.1.0",
"vega-selections": "^6.1.0",
"vega-statistics": "^2.0.0",
"vega-time": "^3.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-geo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.0.tgz",
"integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-color": "^3.1.0",
"d3-geo": "^3.1.1",
"vega-canvas": "^2.0.0",
"vega-dataflow": "^6.1.0",
"vega-projection": "^2.1.0",
"vega-statistics": "^2.0.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-hierarchy": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz",
"integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-hierarchy": "^3.1.2",
"vega-dataflow": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-label": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.0.tgz",
"integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-canvas": "^2.0.0",
"vega-dataflow": "^6.1.0",
"vega-scenegraph": "^5.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-lite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.1.tgz",
"integrity": "sha512-KO3ybHNouRK4A0al/+2fN9UqgTEfxrd/ntGLY933Hg5UOYotDVQdshR3zn7OfXwQ7uj0W96Vfa5R+QxO8am3IQ==",
"license": "BSD-3-Clause",
"dependencies": {
"json-stringify-pretty-compact": "~4.0.0",
"tslib": "~2.8.1",
"vega-event-selector": "~4.0.0",
"vega-expression": "~6.1.0",
"vega-util": "~2.1.0",
"yargs": "~18.0.0"
},
"bin": {
"vl2pdf": "bin/vl2pdf",
"vl2png": "bin/vl2png",
"vl2svg": "bin/vl2svg",
"vl2vg": "bin/vl2vg"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
},
"peerDependencies": {
"vega": "^6.0.0"
}
},
"node_modules/vega-loader": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.0.tgz",
"integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dsv": "^3.0.1",
"topojson-client": "^3.1.0",
"vega-format": "^2.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-parser": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.0.tgz",
"integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-dataflow": "^6.1.0",
"vega-event-selector": "^4.0.0",
"vega-functions": "^6.1.0",
"vega-scale": "^8.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-projection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.0.tgz",
"integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-geo": "^3.1.1",
"d3-geo-projection": "^4.0.0",
"vega-scale": "^8.1.0"
}
},
"node_modules/vega-regression": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.0.tgz",
"integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"vega-dataflow": "^6.1.0",
"vega-statistics": "^2.0.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-runtime": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.0.tgz",
"integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-dataflow": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-scale": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.0.tgz",
"integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.1.0",
"vega-time": "^3.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-scenegraph": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz",
"integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-path": "^3.1.0",
"d3-shape": "^3.2.0",
"vega-canvas": "^2.0.0",
"vega-loader": "^5.1.0",
"vega-scale": "^8.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-selections": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.0.tgz",
"integrity": "sha512-WaHM7D7ghHceEfMsgFeaZnDToWL0mgCFtStVOobNh/OJLh0CL7yNKeKQBqRXJv2Lx74dPNf6nj08+52ytWfW7g==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "3.2.4",
"vega-expression": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-statistics": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz",
"integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4"
}
},
"node_modules/vega-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.1.0.tgz",
"integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-time": "^3.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-transforms": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.1.0.tgz",
"integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"vega-dataflow": "^6.1.0",
"vega-statistics": "^2.0.0",
"vega-time": "^3.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-typings": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.1.0.tgz",
"integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/geojson": "7946.0.16",
"vega-event-selector": "^4.0.0",
"vega-expression": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-util": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.0.tgz",
"integrity": "sha512-PGfp0m0QCufDmcxKJCWQy4Ov23FoF8DSXmoJwSezi3itQaa2hbxK0+xwsTMP2vy4PR16Pu25HMzgMwXVW1+33w==",
"license": "BSD-3-Clause"
},
"node_modules/vega-view": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.1.0.tgz",
"integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^3.2.4",
"d3-timer": "^3.0.1",
"vega-dataflow": "^6.1.0",
"vega-format": "^2.1.0",
"vega-functions": "^6.1.0",
"vega-runtime": "^7.1.0",
"vega-scenegraph": "^5.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-view-transforms": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz",
"integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-dataflow": "^6.1.0",
"vega-scenegraph": "^5.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-voronoi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.0.tgz",
"integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-delaunay": "^6.0.4",
"vega-dataflow": "^6.1.0",
"vega-util": "^2.1.0"
}
},
"node_modules/vega-wordcloud": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz",
"integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==",
"license": "BSD-3-Clause",
"dependencies": {
"vega-canvas": "^2.0.0",
"vega-dataflow": "^6.1.0",
"vega-scale": "^8.1.0",
"vega-statistics": "^2.0.0",
"vega-util": "^2.1.0"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@ -14209,6 +14801,15 @@
"yjs": "^13.0.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@ -14230,6 +14831,82 @@
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.32",
"version": "0.6.33",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@ -137,6 +137,8 @@
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.3.0",
"uuid": "^9.0.1",
"vega": "^6.2.0",
"vega-lite": "^6.4.1",
"vite-plugin-static-copy": "^2.2.0",
"y-prosemirror": "^1.3.7",
"yaml": "^2.7.1",

View file

@ -6,17 +6,16 @@ authors = [
]
license = { file = "LICENSE" }
dependencies = [
"fastapi==0.115.7",
"uvicorn[standard]==0.35.0",
"pydantic==2.11.7",
"fastapi==0.118.0",
"uvicorn[standard]==0.37.0",
"pydantic==2.11.9",
"python-multipart==0.0.20",
"itsdangerous==2.2.0",
"python-socketio==5.13.0",
"python-jose==3.4.0",
"passlib[bcrypt]==1.7.4",
"cryptography",
"bcrypt==4.3.0",
"bcrypt==5.0.0",
"argon2-cffi==25.1.0",
"PyJWT[crypto]==2.10.1",
"authlib==1.6.3",
@ -76,7 +75,7 @@ dependencies = [
"python-pptx==1.0.2",
"unstructured==0.16.17",
"nltk==3.9.1",
"Markdown==3.8.2",
"Markdown==3.9",
"pypandoc==1.15",
"pandas==2.2.3",
"openpyxl==3.1.5",
@ -96,8 +95,8 @@ dependencies = [
"onnxruntime==1.20.1",
"faster-whisper==1.1.1",
"black==25.1.0",
"youtube-transcript-api==1.1.0",
"black==25.9.0",
"youtube-transcript-api==1.2.2",
"pytube==15.0.0",
"pydub",
@ -107,8 +106,6 @@ dependencies = [
"google-auth-httplib2",
"google-auth-oauthlib",
"googleapis-common-protos==1.70.0",
"google-cloud-storage==2.19.0",
@ -116,12 +113,6 @@ dependencies = [
"azure-storage-blob==12.24.1",
"ldap3==2.9.1",
"firecrawl-py==1.12.0",
"tencentcloud-sdk-python==3.0.1336",
"oracledb>=3.2.0",
]
readme = "README.md"
requires-python = ">= 3.11, < 3.13.0a1"
@ -155,11 +146,14 @@ all = [
"elasticsearch==9.1.0",
"qdrant-client==1.14.3",
"pymilvus==2.5.0",
"pymilvus==2.6.2",
"pinecone==6.0.2",
"oracledb==3.2.0",
"colbert-ai==0.2.21",
"firecrawl-py==1.12.0",
"tencentcloud-sdk-python==3.0.1336",
]
[project.scripts]

View file

@ -62,6 +62,37 @@ export const getFunctions = async (token: string = '') => {
return res;
};
export const getFunctionList = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/list`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const loadFunctionByUrl = async (token: string = '', url: string) => {
let error = null;

View file

@ -17,6 +17,7 @@
import Tags from './common/Tags.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import Textarea from './common/Textarea.svelte';
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
@ -42,6 +43,8 @@
let enable = true;
let apiVersion = '';
let headers = '';
let tags = [];
let modelId = '';
@ -69,6 +72,19 @@
// remove trailing slash from url
url = url.replace(/\/$/, '');
if (headers) {
try {
const _headers = JSON.parse(headers);
if (typeof _headers !== 'object' || Array.isArray(_headers)) {
throw new Error('Headers must be a valid JSON object');
}
headers = JSON.stringify(_headers, null, 2);
} catch (error) {
toast.error($i18n.t('Headers must be a valid JSON object'));
return;
}
}
const res = await verifyOpenAIConnection(
localStorage.token,
{
@ -77,7 +93,8 @@
config: {
auth_type,
azure: azure,
api_version: apiVersion
api_version: apiVersion,
headers: JSON.parse(headers)
}
},
direct
@ -136,6 +153,19 @@
}
}
if (headers) {
try {
const _headers = JSON.parse(headers);
if (typeof _headers !== 'object' || Array.isArray(_headers)) {
throw new Error('Headers must be a valid JSON object');
}
headers = JSON.stringify(_headers, null, 2);
} catch (error) {
toast.error($i18n.t('Headers must be a valid JSON object'));
return;
}
}
// remove trailing slash from url
url = url.replace(/\/$/, '');
@ -149,6 +179,7 @@
model_ids: modelIds,
connection_type: connectionType,
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
...(!ollama && azure ? { azure: true, api_version: apiVersion } : {})
}
};
@ -172,6 +203,9 @@
key = connection.key;
auth_type = connection.config.auth_type ?? 'bearer';
headers = connection.config?.headers
? JSON.stringify(connection.config.headers, null, 2)
: '';
enable = connection.config?.enable ?? true;
tags = connection.config?.tags ?? [];
@ -376,6 +410,35 @@
</div>
</div>
{#if !ollama && !direct}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="headers-input"
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>{$i18n.t('Headers')}</label
>
<div class="flex-1">
<Tooltip
content={$i18n.t(
'Enter additional headers in JSON format (e.g. {{\'{{"X-Custom-Header": "value"}}\'}})'
)}
>
<Textarea
className="w-full text-sm outline-hidden"
bind:value={headers}
placeholder={$i18n.t('Enter additional headers in JSON format')}
required={false}
minSize={30}
/>
</Tooltip>
</div>
</div>
</div>
{/if}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label

View file

@ -98,10 +98,17 @@
return;
}
if (path === '') {
if (['openapi', ''].includes(type)) {
if (spec_type === 'json' && spec === '') {
toast.error($i18n.t('Please enter a valid JSON spec'));
return;
}
if (spec_type === 'url' && path === '') {
toast.error($i18n.t('Please enter a valid path'));
return;
}
}
if (direct) {
const res = await getToolServerData(

View file

@ -3,7 +3,7 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores';
import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
import { onMount, getContext, tick } from 'svelte';
import { goto } from '$app/navigation';
@ -12,6 +12,7 @@
deleteFunctionById,
exportFunctions,
getFunctionById,
getFunctionList,
getFunctions,
loadFunctionByUrl,
toggleFunctionById,
@ -36,6 +37,10 @@
import XMark from '../icons/XMark.svelte';
import AddFunctionMenu from './Functions/AddFunctionMenu.svelte';
import ImportModal from '../ImportModal.svelte';
import ViewSelector from '../workspace/common/ViewSelector.svelte';
import TagSelector from '../workspace/common/TagSelector.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n');
@ -44,12 +49,16 @@
let functionsImportInputElement: HTMLInputElement;
let importFiles;
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let query = '';
let selectedTag = '';
let selectedType = '';
let showImportModal = false;
let showConfirm = false;
let query = '';
let selectedType = 'all';
let showManifestModal = false;
let showValvesModal = false;
@ -57,17 +66,33 @@
let showDeleteConfirm = false;
let loaded = false;
let functions = null;
let filteredItems = [];
$: filteredItems = $functions
$: if (
functions &&
query !== undefined &&
selectedType !== undefined &&
viewOption !== undefined
) {
setFilteredItems();
}
const setFilteredItems = () => {
filteredItems = functions
.filter(
(f) =>
(selectedType !== 'all' ? f.type === selectedType : true) &&
(selectedType !== '' ? f.type === selectedType : true) &&
(query === '' ||
f.name.toLowerCase().includes(query.toLowerCase()) ||
f.id.toLowerCase().includes(query.toLowerCase()))
f.id.toLowerCase().includes(query.toLowerCase())) &&
(viewOption === '' ||
(viewOption === 'created' && f.user_id === $user?.id) ||
(viewOption === 'shared' && f.user_id !== $user?.id))
)
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
};
const shareHandler = async (func) => {
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(`${error}`);
@ -134,7 +159,7 @@
if (res) {
toast.success($i18n.t('Function deleted successfully'));
functions.set(await getFunctions(localStorage.token));
_functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
@ -162,7 +187,7 @@
: toast.success($i18n.t('Function is now globally disabled'));
}
functions.set(await getFunctions(localStorage.token));
_functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
@ -174,7 +199,16 @@
}
};
onMount(() => {
onMount(async () => {
viewOption = localStorage?.workspaceViewOption || '';
functions = await getFunctionList(localStorage.token).catch((error) => {
toast.error(`${error}`);
return [];
});
await tick();
loaded = true;
const onKeyDown = (event) => {
if (event.key === 'Shift') {
shiftKey = true;
@ -222,16 +256,95 @@
}}
/>
<div class="flex flex-col mt-1.5 mb-0.5 px-[16px]">
<div class="flex justify-between items-center mb-1">
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
{#if loaded}
<div class="px-4.5 w-full">
<div class="flex flex-col gap-1 px-1 mt-2.5 mb-2">
<div class="flex justify-between items-center mb-1 w-full">
<input
id="documents-import-input"
bind:this={functionsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<div class="flex justify-between items-center w-full">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Functions')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-base font-lg text-gray-500 dark:text-gray-300">{filteredItems.length}</span>
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{filteredItems.length}
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex w-full justify-end gap-1.5">
{#if $user?.role === 'admin'}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={() => {
functionsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if functions.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={async () => {
const _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
<AddFunctionMenu
createHandler={() => {
goto('/admin/functions/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
}}
>
<div
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Function')}</div>
</div>
</AddFunctionMenu>
</div>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
@ -255,69 +368,43 @@
</div>
{/if}
</div>
</div>
<div>
<AddFunctionMenu
createHandler={() => {
goto('/admin/functions/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
<div
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-0.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<Plus className="size-3.5" />
</div>
</AddFunctionMenu>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
await tick();
}}
/>
<TagSelector
bind:value={selectedType}
items={[
{ value: 'pipe', label: $i18n.t('Pipe') },
{ value: 'filter', label: $i18n.t('Filter') },
{ value: 'action', label: $i18n.t('Action') }
]}
/>
</div>
</div>
<div class=" flex w-full">
<div
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent"
>
<button
class="min-w-fit p-1.5 {selectedType === 'all'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'all';
}}>{$i18n.t('All')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'pipe'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'pipe';
}}>{$i18n.t('Pipe')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'filter'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'filter';
}}>{$i18n.t('Filter')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'action'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'action';
}}>{$i18n.t('Action')}</button
>
</div>
</div>
</div>
<div class="mb-5 px-[16px]">
{#if (filteredItems ?? []).length !== 0}
<div class="px-3 my-2 gap-1 lg:gap-2 grid lg:grid-cols-2">
{#each filteredItems as func (func.id)}
<div
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
@ -328,29 +415,39 @@
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5">
<Tooltip content={func.id} placement="top-start">
<div class=" flex items-center gap-1.5">
<div
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{func.type}
</div>
<div class="line-clamp-1 text-sm">
{func.name}
</div>
{#if func?.meta?.manifest?.version}
<div
class="text-xs font-semibold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
<div class=" text-gray-500 text-xs font-medium shrink-0">
v{func?.meta?.manifest?.version ?? ''}
</div>
{/if}
<div class=" line-clamp-1">
{func.name}
</div>
</div>
</Tooltip>
<div class="flex gap-1.5 px-1">
<div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div>
<div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={func?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
)
})}
</Tooltip>
</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{func.meta.description}
</div>
@ -475,6 +572,18 @@
</div>
{/each}
</div>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No functions found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</div>
</div>
{/if}
</div>
<!-- <div class=" text-gray-500 text-xs mt-1 mb-2">
{$i18n.t(
@ -482,87 +591,8 @@
)}
</div> -->
<div class=" flex justify-end w-full mb-2 px-[16px]">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:this={functionsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
functionsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Functions')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{#if $functions.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
const _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Functions')} ({$functions.length})
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div>
</div>
{#if $config?.features.enable_community_sharing}
<div class=" my-16 px-[16px]">
<div class=" my-16">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by Open WebUI Community')}
</div>
@ -587,6 +617,7 @@
</a>
</div>
{/if}
</div>
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
@ -670,3 +701,8 @@
</div>
</div>
</ConfirmDialog>
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
{/if}

View file

@ -41,27 +41,27 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[190px] text-sm rounded-xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
sideOffset={-2}
class="w-full max-w-[190px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={6}
side="bottom"
align="start"
transition={flyAndScale}
>
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
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 w-full"
on:click={async () => {
createHandler();
show = false;
}}
>
<div class=" self-center mr-2">
<PencilSolid />
<Pencil />
</div>
<div class=" self-center truncate">{$i18n.t('New Function')}</div>
</button>
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
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 w-full"
on:click={async () => {
importFromLinkHandler();
show = false;

View file

@ -19,6 +19,7 @@
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
import Textarea from '$lib/components/common/Textarea.svelte';
const i18n = getContext<Writable<i18nType>>('i18n');
@ -31,6 +32,7 @@
let TTS_ENGINE = '';
let TTS_MODEL = '';
let TTS_VOICE = '';
let TTS_OPENAI_PARAMS = '';
let TTS_SPLIT_ON: TTS_RESPONSE_SPLIT = TTS_RESPONSE_SPLIT.PUNCTUATION;
let TTS_AZURE_SPEECH_REGION = '';
let TTS_AZURE_SPEECH_BASE_URL = '';
@ -98,18 +100,28 @@
};
const updateConfigHandler = async () => {
let openaiParams = {};
try {
openaiParams = TTS_OPENAI_PARAMS ? JSON.parse(TTS_OPENAI_PARAMS) : {};
TTS_OPENAI_PARAMS = JSON.stringify(openaiParams, null, 2);
} catch (e) {
toast.error($i18n.t('Invalid JSON format for Parameters'));
return;
}
const res = await updateAudioConfig(localStorage.token, {
tts: {
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
OPENAI_PARAMS: openaiParams,
API_KEY: TTS_API_KEY,
ENGINE: TTS_ENGINE,
MODEL: TTS_MODEL,
VOICE: TTS_VOICE,
SPLIT_ON: TTS_SPLIT_ON,
AZURE_SPEECH_REGION: TTS_AZURE_SPEECH_REGION,
AZURE_SPEECH_BASE_URL: TTS_AZURE_SPEECH_BASE_URL,
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT,
SPLIT_ON: TTS_SPLIT_ON
},
stt: {
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
@ -146,6 +158,7 @@
console.log(res);
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
TTS_OPENAI_PARAMS = JSON.stringify(res?.tts?.OPENAI_PARAMS ?? '', null, 2);
TTS_API_KEY = res.tts.API_KEY;
TTS_ENGINE = res.tts.ENGINE;
@ -612,6 +625,22 @@
</div>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Additional Parameters')}</div>
<div class="flex w-full">
<div class="flex-1">
<Textarea
className="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={TTS_OPENAI_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100}
/>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2">
<div class="w-full">

View file

@ -714,6 +714,21 @@
</div>
{/if}
{/if}
<div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-medium">
<Tooltip content={''} placement="top-start">
{$i18n.t('Parameters')}
</Tooltip>
</div>
<div class="">
<Textarea
bind:value={RAGConfig.DOCLING_PARAMETERS}
placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100}
/>
</div>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="my-0.5 flex gap-2 pr-2">
<input

View file

@ -36,11 +36,13 @@
let updateModelId = null;
let updateProgress = null;
let updateModelsControllers = {};
let updateCancelled = false;
let showExperimentalOllama = false;
const MAX_PARALLEL_DOWNLOADS = 3;
let modelTransferring = false;
let modelLoading = false;
let modelTag = '';
let createModelLoading = false;
@ -65,17 +67,31 @@
let deleteModelTag = '';
const updateModelsHandler = async () => {
updateCancelled = false;
toast.info('Checking for model updates...');
for (const model of ollamaModels) {
if (updateCancelled) {
break;
}
console.debug(model);
updateModelId = model.id;
const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch(
(error) => {
if (error.name !== 'AbortError') {
toast.error(`${error}`);
return null;
}
return [null, null];
}
);
updateModelsControllers = {
...updateModelsControllers,
[model.id]: controller
};
if (res) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
@ -108,19 +124,28 @@
} else {
updateProgress = 100;
}
} else {
toast.success(data.status);
}
}
}
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err);
}
break;
}
}
}
delete updateModelsControllers[model.id];
updateModelsControllers = { ...updateModelsControllers };
}
if (updateCancelled) {
toast.info('Model update cancelled');
} else {
toast.success('All models are up to date');
}
updateModelId = null;
updateProgress = null;
};
@ -143,10 +168,13 @@
return;
}
modelLoading = true;
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, urlIdx).catch(
(error) => {
if (error.name !== 'AbortError') {
toast.error(`${error}`);
return null;
}
return [null, null];
}
);
@ -202,8 +230,6 @@
}
});
} else {
toast.success(data.status);
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
@ -216,6 +242,7 @@
}
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err);
if (typeof err !== 'string') {
err = err.message;
@ -223,12 +250,15 @@
toast.error(`${err}`);
// opts.callback({ success: false, error, modelName: opts.modelName });
} else {
break;
}
}
}
console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]?.done) {
toast.success(
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
modelName: sanitizedModelTag
@ -253,11 +283,11 @@
}
modelTag = '';
modelTransferring = false;
modelLoading = false;
};
const uploadModelHandler = async () => {
modelTransferring = true;
modelLoading = true;
let uploaded = false;
let fileResponse = null;
@ -396,7 +426,7 @@
modelUploadInputElement.value = '';
}
modelInputFile = null;
modelTransferring = false;
modelLoading = false;
uploadProgress = null;
models.set(
@ -425,6 +455,14 @@
);
};
const cancelUpdateModelHandler = async (model: string) => {
const controller = updateModelsControllers[model];
if (controller) {
controller.abort();
updateCancelled = true;
}
};
const cancelModelPullHandler = async (model: string) => {
const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
if (abortController) {
@ -605,14 +643,15 @@
bind:value={modelTag}
/>
</div>
<Tooltip content={$i18n.t('Pull Model')} placement="top">
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
pullModelHandler();
}}
disabled={modelTransferring}
disabled={modelLoading || modelTag.trim() === ''}
>
{#if modelTransferring}
{#if modelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
@ -658,6 +697,7 @@
</svg>
{/if}
</button>
</Tooltip>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
@ -670,8 +710,35 @@
</div>
{#if updateModelId}
<div class="text-xs">
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
<div class="text-xs flex justify-between items-center">
<div>Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}</div>
<Tooltip content={$i18n.t('Cancel')}>
<button
class="text-gray-800 dark:text-gray-100"
on:click={() => {
cancelUpdateModelHandler(updateModelId);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
@ -754,11 +821,13 @@
{/each}
</select>
</div>
<Tooltip content={$i18n.t('Delete Model')} placement="top">
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
showModelDeleteConfirm = true;
}}
disabled={deleteModelTag === ''}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -773,6 +842,7 @@
/>
</svg>
</button>
</Tooltip>
</div>
</div>
@ -799,12 +869,15 @@
</div>
<div class="flex self-start">
<Tooltip content={$i18n.t('Create Model')} placement="top">
<button
class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
on:click={() => {
createModelHandler();
}}
disabled={createModelLoading}
disabled={createModelLoading ||
createModelName.trim() === '' ||
createModelObject.trim() === ''}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -820,6 +893,7 @@
/>
</svg>
</button>
</Tooltip>
</div>
</div>
@ -936,12 +1010,13 @@
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<Tooltip content={$i18n.t('Upload Model')} placement="top">
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
type="submit"
disabled={modelTransferring}
disabled={modelLoading}
>
{#if modelTransferring}
{#if modelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
@ -987,6 +1062,7 @@
</svg>
{/if}
</button>
</Tooltip>
{/if}
</div>

View file

@ -83,7 +83,7 @@
</Tooltip>
</div>
<div class="flex flex-col gap-1.5">
<div class="flex flex-col gap-1">
{#each servers as server, idx}
<Connection
bind:connection={server}

View file

@ -56,14 +56,7 @@
let showDefaultPermissionsModal = false;
const setGroups = async () => {
const allGroups = await getGroups(localStorage.token);
const userGroup = allGroups.find((g) => g.name.toLowerCase() === 'user');
if (userGroup) {
defaultPermissions = userGroup.permissions;
}
groups = allGroups.filter((g) => g.name.toLowerCase() !== 'user');
groups = await getGroups(localStorage.token);
};
const addGroupHandler = async (group) => {
@ -110,6 +103,7 @@
total = res.total;
}
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
await setGroups();
loaded = true;
});

View file

@ -87,7 +87,11 @@
<div class=" px-5 pt-3 pb-5 w-full">
<div class="flex self-center w-full">
<div class=" self-start h-full mr-6">
<UserProfileImage bind:profileImageUrl={_user.profile_image_url} user={_user} />
<UserProfileImage
imageClassName="size-14"
bind:profileImageUrl={_user.profile_image_url}
user={_user}
/>
</div>
<div class=" flex-1">

View file

@ -160,7 +160,7 @@
};
const onChange = async () => {
$socket?.emit('channel-events', {
$socket?.emit('events:channel', {
channel_id: id,
message_id: null,
data: {
@ -180,7 +180,7 @@
chatId.set('');
}
$socket?.on('channel-events', channelEventHandler);
$socket?.on('events:channel', channelEventHandler);
mediaQuery = window.matchMedia('(min-width: 1024px)');
@ -197,7 +197,7 @@
});
onDestroy(() => {
$socket?.off('channel-events', channelEventHandler);
$socket?.off('events:channel', channelEventHandler);
});
</script>

View file

@ -876,12 +876,12 @@
richText={$settings?.richTextInput ?? true}
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
!$mobile &&
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
))}
)}
largeTextAsFile={$settings?.largeTextAsFile ?? false}
floatingMenuPlacement={'top-start'}
{suggestions}

View file

@ -143,7 +143,7 @@
};
const onChange = async () => {
$socket?.emit('channel-events', {
$socket?.emit('events:channel', {
channel_id: channel.id,
message_id: threadId,
data: {
@ -156,11 +156,11 @@
};
onMount(() => {
$socket?.on('channel-events', channelEventHandler);
$socket?.on('events:channel', channelEventHandler);
});
onDestroy(() => {
$socket?.off('channel-events', channelEventHandler);
$socket?.off('events:channel', channelEventHandler);
});
</script>

View file

@ -91,6 +91,7 @@
import Sidebar from '../icons/Sidebar.svelte';
import { getFunctions } from '$lib/apis/functions';
import Image from '../common/Image.svelte';
import { updateFolderById } from '$lib/apis/folders';
export let chatIdProp = '';
@ -219,10 +220,15 @@
}
const saveSessionSelectedModels = () => {
if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
const selectedModelsString = JSON.stringify(selectedModels);
if (
selectedModels.length === 0 ||
(selectedModels.length === 1 && selectedModels[0] === '') ||
sessionStorage.selectedModels === selectedModelsString
) {
return;
}
sessionStorage.selectedModels = JSON.stringify(selectedModels);
sessionStorage.selectedModels = selectedModelsString;
console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
};
@ -294,7 +300,7 @@
}
};
const showMessage = async (message) => {
const showMessage = async (message, ignoreSettings = false) => {
await tick();
const _chatId = JSON.parse(JSON.stringify($chatId));
@ -320,7 +326,7 @@
await tick();
await tick();
if ($settings?.scrollOnBranchChange ?? true) {
if (($settings?.scrollOnBranchChange ?? true) || ignoreSettings) {
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
@ -499,12 +505,33 @@
}
};
const savedModelIds = async () => {
if (
$selectedFolder &&
selectedModels.filter((modelId) => modelId !== '').length > 0 &&
JSON.stringify($selectedFolder?.data?.model_ids) !== JSON.stringify(selectedModels)
) {
const res = await updateFolderById(localStorage.token, $selectedFolder.id, {
data: {
model_ids: selectedModels
}
});
}
};
$: if (selectedModels !== null) {
savedModelIds();
}
let pageSubscribe = null;
let showControlsSubscribe = null;
let selectedFolderSubscribe = null;
onMount(async () => {
loading = true;
console.log('mounted');
window.addEventListener('message', onMessageHandler);
$socket?.on('chat-events', chatEventHandler);
$socket?.on('events', chatEventHandler);
pageSubscribe = page.subscribe(async (p) => {
if (p.url.pathname === '/') {
@ -548,7 +575,7 @@
} catch (e) {}
}
showControls.subscribe(async (value) => {
showControlsSubscribe = showControls.subscribe(async (value) => {
if (controlPane && !$mobile) {
try {
if (value) {
@ -569,17 +596,32 @@
}
});
selectedFolderSubscribe = selectedFolder.subscribe(async (folder) => {
if (
folder?.data?.model_ids &&
JSON.stringify(selectedModels) !== JSON.stringify(folder.data.model_ids)
) {
selectedModels = folder.data.model_ids;
console.log('Set selectedModels from folder data:', selectedModels);
}
});
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
chats.subscribe(() => {});
});
onDestroy(() => {
try {
pageSubscribe();
showControlsSubscribe();
selectedFolderSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('chat-events', chatEventHandler);
$socket?.off('events', chatEventHandler);
} catch (e) {
console.error(e);
}
});
// File upload functions
@ -780,6 +822,7 @@
//////////////////////////
const initNewChat = async () => {
console.log('initNewChat');
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
await temporaryChatEnabled.set(true);
}
@ -829,6 +872,9 @@
selectedModels = selectedModels.filter((modelId) =>
$models.map((m) => m.id).includes(modelId)
);
} else {
if ($selectedFolder?.data?.model_ids) {
selectedModels = $selectedFolder?.data?.model_ids;
} else {
if (sessionStorage.selectedModels) {
selectedModels = JSON.parse(sessionStorage.selectedModels);
@ -841,6 +887,8 @@
selectedModels = $config?.default_models.split(',');
}
}
}
selectedModels = selectedModels.filter((modelId) => availableModels.includes(modelId));
}
@ -1499,7 +1547,7 @@
chatFiles.push(
..._files.filter((item) =>
['doc', 'text', 'file', 'note', 'chat', 'collection'].includes(item.type)
['doc', 'text', 'file', 'note', 'chat', 'folder', 'collection'].includes(item.type)
)
);
chatFiles = chatFiles.filter(
@ -2159,8 +2207,8 @@
selectedFolder.set(null);
} else {
_chatId = 'local';
await chatId.set('local');
_chatId = `local:${$socket?.id}`; // Use socket id for temporary chat
await chatId.set(_chatId);
}
await tick();

View file

@ -13,12 +13,9 @@
showEmbeds
} from '$lib/stores';
import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte';
import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte';
import Overview from './Overview.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Artifacts from './Artifacts.svelte';
import Embeds from './ChatControls/Embeds.svelte';
@ -154,7 +151,6 @@
}
</script>
<SvelteFlowProvider>
{#if !largeScreen}
{#if $showControls}
<Drawer
@ -189,15 +185,18 @@
{:else if $showArtifacts}
<Artifacts {history} />
{:else if $showOverview}
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
on:nodeclick={(e) => {
showMessage(e.detail.node.data.message);
onNodeClick={(e) => {
const node = e.node;
showMessage(node.data.message, true);
}}
on:close={() => {
onClose={() => {
showControls.set(false);
}}
/>
{/await}
{:else}
<Controls
on:close={() => {
@ -276,21 +275,24 @@
{:else if $showArtifacts}
<Artifacts {history} overlay={dragged} />
{:else if $showOverview}
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
on:nodeclick={(e) => {
if (e.detail.node.data.message.favorite) {
history.messages[e.detail.node.data.message.id].favorite = true;
onNodeClick={(e) => {
const node = e.node;
if (node?.data?.message?.favorite) {
history.messages[node.data.message.id].favorite = true;
} else {
history.messages[e.detail.node.data.message.id].favorite = null;
history.messages[node.data.message.id].favorite = null;
}
showMessage(e.detail.node.data.message);
showMessage(node.data.message, true);
}}
on:close={() => {
onClose={() => {
showControls.set(false);
}}
/>
{/await}
{:else}
<Controls
on:close={() => {
@ -306,4 +308,3 @@
{/if}
</Pane>
{/if}
</SvelteFlowProvider>

View file

@ -13,9 +13,14 @@
class="pointer-events-auto z-20 flex justify-between items-center py-3 px-2 font-primar text-gray-900 dark:text-white"
>
<div class="flex-1 flex items-center justify-between pl-2">
<div class="flex items-center space-x-2">
<a
class="flex items-center space-x-2 hover:underline"
href={$embed?.url}
target="_blank"
rel="noopener noreferrer"
>
{$embed?.title ?? 'Embedded Content'}
</div>
</a>
</div>
<button

View file

@ -13,6 +13,7 @@
import LightBulb from '$lib/components/icons/LightBulb.svelte';
import Markdown from '../Messages/Markdown.svelte';
import Skeleton from '../Messages/Skeleton.svelte';
import { chatId, models, socket } from '$lib/stores';
export let id = '';
export let messageId = '';
@ -118,6 +119,9 @@
let res;
[res, controller] = await chatCompletion(localStorage.token, {
model: model,
model_item: $models.find((m) => m.id === model),
session_id: $socket?.id,
chat_id: $chatId,
messages: [
...messages,
{
@ -246,11 +250,11 @@
{#if responseContent === null}
{#if !floatingInput}
<div
class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
class="flex flex-row shrink-0 p-0.5 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl shadow-xl border border-gray-100 dark:border-gray-800"
>
{#each actions as action}
<button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
class="px-1.5 py-[1px] hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl flex items-center gap-1 min-w-fit transition"
on:click={async () => {
selectedText = window.getSelection().toString();
selectedAction = action;
@ -280,7 +284,7 @@
</div>
{:else}
<div
class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-850 w-72 rounded-full shadow-xl"
class="py-1 flex dark:text-gray-100 bg-white dark:bg-gray-850 border border-gray-100 dark:border-gray-800 w-72 rounded-full shadow-xl"
>
<input
type="text"
@ -295,7 +299,7 @@
}}
/>
<div class="ml-1 mr-2">
<div class="ml-1 mr-1">
<button
class="{floatingInputValue !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
@ -321,19 +325,22 @@
</div>
{/if}
{:else}
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-xl shadow-xl w-80 max-w-full">
<div
class="bg-gray-50/50 dark:bg-gray-800 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-3xl shadow-xl w-80 max-w-full border border-gray-100 dark:border-gray-800"
>
<div
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-3xl px-3.5 pt-3 w-full"
>
<div class="font-medium">
<Markdown id={`${id}-float-prompt`} {content} />
</div>
</div>
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-4xl w-full">
<div
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
class=" max-h-80 overflow-y-auto w-full markdown-prose-xs px-3.5 py-3"
id="response-container"
>
<div class=" max-h-80 overflow-y-auto w-full markdown-prose-xs" id="response-container">
{#if !responseContent || responseContent?.trim() === ''}
<Skeleton size="sm" />
{:else}

View file

@ -885,8 +885,6 @@
})
}
];
console.log(suggestions);
loaded = true;
window.setTimeout(() => {
@ -1201,12 +1199,12 @@
floatingMenuPlacement={'top-start'}
insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
!$mobile &&
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
))}
)}
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
largeTextAsFile={($settings?.largeTextAsFile ?? false) && !shiftKey}
autocomplete={$config?.features?.enable_autocomplete_generation &&

View file

@ -13,6 +13,8 @@
import Database from '$lib/components/icons/Database.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Youtube from '$lib/components/icons/Youtube.svelte';
import { folders } from '$lib/stores';
import Folder from '$lib/components/icons/Folder.svelte';
const i18n = getContext('i18n');
@ -144,14 +146,25 @@
]
: [];
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
(item) => {
let folder_items = $folders.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}));
items = [
...folder_items,
...collections,
...collection_files,
...legacy_collections,
...legacy_documents
].map((item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
}
);
});
fuse = new Fuse(items, {
keys: ['name', 'description']
@ -213,6 +226,8 @@
>
{#if item?.type === 'collection'}
<Database className="size-4" />
{:else if item?.type === 'folder'}
<Folder className="size-4" />
{:else}
<DocumentPage className="size-4" />
{/if}

View file

@ -330,7 +330,7 @@
{#each Object.keys(tools) as toolId}
<button
class="relative flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
on:click={(e) => {
on:click={async (e) => {
if (!(tools[toolId]?.authenticated ?? true)) {
e.preventDefault();
@ -338,9 +338,18 @@
let serverId = parts?.at(-1) ?? toolId;
const authUrl = getOAuthClientAuthorizationUrl(serverId, 'mcp');
window.open(authUrl, '_blank', 'noopener');
window.open(authUrl, '_self', 'noopener');
} else {
tools[toolId].enabled = !tools[toolId].enabled;
const state = tools[toolId].enabled;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}
}}
>
@ -383,18 +392,7 @@
{/if}
<div class=" shrink-0">
<Switch
state={tools[toolId].enabled}
on:change={async (e) => {
const state = e.detail;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}}
/>
<Switch state={tools[toolId].enabled} />
</div>
</button>
{/each}

View file

@ -7,6 +7,7 @@
export let id = '';
export let sources = [];
export let readOnly = false;
let citations = [];
let showPercentage = false;
@ -26,12 +27,18 @@
if (citations[sourceIdx]?.source?.embed_url) {
const embedUrl = citations[sourceIdx].source.embed_url;
if (embedUrl) {
if (readOnly) {
// Open in new tab if readOnly
window.open(embedUrl, '_blank');
return;
} else {
showControls.set(true);
showEmbeds.set(true);
embed.set({
title: citations[sourceIdx]?.source?.name || 'Embedded Content',
url: embedUrl
});
}
} else {
selectedCitation = citations[sourceIdx];
showCitationModal = true;

View file

@ -6,7 +6,7 @@
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import { executeCode } from '$lib/apis/utils';
import { copyToClipboard, renderMermaidDiagram } from '$lib/utils';
import { copyToClipboard, renderMermaidDiagram, renderVegaVisualization } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
@ -55,6 +55,7 @@
let _token = null;
let mermaidHtml = null;
let vegaHtml = null;
let highlightedCode = null;
let executing = false;
@ -325,7 +326,26 @@
const render = async () => {
onUpdate(token);
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
try {
mermaidHtml = await renderMermaidDiagram(code);
} catch (error) {
console.error('Failed to render mermaid diagram:', error);
const errorMsg = error instanceof Error ? error.message : String(error);
toast.error($i18n.t('Failed to render diagram') + `: ${errorMsg}`);
mermaidHtml = null;
}
} else if (
(lang === 'vega' || lang === 'vega-lite') &&
(token?.raw ?? '').slice(-4).includes('```')
) {
try {
vegaHtml = await renderVegaVisualization(code);
} catch (error) {
console.error('Failed to render Vega visualization:', error);
const errorMsg = error instanceof Error ? error.message : String(error);
toast.error($i18n.t('Failed to render diagram') + `: ${errorMsg}`);
vegaHtml = null;
}
}
};
@ -397,6 +417,16 @@
{:else}
<pre class="mermaid">{code}</pre>
{/if}
{:else if lang === 'vega' || lang === 'vega-lite'}
{#if vegaHtml}
<SvgPanZoom
className="rounded-3xl max-h-fit overflow-hidden"
svg={vegaHtml}
content={_token.text}
/>
{:else}
<pre class="vega">{code}</pre>
{/if}
{:else}
<div
class="absolute left-0 right-0 py-2.5 pr-3 text-text-300 pl-4.5 text-xs font-medium dark:text-white"

View file

@ -576,8 +576,6 @@
await tick();
if (buttonsContainerElement) {
console.log(buttonsContainerElement);
buttonsContainerElement.addEventListener('wheel', function (event) {
if (buttonsContainerElement.scrollWidth <= buttonsContainerElement.clientWidth) {
// If the container is not scrollable, horizontal scroll
@ -811,6 +809,7 @@
bind:this={citationsElement}
id={message?.id}
sources={message?.sources ?? message?.citations}
{readOnly}
/>
{/if}

View file

@ -29,7 +29,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
sideOffset={-2}
side="bottom"
align="start"

View file

@ -68,6 +68,7 @@
}}
>
<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}
@ -75,10 +76,12 @@
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
class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
></span>
</span>
</div>
{/if}
<StatusItem {status} />
</div>
</button>

View file

@ -39,9 +39,13 @@
};
$: if (selectedModels.length > 0 && $models.length > 0) {
selectedModels = selectedModels.map((model) =>
const _selectedModels = selectedModels.map((model) =>
$models.map((m) => m.id).includes(model) ? model : ''
);
if (JSON.stringify(_selectedModels) !== JSON.stringify(selectedModels)) {
selectedModels = _selectedModels;
}
}
</script>

View file

@ -90,6 +90,26 @@
}
);
const updateFuse = () => {
if (fuse) {
fuse.setCollection(
items.map((item) => {
const _item = {
...item,
modelName: item.model?.name,
tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '),
desc: item.model?.info?.meta?.description
};
return _item;
})
);
}
};
$: if (items) {
updateFuse();
}
$: filteredItems = (
searchValue
? fuse

View file

@ -248,7 +248,7 @@
</div>
</div>
{#if $temporaryChatEnabled && $chatId === 'local'}
{#if $temporaryChatEnabled && ($chatId ?? '').startsWith('local:')}
<div class=" w-full z-30 text-center">
<div class="text-xs text-gray-500">{$i18n.t('Temporary Chat')}</div>
</div>

View file

@ -1,199 +1,17 @@
<script lang="ts">
import { getContext, createEventDispatcher, onDestroy } from 'svelte';
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
import { useSvelteFlow, useNodesInitialized, useStore, SvelteFlowProvider } from '@xyflow/svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { models, showOverview, theme, user } from '$lib/stores';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './Overview/Node.svelte';
import Flow from './Overview/Flow.svelte';
import XMark from '../icons/XMark.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte';
const { width, height } = useStore();
const { fitView, getViewport } = useSvelteFlow();
const nodesInitialized = useNodesInitialized();
import View from './Overview/View.svelte';
export let history;
let selectedMessageId = null;
const nodes = writable([]);
const edges = writable([]);
const nodeTypes = {
custom: CustomNode
};
$: if (history) {
drawFlow();
}
$: if (history && history.currentId) {
focusNode();
}
const focusNode = async () => {
if (selectedMessageId === null) {
await fitView({ nodes: [{ id: history.currentId }] });
} else {
await fitView({ nodes: [{ id: selectedMessageId }] });
}
selectedMessageId = null;
};
const drawFlow = async () => {
const nodeList = [];
const edgeList = [];
const levelOffset = 150; // Vertical spacing between layers
const siblingOffset = 250; // Horizontal spacing between nodes at the same layer
// Map to keep track of node positions at each level
let positionMap = new Map();
// Helper function to truncate labels
function createLabel(content) {
const maxLength = 100;
return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
}
// Create nodes and map children to ensure alignment in width
let layerWidths = {}; // Track widths of each layer
Object.keys(history.messages).forEach((id) => {
const message = history.messages[id];
const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
if (!layerWidths[level]) layerWidths[level] = 0;
positionMap.set(id, {
id: message.id,
level,
position: layerWidths[level]++
});
});
// Adjust positions based on siblings count to centralize vertical spacing
Object.keys(history.messages).forEach((id) => {
const pos = positionMap.get(id);
const xOffset = pos.position * siblingOffset;
const y = pos.level * levelOffset;
const x = xOffset;
nodeList.push({
id: pos.id,
type: 'custom',
data: {
user: $user,
message: history.messages[id],
model: $models.find((model) => model.id === history.messages[id].model)
},
position: { x, y }
});
// Create edges
const parentId = history.messages[id].parentId;
if (parentId) {
edgeList.push({
id: parentId + '-' + pos.id,
source: parentId,
target: pos.id,
selectable: false,
class: ' dark:fill-gray-300 fill-gray-300',
type: 'smoothstep',
animated: history.currentId === id || recurseCheckChild(id, history.currentId)
});
}
});
await edges.set([...edgeList]);
await nodes.set([...nodeList]);
};
const recurseCheckChild = (nodeId, currentId) => {
const node = history.messages[nodeId];
return (
node.childrenIds &&
node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
);
};
onMount(() => {
drawFlow();
nodesInitialized.subscribe(async (initialized) => {
if (initialized) {
await tick();
const res = await fitView({ nodes: [{ id: history.currentId }] });
}
});
width.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
height.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
});
onDestroy(() => {
console.log('Overview destroyed');
nodes.set([]);
edges.set([]);
});
export let onClose;
export let onNodeClick;
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
<div class="flex items-center gap-2.5">
<button
class="self-center p-0.5"
on:click={() => {
showOverview.set(false);
}}
>
<ArrowLeft className="size-3.5" />
</button>
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
</div>
<button
class="self-center p-0.5"
on:click={() => {
dispatch('close');
showOverview.set(false);
}}
>
<XMark className="size-3.5" />
</button>
</div>
{#if $nodes.length > 0}
<Flow
{nodes}
{nodeTypes}
{edges}
on:nodeclick={(e) => {
console.log(e.detail.node.data);
dispatch('nodeclick', e.detail);
selectedMessageId = e.detail.node.data.message.id;
fitView({ nodes: [{ id: selectedMessageId }] });
}}
/>
{/if}
</div>
<SvelteFlowProvider>
<View {history} {onClose} {onNodeClick} />
</SvelteFlowProvider>

View file

@ -4,11 +4,22 @@
const dispatch = createEventDispatcher();
import { theme } from '$lib/stores';
import { Background, Controls, SvelteFlow, BackgroundVariant } from '@xyflow/svelte';
import {
Background,
Controls,
SvelteFlow,
BackgroundVariant,
ControlButton
} from '@xyflow/svelte';
import BarsArrowUp from '$lib/components/icons/BarsArrowUp.svelte';
import Bars3BottomLeft from '$lib/components/icons/Bars3BottomLeft.svelte';
import AlignVertical from '$lib/components/icons/AlignVertical.svelte';
import AlignHorizontal from '$lib/components/icons/AlignHorizontal.svelte';
export let nodes;
export let nodeTypes;
export let edges;
export let setLayoutDirection;
</script>
<SvelteFlow
@ -31,6 +42,13 @@
console.log('Flow initialized');
}}
>
<Controls showLock={false} />
<Controls showLock={false}>
<ControlButton on:click={() => setLayoutDirection('vertical')} title="Vertical Layout">
<AlignVertical className="size-4" />
</ControlButton>
<ControlButton on:click={() => setLayoutDirection('horizontal')} title="Horizontal Layout">
<AlignHorizontal className="size-4" />
</ControlButton>
</Controls>
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>

View file

@ -0,0 +1,207 @@
<script lang="ts">
import { getContext, createEventDispatcher, onDestroy } from 'svelte';
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { models, showOverview, theme, user } from '$lib/stores';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './Node.svelte';
import Flow from './Flow.svelte';
import XMark from '../../icons/XMark.svelte';
import ArrowLeft from '../../icons/ArrowLeft.svelte';
const { width, height } = useStore();
const { fitView, getViewport } = useSvelteFlow();
const nodesInitialized = useNodesInitialized();
export let history;
export let onClose;
export let onNodeClick;
let selectedMessageId = null;
const nodes = writable([]);
const edges = writable([]);
let layoutDirection = 'vertical';
const nodeTypes = {
custom: CustomNode
};
$: if (history) {
drawFlow(layoutDirection);
}
$: if (history && history.currentId) {
focusNode();
}
const focusNode = async () => {
if (selectedMessageId === null) {
await fitView({ nodes: [{ id: history.currentId }] });
} else {
await fitView({ nodes: [{ id: selectedMessageId }] });
}
selectedMessageId = null;
};
const drawFlow = async (direction) => {
const nodeList = [];
const edgeList = [];
const levelOffset = direction === 'vertical' ? 150 : 300;
const siblingOffset = direction === 'vertical' ? 250 : 150;
// Map to keep track of node positions at each level
let positionMap = new Map();
// Helper function to truncate labels
function createLabel(content) {
const maxLength = 100;
return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
}
// Create nodes and map children to ensure alignment in width
let layerWidths = {}; // Track widths of each layer
Object.keys(history.messages).forEach((id) => {
const message = history.messages[id];
const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
if (!layerWidths[level]) layerWidths[level] = 0;
positionMap.set(id, {
id: message.id,
level,
position: layerWidths[level]++
});
});
// Adjust positions based on siblings count to centralize vertical spacing
Object.keys(history.messages).forEach((id) => {
const pos = positionMap.get(id);
const x = direction === 'vertical' ? pos.position * siblingOffset : pos.level * levelOffset;
const y = direction === 'vertical' ? pos.level * levelOffset : pos.position * siblingOffset;
nodeList.push({
id: pos.id,
type: 'custom',
data: {
user: $user,
message: history.messages[id],
model: $models.find((model) => model.id === history.messages[id].model)
},
position: { x, y }
});
// Create edges
const parentId = history.messages[id].parentId;
if (parentId) {
edgeList.push({
id: parentId + '-' + pos.id,
source: parentId,
target: pos.id,
selectable: false,
class: ' dark:fill-gray-300 fill-gray-300',
type: 'smoothstep',
animated: history.currentId === id || recurseCheckChild(id, history.currentId)
});
}
});
await edges.set([...edgeList]);
await nodes.set([...nodeList]);
};
const recurseCheckChild = (nodeId, currentId) => {
const node = history.messages[nodeId];
return (
node.childrenIds &&
node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
);
};
const setLayoutDirection = (direction) => {
layoutDirection = direction;
drawFlow(layoutDirection);
};
onMount(() => {
drawFlow(layoutDirection);
nodesInitialized.subscribe(async (initialized) => {
if (initialized) {
await tick();
const res = await fitView({ nodes: [{ id: history.currentId }] });
}
});
width.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
height.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
});
onDestroy(() => {
console.log('Overview destroyed');
nodes.set([]);
edges.set([]);
});
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
<div class="flex items-center gap-2.5">
<button
class="self-center p-0.5"
on:click={() => {
showOverview.set(false);
}}
>
<ArrowLeft className="size-3.5" />
</button>
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
</div>
<button
class="self-center p-0.5"
on:click={() => {
onClose();
showOverview.set(false);
}}
>
<XMark className="size-3.5" />
</button>
</div>
{#if $nodes.length > 0}
<Flow
{nodes}
{nodeTypes}
{edges}
{setLayoutDirection}
on:nodeclick={(e) => {
onNodeClick(e.detail);
selectedMessageId = e.detail.node.data.message.id;
fitView({ nodes: [{ id: selectedMessageId }] });
}}
/>
{/if}
</div>

View file

@ -7,6 +7,9 @@
const dispatch = createEventDispatcher();
import { getChatList } from '$lib/apis/chats';
import { updateFolderById } from '$lib/apis/folders';
import {
config,
user,
@ -25,7 +28,6 @@
import MessageInput from './MessageInput.svelte';
import FolderPlaceholder from './Placeholder/FolderPlaceholder.svelte';
import FolderTitle from './Placeholder/FolderTitle.svelte';
import { getChatList } from '$lib/apis/chats';
const i18n = getContext('i18n');
@ -58,7 +60,6 @@
export let toolServers = [];
let models = [];
let selectedModelIdx = 0;
$: if (selectedModels.length > 0) {
@ -66,8 +67,6 @@
}
$: models = selectedModels.map((id) => $_models.find((m) => m.id === id));
onMount(() => {});
</script>
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
@ -91,8 +90,6 @@
<FolderTitle
folder={$selectedFolder}
onUpdate={async (folder) => {
selectedFolder.set(folder);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
}}

View file

@ -11,7 +11,7 @@
import { selectedFolder } from '$lib/stores';
import { deleteFolderById, updateFolderById } from '$lib/apis/folders';
import { deleteFolderById, getFolderById, updateFolderById } from '$lib/apis/folders';
import { getChatsByFolderId } from '$lib/apis/chats';
import FolderModal from '$lib/components/layout/Sidebar/Folders/FolderModal.svelte';
@ -61,8 +61,14 @@
}
toast.success($i18n.t('Folder updated successfully'));
selectedFolder.set(folder);
onUpdate(folder);
const _folder = await getFolderById(localStorage.token, folder.id).catch((error) => {
toast.error(`${error}`);
return null;
});
await selectedFolder.set(_folder);
onUpdate(_folder);
}
};
@ -80,8 +86,14 @@
folder.meta = { ...folder.meta, icon: iconName };
toast.success($i18n.t('Folder updated successfully'));
selectedFolder.set(folder);
onUpdate(folder);
const _folder = await getFolderById(localStorage.token, folder.id).catch((error) => {
toast.error(`${error}`);
return null;
});
await selectedFolder.set(_folder);
onUpdate(_folder);
}
};

View file

@ -12,6 +12,8 @@
export let profileImageUrl;
export let user = null;
export let imageClassName = 'size-14 md:size-18';
let profileImageInputElement;
</script>
@ -89,7 +91,7 @@
<img
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
alt="profile"
class=" rounded-full size-14 md:size-18 object-cover"
class=" rounded-full {imageClassName} object-cover"
/>
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">

View file

@ -7,6 +7,7 @@
import Cog6 from '$lib/components/icons/Cog6.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import AddToolServerModal from '$lib/components/AddToolServerModal.svelte';
import WrenchAlt from '$lib/components/icons/WrenchAlt.svelte';
export let onDelete = () => {};
export let onSubmit = () => {};
@ -41,33 +42,21 @@
/>
<div class="flex w-full gap-2 items-center">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}"`, {
url: `${connection?.url}/${connection?.path ?? 'openapi.json'}`
})}
placement="top-start"
>
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div class="flex-1 relative">
<input
class=" outline-hidden w-full bg-transparent {!(connection?.config?.enable ?? true)
<div
class="flex-1 relative flex gap-1.5 items-center {!(connection?.config?.enable ?? true)
? 'opacity-50'
: ''}"
placeholder={$i18n.t('API Base URL')}
bind:value={connection.url}
autocomplete="off"
/>
>
<Tooltip content={connection?.type === 'mcp' ? $i18n.t('MCP') : $i18n.t('OpenAPI')}>
<WrenchAlt />
</Tooltip>
<div class=" capitalize outline-hidden w-full bg-transparent">
{connection?.info?.name ?? connection?.url}
<span class="text-gray-500">{connection?.info?.id}</span>
</div>
</div>
{#if (connection?.auth_type ?? 'bearer') === 'bearer'}
<SensitiveInput
inputClassName=" outline-hidden bg-transparent w-full"
placeholder={$i18n.t('API Key')}
bind:value={connection.key}
required={false}
/>
{/if}
</div>
</Tooltip>

View file

@ -250,7 +250,6 @@ print("${endTag}")
};
onMount(() => {
console.log(value);
if (value === '') {
value = boilerplate;
}

View file

@ -102,14 +102,14 @@
}}
>
<div
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl"
class=" m-auto max-w-full w-[32rem] mx-2 bg-white/95 dark:bg-gray-950/95 backdrop-blur-sm rounded-4xl max-h-[100dvh] shadow-3xl border border-white dark:border-gray-900"
in:flyAndScale
on:mousedown={(e) => {
e.stopPropagation();
}}
>
<div class="px-[1.75rem] py-6 flex flex-col">
<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">
<div class=" text-lg font-medium dark:text-gray-200 mb-2.5">
{#if title !== ''}
{title}
{:else}
@ -140,7 +140,7 @@
<div class="mt-6 flex justify-between gap-1.5">
<button
class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition"
class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2 rounded-3xl transition"
on:click={() => {
show = false;
dispatch('cancel');
@ -150,7 +150,7 @@
{cancelLabel}
</button>
<button
class="bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2.5 rounded-lg transition"
class="text-sm bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2 rounded-3xl transition"
on:click={() => {
confirmHandler();
}}

View file

@ -33,6 +33,7 @@
import Database from '../icons/Database.svelte';
import PageEdit from '../icons/PageEdit.svelte';
import ChatBubble from '../icons/ChatBubble.svelte';
import Folder from '../icons/Folder.svelte';
let showModal = false;
const decodeString = (str: string) => {
@ -115,6 +116,8 @@
<PageEdit />
{:else if type === 'chat'}
<ChatBubble />
{:else if type === 'folder'}
<Folder />
{:else}
<DocumentPage />
{/if}

View file

@ -672,16 +672,10 @@
}
}
console.log('content', content);
if (collaboration && documentId && socket && user) {
const { SocketIOCollaborationProvider } = await import('./RichTextInput/Collaboration');
provider = new SocketIOCollaborationProvider(documentId, socket, user, content);
}
console.log(bubbleMenuElement, floatingMenuElement);
console.log(suggestions);
editor = new Editor({
element: element,
extensions: [
@ -781,6 +775,8 @@
htmlValue = editor.getHTML();
jsonValue = editor.getJSON();
if (richText) {
mdValue = turndownService
.turndown(
htmlValue
@ -788,6 +784,20 @@
.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
)
.replace(/\u00a0/g, ' ');
} else {
mdValue = turndownService
.turndown(
htmlValue
// Replace empty paragraphs with line breaks
.replace(/<p><\/p>/g, '<br/>')
// Replace multiple spaces with non-breaking spaces
.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
// Replace tabs with non-breaking spaces (preserve indentation)
.replace(/\t/g, '\u00a0\u00a0\u00a0\u00a0') // 1 tab = 4 spaces
)
// Convert non-breaking spaces back to regular spaces for markdown
.replace(/\u00a0/g, ' ');
}
onChange({
html: htmlValue,
@ -1035,10 +1045,15 @@
if (!event.clipboardData) return false;
if (richText) return false; // Let ProseMirror handle normal copy in rich text mode
const plain = editor.getText();
const html = editor.getHTML();
const { state } = view;
const { from, to } = state.selection;
event.clipboardData.setData('text/plain', plain.replaceAll('\n\n', '\n'));
// Only take the selected text & HTML, not the full doc
const plain = state.doc.textBetween(from, to, '\n');
const slice = state.doc.cut(from, to);
const html = editor.schema ? editor.getHTML(slice) : editor.getHTML(); // depending on your editor API
event.clipboardData.setData('text/plain', plain);
event.clipboardData.setData('text/html', html);
event.preventDefault();

View file

@ -1,7 +1,9 @@
<script lang="ts">
import { createEventDispatcher, tick, getContext } from 'svelte';
import { Switch } from 'bits-ui';
import { createEventDispatcher, tick, getContext } from 'svelte';
import { settings } from '$lib/stores';
import Tooltip from './Tooltip.svelte';
export let state = true;
export let id = '';
@ -10,8 +12,6 @@
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
$: dispatch('change', state);
</script>
<Tooltip
@ -28,6 +28,10 @@
: 'outline outline-1 outline-gray-100 dark:outline-gray-800'} {state
? ' bg-emerald-500 dark:bg-emerald-700'
: 'bg-gray-200 dark:bg-transparent'}"
onCheckedChange={async () => {
await tick();
dispatch('change', state);
}}
>
<Switch.Thumb
class="pointer-events-none block size-3 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "

View file

@ -0,0 +1,21 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M3 22L3 2" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M21 22V2"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M15 16H9C7.89543 16 7 15.1046 7 14V10C7 8.89543 7.89543 8 9 8H15C16.1046 8 17 8.89543 17 10V14C17 15.1046 16.1046 16 15 16Z"
></path></svg
>

View file

@ -0,0 +1,21 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M22 3L2 3" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M22 21L2 21"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M8 15V9C8 7.89543 8.89543 7 10 7H14C15.1046 7 16 7.89543 16 9V15C16 16.1046 15.1046 17 14 17H10C8.89543 17 8 16.1046 8 15Z"
></path></svg
>

View file

@ -232,7 +232,7 @@
if (chat.id) {
let chatObj = null;
if (chat.id === 'local' || $temporaryChatEnabled) {
if ((chat?.id ?? '').startsWith('local') || $temporaryChatEnabled) {
chatObj = chat;
} else {
chatObj = await getChatById(localStorage.token, chat.id);
@ -431,9 +431,9 @@
<div class="flex items-center">{$i18n.t('Copy')}</div>
</DropdownMenu.Item>
{#if !$temporaryChatEnabled && chat?.id}
<hr class="border-gray-50 dark:border-gray-800 my-1" />
{#if chat?.id}
<DropdownMenu.Sub>
<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"
@ -461,7 +461,6 @@
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
<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"
@ -473,7 +472,6 @@
<div class="flex items-center">{$i18n.t('Archive')}</div>
</DropdownMenu.Item>
{#if !$temporaryChatEnabled}
<hr class="border-gray-50 dark:border-gray-800 my-1" />
<div class="flex p-1">

View file

@ -91,7 +91,7 @@
toast.error(`${error}`);
return [];
});
_folders.set(folderList);
_folders.set(folderList.sort((a, b) => b.updated_at - a.updated_at));
folders = {};
@ -125,13 +125,6 @@
});
}
}
await tick();
for (const folderId in folders) {
if (folders[folderId] && folders[folderId].is_expanded) {
folderRegistry[folderId]?.setFolderItems();
}
}
};
const createFolder = async ({ name, data }) => {
@ -185,14 +178,28 @@
const initChatList = async () => {
// Reset pagination variables
tags.set(await getAllTags(localStorage.token));
pinnedChats.set(await getPinnedChatList(localStorage.token));
initFolders();
console.log('initChatList');
currentChatPage.set(1);
allChatsLoaded = false;
await chats.set(await getChatList(localStorage.token, $currentChatPage));
initFolders();
await Promise.all([
await (async () => {
console.log('Init tags');
const _tags = await getAllTags(localStorage.token);
tags.set(_tags);
})(),
await (async () => {
console.log('Init pinned chats');
const _pinnedChats = await getPinnedChatList(localStorage.token);
pinnedChats.set(_pinnedChats);
})(),
await (async () => {
console.log('Init chat list');
const _chats = await getChatList(localStorage.token, $currentChatPage);
await chats.set(_chats);
})()
]);
// Enable pagination
scrollPaginationEnabled.set(true);
@ -342,9 +349,12 @@
selectedChatId = null;
};
let unsubscribers = [];
onMount(async () => {
showPinnedChat = localStorage?.showPinnedChat ? localStorage.showPinnedChat === 'true' : true;
await showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
unsubscribers = [
mobile.subscribe((value) => {
if ($showSidebar && value) {
showSidebar.set(false);
@ -360,9 +370,7 @@
if (!$showSidebar && !value) {
showSidebar.set(true);
}
});
showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
}),
showSidebar.subscribe(async (value) => {
localStorage.sidebar = value;
@ -381,18 +389,12 @@
}
}
if (!value) {
if (value) {
await initChannels();
await initChatList();
}
});
chats.subscribe((value) => {
initFolders();
});
await initChannels();
await initChatList();
})
];
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
@ -411,6 +413,14 @@
});
onDestroy(() => {
if (unsubscribers && unsubscribers.length > 0) {
unsubscribers.forEach((unsubscriber) => {
if (unsubscriber) {
unsubscriber();
}
});
}
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);

View file

@ -246,11 +246,12 @@
};
onMount(async () => {
folderRegistry[folderId] = {
setFolderItems: () => setFolderItems()
};
open = folders[folderId].is_expanded;
folderRegistry[folderId] = {
setFolderItems: () => {
setFolderItems();
}
};
if (folderElement) {
folderElement.addEventListener('dragover', onDragOver);
folderElement.addEventListener('drop', onDrop);
@ -335,7 +336,7 @@
});
if (folder) {
selectedFolder.set(folder);
await selectedFolder.set(folder);
}
}
dispatch('update');
@ -376,7 +377,7 @@
});
if (folder) {
selectedFolder.set(folder);
await selectedFolder.set(folder);
}
}
} else {
@ -488,17 +489,17 @@
}
clickTimer = setTimeout(async () => {
await goto('/');
const folder = await getFolderById(localStorage.token, folderId).catch((error) => {
toast.error(`${error}`);
return null;
});
if (folder) {
selectedFolder.set(folder);
await selectedFolder.set(folder);
}
await goto('/');
if ($mobile) {
showSidebar.set(!$showSidebar);
}

View file

@ -583,19 +583,48 @@ ${content}
// STEP 1. Get a DOM node to render
const html = note.data?.content?.html ?? '';
const isDarkMode = document.documentElement.classList.contains('dark');
let node;
if (html instanceof HTMLElement) {
node = html;
} else {
// If it's HTML string, render to a temporary hidden element
const virtualWidth = 800; // px, fixed width for cloned element
// Clone and style
node = document.createElement('div');
node.innerHTML = html;
// title node
const titleNode = document.createElement('div');
titleNode.textContent = note.title;
titleNode.style.fontSize = '24px';
titleNode.style.fontWeight = 'medium';
titleNode.style.paddingBottom = '20px';
titleNode.style.color = isDarkMode ? 'white' : 'black';
node.appendChild(titleNode);
const contentNode = document.createElement('div');
contentNode.innerHTML = html;
node.appendChild(contentNode);
node.classList.add('text-black');
node.classList.add('dark:text-white');
node.style.width = `${virtualWidth}px`;
node.style.position = 'absolute';
node.style.left = '-9999px';
node.style.height = 'auto';
node.style.padding = '40px 40px';
console.log(node);
document.body.appendChild(node);
}
// Render to canvas with predefined width
const canvas = await html2canvas(node, {
useCORS: true,
backgroundColor: isDarkMode ? '#000' : '#fff',
scale: 2, // Keep at 1x to avoid unexpected enlargements
width: virtualWidth, // Set fixed virtual screen width
windowWidth: virtualWidth, // Ensure consistent rendering
@ -612,7 +641,14 @@ ${content}
// A4 page settings
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 width in mm
const pageWidthMM = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
const pageHeightMM = 297; // A4 height in mm
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, pageWidthMM, pageHeightMM, 'F'); // black bg
}
// Maintain aspect ratio
const imgHeight = (canvas.height * imgWidth) / canvas.width;
@ -627,6 +663,11 @@ ${content}
position -= pageHeight;
pdf.addPage();
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, pageWidthMM, pageHeightMM, 'F'); // black bg
}
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}

View file

@ -6,10 +6,10 @@
dayjs.extend(relativeTime);
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte';
import { onMount, getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import { WEBUI_NAME, knowledge } from '$lib/stores';
import { WEBUI_NAME, knowledge, user } from '$lib/stores';
import {
getKnowledgeBases,
deleteKnowledgeById,
@ -17,6 +17,7 @@
} from '$lib/apis/knowledge';
import { goto } from '$app/navigation';
import { capitalizeFirstLetter } from '$lib/utils';
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
import ItemMenu from './Knowledge/ItemMenu.svelte';
@ -24,9 +25,9 @@
import Search from '../icons/Search.svelte';
import Plus from '../icons/Plus.svelte';
import Spinner from '../common/Spinner.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import Tooltip from '../common/Tooltip.svelte';
import XMark from '../icons/XMark.svelte';
import ViewSelector from './common/ViewSelector.svelte';
let loaded = false;
@ -34,14 +35,25 @@
let selectedItem = null;
let showDeleteConfirm = false;
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let fuse = null;
let knowledgeBases = [];
let items = [];
let filteredItems = [];
$: if (knowledgeBases.length > 0) {
// Added a check for non-empty array, good practice
fuse = new Fuse(knowledgeBases, {
const setFuse = async () => {
items = knowledgeBases.filter(
(item) =>
viewOption === '' ||
(viewOption === 'created' && item.user_id === $user?.id) ||
(viewOption === 'shared' && item.user_id !== $user?.id)
);
fuse = new Fuse(items, {
keys: [
'name',
'description',
@ -50,16 +62,24 @@
],
threshold: 0.3
});
await tick();
setFilteredItems();
};
$: if (knowledgeBases.length > 0 && viewOption !== undefined) {
// Added a check for non-empty array, good practice
setFuse();
} else {
fuse = null; // Reset fuse if knowledgeBases is empty
}
$: if (fuse) {
filteredItems = query
? fuse.search(query).map((e) => {
return e.item;
})
: knowledgeBases;
const setFilteredItems = () => {
filteredItems = query ? fuse.search(query).map((result) => result.item) : items;
};
$: if (query !== undefined && fuse) {
setFilteredItems();
}
const deleteHandler = async (item) => {
@ -75,6 +95,7 @@
};
onMount(async () => {
viewOption = localStorage?.workspaceViewOption || '';
knowledgeBases = await getKnowledgeBaseList(localStorage.token);
loaded = true;
});
@ -94,18 +115,35 @@
}}
/>
<div class="flex flex-col gap-1 my-1.5">
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<div class="flex justify-between items-center">
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Knowledge')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{filteredItems.length}</span
>
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{filteredItems.length}
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex w-full justify-end gap-1.5">
<a
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
href="/workspace/knowledge/create"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Knowledge')}</div>
</a>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
@ -128,25 +166,39 @@
</div>
{/if}
</div>
</div>
<div>
<button
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
aria-label={$i18n.t('Create Knowledge')}
on:click={() => {
goto('/workspace/knowledge/create');
<div
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-1"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<Plus className="size-3.5" />
</button>
</div>
<div
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
await tick();
}}
/>
</div>
</div>
<div class="mb-5 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
{#if (filteredItems ?? []).length !== 0}
<!-- The Aleph dreams itself into being, and the void learns its own name -->
<div class=" my-2 px-3 grid grid-cols-1 lg:grid-cols-2 gap-2">
{#each filteredItems as item}
<Tooltip content={item?.description ?? item.name}>
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 hover:bg-black/5 dark:hover:bg-white/5 transition rounded-2xl"
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
on:click={() => {
if (item?.meta?.document) {
toast.error(
@ -160,14 +212,25 @@
}}
>
<div class=" w-full">
<div class="flex items-center justify-between -mt-1">
<div class=" self-center flex-1">
<div class="flex items-center justify-between -my-1">
<div class=" flex gap-2 items-center">
<div>
{#if item?.meta?.document}
<Badge type="muted" content={$i18n.t('Document')} />
{:else}
<Badge type="success" content={$i18n.t('Collection')} />
{/if}
</div>
<div class=" flex self-center -mr-1 translate-y-1">
<div class=" text-xs text-gray-500 line-clamp-1">
{$i18n.t('Updated')}
{dayjs(item.updated_at * 1000).fromNow()}
</div>
</div>
<div class="flex items-center gap-2">
<div class=" flex self-center">
<ItemMenu
on:delete={() => {
selectedItem = item;
@ -176,15 +239,14 @@
/>
</div>
</div>
<div class=" self-center flex-1 px-1 mb-1">
<div class=" font-semibold line-clamp-1 h-fit">{item.name}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{item.description}
</div>
<div class="mt-3 flex justify-between">
<div class=" flex items-center gap-1 justify-between px-1.5">
<div class=" flex items-center gap-2">
<div class=" text-sm font-medium line-clamp-1 capitalize">{item.name}</div>
</div>
<div>
<div class="text-xs text-gray-500">
<Tooltip
content={item?.user?.email ?? $i18n.t('Deleted User')}
@ -198,18 +260,28 @@
})}
</Tooltip>
</div>
<div class=" text-xs text-gray-500 line-clamp-1">
{$i18n.t('Updated')}
{dayjs(item.updated_at * 1000).fromNow()}
</div>
</div>
</div>
</div>
</button>
</Tooltip>
{/each}
</div>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No knowledge found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</div>
</div>
{/if}
</div>
<div class=" text-gray-500 text-xs mt-1 mb-2">
<div class=" text-gray-500 text-xs m-2">
{$i18n.t("Use '#' in the prompt input to load and include your knowledge.")}
</div>
{:else}

View file

@ -24,6 +24,8 @@
import { getModels } from '$lib/apis';
import { getGroups } from '$lib/apis/groups';
import { capitalizeFirstLetter, copyToClipboard } from '$lib/utils';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import ModelMenu from './Models/ModelMenu.svelte';
import ModelDeleteConfirmDialog from '../common/ConfirmDialog.svelte';
@ -34,10 +36,11 @@
import ChevronRight from '../icons/ChevronRight.svelte';
import Switch from '../common/Switch.svelte';
import Spinner from '../common/Spinner.svelte';
import { capitalizeFirstLetter, copyToClipboard } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import EyeSlash from '../icons/EyeSlash.svelte';
import Eye from '../icons/Eye.svelte';
import ViewSelector from './common/ViewSelector.svelte';
import TagSelector from './common/TagSelector.svelte';
let shiftKey = false;
@ -49,6 +52,8 @@
let models = [];
let tags = [];
let viewOption = '';
let selectedTag = '';
let filteredModels = [];
@ -58,22 +63,28 @@
let group_ids = [];
$: if (models) {
$: if (models && query !== undefined && selectedTag !== undefined && viewOption !== undefined) {
setFilteredModels();
}
const setFilteredModels = async () => {
filteredModels = models.filter((m) => {
if (query === '' && selectedTag === '') return true;
if (query === '' && selectedTag === '' && viewOption === '') return true;
const lowerQuery = query.toLowerCase();
return (
((m.name || '').toLowerCase().includes(lowerQuery) ||
(m.user?.name || '').toLowerCase().includes(lowerQuery) || // Search by user name
(m.user?.email || '').toLowerCase().includes(lowerQuery)) && // Search by user email
(selectedTag === '' ||
m?.meta?.tags?.some((tag) => tag.name.toLowerCase() === selectedTag.toLowerCase()))
m?.meta?.tags?.some((tag) => tag.name.toLowerCase() === selectedTag.toLowerCase())) &&
(viewOption === '' ||
(viewOption === 'created' && m.user_id === $user?.id) ||
(viewOption === 'shared' && m.user_id !== $user?.id))
);
});
}
};
let query = '';
const deleteModelHandler = async (model) => {
const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
toast.error(`${e}`);
@ -173,11 +184,7 @@
saveAs(blob, `${model.id}-${Date.now()}.json`);
};
onMount(async () => {
models = await getWorkspaceModels(localStorage.token);
let groups = await getGroups(localStorage.token);
group_ids = groups.map((group) => group.id);
const setTags = () => {
if (models) {
tags = models
.filter((model) => !(model?.meta?.hidden ?? false))
@ -187,7 +194,16 @@
// Remove duplicates and sort
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
}
};
onMount(async () => {
viewOption = localStorage.workspaceViewOption ?? '';
models = await getWorkspaceModels(localStorage.token);
let groups = await getGroups(localStorage.token);
group_ids = groups.map((group) => group.id);
setTags();
loaded = true;
const onKeyDown = (event) => {
@ -232,18 +248,107 @@
}}
/>
<div class="flex flex-col gap-1 mt-1.5">
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<input
id="models-import-input"
bind:this={modelsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
let reader = new FileReader();
reader.onload = async (event) => {
let savedModels = JSON.parse(event.target.result);
console.log(savedModels);
for (const model of savedModels) {
if (model?.info ?? false) {
if ($_models.find((m) => m.id === model.id)) {
await updateModelById(localStorage.token, model.id, model.info).catch((error) => {
return null;
});
} else {
await createNewModel(localStorage.token, model.info).catch((error) => {
return null;
});
}
} else {
if (model?.id && model?.name) {
await createNewModel(localStorage.token, model).catch((error) => {
return null;
});
}
}
}
await _models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
models = await getWorkspaceModels(localStorage.token);
};
reader.readAsText(importFiles[0]);
}}
/>
<div class="flex justify-between items-center">
<div class="flex items-center md:self-center text-xl font-medium px-0.5">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Models')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{filteredModels.length}</span
>
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{filteredModels.length}
</div>
</div>
<div class=" flex flex-1 items-center w-full space-x-2">
<div class="flex w-full justify-end gap-1.5">
{#if $user?.role === 'admin'}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={() => {
modelsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if models.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={async () => {
downloadModels(models);
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
<a
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
href="/workspace/models/create"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Model')}</div>
</a>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
<div class="flex flex-1 items-center">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
@ -267,21 +372,10 @@
</div>
{/if}
</div>
<div>
<a
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
href="/workspace/models/create"
>
<Plus className="size-3.5" />
</a>
</div>
</div>
</div>
{#if tags.length > 0}
<div
class=" flex w-full bg-transparent overflow-x-auto scrollbar-none"
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
@ -290,108 +384,93 @@
}}
>
<div
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full"
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-0.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
{#each tags as tag}
<Tooltip content={tag}>
await tick();
setTags();
}}
/>
{#if (tags ?? []).length > 0}
<TagSelector
bind:value={selectedTag}
items={tags.map((tag) => {
return { value: tag, label: tag };
})}
/>
{/if}
</div>
</div>
{#if (filteredModels ?? []).length !== 0}
<div class=" px-3 my-2 gap-1 lg:gap-2 grid lg:grid-cols-2" id="model-list">
{#each filteredModels as model (model.id)}
<button
class="min-w-fit outline-none p-1.5 {selectedTag === tag
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
class=" flex cursor-pointer dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl w-full p-2.5"
id="model-item-{model.id}"
on:click={() => {
selectedTag = tag;
if (
$user?.role === 'admin' ||
model.user_id === $user?.id ||
model.access_control.write.group_ids.some((wg) => group_ids.includes(wg))
) {
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
}
}}
>
{tag.length > 32 ? `${tag.slice(0, 32)}...` : tag}
</button>
</Tooltip>
{/each}
</div>
</div>
{/if}
<div class=" my-2 mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3" id="model-list">
{#each filteredModels as model (model.id)}
<div class="flex group/item gap-3.5 w-full">
<div class="self-center pl-0.5">
<div class="flex bg-white rounded-2xl">
<div
class=" flex flex-col cursor-pointer w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-2xl transition"
id="model-item-{model.id}"
>
<div class="flex gap-4 mt-1 mb-0.5">
<div class=" w-10">
<div
class=" rounded-full object-cover {model.is_active
? ''
: 'opacity-50 dark:opacity-50'} "
class="{model.is_active ? '' : 'opacity-50 dark:opacity-50'} {model.meta
.profile_image_url !== `${WEBUI_BASE_URL}/static/favicon.png`
? 'bg-transparent'
: 'bg-white'} rounded-2xl"
>
<img
src={model?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover"
class=" rounded-2xl size-12 object-cover"
/>
</div>
</div>
</div>
<div class=" shrink-0 flex w-full min-w-0 flex-1 pr-1 self-center">
<div class="flex h-full w-full flex-1 flex-col justify-start self-center group">
<div class="flex-1 w-full">
<div class="flex items-center justify-between w-full">
<Tooltip content={model.name} className=" w-fit" placement="top-start">
<a
class=" flex flex-1 cursor-pointer w-full"
class=" font-semibold line-clamp-1 hover:underline capitalize"
href={`/?models=${encodeURIComponent(model.id)}`}
>
<div class=" flex-1 self-center {model.is_active ? '' : 'text-gray-500'}">
<Tooltip
content={marked.parse(model?.meta?.description ?? model.id)}
className=" w-fit"
placement="top-start"
>
<div class=" font-semibold line-clamp-1">{model.name}</div>
</Tooltip>
<div class="flex gap-1 text-xs overflow-hidden">
<div class="line-clamp-1">
{#if (model?.meta?.description ?? '').trim()}
{model?.meta?.description}
{:else}
{model.id}
{/if}
</div>
</div>
</div>
{model.name}
</a>
</div>
<div class="flex justify-between items-center -mb-0.5 px-0.5 mt-1.5">
<div class=" text-xs mt-0.5">
<Tooltip
content={model?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
model?.user?.name ?? model?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div>
</Tooltip>
</div>
<div class=" flex items-center gap-1">
<div
class="flex justify-end w-full {model.is_active ? '' : 'text-gray-500'}"
>
<div class="flex justify-between items-center w-full">
<div class=""></div>
<div class="flex flex-row gap-0.5 items-center">
{#if shiftKey}
<Tooltip content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}>
<Tooltip
content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}
>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
on:click={(e) => {
e.stopPropagation();
hideModelHandler(model);
}}
>
@ -405,9 +484,10 @@
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
on:click={(e) => {
e.stopPropagation();
deleteModelHandler(model);
}}
>
@ -415,29 +495,6 @@
</button>
</Tooltip>
{:else}
{#if $user?.role === 'admin' || model.user_id === $user?.id || model.access_control.write.group_ids.some( (wg) => group_ids.includes(wg) )}
<a
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
</a>
{/if}
<ModelMenu
user={$user}
{model}
@ -462,19 +519,28 @@
}}
onClose={() => {}}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
<div
class="self-center w-fit p-1 text-sm dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
>
<EllipsisHorizontal className="size-5" />
</button>
</div>
</ModelMenu>
{/if}
</div>
</div>
</div>
<div class="ml-1">
<Tooltip content={model.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<button
on:click={(e) => {
e.stopPropagation();
}}
>
<Tooltip
content={model.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
>
<Switch
bind:state={model.is_active}
on:change={async (e) => {
on:change={async () => {
toggleModelById(localStorage.token, model.id);
_models.set(
await getModels(
@ -486,122 +552,62 @@
}}
/>
</Tooltip>
</button>
</div>
</div>
<div class=" flex gap-1 pr-2 -mt-1 items-center">
<Tooltip
content={model?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500 text-xs">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
model?.user?.name ?? model?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div>
</Tooltip>
<div>·</div>
<Tooltip
content={marked.parse(model?.meta?.description ?? model.id)}
className=" w-fit text-left"
placement="top-start"
>
<div class="flex gap-1 text-xs overflow-hidden">
<div class="line-clamp-1">
{#if (model?.meta?.description ?? '').trim()}
{model?.meta?.description}
{:else}
{model.id}
{/if}
</div>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
</button>
{/each}
</div>
{#if $user?.role === 'admin'}
<div class=" flex justify-end w-full mb-3">
<div class="flex space-x-1">
<input
id="models-import-input"
bind:this={modelsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
let reader = new FileReader();
reader.onload = async (event) => {
let savedModels = JSON.parse(event.target.result);
console.log(savedModels);
for (const model of savedModels) {
if (model?.info ?? false) {
if ($_models.find((m) => m.id === model.id)) {
await updateModelById(localStorage.token, model.id, model.info).catch(
(error) => {
return null;
}
);
} else {
await createNewModel(localStorage.token, model.info).catch((error) => {
return null;
});
}
} else {
if (model?.id && model?.name) {
await createNewModel(localStorage.token, model).catch((error) => {
return null;
});
}
}
}
await _models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null)
)
);
models = await getWorkspaceModels(localStorage.token);
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
modelsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No models found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</button>
{#if models.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
downloadModels(models);
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Models')} ({models.length})
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div>
</div>
{/if}
</div>
{#if $config?.features.enable_community_sharing}
<div class=" my-16">

View file

@ -526,7 +526,7 @@
<option value={null} class=" text-gray-900"
>{$i18n.t('Select a base model')}</option
>
{#each $models.filter((m) => (model ? m.id !== model.id : true) && !m?.preset && m?.owned_by !== 'arena') as model}
{#each $models.filter((m) => (model ? m.id !== model.id : true) && !m?.preset && m?.owned_by !== 'arena' && !(m?.direct ?? false)) as model}
<option value={model.id} class=" text-gray-900">{model.name}</option>
{/each}
</select>

View file

@ -43,7 +43,14 @@
}}
>
<Tooltip content={$i18n.t('More')}>
<button
on:click={(e) => {
e.stopPropagation();
show = !show;
}}
>
<slot />
</button>
</Tooltip>
<div slot="content">

View file

@ -4,7 +4,7 @@
const { saveAs } = fileSaver;
import { goto } from '$app/navigation';
import { onMount, getContext } from 'svelte';
import { onMount, getContext, tick } from 'svelte';
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
import {
@ -13,6 +13,7 @@
getPrompts,
getPromptList
} from '$lib/apis/prompts';
import { capitalizeFirstLetter, slugify } from '$lib/utils';
import PromptMenu from './Prompts/PromptMenu.svelte';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
@ -22,9 +23,9 @@
import ChevronRight from '../icons/ChevronRight.svelte';
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
import { capitalizeFirstLetter, slugify } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import GarbageBin from '../icons/GarbageBin.svelte';
import ViewSelector from './common/ViewSelector.svelte';
let shiftKey = false;
@ -40,17 +41,30 @@
let showDeleteConfirm = false;
let deletePrompt = null;
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let filteredItems = [];
$: filteredItems = prompts.filter((p) => {
if (query === '') return true;
$: if (prompts && query !== undefined && viewOption !== undefined) {
setFilteredItems();
}
const setFilteredItems = () => {
filteredItems = prompts.filter((p) => {
if (query === '' && viewOption === '') return true;
const lowerQuery = query.toLowerCase();
return (
(p.title || '').toLowerCase().includes(lowerQuery) ||
((p.title || '').toLowerCase().includes(lowerQuery) ||
(p.command || '').toLowerCase().includes(lowerQuery) ||
(p.user?.name || '').toLowerCase().includes(lowerQuery) ||
(p.user?.email || '').toLowerCase().includes(lowerQuery)
(p.user?.email || '').toLowerCase().includes(lowerQuery)) &&
(viewOption === '' ||
(viewOption === 'created' && p.user_id === $user?.id) ||
(viewOption === 'shared' && p.user_id !== $user?.id))
);
});
};
const shareHandler = async (prompt) => {
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
@ -111,6 +125,7 @@
};
onMount(async () => {
viewOption = localStorage?.workspaceViewOption || '';
await init();
loaded = true;
@ -161,18 +176,99 @@
</div>
</DeleteConfirmDialog>
<div class="flex flex-col gap-1 my-1.5">
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<input
id="prompts-import-input"
bind:this={promptsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
const reader = new FileReader();
reader.onload = async (event) => {
const savedPrompts = JSON.parse(event.target.result);
console.log(savedPrompts);
for (const prompt of savedPrompts) {
await createNewPrompt(localStorage.token, {
command: prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command,
title: prompt.title,
content: prompt.content
}).catch((error) => {
toast.error(`${error}`);
return null;
});
}
prompts = await getPromptList(localStorage.token);
await _prompts.set(await getPrompts(localStorage.token));
importFiles = [];
promptsImportInputElement.value = '';
};
reader.readAsText(importFiles[0]);
}}
/>
<div class="flex justify-between items-center">
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Prompts')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{filteredItems.length}</span
>
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{filteredItems.length}
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex w-full justify-end gap-1.5">
{#if $user?.role === 'admin'}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={() => {
promptsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if prompts.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={async () => {
let blob = new Blob([JSON.stringify(prompts)], {
type: 'application/json'
});
saveAs(blob, `prompts-export-${Date.now()}.json`);
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
<a
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
href="/workspace/prompts/create"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Prompt')}</div>
</a>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
@ -196,33 +292,49 @@
</div>
{/if}
</div>
<div>
<a
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
href="/workspace/prompts/create"
>
<Plus className="size-3.5" />
</a>
</div>
</div>
</div>
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3">
{#each filteredItems as prompt}
<div
class=" flex space-x-4 cursor-pointer w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-2xl transition"
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-1"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
<div class=" flex-1 flex items-center gap-2 self-center">
<div class=" font-semibold line-clamp-1 capitalize">{prompt.title}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
<div
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
await tick();
}}
/>
</div>
</div>
{#if (filteredItems ?? []).length !== 0}
<!-- Before they call, I will answer; while they are yet speaking, I will hear. -->
<div class="gap-2 grid my-2 px-3 lg:grid-cols-2">
{#each filteredItems as prompt}
<a
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
>
<div class=" flex flex-col flex-1 space-x-4 cursor-pointer w-full pl-1">
<div class=" flex-1 flex items-center gap-2 self-start">
<div class=" font-medium line-clamp-1 capitalize">{prompt.title}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
{prompt.command}
</div>
</div>
<div class=" text-xs px-0.5">
<div class=" text-xs">
<Tooltip
content={prompt?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
@ -237,7 +349,6 @@
</div>
</Tooltip>
</div>
</a>
</div>
<div class="flex flex-row gap-0.5 self-center">
{#if shiftKey}
@ -253,27 +364,6 @@
</button>
</Tooltip>
{:else}
<a
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</a>
<PromptMenu
shareHandler={() => {
shareHandler(prompt);
@ -299,108 +389,21 @@
</PromptMenu>
{/if}
</div>
</div>
</a>
{/each}
</div>
{#if $user?.role === 'admin'}
<div class=" flex justify-end w-full mb-3">
<div class="flex space-x-2">
<input
id="prompts-import-input"
bind:this={promptsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
const reader = new FileReader();
reader.onload = async (event) => {
const savedPrompts = JSON.parse(event.target.result);
console.log(savedPrompts);
for (const prompt of savedPrompts) {
await createNewPrompt(localStorage.token, {
command:
prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command,
title: prompt.title,
content: prompt.content
}).catch((error) => {
toast.error(`${error}`);
return null;
});
}
prompts = await getPromptList(localStorage.token);
await _prompts.set(await getPrompts(localStorage.token));
importFiles = [];
promptsImportInputElement.value = '';
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
promptsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No prompts found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</button>
{#if prompts.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
let blob = new Blob([JSON.stringify(prompts)], {
type: 'application/json'
});
saveAs(blob, `prompts-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Prompts')} ({prompts.length})
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div>
</div>
{/if}
</div>
{#if $config?.features.enable_community_sharing}
<div class=" my-16">

View file

@ -3,9 +3,10 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte';
import { onMount, getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import { WEBUI_NAME, config, prompts, tools as _tools, user } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { goto } from '$app/navigation';
import {
@ -17,7 +18,8 @@
getToolList,
getTools
} from '$lib/apis/tools';
import Download from '../icons/Download.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import Tooltip from '../common/Tooltip.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte';
import ToolMenu from './Tools/ToolMenu.svelte';
@ -31,12 +33,10 @@
import Plus from '../icons/Plus.svelte';
import ChevronRight from '../icons/ChevronRight.svelte';
import Spinner from '../common/Spinner.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import AddToolMenu from './Tools/AddToolMenu.svelte';
import ImportModal from '../ImportModal.svelte';
const i18n = getContext('i18n');
import ViewSelector from './common/ViewSelector.svelte';
let shiftKey = false;
let loaded = false;
@ -56,18 +56,30 @@
let tools = [];
let filteredItems = [];
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let showImportModal = false;
$: filteredItems = tools.filter((t) => {
if (query === '') return true;
$: if (tools && query !== undefined && viewOption !== undefined) {
setFilteredItems();
}
const setFilteredItems = () => {
filteredItems = tools.filter((t) => {
if (query === '' && viewOption === '') return true;
const lowerQuery = query.toLowerCase();
return (
(t.name || '').toLowerCase().includes(lowerQuery) ||
((t.name || '').toLowerCase().includes(lowerQuery) ||
(t.id || '').toLowerCase().includes(lowerQuery) ||
(t.user?.name || '').toLowerCase().includes(lowerQuery) || // Search by user name
(t.user?.email || '').toLowerCase().includes(lowerQuery) // Search by user email
(t.user?.email || '').toLowerCase().includes(lowerQuery)) && // Search by user email
(viewOption === '' ||
(viewOption === 'created' && t.user_id === $user?.id) ||
(viewOption === 'shared' && t.user_id !== $user?.id))
);
});
};
const shareHandler = async (tool) => {
const item = await getToolById(localStorage.token, tool.id).catch((error) => {
@ -141,6 +153,7 @@
};
onMount(async () => {
viewOption = localStorage?.workspaceViewOption || '';
await init();
loaded = true;
@ -193,18 +206,104 @@
/>
{#if loaded}
<div class="flex flex-col gap-1 my-1.5">
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<input
id="documents-import-input"
bind:this={toolsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<div class="flex justify-between items-center">
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Tools')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{filteredItems.length}</span
>
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{filteredItems.length}
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex w-full justify-end gap-1.5">
{#if $user?.role === 'admin'}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={() => {
toolsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if tools.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={async () => {
const _tools = await exportTools(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_tools) {
let blob = new Blob([JSON.stringify(_tools)], {
type: 'application/json'
});
saveAs(blob, `tools-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
{#if $user?.role === 'admin'}
<AddToolMenu
createHandler={() => {
goto('/workspace/tools/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
}}
>
<div
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Tool')}</div>
</div>
</AddToolMenu>
{:else}
<a
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
href="/workspace/tools/create"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Tool')}</div></a
>
{/if}
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<!-- The iron remembers its forge. -->
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
@ -227,39 +326,38 @@
</div>
{/if}
</div>
</div>
<div>
{#if $user?.role === 'admin'}
<AddToolMenu
createHandler={() => {
goto('/workspace/tools/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
<div
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-1"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<Plus className="size-3.5" />
</div>
</AddToolMenu>
{:else}
<a
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
href="/workspace/tools/create"
>
<Plus className="size-3.5" />
</a>
{/if}
</div>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
await tick();
}}
/>
</div>
</div>
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3">
{#if (filteredItems ?? []).length !== 0}
<div class=" my-2 gap-2 grid px-3 lg:grid-cols-2">
{#each filteredItems as tool}
<Tooltip content={tool?.meta?.description ?? tool?.id}>
<div
class=" flex space-x-4 cursor-pointer w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-2xl transition"
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
>
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
@ -267,37 +365,20 @@
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center">
<Tooltip content={tool?.meta?.description ?? ''} placement="top-start">
<div class=" font-semibold flex items-center gap-1.5">
<div
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
TOOL
<Tooltip content={tool.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="line-clamp-1 text-sm">
{tool.name}
</div>
{#if tool?.meta?.manifest?.version}
<div
class="text-xs font-semibold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
<div class=" text-gray-500 text-xs font-medium shrink-0">
v{tool?.meta?.manifest?.version ?? ''}
</div>
{/if}
<div class="line-clamp-1">
{tool.name}
<span class=" text-gray-500 text-xs font-medium shrink-0">{tool.id}</span>
</div>
</div>
</Tooltip>
<div class="px-0.5">
<div class="flex gap-1.5 mt-0.5 mb-0.5">
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{tool.meta.description}
</div>
</div>
<div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')}
@ -404,89 +485,21 @@
{/if}
</div>
</div>
</Tooltip>
{/each}
</div>
{#if $user?.role === 'admin'}
<div class=" flex justify-end w-full mb-2">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:this={toolsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
toolsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No tools found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</button>
{#if tools.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
const _tools = await exportTools(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_tools) {
let blob = new Blob([JSON.stringify(_tools)], {
type: 'application/json'
});
saveAs(blob, `tools-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Tools')} ({tools.length})
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div>
</div>
{/if}
</div>
{#if $config?.features.enable_community_sharing}
<div class=" my-16">

View file

@ -41,27 +41,27 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[190px] text-sm rounded-xl px-1 py-1 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850"
sideOffset={-2}
class="w-full max-w-[190px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={6}
side="bottom"
align="start"
transition={flyAndScale}
>
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
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 w-full"
on:click={async () => {
createHandler();
show = false;
}}
>
<div class=" self-center mr-2">
<PencilSolid />
<Pencil />
</div>
<div class=" self-center truncate">{$i18n.t('New Tool')}</div>
</button>
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
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 w-full"
on:click={async () => {
importFromLinkHandler();
show = false;

View file

@ -0,0 +1,106 @@
<script lang="ts">
import { Select } from 'bits-ui';
import { getContext } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
const i18n = getContext('i18n');
export let value = '';
export let placeholder = $i18n.t('Tag');
export let onChange: (value: string) => void = () => {};
export let items = [];
</script>
<Select.Root
selected={value ? items.find((item) => item.value === value) : null}
{items}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
onChange(value);
}}
>
<Select.Trigger
class="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 rounded-xl "
aria-label={placeholder}
>
<Select.Value
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden capitalize"
{placeholder}
/>
{#if value}
<button
class="outline-none"
on:click={() => {
value = '';
onChange(value);
}}
>
<XMark className="size-3.5" />
</button>
{:else}
<ChevronDown className=" size-3.5" strokeWidth="2.5" />
{/if}
</Select.Trigger>
<Select.Content
class="rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sameWidth={false}
align="start"
>
<slot>
{#each items as item}
<Select.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 capitalize"
value={item.value}
label={item.label}
>
{item.label.length > 32 ? `${item.label.slice(0, 32)}...` : item.label}
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{/each}
</slot>
</Select.Content>
</Select.Root>
<!-- <button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Created by you')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Shared with you')}
</button> -->

View file

@ -0,0 +1,96 @@
<script lang="ts">
import { Select } from 'bits-ui';
import { getContext } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
const i18n = getContext('i18n');
export let value = '';
export let placeholder = $i18n.t('Select view');
export let onChange: (value: string) => void = () => {};
const items = [
{ value: '', label: $i18n.t('All') },
{ value: 'created', label: $i18n.t('Created by you') },
{ value: 'shared', label: $i18n.t('Shared with you') }
];
</script>
<Select.Root
selected={items.find((item) => item.value === value)}
{items}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
onChange(value);
}}
>
<Select.Trigger
class="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl "
aria-label={placeholder}
>
<Select.Value
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
{placeholder}
/>
<ChevronDown className=" size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class="rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sameWidth={false}
align="start"
>
<slot>
{#each items as item}
<Select.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"
value={item.value}
label={item.label}
>
{item.label}
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{/each}
</slot>
</Select.Content>
</Select.Root>
<!-- <button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Created by you')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Shared with you')}
</button> -->

View file

@ -65,6 +65,7 @@
"Add User Group": "",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين",
"admin": "المشرف",
"Admin": "",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create Model": "",
"Create new key": "عمل مفتاح جديد",
"Create new secret key": "عمل سر جديد",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "أنشئت في",
"Created At": "أنشئت من",
"Created by": "",
"Created by you": "",
"CSV Import": "",
"Ctrl+Enter to Send": "",
"Current Model": "الموديل المختار",
@ -396,6 +399,7 @@
"Delete function?": "",
"Delete Message": "",
"Delete message?": "",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "",
"delete this link": "أحذف هذا الرابط",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Application DN": "",
"Enter Application DN Password": "",
@ -667,13 +674,9 @@
"Export chat (.json)": "",
"Export Chats": "تصدير جميع الدردشات",
"Export Config to JSON File": "",
"Export Functions": "",
"Export Models": "نماذج التصدير",
"Export Presets": "",
"Export Prompt Suggestions": "",
"Export Prompts": "مطالبات التصدير",
"Export to CSV": "",
"Export Tools": "",
"Export Users": "",
"External": "",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "فشل في حفظ المحادثة",
"Failed to save models configuration": "",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "التركيز على إدخال الدردشة",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": " {{name}} مرحبا",
"Help": "مساعدة",
@ -841,14 +848,10 @@
"Import Chats": "استيراد الدردشات",
"Import Config from JSON File": "",
"Import From Link": "",
"Import Functions": "",
"Import Models": "استيراد النماذج",
"Import Notes": "",
"Import Presets": "",
"Import Prompt Suggestions": "",
"Import Prompts": "مطالبات الاستيراد",
"Import successful": "",
"Import Tools": "",
"Important Update": "تحديث مهم",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "",
@ -876,6 +879,7 @@
"Invalid file format.": "",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "تاق غير صالحة",
"is typing...": "",
@ -1034,8 +1038,11 @@
"New Chat": "دردشة جديدة",
"New Folder": "",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "",
"New Password": "كلمة المرور الجديدة",
"New Prompt": "",
"New Tool": "",
"new-channel": "",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No functions found": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -1061,12 +1069,14 @@
"No models selected": "",
"No Notes": "",
"No notes found": "",
"No prompts found": "",
"No results": "لا توجد نتائج",
"No results found": "لا توجد نتايج",
"No search query generated": "لم يتم إنشاء استعلام بحث",
"No source available": "لا يوجد مصدر متاح",
"No sources found": "",
"No suggestion prompts": "لا توجد مطالبات مقترحة",
"No tools found": "",
"No users were found.": "",
"No valves": "",
"No valves to update": "",
@ -1225,6 +1235,7 @@
"Public": "",
"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com \"{{searchValue}}\" أسحب من ",
"Pull a model from Ollama.com": "Ollama.com سحب الموديل من ",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "",
"Querying": "",
@ -1376,6 +1387,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "",
"Select only one model to call": "",
"Select view": "",
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
"semantic": "",
"Send": "تم",
@ -1416,6 +1428,7 @@
"Share Chat": "مشاركة الدردشة",
"Share to Open WebUI Community": "OpenWebUI شارك في مجتمع",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "عرض",
@ -1483,6 +1496,7 @@
"System Instructions": "",
"System Prompt": "محادثة النظام",
"Table Mode": "",
"Tag": "",
"Tags": "",
"Tags Generation": "",
"Tags Generation Prompt": "",
@ -1594,6 +1608,7 @@
"Transformers": "",
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
"Trust Proxy Environment": "",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "",
"TTS Settings": "TTS اعدادات",
@ -1630,6 +1645,7 @@
"Upload directory": "",
"Upload files": "",
"Upload Files": "تحميل الملفات",
"Upload Model": "",
"Upload Pipeline": "",
"Upload Progress": "جاري التحميل",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "إضافة مجموعة مستخدمين",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "تعديل هذه الإعدادات سيطبق التغييرات على جميع المستخدمين بشكل عام.",
"admin": "المسؤول",
"Admin": "المسؤول",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "إنشاء مجموعة",
"Create Knowledge": "إنشاء معرفة",
"Create Model": "",
"Create new key": "إنشاء مفتاح جديد",
"Create new secret key": "إنشاء مفتاح سري جديد",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "تم الإنشاء في",
"Created At": "تاريخ الإنشاء",
"Created by": "تم الإنشاء بواسطة",
"Created by you": "",
"CSV Import": "استيراد CSV",
"Ctrl+Enter to Send": "اضغط Ctrl+Enter للإرسال",
"Current Model": "النموذج الحالي",
@ -396,6 +399,7 @@
"Delete function?": "هل تريد حذف الوظيفة؟",
"Delete Message": "حذف الرسالة",
"Delete message?": "هل تريد حذف الرسالة؟",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "هل تريد حذف الموجه؟",
"delete this link": "أحذف هذا الرابط",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "أدخل سلسلة توثيق API (مثال: username:password)",
"Enter Application DN": "أدخل DN التطبيق",
"Enter Application DN Password": "أدخل كلمة مرور DN التطبيق",
@ -667,13 +674,9 @@
"Export chat (.json)": "تصدير المحادثة (.json)",
"Export Chats": "تصدير جميع الدردشات",
"Export Config to JSON File": "تصدير الإعدادات إلى ملف JSON",
"Export Functions": "تصدير الوظائف",
"Export Models": "نماذج التصدير",
"Export Presets": "تصدير الإعدادات المسبقة",
"Export Prompt Suggestions": "",
"Export Prompts": "مطالبات التصدير",
"Export to CSV": "تصدير إلى CSV",
"Export Tools": "تصدير الأدوات",
"Export Users": "",
"External": "",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "فشل في حفظ المحادثة",
"Failed to save models configuration": "فشل في حفظ إعدادات النماذج",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "التركيز على إدخال الدردشة",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "تم حذف المجلد بنجاح",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "الاهتزاز اللمسي",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": " {{name}} مرحبا",
"Help": "مساعدة",
@ -841,14 +848,10 @@
"Import Chats": "استيراد الدردشات",
"Import Config from JSON File": "استيراد الإعدادات من ملف JSON",
"Import From Link": "",
"Import Functions": "استيراد الوظائف",
"Import Models": "استيراد النماذج",
"Import Notes": "",
"Import Presets": "استيراد الإعدادات المسبقة",
"Import Prompt Suggestions": "",
"Import Prompts": "مطالبات الاستيراد",
"Import successful": "",
"Import Tools": "استيراد الأدوات",
"Important Update": "تحديث مهم",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "تضمين",
@ -876,6 +879,7 @@
"Invalid file format.": "تنسيق ملف غير صالح.",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "تاق غير صالحة",
"is typing...": "يكتب...",
@ -1034,8 +1038,11 @@
"New Chat": "دردشة جديدة",
"New Folder": "مجلد جديد",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "",
"New Password": "كلمة المرور الجديدة",
"New Prompt": "",
"New Tool": "",
"new-channel": "قناة جديدة",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "لا توجد مسافة متاحة",
"No feedbacks found": "لم يتم العثور على ملاحظات",
"No file selected": "لم يتم تحديد ملف",
"No functions found": "",
"No groups with access, add a group to grant access": "لا توجد مجموعات لها حق الوصول، أضف مجموعة لمنح الوصول",
"No HTML, CSS, or JavaScript content found.": "لم يتم العثور على محتوى HTML أو CSS أو JavaScript.",
"No inference engine with management support found": "لم يتم العثور على محرك استدلال يدعم الإدارة",
@ -1061,12 +1069,14 @@
"No models selected": "لم يتم اختيار نماذج",
"No Notes": "",
"No notes found": "",
"No prompts found": "",
"No results": "لا توجد نتائج",
"No results found": "لا توجد نتايج",
"No search query generated": "لم يتم إنشاء استعلام بحث",
"No source available": "لا يوجد مصدر متاح",
"No sources found": "",
"No suggestion prompts": "لا توجد مطالبات مقترحة",
"No tools found": "",
"No users were found.": "لم يتم العثور على مستخدمين.",
"No valves": "",
"No valves to update": "لا توجد صمامات للتحديث",
@ -1225,6 +1235,7 @@
"Public": "",
"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com \"{{searchValue}}\" أسحب من ",
"Pull a model from Ollama.com": "Ollama.com سحب الموديل من ",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "توجيه إنشاء الاستعلام",
"Querying": "",
@ -1376,6 +1387,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "اختر المعرفة",
"Select only one model to call": "اختر نموذجًا واحدًا فقط للاستدعاء",
"Select view": "",
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
"semantic": "",
"Send": "تم",
@ -1416,6 +1428,7 @@
"Share Chat": "مشاركة الدردشة",
"Share to Open WebUI Community": "OpenWebUI شارك في مجتمع",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "عرض",
@ -1483,6 +1496,7 @@
"System Instructions": "تعليمات النظام",
"System Prompt": "محادثة النظام",
"Table Mode": "",
"Tag": "",
"Tags": "",
"Tags Generation": "إنشاء الوسوم",
"Tags Generation Prompt": "توجيه إنشاء الوسوم",
@ -1594,6 +1608,7 @@
"Transformers": "Transformers",
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
"Trust Proxy Environment": "بيئة البروكسي الموثوقة",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "نموذج تحويل النص إلى كلام (TTS)",
"TTS Settings": "TTS اعدادات",
@ -1630,6 +1645,7 @@
"Upload directory": "رفع مجلد",
"Upload files": "رفع ملفات",
"Upload Files": "تحميل الملفات",
"Upload Model": "",
"Upload Pipeline": "رفع خط المعالجة",
"Upload Progress": "جاري التحميل",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "Добавяне на потребителска група",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "При промяна на тези настройки промените се прилагат за всички потребители.",
"admin": "админ",
"Admin": "Администратор",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "Създаване на група",
"Create Knowledge": "Създаване на знания",
"Create Model": "",
"Create new key": "Създаване на нов ключ",
"Create new secret key": "Създаване на нов секретен ключ",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "Създадено на",
"Created At": "Създадено на",
"Created by": "Създадено от",
"Created by you": "",
"CSV Import": "Импортиране на CSV",
"Ctrl+Enter to Send": "",
"Current Model": "Текущ модел",
@ -396,6 +399,7 @@
"Delete function?": "Изтриване на функцията?",
"Delete Message": "Изтриване на съобщение",
"Delete message?": "Изтриване на съобщението?",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "Изтриване на промпта?",
"delete this link": "Изтриване на този линк",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "Въведете подробности за себе си, за да ги запомнят вашите LLMs",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "Въведете низ за удостоверяване на API (напр. потребителско_име:парола)",
"Enter Application DN": "Въведете DN на приложението",
"Enter Application DN Password": "Въведете парола за DN на приложението",
@ -667,13 +674,9 @@
"Export chat (.json)": "Експортиране на чат (.json)",
"Export Chats": "Експортване на чатове",
"Export Config to JSON File": "Експортиране на конфигурацията в JSON файл",
"Export Functions": "Експортиране на функции",
"Export Models": "Експортиране на модели",
"Export Presets": "Експортиране на предварителни настройки",
"Export Prompt Suggestions": "",
"Export Prompts": "Експортване на промптове",
"Export to CSV": "Експортиране в CSV",
"Export Tools": "Експортиране на инструменти",
"Export Users": "",
"External": "",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "Неуспешно запазване на разговора",
"Failed to save models configuration": "Неуспешно запазване на конфигурацията на моделите",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "Фокусиране на чат вход",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "Папката е изтрита успешно",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "Тактилна обратна връзка",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": "Здравей, {{name}}",
"Help": "Помощ",
@ -841,14 +848,10 @@
"Import Chats": "Импортване на чатове",
"Import Config from JSON File": "Импортиране на конфигурация от JSON файл",
"Import From Link": "",
"Import Functions": "Импортиране на функции",
"Import Models": "Импортиране на модели",
"Import Notes": "",
"Import Presets": "Импортиране на предварителни настройки",
"Import Prompt Suggestions": "",
"Import Prompts": "Импортване на промптове",
"Import successful": "",
"Import Tools": "Импортиране на инструменти",
"Important Update": "Важна актуализация",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "Включи",
@ -876,6 +879,7 @@
"Invalid file format.": "Невалиден формат на файла.",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "Невалиден таг",
"is typing...": "пише...",
@ -1034,8 +1038,11 @@
"New Chat": "Нов чат",
"New Folder": "Нова папка",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "Нова бележка",
"New Password": "Нова парола",
"New Prompt": "",
"New Tool": "",
"new-channel": "нов-канал",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "Няма налично разстояние",
"No feedbacks found": "Не са намерени обратни връзки",
"No file selected": "Не е избран файл",
"No functions found": "",
"No groups with access, add a group to grant access": "Няма групи с достъп, добавете група, за да предоставите достъп",
"No HTML, CSS, or JavaScript content found.": "Не е намерено HTML, CSS или JavaScript съдържание.",
"No inference engine with management support found": "Не е намерен механизъм за извод с поддръжка на управлението",
@ -1061,12 +1069,14 @@
"No models selected": "Няма избрани модели",
"No Notes": "Няма бележки",
"No notes found": "",
"No prompts found": "",
"No results": "Няма намерени резултати",
"No results found": "Няма намерени резултати",
"No search query generated": "Не е генерирана заявка за търсене",
"No source available": "Няма наличен източник",
"No sources found": "",
"No suggestion prompts": "Няма предложени подсказки",
"No tools found": "",
"No users were found.": "Не са намерени потребители.",
"No valves": "",
"No valves to update": "Няма клапани за актуализиране",
@ -1225,6 +1235,7 @@
"Public": "Публично",
"Pull \"{{searchValue}}\" from Ollama.com": "Извади \"{{searchValue}}\" от Ollama.com",
"Pull a model from Ollama.com": "Издърпайте модела от Ollama.com",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "Промпт за генериране на запитвания",
"Querying": "",
@ -1372,6 +1383,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "Изберете знание",
"Select only one model to call": "Изберете само един модел за извикване",
"Select view": "",
"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
"semantic": "",
"Send": "Изпрати",
@ -1412,6 +1424,7 @@
"Share Chat": "Подели Чат",
"Share to Open WebUI Community": "Споделете с OpenWebUI Общността",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "Права за споделяне",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "Покажи",
@ -1479,6 +1492,7 @@
"System Instructions": "Системни инструкции",
"System Prompt": "Системен Промпт",
"Table Mode": "",
"Tag": "",
"Tags": "Тагове",
"Tags Generation": "Генериране на тагове",
"Tags Generation Prompt": "Промпт за генериране на тагове",
@ -1590,6 +1604,7 @@
"Transformers": "Трансформатори",
"Trouble accessing Ollama?": "Проблеми с достъпа до Ollama?",
"Trust Proxy Environment": "",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "TTS Модел",
"TTS Settings": "TTS Настройки",
@ -1626,6 +1641,7 @@
"Upload directory": "Качване на директория",
"Upload files": "Качване на файлове",
"Upload Files": "Качване на файлове",
"Upload Model": "",
"Upload Pipeline": "Качване на конвейер",
"Upload Progress": "Прогрес на качването",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "এই সেটিংগুলো পরিবর্তন করলে তা সব ইউজারের উপরেই প্রয়োগ করা হবে",
"admin": "এডমিন",
"Admin": "",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create Model": "",
"Create new key": "একটি নতুন কী তৈরি করুন",
"Create new secret key": "একটি নতুন সিক্রেট কী তৈরি করুন",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "নির্মানকাল",
"Created At": "নির্মানকাল",
"Created by": "",
"Created by you": "",
"CSV Import": "",
"Ctrl+Enter to Send": "",
"Current Model": "বর্তমান মডেল",
@ -396,6 +399,7 @@
"Delete function?": "",
"Delete Message": "",
"Delete message?": "",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "",
"delete this link": "এই লিংক মুছে ফেলুন",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "আপনার এলএলএমগুলি স্মরণ করার জন্য নিজের সম্পর্কে একটি বিশদ লিখুন",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Application DN": "",
"Enter Application DN Password": "",
@ -667,13 +674,9 @@
"Export chat (.json)": "",
"Export Chats": "চ্যাটগুলো এক্সপোর্ট করুন",
"Export Config to JSON File": "",
"Export Functions": "",
"Export Models": "রপ্তানি মডেল",
"Export Presets": "",
"Export Prompt Suggestions": "",
"Export Prompts": "প্রম্পটগুলো একপোর্ট করুন",
"Export to CSV": "",
"Export Tools": "",
"Export Users": "",
"External": "",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "কথোপকথন সংরক্ষণ করতে ব্যর্থ",
"Failed to save models configuration": "",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": "হ্যালো, {{name}}",
"Help": "সহায়তা",
@ -841,14 +848,10 @@
"Import Chats": "চ্যাটগুলি ইমপোর্ট করুন",
"Import Config from JSON File": "",
"Import From Link": "",
"Import Functions": "",
"Import Models": "মডেল আমদানি করুন",
"Import Notes": "",
"Import Presets": "",
"Import Prompt Suggestions": "",
"Import Prompts": "প্রম্পটগুলো ইমপোর্ট করুন",
"Import successful": "",
"Import Tools": "",
"Important Update": "গুরুত্বপূর্ণ আপডেট",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "",
@ -876,6 +879,7 @@
"Invalid file format.": "",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "অবৈধ ট্যাগ",
"is typing...": "",
@ -1034,8 +1038,11 @@
"New Chat": "নতুন চ্যাট",
"New Folder": "",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "",
"New Password": "নতুন পাসওয়ার্ড",
"New Prompt": "",
"New Tool": "",
"new-channel": "",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No functions found": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -1061,12 +1069,14 @@
"No models selected": "",
"No Notes": "",
"No notes found": "",
"No prompts found": "",
"No results": "কোন ফলাফল পাওয়া যায়নি",
"No results found": "কোন ফলাফল পাওয়া যায়নি",
"No search query generated": "কোনও অনুসন্ধান ক্যোয়ারী উত্পন্ন হয়নি",
"No source available": "কোন উৎস পাওয়া যায়নি",
"No sources found": "",
"No suggestion prompts": "কোনো প্রস্তাবিত প্রম্পট নেই",
"No tools found": "",
"No users were found.": "",
"No valves": "",
"No valves to update": "",
@ -1225,6 +1235,7 @@
"Public": "",
"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com থেকে \"{{searchValue}}\" টানুন",
"Pull a model from Ollama.com": "Ollama.com থেকে একটি টেনে আনুন আনুন",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "",
"Querying": "",
@ -1372,6 +1383,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "",
"Select only one model to call": "",
"Select view": "",
"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
"semantic": "",
"Send": "পাঠান",
@ -1412,6 +1424,7 @@
"Share Chat": "চ্যাট শেয়ার করুন",
"Share to Open WebUI Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "দেখান",
@ -1479,6 +1492,7 @@
"System Instructions": "",
"System Prompt": "সিস্টেম প্রম্পট",
"Table Mode": "",
"Tag": "",
"Tags": "",
"Tags Generation": "",
"Tags Generation Prompt": "",
@ -1590,6 +1604,7 @@
"Transformers": "",
"Trouble accessing Ollama?": "Ollama এক্সেস করতে সমস্যা হচ্ছে?",
"Trust Proxy Environment": "",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "",
"TTS Settings": "TTS সেটিংসমূহ",
@ -1626,6 +1641,7 @@
"Upload directory": "",
"Upload files": "",
"Upload Files": "ফাইল আপলোড করুন",
"Upload Model": "",
"Upload Pipeline": "",
"Upload Progress": "আপলোড হচ্ছে",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "བེད་སྤྱོད་མཁན་ཚོགས་པ་སྣོན་པ།",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "སྒྲིག་འགོད་འདི་དག་ལེགས་སྒྲིག་བྱས་ན་བེད་སྤྱོད་མཁན་ཡོངས་ལ་འགྱུར་བ་དེ་བཀོལ་སྤྱོད་བྱེད་ངེས།",
"admin": "དོ་དམ་པ།",
"Admin": "དོ་དམ་པ།",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "ཚོགས་པ་གསར་བཟོ།",
"Create Knowledge": "ཤེས་བྱ་གསར་བཟོ།",
"Create Model": "",
"Create new key": "ལྡེ་མིག་གསར་པ་བཟོ་བ།",
"Create new secret key": "གསང་བའི་ལྡེ་མིག་གསར་པ་བཟོ་བ།",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "གསར་བཟོ་བྱེད་དུས།",
"Created At": "གསར་བཟོ་བྱེད་དུས།",
"Created by": "གསར་བཟོ་བྱེད་མཁན།",
"Created by you": "",
"CSV Import": "CSV ནང་འདྲེན།",
"Ctrl+Enter to Send": "Ctrl+Enter གཏོང་བ།",
"Current Model": "ད་ལྟའི་དཔེ་དབྱིབས།",
@ -396,6 +399,7 @@
"Delete function?": "ལས་འགན་བསུབ་པ།?",
"Delete Message": "འཕྲིན་བསུབ་པ།",
"Delete message?": "འཕྲིན་བསུབ་པ།?",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "འགུལ་སློང་བསུབ་པ།?",
"delete this link": "སྦྲེལ་ཐག་འདི་བསུབ་པ།",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "ཁྱེད་ཀྱི་ LLMs ཡིས་ཕྱིར་དྲན་ཆེད་དུ་ཁྱེད་རང་གི་སྐོར་གྱི་ཞིབ་ཕྲ་ཞིག་འཇུག་པ།",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "api auth ཡིག་ཕྲེང་འཇུག་པ། (དཔེར་ན། username:password)",
"Enter Application DN": "Application DN འཇུག་པ།",
"Enter Application DN Password": "Application DN གསང་གྲངས་འཇུག་པ།",
@ -667,13 +674,9 @@
"Export chat (.json)": "ཁ་བརྡ་ཕྱིར་གཏོང་ (.json)",
"Export Chats": "ཁ་བརྡ་ཕྱིར་གཏོང་།",
"Export Config to JSON File": "སྒྲིག་འགོད་ JSON ཡིག་ཆར་ཕྱིར་གཏོང་།",
"Export Functions": "ལས་འགན་ཕྱིར་གཏོང་།",
"Export Models": "དཔེ་དབྱིབས་ཕྱིར་གཏོང་།",
"Export Presets": "སྔོན་སྒྲིག་ཕྱིར་གཏོང་།",
"Export Prompt Suggestions": "",
"Export Prompts": "འགུལ་སློང་ཕྱིར་གཏོང་།",
"Export to CSV": "CSV ལ་ཕྱིར་གཏོང་།",
"Export Tools": "ལག་ཆ་ཕྱིར་གཏོང་།",
"Export Users": "",
"External": "ཕྱི་རོལ།",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "སྦྱར་སྡེར་གྱི་ནང་དོན་ཀློག་མ་ཐུབ།",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "གླེང་མོལ་ཉར་ཚགས་བྱེད་མ་ཐུབ།",
"Failed to save models configuration": "དཔེ་དབྱིབས་སྒྲིག་འགོད་ཉར་ཚགས་བྱེད་མ་ཐུབ།",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "ཁ་བརྡའི་ནང་འཇུག་ལ་དམིགས་པ།",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "ཡིག་སྣོད་ལེགས་པར་བསུབས་ཟིན།",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "འདར་འཕྲུལ་གྱི་བསམ་འཆར།",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": "བཀྲ་ཤིས་བདེ་ལེགས། {{name}}",
"Help": "རོགས་རམ།",
@ -841,14 +848,10 @@
"Import Chats": "ཁ་བརྡ་ནང་འདྲེན།",
"Import Config from JSON File": "JSON ཡིག་ཆ་ནས་སྒྲིག་འགོད་ནང་འདྲེན།",
"Import From Link": "",
"Import Functions": "ལས་འགན་ནང་འདྲེན།",
"Import Models": "དཔེ་དབྱིབས་ནང་འདྲེན།",
"Import Notes": "",
"Import Presets": "སྔོན་སྒྲིག་ནང་འདྲེན།",
"Import Prompt Suggestions": "",
"Import Prompts": "འགུལ་སློང་ནང་འདྲེན།",
"Import successful": "",
"Import Tools": "ལག་ཆ་ནང་འདྲེན།",
"Important Update": "གལ་ཆེ་པའི་གསར་སྒྱུར་",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "ཚུད་པ།",
@ -876,6 +879,7 @@
"Invalid file format.": "ཡིག་ཆའི་བཀོད་པ་ནུས་མེད།",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "རྟགས་ནུས་མེད།",
"is typing...": "ཡིག་འབྲུ་རྒྱག་བཞིན་པ།...",
@ -1034,8 +1038,11 @@
"New Chat": "ཁ་བརྡ་གསར་པ།",
"New Folder": "ཡིག་སྣོད་གསར་པ།",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "",
"New Password": "གསང་གྲངས་གསར་པ།",
"New Prompt": "",
"New Tool": "",
"new-channel": "བགྲོ་གླེང་གསར་པ།",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "ཐག་རིང་ཚད་མེད།",
"No feedbacks found": "བསམ་འཆར་མ་རྙེད།",
"No file selected": "ཡིག་ཆ་གདམ་ག་མ་བྱས།",
"No functions found": "",
"No groups with access, add a group to grant access": "འཛུལ་སྤྱོད་ཡོད་པའི་ཚོགས་པ་མེད། འཛུལ་སྤྱོད་སྤྲོད་པར་ཚོགས་པ་ཞིག་སྣོན་པ།",
"No HTML, CSS, or JavaScript content found.": "HTML, CSS, ཡང་ན་ JavaScript གི་ནང་དོན་མ་རྙེད།",
"No inference engine with management support found": "དོ་དམ་རྒྱབ་སྐྱོར་ཡོད་པའི་དཔོག་རྩིས་འཕྲུལ་འཁོར་མ་རྙེད།",
@ -1061,12 +1069,14 @@
"No models selected": "དཔེ་དབྱིབས་གདམ་ག་མ་བྱས།",
"No Notes": "",
"No notes found": "",
"No prompts found": "",
"No results": "འབྲས་བུ་མ་རྙེད།",
"No results found": "འབྲས་བུ་མ་རྙེད།",
"No search query generated": "འཚོལ་བཤེར་འདྲི་བ་བཟོས་མེད།",
"No source available": "འབྱུང་ཁུངས་མེད།",
"No sources found": "",
"No suggestion prompts": "གསལ་འདེབས་མེད།",
"No tools found": "",
"No users were found.": "བེད་སྤྱོད་མཁན་མ་རྙེད།",
"No valves": "",
"No valves to update": "གསར་སྒྱུར་བྱེད་རྒྱུའི་ Valve མེད།",
@ -1225,6 +1235,7 @@
"Public": "སྤྱི་སྤྱོད།",
"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com ནས་ \"{{searchValue}}\" འཐེན་པ།",
"Pull a model from Ollama.com": "Ollama.com ནས་དཔེ་དབྱིབས་ཤིག་འཐེན་པ།",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "འདྲི་བ་བཟོ་སྐྲུན་གྱི་འགུལ་སློང་།",
"Querying": "",
@ -1371,6 +1382,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "ཤེས་བྱ་གདམ་པ།",
"Select only one model to call": "འབོད་པར་དཔེ་དབྱིབས་གཅིག་ཁོ་ན་གདམ་པ།",
"Select view": "",
"Selected model(s) do not support image inputs": "གདམ་ཟིན་པའི་དཔེ་དབྱིབས་(ཚོ)ས་པར་གྱི་ནང་འཇུག་ལ་རྒྱབ་སྐྱོར་མི་བྱེད།",
"semantic": "",
"Send": "གཏོང་བ།",
@ -1411,6 +1423,7 @@
"Share Chat": "ཁ་བརྡ་མཉམ་སྤྱོད།",
"Share to Open WebUI Community": "Open WebUI སྤྱི་ཚོགས་ལ་མཉམ་སྤྱོད།",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "མཉམ་སྤྱོད་དབང་ཚད།",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "སྟོན་པ།",
@ -1478,6 +1491,7 @@
"System Instructions": "མ་ལག་གི་ལམ་སྟོན།",
"System Prompt": "མ་ལག་གི་འགུལ་སློང་།",
"Table Mode": "",
"Tag": "",
"Tags": "རྟགས།",
"Tags Generation": "རྟགས་བཟོ་སྐྲུན།",
"Tags Generation Prompt": "རྟགས་བཟོ་སྐྲུན་གྱི་འགུལ་སློང་།",
@ -1589,6 +1603,7 @@
"Transformers": "Transformers",
"Trouble accessing Ollama?": "Ollama འཛུལ་སྤྱོད་སྐབས་དཀའ་ངལ་འཕྲད་དམ།",
"Trust Proxy Environment": "Proxy ཁོར་ཡུག་ལ་ཡིད་ཆེས།",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "TTS དཔེ་དབྱིབས།",
"TTS Settings": "TTS སྒྲིག་འགོད།",
@ -1625,6 +1640,7 @@
"Upload directory": "སྤར་བའི་ཐོ་འཚོལ།",
"Upload files": "ཡིག་ཆ་སྤར་བ།",
"Upload Files": "ཡིག་ཆ་སྤར་བ།",
"Upload Model": "",
"Upload Pipeline": "རྒྱུ་ལམ་སྤར་བ།",
"Upload Progress": "སྤར་བའི་འཕེལ་རིམ།",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "Podešavanje će se primijeniti univerzalno na sve korisnike.",
"admin": "administrator",
"Admin": "Admin",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create Model": "",
"Create new key": "Stvori novi ključ",
"Create new secret key": "Stvori novi tajni ključ",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "Stvoreno",
"Created At": "Stvoreno",
"Created by": "",
"Created by you": "",
"CSV Import": "",
"Ctrl+Enter to Send": "",
"Current Model": "Trenutni model",
@ -396,6 +399,7 @@
"Delete function?": "",
"Delete Message": "",
"Delete message?": "",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "",
"delete this link": "izbriši ovu vezu",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "Unesite pojedinosti o sebi da bi učitali memoriju u LLM",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Application DN": "",
"Enter Application DN Password": "",
@ -667,13 +674,9 @@
"Export chat (.json)": "Izvoz četa (.json)",
"Export Chats": "Izvoz razgovora",
"Export Config to JSON File": "",
"Export Functions": "",
"Export Models": "Izvoz modela",
"Export Presets": "",
"Export Prompt Suggestions": "",
"Export Prompts": "Izvoz prompta",
"Export to CSV": "",
"Export Tools": "Izvoz alata",
"Export Users": "",
"External": "",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "Neuspješno čitanje sadržaja međuspremnika",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "Neuspješno spremanje razgovora",
"Failed to save models configuration": "",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "Fokusiraj unos razgovora",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": "Bok, {{name}}",
"Help": "Pomoć",
@ -841,14 +848,10 @@
"Import Chats": "Uvoz razgovora",
"Import Config from JSON File": "",
"Import From Link": "",
"Import Functions": "",
"Import Models": "Uvoz modela",
"Import Notes": "",
"Import Presets": "",
"Import Prompt Suggestions": "",
"Import Prompts": "Uvoz prompta",
"Import successful": "",
"Import Tools": "Uvoz alata",
"Important Update": "Važno ažuriranje",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "",
@ -876,6 +879,7 @@
"Invalid file format.": "",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "Nevažeća oznaka",
"is typing...": "",
@ -1034,8 +1038,11 @@
"New Chat": "Novi razgovor",
"New Folder": "",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "",
"New Password": "Nova lozinka",
"New Prompt": "",
"New Tool": "",
"new-channel": "",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No functions found": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -1061,12 +1069,14 @@
"No models selected": "",
"No Notes": "",
"No notes found": "",
"No prompts found": "",
"No results": "Nema rezultata",
"No results found": "Nema rezultata",
"No search query generated": "Nije generiran upit za pretraživanje",
"No source available": "Nema dostupnog izvora",
"No sources found": "",
"No suggestion prompts": "Nema predloženih prompta",
"No tools found": "",
"No users were found.": "",
"No valves": "",
"No valves to update": "",
@ -1225,6 +1235,7 @@
"Public": "",
"Pull \"{{searchValue}}\" from Ollama.com": "Povucite \"{{searchValue}}\" s Ollama.com",
"Pull a model from Ollama.com": "Povucite model s Ollama.com",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "",
"Querying": "",
@ -1373,6 +1384,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "",
"Select only one model to call": "Odaberite samo jedan model za poziv",
"Select view": "",
"Selected model(s) do not support image inputs": "Odabrani modeli ne podržavaju unose slika",
"semantic": "",
"Send": "Pošalji",
@ -1413,6 +1425,7 @@
"Share Chat": "Podijeli razgovor",
"Share to Open WebUI Community": "Podijeli u OpenWebUI zajednici",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "Pokaži",
@ -1480,6 +1493,7 @@
"System Instructions": "",
"System Prompt": "Sistemski prompt",
"Table Mode": "",
"Tag": "",
"Tags": "",
"Tags Generation": "",
"Tags Generation Prompt": "",
@ -1591,6 +1605,7 @@
"Transformers": "",
"Trouble accessing Ollama?": "Problemi s pristupom Ollama?",
"Trust Proxy Environment": "",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "TTS model",
"TTS Settings": "TTS postavke",
@ -1627,6 +1642,7 @@
"Upload directory": "",
"Upload files": "",
"Upload Files": "Prijenos datoteka",
"Upload Model": "",
"Upload Pipeline": "Prijenos kanala",
"Upload Progress": "Napredak učitavanja",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "Afegir grup d'usuaris",
"Additional Config": "Configuració addicional",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "Opcions de configuració addicionals per al marcador. Hauria de ser una cadena JSON amb parelles clau-valor. Per exemple, '{\"key\": \"value\"}'. Les claus compatibles inclouen: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "Si ajustes aquesta preferència, els canvis s'aplicaran de manera universal a tots els usuaris.",
"admin": "administrador",
"Admin": "Administrador",
@ -343,6 +344,7 @@
"Create Folder": "Crear carpeta",
"Create Group": "Crear grup",
"Create Knowledge": "Crear Coneixement",
"Create Model": "",
"Create new key": "Crear una nova clau",
"Create new secret key": "Crear una nova clau secreta",
"Create Note": "Crea nota",
@ -350,6 +352,7 @@
"Created at": "Creat el",
"Created At": "Creat el",
"Created by": "Creat per",
"Created by you": "",
"CSV Import": "Importar CSV",
"Ctrl+Enter to Send": "Ctrl+Enter per enviar",
"Current Model": "Model actual",
@ -396,6 +399,7 @@
"Delete function?": "Eliminar funció?",
"Delete Message": "Eliminar el missatge",
"Delete message?": "Eliminar el missatge?",
"Delete Model": "",
"Delete note?": "Eliminar la nota?",
"Delete prompt?": "Eliminar indicació?",
"delete this link": "Eliminar aquest enllaç",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "Introdueix un detall sobre tu què els teus models de llenguatge puguin recordar",
"Enter a title for the pending user info overlay. Leave empty for default.": "Introdueix un títol per a la finestra de dades d'usuari pendent. Deixa buit per a valor per defecte.",
"Enter a watermark for the response. Leave empty for none.": "Introdueix una marca d'aigua per a la resposta. Deixa-ho buit per a cap.",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "Entra la cadena d'autenticació api (p. ex. nom d'usuari:contrasenya)",
"Enter Application DN": "Introdueix el DN d'aplicació",
"Enter Application DN Password": "Introdueix la contrasenya del DN d'aplicació",
@ -667,13 +674,9 @@
"Export chat (.json)": "Exportar el xat (.json)",
"Export Chats": "Exportar els xats",
"Export Config to JSON File": "Exportar la configuració a un arxiu JSON",
"Export Functions": "Exportar funcions",
"Export Models": "Exportar els models",
"Export Presets": "Exportar les configuracions",
"Export Prompt Suggestions": "Exportar els suggeriments d'indicació",
"Export Prompts": "Exportar les indicacions",
"Export to CSV": "Exportar a CSV",
"Export Tools": "Exportar les eines",
"Export Users": "Exportar els usuaris",
"External": "Extern",
"External Document Loader URL required.": "Fa falta la URL per a Document Loader",
@ -698,6 +701,7 @@
"Failed to load file content.": "No s'ha pogut carregar el contingut del fitxer",
"Failed to move chat": "No s'ha pogut moure el xat",
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
"Failed to render diagram": "",
"Failed to save connections": "No s'han pogut desar les connexions",
"Failed to save conversation": "No s'ha pogut desar la conversa",
"Failed to save models configuration": "No s'ha pogut desar la configuració dels models",
@ -731,6 +735,7 @@
"Firecrawl API Key": "Clau API de Firecrawl",
"Floating Quick Actions": "Accions ràpides flotants",
"Focus chat input": "Estableix el focus a l'entrada del xat",
"Folder": "",
"Folder Background Image": "Imatge del fons de la carpeta",
"Folder deleted successfully": "Carpeta eliminada correctament",
"Folder Name": "Nom de la carpeta",
@ -800,6 +805,8 @@
"H2": "H2",
"H3": "H3",
"Haptic Feedback": "Retorn hàptic",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "Alçada",
"Hello, {{name}}": "Hola, {{name}}",
"Help": "Ajuda",
@ -841,14 +848,10 @@
"Import Chats": "Importar xats",
"Import Config from JSON File": "Importar la configuració des d'un arxiu JSON",
"Import From Link": "Importar des d'un enllaç",
"Import Functions": "Importar funcions",
"Import Models": "Importar models",
"Import Notes": "Importar nota",
"Import Presets": "Importar configuracions",
"Import Prompt Suggestions": "Importar suggeriments d'indicacions",
"Import Prompts": "Importar indicacions",
"Import successful": "",
"Import Tools": "Importar eines",
"Important Update": "Actualització important",
"In order to force OCR, performing OCR must be enabled.": "Per forçar l'OCR, cal activar l'OCR.",
"Include": "Incloure",
@ -876,6 +879,7 @@
"Invalid file format.": "Format d'arxiu no vàlid.",
"Invalid JSON file": "Arxiu JSON no vàlid",
"Invalid JSON format for ComfyUI Workflow.": "Arxiu JSON de Workflow ComfyUI no vàlid.",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "Format JSON no vàlid a la configuració addicional",
"Invalid Tag": "Etiqueta no vàlida",
"is typing...": "està escrivint...",
@ -1034,8 +1038,11 @@
"New Chat": "Nou xat",
"New Folder": "Nova carpeta",
"New Function": "Nova funció",
"New Knowledge": "",
"New Model": "",
"New Note": "Nova nota",
"New Password": "Nova contrasenya",
"New Prompt": "",
"New Tool": "Nova eina",
"new-channel": "nou-canal",
"Next message": "Missatge següent",
@ -1051,6 +1058,7 @@
"No distance available": "No hi ha distància disponible",
"No feedbacks found": "No s'han trobat comentaris",
"No file selected": "No s'ha escollit cap fitxer",
"No functions found": "",
"No groups with access, add a group to grant access": "No hi ha cap grup amb accés, afegeix un grup per concedir accés",
"No HTML, CSS, or JavaScript content found.": "No s'ha trobat contingut HTML, CSS o JavaScript.",
"No inference engine with management support found": "No s'ha trobat un motor d'inferència amb suport de gestió",
@ -1061,12 +1069,14 @@
"No models selected": "No s'ha seleccionat cap model",
"No Notes": "No hi ha notes",
"No notes found": "No s'han trobat notes",
"No prompts found": "",
"No results": "No s'han trobat resultats",
"No results found": "No s'han trobat resultats",
"No search query generated": "No s'ha generat cap consulta",
"No source available": "Sense font disponible",
"No sources found": "No s'han trobat fonts",
"No suggestion prompts": "Cap prompt suggerit",
"No tools found": "",
"No users were found.": "No s'han trobat usuaris",
"No valves": "No hi ha valves",
"No valves to update": "No hi ha cap Valve per actualitzar",
@ -1225,6 +1235,7 @@
"Public": "Públic",
"Pull \"{{searchValue}}\" from Ollama.com": "Obtenir \"{{searchValue}}\" de Ollama.com",
"Pull a model from Ollama.com": "Obtenir un model d'Ollama.com",
"Pull Model": "",
"pypdfium2": "pypdfium2",
"Query Generation Prompt": "Indicació per a generació de consulta",
"Querying": "Consultes",
@ -1373,6 +1384,7 @@
"Select how to split message text for TTS requests": "Seleccionar com separar un missatge per a peticions TTS",
"Select Knowledge": "Seleccionar coneixement",
"Select only one model to call": "Seleccionar només un model per trucar",
"Select view": "",
"Selected model(s) do not support image inputs": "El(s) model(s) seleccionats no admeten l'entrada d'imatges",
"semantic": "semàntic",
"Send": "Enviar",
@ -1413,6 +1425,7 @@
"Share Chat": "Compartir el xat",
"Share to Open WebUI Community": "Compartir amb la comunitat OpenWebUI",
"Share your background and interests": "Compartir la teva informació i interessos",
"Shared with you": "",
"Sharing Permissions": "Compartir els permisos",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "Les dreceres de teclat amb un asterisc (*) són situacionals i només actives sota condicions específiques.",
"Show": "Mostrar",
@ -1480,6 +1493,7 @@
"System Instructions": "Instruccions de sistema",
"System Prompt": "Indicació del Sistema",
"Table Mode": "Mode de taula",
"Tag": "",
"Tags": "Etiquetes",
"Tags Generation": "Generació d'etiquetes",
"Tags Generation Prompt": "Indicació per a la generació d'etiquetes",
@ -1591,6 +1605,7 @@
"Transformers": "Transformadors",
"Trouble accessing Ollama?": "Problemes en accedir a Ollama?",
"Trust Proxy Environment": "Confiar en l'entorn proxy",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "Tornar a intentar-ho",
"TTS Model": "Model TTS",
"TTS Settings": "Preferències de TTS",
@ -1627,6 +1642,7 @@
"Upload directory": "Pujar directori",
"Upload files": "Pujar fitxers",
"Upload Files": "Pujar fitxers",
"Upload Model": "",
"Upload Pipeline": "Pujar una Pipeline",
"Upload Progress": "Progrés de càrrega",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "Progrés de la pujada: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",

View file

@ -65,6 +65,7 @@
"Add User Group": "",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "Ang pag-adjust niini nga mga setting magamit ang mga pagbag-o sa tanan nga tiggamit.",
"admin": "Administrator",
"Admin": "",
@ -343,6 +344,7 @@
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create Model": "",
"Create new key": "",
"Create new secret key": "",
"Create Note": "",
@ -350,6 +352,7 @@
"Created at": "Gihimo ang",
"Created At": "",
"Created by": "",
"Created by you": "",
"CSV Import": "",
"Ctrl+Enter to Send": "",
"Current Model": "Kasamtangang modelo",
@ -396,6 +399,7 @@
"Delete function?": "",
"Delete Message": "",
"Delete message?": "",
"Delete Model": "",
"Delete note?": "",
"Delete prompt?": "",
"delete this link": "",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Application DN": "",
"Enter Application DN Password": "",
@ -667,13 +674,9 @@
"Export chat (.json)": "",
"Export Chats": "I-export ang mga chat",
"Export Config to JSON File": "",
"Export Functions": "",
"Export Models": "",
"Export Presets": "",
"Export Prompt Suggestions": "",
"Export Prompts": "Export prompts",
"Export to CSV": "",
"Export Tools": "",
"Export Users": "",
"External": "",
"External Document Loader URL required.": "",
@ -698,6 +701,7 @@
"Failed to load file content.": "",
"Failed to move chat": "",
"Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard",
"Failed to render diagram": "",
"Failed to save connections": "",
"Failed to save conversation": "Napakyas sa pagtipig sa panag-istorya",
"Failed to save models configuration": "",
@ -731,6 +735,7 @@
"Firecrawl API Key": "",
"Floating Quick Actions": "",
"Focus chat input": "Pag-focus sa entry sa diskusyon",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "",
"Folder Name": "",
@ -800,6 +805,8 @@
"H2": "",
"H3": "",
"Haptic Feedback": "",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": "Maayong buntag, {{name}}",
"Help": "",
@ -841,14 +848,10 @@
"Import Chats": "Import nga mga chat",
"Import Config from JSON File": "",
"Import From Link": "",
"Import Functions": "",
"Import Models": "",
"Import Notes": "",
"Import Presets": "",
"Import Prompt Suggestions": "",
"Import Prompts": "Import prompt",
"Import successful": "",
"Import Tools": "",
"Important Update": "Mahinungdanong update",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "",
@ -876,6 +879,7 @@
"Invalid file format.": "",
"Invalid JSON file": "",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "",
"is typing...": "",
@ -1034,8 +1038,11 @@
"New Chat": "Bag-ong diskusyon",
"New Folder": "",
"New Function": "",
"New Knowledge": "",
"New Model": "",
"New Note": "",
"New Password": "Bag-ong Password",
"New Prompt": "",
"New Tool": "",
"new-channel": "",
"Next message": "",
@ -1051,6 +1058,7 @@
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No functions found": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -1061,12 +1069,14 @@
"No models selected": "",
"No Notes": "",
"No notes found": "",
"No prompts found": "",
"No results": "Walay resulta",
"No results found": "",
"No search query generated": "",
"No source available": "Walay tinubdan nga anaa",
"No sources found": "",
"No suggestion prompts": "Walay gisugyot nga prompt",
"No tools found": "",
"No users were found.": "",
"No valves": "",
"No valves to update": "",
@ -1225,6 +1235,7 @@
"Public": "",
"Pull \"{{searchValue}}\" from Ollama.com": "",
"Pull a model from Ollama.com": "Pagkuha ug template gikan sa Ollama.com",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "",
"Querying": "",
@ -1372,6 +1383,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "",
"Select only one model to call": "",
"Select view": "",
"Selected model(s) do not support image inputs": "",
"semantic": "",
"Send": "",
@ -1412,6 +1424,7 @@
"Share Chat": "",
"Share to Open WebUI Community": "Ipakigbahin sa komunidad sa OpenWebUI",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "",
"Show": "Pagpakita",
@ -1479,6 +1492,7 @@
"System Instructions": "",
"System Prompt": "Madasig nga Sistema",
"Table Mode": "",
"Tag": "",
"Tags": "",
"Tags Generation": "",
"Tags Generation Prompt": "",
@ -1590,6 +1604,7 @@
"Transformers": "",
"Trouble accessing Ollama?": "Adunay mga problema sa pag-access sa Ollama?",
"Trust Proxy Environment": "",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "",
"TTS Settings": "Mga Setting sa TTS",
@ -1626,6 +1641,7 @@
"Upload directory": "",
"Upload files": "",
"Upload Files": "",
"Upload Model": "",
"Upload Pipeline": "",
"Upload Progress": "Pag-uswag sa Pag-upload",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

View file

@ -65,6 +65,7 @@
"Add User Group": "Přidat skupinu uživatelů",
"Additional Config": "Dodatečná konfigurace",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "Další možnosti konfigurace pro marker. Měl by to být řetězec JSON s páry klíč-hodnota. Například: '{\"key\": \"value\"}'. Podporované klíče zahrnují: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "Úprava těchto nastavení se projeví u všech uživatelů.",
"admin": "administrátor",
"Admin": "Administrátor",
@ -343,6 +344,7 @@
"Create Folder": "Vytvořit složku",
"Create Group": "Vytvořit skupinu",
"Create Knowledge": "Vytvořit znalost",
"Create Model": "",
"Create new key": "Vytvořit nový klíč",
"Create new secret key": "Vytvořit nový tajný klíč",
"Create Note": "Vytvořit poznámku",
@ -350,6 +352,7 @@
"Created at": "Vytvořeno",
"Created At": "Vytvořeno",
"Created by": "Vytvořil/a",
"Created by you": "",
"CSV Import": "Import z CSV",
"Ctrl+Enter to Send": "Ctrl+Enter pro odeslání",
"Current Model": "Aktuální model",
@ -396,6 +399,7 @@
"Delete function?": "Smazat funkci?",
"Delete Message": "Smazat zprávu",
"Delete message?": "Smazat zprávu?",
"Delete Model": "",
"Delete note?": "Smazat poznámku?",
"Delete prompt?": "Smazat instrukci?",
"delete this link": "smazat tento odkaz",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "Zadejte podrobnost o sobě, kterou si vaše LLM mají pamatovat.",
"Enter a title for the pending user info overlay. Leave empty for default.": "Zadejte název pro překryvnou vrstvu s informacemi o čekajícím uživateli. Pro výchozí ponechte prázdné.",
"Enter a watermark for the response. Leave empty for none.": "Zadejte vodoznak pro odpověď. Pro žádný vodoznak ponechte prázdné.",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "Zadejte ověřovací řetězec API (např. uzivatelske_jmeno:heslo)",
"Enter Application DN": "Zadejte Application DN",
"Enter Application DN Password": "Zadejte heslo pro Application DN",
@ -667,13 +674,9 @@
"Export chat (.json)": "Exportovat konverzaci (.json)",
"Export Chats": "Exportovat konverzace",
"Export Config to JSON File": "Exportovat konfiguraci do souboru JSON",
"Export Functions": "Exportovat funkce",
"Export Models": "Exportovat modely",
"Export Presets": "Exportovat předvolby",
"Export Prompt Suggestions": "Exportovat návrhy instrukcí",
"Export Prompts": "Exportovat instrukce",
"Export to CSV": "Exportovat do CSV",
"Export Tools": "Exportovat nástroje",
"Export Users": "Exportovat uživatele",
"External": "Externí",
"External Document Loader URL required.": "Je vyžadována URL externího zavaděče dokumentů.",
@ -698,6 +701,7 @@
"Failed to load file content.": "Nepodařilo se načíst obsah souboru.",
"Failed to move chat": "",
"Failed to read clipboard contents": "Nepodařilo se přečíst obsah schránky",
"Failed to render diagram": "",
"Failed to save connections": "Nepodařilo se uložit připojení",
"Failed to save conversation": "Nepodařilo se uložit konverzaci",
"Failed to save models configuration": "Nepodařilo se uložit konfiguraci modelů",
@ -731,6 +735,7 @@
"Firecrawl API Key": "API klíč pro Firecrawl",
"Floating Quick Actions": "Plovoucí rychlé akce",
"Focus chat input": "Zaměřit vstupní pole konverzace",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "Složka byla úspěšně smazána",
"Folder Name": "Název složky",
@ -800,6 +805,8 @@
"H2": "H2",
"H3": "H3",
"Haptic Feedback": "Haptická odezva",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "Výška",
"Hello, {{name}}": "Dobrý den, {{name}}",
"Help": "Nápověda",
@ -841,14 +848,10 @@
"Import Chats": "Importovat konverzace",
"Import Config from JSON File": "Importovat konfiguraci ze souboru JSON",
"Import From Link": "Importovat z odkazu",
"Import Functions": "Importovat funkce",
"Import Models": "Importovat modely",
"Import Notes": "Importovat poznámky",
"Import Presets": "Importovat předvolby",
"Import Prompt Suggestions": "Importovat návrhy instrukcí",
"Import Prompts": "Importovat instrukce",
"Import successful": "",
"Import Tools": "Importovat nástroje",
"Important Update": "Důležitá aktualizace",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "Zahrnout",
@ -876,6 +879,7 @@
"Invalid file format.": "Neplatný formát souboru.",
"Invalid JSON file": "Neplatný soubor JSON",
"Invalid JSON format for ComfyUI Workflow.": "Neplatný formát JSON pro pracovní postup ComfyUI.",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "Neplatný formát JSON v dodatečné konfiguraci",
"Invalid Tag": "Neplatný štítek",
"is typing...": "píše...",
@ -1034,8 +1038,11 @@
"New Chat": "Nová konverzace",
"New Folder": "Nová složka",
"New Function": "Nová funkce",
"New Knowledge": "",
"New Model": "",
"New Note": "Nová poznámka",
"New Password": "Nové heslo",
"New Prompt": "",
"New Tool": "Nový nástroj",
"new-channel": "novy-kanal",
"Next message": "Další zpráva",
@ -1051,6 +1058,7 @@
"No distance available": "Vzdálenost není k dispozici",
"No feedbacks found": "Nebyla nalezena žádná zpětná vazba",
"No file selected": "Nebyl vybrán žádný soubor",
"No functions found": "",
"No groups with access, add a group to grant access": "Žádné skupiny s přístupem, přidejte skupinu pro udělení přístupu",
"No HTML, CSS, or JavaScript content found.": "Nebyl nalezen žádný obsah HTML, CSS ani JavaScriptu.",
"No inference engine with management support found": "Nebyl nalezeno žádné inferenční jádro s podporou správy",
@ -1061,12 +1069,14 @@
"No models selected": "Nebyly vybrány žádné modely",
"No Notes": "Žádné poznámky",
"No notes found": "",
"No prompts found": "",
"No results": "Nebyly nalezeny žádné výsledky",
"No results found": "Nebyly nalezeny žádné výsledky",
"No search query generated": "Nebyl vygenerován žádný vyhledávací dotaz.",
"No source available": "Není k dispozici žádný zdroj.",
"No sources found": "",
"No suggestion prompts": "Žádné návrhy promptů",
"No tools found": "",
"No users were found.": "Nebyli nalezeni žádní uživatelé.",
"No valves": "Žádné ventily",
"No valves to update": "Žádné ventily k aktualizaci",
@ -1225,6 +1235,7 @@
"Public": "Veřejné",
"Pull \"{{searchValue}}\" from Ollama.com": "Stáhnout \"{{searchValue}}\" z Ollama.com",
"Pull a model from Ollama.com": "Stáhnout model z Ollama.com",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "Instrukce pro generování dotazu",
"Querying": "",
@ -1374,6 +1385,7 @@
"Select how to split message text for TTS requests": "Vyberte, jak dělit text zprávy pro požadavky TTS",
"Select Knowledge": "Vybrat znalosti",
"Select only one model to call": "Vyberte pouze jeden model k volání",
"Select view": "",
"Selected model(s) do not support image inputs": "Vybraný model (modely) nepodporuje obrazové vstupy.",
"semantic": "sémantický",
"Send": "Odeslat",
@ -1414,6 +1426,7 @@
"Share Chat": "Sdílet konverzaci",
"Share to Open WebUI Community": "Sdílet s komunitou Open WebUI",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "Oprávnění pro sdílení",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "Zkratky s hvězdičkou (*) jsou situační a aktivní pouze za specifických podmínek.",
"Show": "Zobrazit",
@ -1481,6 +1494,7 @@
"System Instructions": "Systémové instrukce",
"System Prompt": "Systémové instrukce",
"Table Mode": "",
"Tag": "",
"Tags": "Štítky",
"Tags Generation": "Generování štítků",
"Tags Generation Prompt": "Instrukce pro generování štítků",
@ -1592,6 +1606,7 @@
"Transformers": "Transformers",
"Trouble accessing Ollama?": "Máte potíže s přístupem k Ollama?",
"Trust Proxy Environment": "Důvěřovat prostředí proxy",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "Zkusit znovu",
"TTS Model": "Model TTS",
"TTS Settings": "Nastavení TTS",
@ -1628,6 +1643,7 @@
"Upload directory": "Adresář pro nahrávání",
"Upload files": "Nahrát soubory",
"Upload Files": "Nahrát soubory",
"Upload Model": "",
"Upload Pipeline": "Nahrát potrubí",
"Upload Progress": "Průběh nahrávání",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "Průběh nahrávání: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",

View file

@ -65,6 +65,7 @@
"Add User Group": "Tilføj Brugergruppe",
"Additional Config": "",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
"Additional Parameters": "",
"Adjusting these settings will apply changes universally to all users.": "Ændringer af disse indstillinger har konsekvenser for alle brugere.",
"admin": "administrator",
"Admin": "Administrator",
@ -343,6 +344,7 @@
"Create Folder": "Opret mappe",
"Create Group": "Opret gruppe",
"Create Knowledge": "Opret Viden",
"Create Model": "",
"Create new key": "Opret en ny nøgle",
"Create new secret key": "Opret en ny hemmelig nøgle",
"Create Note": "Opret note",
@ -350,6 +352,7 @@
"Created at": "Oprettet",
"Created At": "Oprettet",
"Created by": "Oprettet af",
"Created by you": "",
"CSV Import": "Importer CSV",
"Ctrl+Enter to Send": "Ctrl+Enter til at sende",
"Current Model": "Nuværende model",
@ -396,6 +399,7 @@
"Delete function?": "Slet funktion?",
"Delete Message": "Slet besked",
"Delete message?": "Slet besked?",
"Delete Model": "",
"Delete note?": "Slet note?",
"Delete prompt?": "Slet prompt?",
"delete this link": "slet dette link",
@ -523,6 +527,9 @@
"Enter a detail about yourself for your LLMs to recall": "Indtast en detalje om dig selv, som dine LLMs kan huske",
"Enter a title for the pending user info overlay. Leave empty for default.": "Indtast en titel til afventende bruger informations overlay. Lad være tom for standard.",
"Enter a watermark for the response. Leave empty for none.": "Indtast et vandmærke til svaret. Lad være tom for ingen.",
"Enter additional headers in JSON format": "",
"Enter additional headers in JSON format (e.g. {{'{{\"X-Custom-Header\": \"value\"}}'}})": "",
"Enter additional parameters in JSON format": "",
"Enter api auth string (e.g. username:password)": "Indtast api-godkendelsesstreng (f.eks. brugernavn:adgangskode)",
"Enter Application DN": "Indtast Application DN",
"Enter Application DN Password": "Indtast Application DN Password",
@ -667,13 +674,9 @@
"Export chat (.json)": "Eksportér chat (.json)",
"Export Chats": "Eksportér chats",
"Export Config to JSON File": "Eksportér konfiguration til JSON-fil",
"Export Functions": "Eksportér funktioner",
"Export Models": "Eksportér modeller",
"Export Presets": "Eksportér indstillinger",
"Export Prompt Suggestions": "Eksportér prompt-forslag",
"Export Prompts": "Eksportér prompts",
"Export to CSV": "Eksportér til CSV",
"Export Tools": "Eksportér værktøjer",
"Export Users": "",
"External": "Ekstern",
"External Document Loader URL required.": "External Dokument Loader URL påkrævet.",
@ -698,6 +701,7 @@
"Failed to load file content.": "Kunne ikke indlæse filindhold.",
"Failed to move chat": "",
"Failed to read clipboard contents": "Kunne ikke læse indholdet af udklipsholderen",
"Failed to render diagram": "",
"Failed to save connections": "Kunne ikke gemme forbindelser",
"Failed to save conversation": "Kunne ikke gemme samtalen",
"Failed to save models configuration": "Kunne ikke gemme modeller konfiguration",
@ -731,6 +735,7 @@
"Firecrawl API Key": "Firecrawl API nøgle",
"Floating Quick Actions": "",
"Focus chat input": "Fokuser på chatinput",
"Folder": "",
"Folder Background Image": "",
"Folder deleted successfully": "Mappe fjernet.",
"Folder Name": "Mappenavn",
@ -800,6 +805,8 @@
"H2": "H2",
"H3": "H3",
"Haptic Feedback": "Haptisk feedback",
"Headers": "",
"Headers must be a valid JSON object": "",
"Height": "",
"Hello, {{name}}": "Hej {{name}}",
"Help": "Hjælp",
@ -841,14 +848,10 @@
"Import Chats": "Importer chats",
"Import Config from JSON File": "Importer konfiguration fra JSON-fil",
"Import From Link": "Importer fra et link",
"Import Functions": "Importer funktioner",
"Import Models": "Importer modeller",
"Import Notes": "Importer noter",
"Import Presets": "Importer Presets",
"Import Prompt Suggestions": "Importer prompt forslag",
"Import Prompts": "Importer prompts",
"Import successful": "",
"Import Tools": "Importer værktøjer",
"Important Update": "Vigtig opdatering",
"In order to force OCR, performing OCR must be enabled.": "",
"Include": "Inkluder",
@ -876,6 +879,7 @@
"Invalid file format.": "Ugyldigt filformat.",
"Invalid JSON file": "Ugyldig JSON fil",
"Invalid JSON format for ComfyUI Workflow.": "",
"Invalid JSON format for Parameters": "",
"Invalid JSON format in Additional Config": "",
"Invalid Tag": "Ugyldigt tag",
"is typing...": "er i gang med at skrive...",
@ -1034,8 +1038,11 @@
"New Chat": "Ny chat",
"New Folder": "Ny mappe",
"New Function": "Ny funktion",
"New Knowledge": "",
"New Model": "",
"New Note": "Ny note",
"New Password": "Ny adgangskode",
"New Prompt": "",
"New Tool": "Nyt værktøj",
"new-channel": "ny-kanal",
"Next message": "Næste besked",
@ -1051,6 +1058,7 @@
"No distance available": "Ingen afstand tilgængelig",
"No feedbacks found": "Ingen feedback fundet",
"No file selected": "Ingen fil valgt",
"No functions found": "",
"No groups with access, add a group to grant access": "Ingen grupper med adgang, tilføj en gruppe for at give adgang",
"No HTML, CSS, or JavaScript content found.": "Intet HTML-, CSS- eller JavaScript-indhold fundet.",
"No inference engine with management support found": "Ingen inference-engine med støtte til administration fundet",
@ -1061,12 +1069,14 @@
"No models selected": "Ingen modeller valgt",
"No Notes": "Ingen noter",
"No notes found": "",
"No prompts found": "",
"No results": "Ingen resultater fundet",
"No results found": "Ingen resultater fundet",
"No search query generated": "Ingen søgeforespørgsel genereret",
"No source available": "Ingen kilde tilgængelig",
"No sources found": "",
"No suggestion prompts": "Ingen forslagsprompter",
"No tools found": "",
"No users were found.": "Ingen brugere blev fundet.",
"No valves": "",
"No valves to update": "Ingen ventiler at opdatere",
@ -1225,9 +1235,10 @@
"Public": "Offentlig",
"Pull \"{{searchValue}}\" from Ollama.com": "Hent \"{{searchValue}}\" fra Ollama.com",
"Pull a model from Ollama.com": "Hent en model fra Ollama.com",
"Pull Model": "",
"pypdfium2": "",
"Query Generation Prompt": "Forespørgsel genererings prompt",
"Querying": "",
"Querying": "Undersøger",
"Quick Actions": "",
"RAG Template": "RAG-skabelon",
"Rating": "Rating",
@ -1289,10 +1300,10 @@
"RESULT": "Resultat",
"Retrieval": "Hentning",
"Retrieval Query Generation": "Hentnings forespørgsel generering",
"Retrieved {{count}} sources": "",
"Retrieved {{count}} sources": "Fandt en kildehenvisning",
"Retrieved {{count}} sources_one": "",
"Retrieved {{count}} sources_other": "",
"Retrieved 1 source": "",
"Retrieved 1 source": "Fandt en kildehenvisning",
"Rich Text Input for Chat": "Rich text input til chat",
"RK": "RK",
"Role": "Rolle",
@ -1372,6 +1383,7 @@
"Select how to split message text for TTS requests": "",
"Select Knowledge": "Vælg viden",
"Select only one model to call": "Vælg kun én model at kalde",
"Select view": "",
"Selected model(s) do not support image inputs": "Valgte model(ler) understøtter ikke billedinput",
"semantic": "",
"Send": "Send",
@ -1412,6 +1424,7 @@
"Share Chat": "Del chat",
"Share to Open WebUI Community": "Del til OpenWebUI Community",
"Share your background and interests": "",
"Shared with you": "",
"Sharing Permissions": "Delingstilladelser",
"Shortcuts with an asterisk (*) are situational and only active under specific conditions.": "Genveje med en stjerne (*) er situationsbestemte og kun aktive under specifikke betingelser.",
"Show": "Vis",
@ -1479,6 +1492,7 @@
"System Instructions": "Systeminstruktioner",
"System Prompt": "Systemprompt",
"Table Mode": "",
"Tag": "",
"Tags": "Tags",
"Tags Generation": "Tag generering",
"Tags Generation Prompt": "Tag genererings prompt",
@ -1564,7 +1578,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "For at vælge værktøjssæt her skal du først tilføje dem til \"Værktøjer\"-arbejdsområdet.",
"Toast notifications for new updates": "Toast-notifikationer for nye opdateringer",
"Today": "I dag",
"Today at {{LOCALIZED_TIME}}": "",
"Today at {{LOCALIZED_TIME}}": "I dag {{LOCALIZED_TIME}}",
"Toggle search": "Skift søgning",
"Toggle settings": "Skift indstillinger",
"Toggle sidebar": "Skift sidebjælke",
@ -1590,6 +1604,7 @@
"Transformers": "Transformers",
"Trouble accessing Ollama?": "Problemer med at få adgang til Ollama?",
"Trust Proxy Environment": "Stol på Proxymiljø",
"Try adjusting your search or filter to find what you are looking for.": "",
"Try Again": "",
"TTS Model": "TTS-model",
"TTS Settings": "TTS-indstillinger",
@ -1626,6 +1641,7 @@
"Upload directory": "Uploadmappe",
"Upload files": "Upload filer",
"Upload Files": "Upload filer",
"Upload Model": "",
"Upload Pipeline": "Upload pipeline",
"Upload Progress": "Uploadfremdrift",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "",

Some files were not shown because too many files have changed in this diff Show more