mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
Compare commits
13 commits
aafccb13e5
...
05a0f354d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a0f354d5 | ||
|
|
87bed3f0b3 | ||
|
|
fa7de50c60 | ||
|
|
1c5128c4aa | ||
|
|
0afe69e1a7 | ||
|
|
f7db201ff5 | ||
|
|
98c0ac9355 | ||
|
|
aad1072874 | ||
|
|
ae549d3e4a | ||
|
|
9d95a0148b | ||
|
|
061fb43432 | ||
|
|
240c795efe | ||
|
|
dc152684d3 |
21 changed files with 1014 additions and 479 deletions
137
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
137
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
name: Bug Report
|
||||
description: Create a detailed bug report to help us improve Open WebUI.
|
||||
description: Tell us what broke in Open WebUI.
|
||||
title: 'issue: '
|
||||
labels: ['bug', 'triage']
|
||||
body:
|
||||
|
|
@ -8,35 +8,26 @@ body:
|
|||
value: |
|
||||
# Bug Report
|
||||
|
||||
## Important Notes
|
||||
Use this for real, reproducible bugs. Include the affected workflow, the expected result, the actual result, and the details needed for someone else to reproduce it.
|
||||
|
||||
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues AND discussions. No matter open or closed.**
|
||||
Before submitting, search open and closed [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions). The issue may already be reported or fixed on `dev`.
|
||||
|
||||
- Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed on the dev branch!**
|
||||
Please do not open a code pull request for this report unless a maintainer asks for one. Actionable reproduction details are the most useful next step.
|
||||
|
||||
- **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.
|
||||
|
||||
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
|
||||
|
||||
- **Scope**: If you want to report a SECURITY VULNERABILITY, then do so through our [GitHub security page](https://github.com/open-webui/open-webui/security).
|
||||
Security vulnerabilities must not be reported publicly. Use the [GitHub security page](https://github.com/open-webui/open-webui/security) instead.
|
||||
|
||||
- type: checkboxes
|
||||
id: issue-check
|
||||
attributes:
|
||||
label: Check Existing Issues
|
||||
description: Confirm that you’ve checked for existing reports before submitting a new one.
|
||||
label: Before Submitting
|
||||
options:
|
||||
- label: I have searched for any existing and/or related issues.
|
||||
- label: I searched open and closed issues and discussions for an existing report.
|
||||
required: true
|
||||
- label: I have searched for any existing and/or related discussions.
|
||||
- label: I checked whether this is already fixed on the `dev` branch or latest source.
|
||||
required: true
|
||||
- label: I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!).
|
||||
- label: I am using the latest available version of Open WebUI for my install method.
|
||||
required: true
|
||||
- label: I have checked whether this issue is already fixed on the `dev` branch or in the latest source.
|
||||
required: true
|
||||
- label: I am using the latest version of Open WebUI.
|
||||
- label: This is not a security vulnerability.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
|
|
@ -45,9 +36,9 @@ body:
|
|||
label: Installation Method
|
||||
description: How did you install Open WebUI?
|
||||
options:
|
||||
- Git Clone
|
||||
- Pip Install
|
||||
- Docker
|
||||
- Pip Install
|
||||
- Git Clone
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
|
@ -56,67 +47,51 @@ body:
|
|||
id: open-webui-version
|
||||
attributes:
|
||||
label: Open WebUI Version
|
||||
description: Specify the version (e.g., v0.11.0)
|
||||
description: Specify the version, commit, or image tag.
|
||||
placeholder: v0.11.0, dev commit SHA, or Docker tag
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: ollama-version
|
||||
attributes:
|
||||
label: Ollama Version (if applicable)
|
||||
description: Specify the version (e.g., v0.32.5, or v0.32.6-rc0)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Specify the OS (e.g., Windows 11, macOS Tahoe, Ubuntu 26.04, Debian 13)
|
||||
description: Specify the OS and version.
|
||||
placeholder: Windows 11, macOS Tahoe, Ubuntu 26.04, Debian 13
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser (if applicable)
|
||||
description: Specify the browser/version (e.g., Chrome 151.0, Firefox 153.0.3)
|
||||
label: Browser
|
||||
description: If the bug appears in the browser, include browser and version.
|
||||
placeholder: Chrome 151.0, Firefox 153.0.3
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: confirmation
|
||||
- type: input
|
||||
id: ollama-version
|
||||
attributes:
|
||||
label: Confirmation
|
||||
description: Ensure the following prerequisites have been met.
|
||||
options:
|
||||
- label: I have read and followed all instructions in `README.md`.
|
||||
required: true
|
||||
- label: I am using the latest version of **both** Open WebUI and Ollama.
|
||||
required: true
|
||||
- label: I have included the browser console logs.
|
||||
required: true
|
||||
- label: I have included the Docker container logs.
|
||||
required: true
|
||||
- label: I have **provided every relevant configuration, setting, and environment variable used in my setup.**
|
||||
required: true
|
||||
- label: I have clearly **listed every relevant configuration, custom setting, environment variable, and command-line option that influences my setup** (such as Docker Compose overrides, .env values, browser settings, authentication configurations, etc).
|
||||
required: true
|
||||
- label: |
|
||||
I have documented **step-by-step reproduction instructions that are precise, sequential, and leave nothing to interpretation**. My steps:
|
||||
- Start with the initial platform/version/OS and dependencies used,
|
||||
- Specify exact install/launch/configure commands,
|
||||
- List URLs visited, user input (incl. example values/emails/passwords if needed),
|
||||
- Describe all options and toggles enabled or changed,
|
||||
- Include any files or environmental changes,
|
||||
- Identify the expected and actual result at each stage,
|
||||
- Ensure any reasonably skilled user can follow and hit the same issue.
|
||||
required: true
|
||||
label: Ollama Version
|
||||
description: Include this if Ollama is involved.
|
||||
placeholder: v0.32.5
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: What is wrong, in a few sentences?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Describe what should have happened.
|
||||
description: What should have happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
|
@ -124,7 +99,7 @@ body:
|
|||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: Describe what actually happened.
|
||||
description: What actually happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
|
@ -132,32 +107,21 @@ body:
|
|||
id: reproduction-steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please provide a **very detailed, step-by-step guide** to reproduce the issue. Your instructions should be so clear and precise that anyone can follow them without guesswork. Include every relevant detail—settings, configuration options, exact commands used, values entered, and any prerequisites or environment variables.
|
||||
**If full reproduction steps and all relevant settings are not provided, your issue may not be addressed.**
|
||||
**If your steps to reproduction are incomplete, lacking detail or not reproducible, your issue can not be addressed.**
|
||||
|
||||
description: Include the exact commands, settings, URLs, model/provider setup, and user actions needed to hit the bug.
|
||||
placeholder: |
|
||||
Example (include every detail):
|
||||
1. Start with a clean Ubuntu 26.04 install.
|
||||
2. Install Docker v29.7.1 and start the service.
|
||||
3. Clone the Open WebUI repo (git clone ...).
|
||||
4. Use the Docker Compose file without modifications.
|
||||
5. Open browser Chrome 151.0 in incognito mode.
|
||||
6. Go to http://localhost:8080 and log in with user "test@example.com".
|
||||
7. Set the language to "English" and theme to "Dark".
|
||||
8. Attempt to connect to Ollama at "http://localhost:11434".
|
||||
9. Observe that the error message "Connection refused" appears at the top right.
|
||||
|
||||
Please list each step carefully and include all relevant configuration, settings, and options.
|
||||
1. Start Open WebUI with ...
|
||||
2. Configure ...
|
||||
3. Open ...
|
||||
4. Click ...
|
||||
5. See ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs-screenshots
|
||||
attributes:
|
||||
label: Logs & Screenshots
|
||||
description: Include relevant logs, errors, or screenshots to help diagnose the issue.
|
||||
placeholder: 'Attach logs from the browser console, Docker logs, or error messages.'
|
||||
label: Logs, Screenshots, and Config
|
||||
description: Include relevant browser console logs, server/container logs, screenshots, and configuration. If something does not apply, say so.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
|
@ -165,13 +129,6 @@ body:
|
|||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Provide any extra details that may assist in understanding the issue.
|
||||
description: Anything else that might help us understand the report.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Note
|
||||
**If the bug report is incomplete, does not follow instructions or is lacking details it may not be addressed.** Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
|
||||
Thank you for contributing to Open WebUI!
|
||||
|
|
|
|||
83
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
83
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
|
|
@ -1,78 +1,71 @@
|
|||
name: Feature Request
|
||||
description: Suggest a new feature or improvement
|
||||
description: Describe what you would like Open WebUI to support.
|
||||
title: 'feat: '
|
||||
labels: ['triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Before Submitting
|
||||
# Feature Request
|
||||
|
||||
Please check **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) for similar requests. If you find one, add your input there instead.
|
||||
Describe the requested behavior, the problem it solves, and any examples, mockups, screenshots, or workflows that clarify the request.
|
||||
|
||||
### Scope Guidelines
|
||||
Search open and closed [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) before submitting. If the request needs broad product, UX, architecture, compatibility, or maintenance discussion, please start in [Discussions](https://github.com/open-webui/open-webui/discussions) so the community can weigh in.
|
||||
|
||||
Feature requests that require significant implementation effort should be posted in the **Ideas** section of [Discussions](https://github.com/open-webui/open-webui/discussions) instead. We will move oversized feature requests to Discussions to keep the Issues tab focused on actionable items.
|
||||
Please do not open a code pull request for this request unless a maintainer asks for one. Clear product context is the most useful next step.
|
||||
|
||||
If your request might impact the broader community, please open a Discussion first so others can weigh in on the design.
|
||||
|
||||
### Be Respectful
|
||||
|
||||
Open WebUI is a volunteer-driven project maintained by a small team. We value constructive, positive communication. Please be mindful of maintainers' time and energy.
|
||||
|
||||
### Contributing
|
||||
|
||||
If you encounter an issue, we encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout and maintain project quality.
|
||||
|
||||
### Reproducibility
|
||||
|
||||
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may be moved to the "Issues" section in Discussions for community assistance.
|
||||
Security vulnerabilities must not be reported publicly. Use the [GitHub security page](https://github.com/open-webui/open-webui/security) instead.
|
||||
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
id: existing-request
|
||||
attributes:
|
||||
label: Check Existing Issues
|
||||
description: Confirm you have searched for similar requests.
|
||||
label: Before Submitting
|
||||
options:
|
||||
- label: I have searched all existing **open AND closed** issues and discussions and found none comparable to my request.
|
||||
- label: I searched open and closed issues and discussions for an existing request.
|
||||
required: true
|
||||
- label: I have checked whether this request is already implemented on the `dev` branch or in the latest source.
|
||||
- label: I checked whether this already exists on the `dev` branch or latest source.
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: feature-scope
|
||||
attributes:
|
||||
label: Verify Feature Scope
|
||||
description: Confirm this request belongs in Issues rather than Discussions.
|
||||
options:
|
||||
- label: I believe this feature request is appropriately scoped for the Issues section as described above.
|
||||
- label: This request is not a security vulnerability.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: Is this related to a problem? Describe the pain point clearly.
|
||||
placeholder: "e.g., I'm frustrated when..."
|
||||
label: Problem
|
||||
description: What is missing, frustrating, confusing, or unnecessarily hard today?
|
||||
placeholder: "I'm trying to..., but..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution-description
|
||||
id: desired-behavior
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe what you would like to happen.
|
||||
label: Desired Behavior
|
||||
description: What would you like to happen instead?
|
||||
placeholder: "I would like Open WebUI to..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: why-it-matters
|
||||
attributes:
|
||||
label: Why This Matters
|
||||
description: Who benefits, and what workflow does this unlock or improve?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Examples or References
|
||||
description: Add mockups, screenshots, links, prompts, workflows, or examples from other tools.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: alternatives-considered
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Describe any alternative solutions or workarounds you have considered.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, mockups, or screenshots about the feature request.
|
||||
label: Alternatives or Workarounds
|
||||
description: What have you tried instead, if anything?
|
||||
validations:
|
||||
required: false
|
||||
|
|
|
|||
138
.github/pull_request_template.md
vendored
138
.github/pull_request_template.md
vendored
|
|
@ -1,126 +1,92 @@
|
|||
<!--
|
||||
⚠️ CRITICAL CHECKS FOR CONTRIBUTORS (READ, DON'T DELETE) ⚠️
|
||||
1. Target the `dev` branch. PRs targeting `main` will be automatically closed.
|
||||
2. First-time contributors should not open pull requests directly unless the pull request contains only i18n/localization updates.
|
||||
Do not open a PR as the first step.
|
||||
For real, reproducible bugs, start with a well-described Issue that explains the problem, why it matters, and what outcome you are looking for.
|
||||
For feature requests, enhancements, behavior changes, UI/UX changes, architecture changes, suspected fixes, or unconfirmed approaches, start with an active Discussion.
|
||||
If you want to propose an implementation, include it only as a reference in the Issue or Discussion, such as a local diff, patch, or branch.
|
||||
Opening an Issue or Discussion does not mean a PR is the right next step. Maintainers will confirm when a PR would be useful.
|
||||
We ask for this because PRs, especially from first-time contributors, often need broader maintainer context on product direction, scope, architecture, UX, edge cases, compatibility, documentation, and long-term maintenance before implementation.
|
||||
We may close unsolicited PRs without review.
|
||||
Contributors with a history of successful merged PRs may be given more latitude.
|
||||
3. Do NOT delete the CLA section at the bottom. It is required for the bot to accept your PR.
|
||||
Important checks for contributors:
|
||||
1. Target the `dev` branch. PRs targeting `main` will be closed.
|
||||
2. Code pull requests should be opened only after maintainer confirmation.
|
||||
3. Do not open a code PR as the first step. Start with an Issue or Discussion unless a maintainer asked for the PR or the change is only i18n/localization.
|
||||
4. Do not delete the Contributor License Agreement section at the bottom. The CLA bot requires it.
|
||||
-->
|
||||
|
||||
# Pull Request Checklist
|
||||
# Pull Request
|
||||
|
||||
### Do not open a pull request as the first step.
|
||||
Thanks for helping improve Open WebUI. Please make sure the linked Issue or Discussion explains the user-facing problem, the expected outcome, and any relevant examples or constraints.
|
||||
|
||||
For real, reproducible bugs, start with a well-described [Issue](https://github.com/open-webui/open-webui/issues) that explains the problem, why it matters, and what outcome you are looking for.
|
||||
Code contributions are not the default path. Open a code pull request only when a maintainer asks for one, or for narrow i18n/localization updates. For real, reproducible bugs, start with a well-described [Issue](https://github.com/open-webui/open-webui/issues). For feature requests, UI/UX changes, behavior changes, architecture changes, suspected fixes, or unconfirmed approaches, start with an active [Discussion](https://github.com/open-webui/open-webui/discussions).
|
||||
|
||||
For feature requests, enhancements, behavior changes, UI/UX changes, architecture changes, suspected fixes, or unconfirmed approaches, start with an active [Discussion](https://github.com/open-webui/open-webui/discussions). Merely opening a discussion is not enough; it needs to be actively discussed.
|
||||
If you have implementation notes, include them as reference in the Issue or Discussion. A local diff, patch, or branch can be useful context, but it does not mean a pull request is expected or will be reviewed.
|
||||
|
||||
If you want to propose an implementation, include it only as a reference in the Issue or Discussion, such as a local diff, patch, or branch.
|
||||
Unsolicited PRs may be closed without review, especially when they introduce product, architecture, compatibility, dependency, or maintenance decisions that have not been discussed.
|
||||
|
||||
Opening an Issue or Discussion does not mean a PR is the right next step. Maintainers will confirm when a PR would be useful.
|
||||
## Checklist
|
||||
|
||||
We ask for this because PRs, especially from first-time contributors, often need broader maintainer context on product direction, scope, architecture, UX, edge cases, compatibility, documentation, and long-term maintenance before implementation.
|
||||
- [ ] This PR targets the `dev` branch.
|
||||
- [ ] This PR links to a confirmed Issue or active Discussion: `Closes #___` / `Relates to #___`.
|
||||
- [ ] A maintainer explicitly asked me to open this PR, or this PR only updates i18n/localization.
|
||||
- [ ] The change is one logical unit with no unrelated commits.
|
||||
- [ ] I matched nearby code patterns and avoided unnecessary new settings, abstractions, or dependencies.
|
||||
- [ ] I manually tested the changed workflow and any nearby behavior that could be affected.
|
||||
- [ ] I updated relevant docs, including the [Open WebUI Docs Repository](https://github.com/open-webui/docs), if needed.
|
||||
- [ ] I added screenshots for UI changes, and a recording when motion or interaction matters.
|
||||
- [ ] I reviewed any AI-generated code before submitting it.
|
||||
- [ ] The PR title uses one of the prefixes listed below.
|
||||
|
||||
Unsolicited PRs may be closed without review. Contributors with a history of successful merged PRs may be given more latitude.
|
||||
## Title Prefix
|
||||
|
||||
<!--
|
||||
### ⚠️ Important: Your PR is a contribution, not a guarantee of merge.
|
||||
Use one of the following prefixes:
|
||||
|
||||
We appreciate thoughtful contributions. Pull requests are for implementation-ready changes that have already been requested, confirmed, or actively discussed in a linked Issue or Discussion. Feature ideas, behavior changes, UI/UX changes, architecture changes, suspected fixes, and unconfirmed approaches should start as an Issue or Discussion instead.
|
||||
- **BREAKING CHANGE**: Changes affecting backward compatibility
|
||||
- **build**: Build system or dependency changes
|
||||
- **ci**: CI/CD workflow changes
|
||||
- **chore**: Refactoring, cleanup, or non-functional changes
|
||||
- **docs**: Documentation additions or updates
|
||||
- **feat**: New features or enhancements
|
||||
- **fix**: Bug fixes or corrections
|
||||
- **i18n**: Internationalization or localization changes
|
||||
- **perf**: Performance improvements
|
||||
- **refactor**: Code restructuring
|
||||
|
||||
Before opening a PR, make sure the change has a clear linked problem, follows nearby patterns, has been manually tested, and accounts for related or downstream behavior. PRs that are ideas, prototypes, unresolved design questions, unchecked AI-generated code, symptom-only patches, one-off patches, or changes where affected paths have not been checked will usually be closed.
|
||||
## Summary
|
||||
|
||||
The most impactful way to contribute to Open WebUI is through well-written bug reports, detailed feature discussions, and thoughtful ideas. These directly shape the project. If you do open a pull request, please know that Open WebUI is held to the highest standard of code quality, consistency, and architectural coherence, and every line merged becomes something the core team must own, maintain, and support indefinitely. Submitted code may be refactored, rewritten, or used as inspiration for a different implementation. This is not a reflection of your work's quality. It is how we ensure that a small team can deeply understand and evolve every part of the codebase.
|
||||
-->
|
||||
Describe the change, the problem it solves, and the impact on users.
|
||||
|
||||
**Before submitting, make sure you've checked and filled out the following:**
|
||||
## Testing
|
||||
|
||||
- [ ] **Linked Issue/Discussion:** This PR references an existing, well-described [Issue](https://github.com/open-webui/open-webui/issues) for a real bug or an active, substantive [Discussion](https://github.com/open-webui/open-webui/discussions) for a feature request or enhancement — `Closes #___` / `Relates to #___`.
|
||||
- [ ] **First-time contributor policy:** This is not my first contribution to Open WebUI, this PR contains only i18n/localization updates, or a maintainer explicitly asked me to open this PR after reviewing the linked Issue or Discussion.
|
||||
- [ ] **Target branch:** The pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
|
||||
- [ ] **Description:** A concise description of the changes is provided below.
|
||||
- [ ] **Changelog:** A changelog entry following [Keep a Changelog](https://keepachangelog.com/) format is included at the bottom.
|
||||
- [ ] **Documentation:** Relevant documentation has been added or updated in the [Open WebUI Docs Repository](https://github.com/open-webui/docs).
|
||||
- [ ] **Dependencies:** Any new or updated dependencies are explained, tested, and documented.
|
||||
- [ ] **Testing:** I performed **manual** end-to-end tests that verify the fix/feature works as intended and does not break related functionality. Automated tests, AI-generated tests, or agent-written test summaries are **not** a substitute for manual verification. I included reproducible steps showing the issue before the fix, tested relevant edge cases (for example URL encoding, HTML entities, and data types), and attached screenshots or recordings for the PR description where applicable.
|
||||
- [ ] **User-facing changes:** I have confirmed whether this PR changes the UI. If it does, screenshots are required, and a video recording is recommended.
|
||||
- [ ] **No Unchecked AI Code:** This PR is either human-written or has undergone thorough human review AND manual testing. Unreviewed AI-generated PRs may be closed immediately.
|
||||
- [ ] **Self-Review:** A self-review of the code has been performed, ensuring adherence to project coding standards.
|
||||
- [ ] **Architecture:** Smart defaults are preferred over new settings. Local state is used for ephemeral UI logic. Major architectural or UX changes have been discussed first.
|
||||
- [ ] **Git Hygiene:** The PR is atomic (one logical change), rebased on `dev`, and contains no unrelated commits.
|
||||
- [ ] **Title Prefix:** The PR title uses one of the following prefixes:
|
||||
- **BREAKING CHANGE**: Changes affecting backward compatibility
|
||||
- **build**: Build system or dependency changes
|
||||
- **ci**: CI/CD workflow changes
|
||||
- **chore**: Refactoring, cleanup, or non-functional changes
|
||||
- **docs**: Documentation additions or updates
|
||||
- **feat**: New features or enhancements
|
||||
- **fix**: Bug fixes or corrections
|
||||
- **i18n**: Internationalization or localization changes
|
||||
- **perf**: Performance improvements
|
||||
- **refactor**: Code restructuring
|
||||
List the exact manual checks you ran. Include commands, setup details, screenshots, or recordings where helpful.
|
||||
|
||||
# Changelog Entry
|
||||
|
||||
### Description
|
||||
|
||||
- [Describe the changes, including motivation and impact]
|
||||
## Changelog Entry
|
||||
|
||||
### Added
|
||||
|
||||
- [New features, functionalities, or additions]
|
||||
-
|
||||
|
||||
### Changed
|
||||
|
||||
- [Changes, updates, refactorings, or optimizations]
|
||||
|
||||
### Deprecated
|
||||
|
||||
- [Deprecated functionality or features]
|
||||
|
||||
### Removed
|
||||
|
||||
- [Removed features, files, or functionalities]
|
||||
-
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Bug fixes or corrections]
|
||||
-
|
||||
|
||||
### Removed
|
||||
|
||||
-
|
||||
|
||||
### Security
|
||||
|
||||
- [Security-related changes or vulnerability fixes]
|
||||
-
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **BREAKING CHANGE**: [Changes affecting compatibility or functionality]
|
||||
-
|
||||
|
||||
---
|
||||
## Additional Context
|
||||
|
||||
### Additional Information
|
||||
Add anything maintainers should know before review.
|
||||
|
||||
- [Any additional context, notes, or references to related issues/commits]
|
||||
|
||||
### Screenshots or Videos
|
||||
|
||||
- [Attach screenshots or videos that demonstrate the fix/feature where applicable. For UI changes, screenshots are required, and a video recording is recommended.]
|
||||
|
||||
### Contributor License Agreement
|
||||
## Contributor License Agreement
|
||||
|
||||
<!--
|
||||
🚨 DO NOT DELETE THE TEXT BELOW 🚨
|
||||
Keep the "Contributor License Agreement" confirmation text intact.
|
||||
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.
|
||||
|
||||
Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
|
||||
DO NOT DELETE THIS SECTION.
|
||||
Your PR will not be reviewed or merged until you check the box below confirming that you have read and agree to the CLA.
|
||||
-->
|
||||
|
||||
- [ ] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
|
||||
|
||||
> [!NOTE]
|
||||
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
|
||||
|
|
|
|||
|
|
@ -2338,7 +2338,7 @@ Strictly return in JSON format:
|
|||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
{{MESSAGES:END:6|ROLES:user,assistant}}
|
||||
</chat_history>
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -533,7 +533,8 @@ class CalendarEventTable:
|
|||
& (CalendarEvent.start_at < end)
|
||||
& or_(
|
||||
CalendarEvent.end_at.is_(None) & (CalendarEvent.start_at >= start),
|
||||
CalendarEvent.end_at.isnot(None) & (CalendarEvent.end_at > start),
|
||||
CalendarEvent.end_at.isnot(None)
|
||||
& ((CalendarEvent.end_at > start) | (CalendarEvent.start_at >= start)),
|
||||
)
|
||||
),
|
||||
# Recurring: fetch all (expansion in Python)
|
||||
|
|
|
|||
|
|
@ -2265,7 +2265,8 @@ def sanitize_tool_pairs(messages: list[dict]) -> list[dict]:
|
|||
return sanitized
|
||||
|
||||
|
||||
SKILL_MENTION_RE = re.compile(r'<(?:\$([^|>]+)(?:\|[^>]*)?|/([^|>]+)\|[^>]*)>')
|
||||
# Ids are validated as [a-z0-9_-]+ on create; matching that keeps ordinary "<$..." text intact.
|
||||
SKILL_MENTION_RE = re.compile(r'<(?:\$([a-z0-9_-]+)(?:\|[^>]*)?|/([a-z0-9_-]+)\|[^>]*)>')
|
||||
|
||||
|
||||
def _get_text_parts(message: dict) -> list[str]:
|
||||
|
|
@ -2287,7 +2288,7 @@ def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]:
|
|||
return ids
|
||||
|
||||
|
||||
SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$[^|>]+(?:\|([^>]*))?|/[^|>]+\|([^>]*))>')
|
||||
SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$[a-z0-9_-]+(?:\|([^>]*))?|/[a-z0-9_-]+\|([^>]*))>')
|
||||
|
||||
|
||||
def strip_skill_mentions(messages: list[dict]) -> None:
|
||||
|
|
@ -5789,17 +5790,6 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
item['arguments'] = tc.get('function', {}).get('arguments', '{}')
|
||||
break
|
||||
|
||||
# Append a new empty message item for the next response
|
||||
output.append(
|
||||
{
|
||||
'type': 'message',
|
||||
'id': output_id('msg'),
|
||||
'status': 'in_progress',
|
||||
'role': 'assistant',
|
||||
'content': [{'type': 'output_text', 'text': ''}],
|
||||
}
|
||||
)
|
||||
|
||||
# Emit citation sources to the frontend for display
|
||||
if citations_enabled:
|
||||
for source in tool_call_sources:
|
||||
|
|
|
|||
|
|
@ -156,6 +156,21 @@ def truncate_content(content: str, max_chars: int, mode: str = 'middletruncate')
|
|||
return f'{content[:half]}...{content[-(max_chars - half) :]}'
|
||||
|
||||
|
||||
def apply_role_filter(messages: list[dict], roles_str: str) -> list[dict]:
|
||||
"""Keep only the messages whose role is listed in roles_str.
|
||||
|
||||
roles_str is a comma separated list of roles, like 'user,assistant'.
|
||||
Roles are matched case-insensitively and may also be separated by spaces.
|
||||
An empty list leaves the messages untouched, so a typo degrades to the
|
||||
previous behaviour instead of silently emptying the chat history.
|
||||
"""
|
||||
roles = {role.lower() for role in re.split(r'[,\s]+', roles_str) if role}
|
||||
if not roles:
|
||||
return messages
|
||||
|
||||
return [message for message in messages if (message.get('role') or '').lower() in roles]
|
||||
|
||||
|
||||
def apply_content_filter(messages: list[dict], filter_str: str) -> list[dict]:
|
||||
"""Apply a content filter to each message's content.
|
||||
|
||||
|
|
@ -197,59 +212,75 @@ def apply_content_filter(messages: list[dict], filter_str: str) -> list[dict]:
|
|||
def replace_messages_variable(
|
||||
template: str, messages: Optional[list[dict]] = None, variable_name: str = 'MESSAGES'
|
||||
) -> str:
|
||||
"""Expand a {{MESSAGES}} variable into a rendered chat history.
|
||||
|
||||
Supported forms, where each may carry zero or more `|filter` suffixes:
|
||||
|
||||
{{MESSAGES}} every message
|
||||
{{MESSAGES:START:4}} the first 4 messages
|
||||
{{MESSAGES:END:6}} the last 6 messages
|
||||
{{MESSAGES:MIDDLETRUNCATE:6}} the first and last 3 messages
|
||||
|
||||
Two kinds of filter may be chained in any order:
|
||||
|
||||
|ROLES:user,assistant keep only these roles
|
||||
|middletruncate:500 truncate each message's content
|
||||
|start:200 |end:200
|
||||
|
||||
Role filters select *which* messages are rendered and are applied before
|
||||
the positional selector, so `{{MESSAGES:END:6|ROLES:user}}` renders the
|
||||
last 6 user messages rather than whatever user messages happen to sit in
|
||||
the last 6 of the whole history. Content filters shorten each surviving
|
||||
message and are applied last.
|
||||
"""
|
||||
|
||||
def replacement_function(match):
|
||||
# Groups: (1) filter for bare MESSAGES
|
||||
# (2) START count, (3) filter for START
|
||||
# (4) END count, (5) filter for END
|
||||
# (6) MIDDLE count,(7) filter for MIDDLE
|
||||
bare_filter = match.group(1)
|
||||
start_length = match.group(2)
|
||||
start_filter = match.group(3)
|
||||
end_length = match.group(4)
|
||||
end_filter = match.group(5)
|
||||
middle_length = match.group(6)
|
||||
middle_filter = match.group(7)
|
||||
selector = match.group('selector')
|
||||
length = match.group('length')
|
||||
filters_str = match.group('filters') or ''
|
||||
|
||||
# If messages is None, handle it as an empty list
|
||||
if messages is None:
|
||||
return ''
|
||||
|
||||
# Select messages based on the variant
|
||||
if start_length is not None:
|
||||
selected = messages[: int(start_length)]
|
||||
content_filter = start_filter
|
||||
elif end_length is not None:
|
||||
selected = messages[-int(end_length) :]
|
||||
content_filter = end_filter
|
||||
elif middle_length is not None:
|
||||
mid = int(middle_length)
|
||||
if len(messages) <= mid:
|
||||
selected = messages
|
||||
else:
|
||||
half = mid // 2
|
||||
start_msgs = messages[:half]
|
||||
end_msgs = messages[-half:] if mid % 2 == 0 else messages[-(half + 1) :]
|
||||
selected = start_msgs + end_msgs
|
||||
content_filter = middle_filter
|
||||
else:
|
||||
# Bare {{MESSAGES}} or {{MESSAGES|filter}}
|
||||
selected = messages
|
||||
content_filter = bare_filter
|
||||
filters = [f for f in filters_str.split('|') if f]
|
||||
role_filters = [f for f in filters if f.split(':', 1)[0].lower() == 'roles']
|
||||
content_filters = [f for f in filters if f not in role_filters]
|
||||
|
||||
# Apply content filter if present
|
||||
if content_filter:
|
||||
# Role filtering runs first so the positional selector below counts
|
||||
# only the messages that are actually rendered.
|
||||
selected = messages
|
||||
for role_filter in role_filters:
|
||||
_, _, roles_str = role_filter.partition(':')
|
||||
selected = apply_role_filter(selected, roles_str)
|
||||
|
||||
selector = (selector or '').upper()
|
||||
if selector == 'START':
|
||||
selected = selected[: int(length)]
|
||||
elif selector == 'END':
|
||||
selected = selected[-int(length) :]
|
||||
elif selector == 'MIDDLETRUNCATE':
|
||||
mid = int(length)
|
||||
if len(selected) > mid:
|
||||
half = mid // 2
|
||||
start_msgs = selected[:half]
|
||||
end_msgs = selected[-half:] if mid % 2 == 0 else selected[-(half + 1) :]
|
||||
selected = start_msgs + end_msgs
|
||||
|
||||
for content_filter in content_filters:
|
||||
selected = apply_content_filter(selected, content_filter)
|
||||
|
||||
return get_messages_content(selected)
|
||||
|
||||
variable_pattern = re.escape(variable_name)
|
||||
# A filter is either ROLES:<comma separated roles> or <mode>:<int>;
|
||||
# any number of them may be chained with '|'.
|
||||
filter_pattern = r'(?:(?i:ROLES):[\w,\s]+|\w+:\d+)'
|
||||
template = re.sub(
|
||||
r'(?:'
|
||||
rf'\{{\{{{variable_pattern}(?:\|(\w+:\d+))?\}}\}}'
|
||||
rf'|\{{\{{{variable_pattern}:START:(\d+)(?:\|(\w+:\d+))?\}}\}}'
|
||||
rf'|\{{\{{{variable_pattern}:END:(\d+)(?:\|(\w+:\d+))?\}}\}}'
|
||||
rf'|\{{\{{{variable_pattern}:MIDDLETRUNCATE:(\d+)(?:\|(\w+:\d+))?\}}\}}'
|
||||
r')',
|
||||
rf'\{{\{{{variable_pattern}'
|
||||
rf'(?::(?P<selector>START|END|MIDDLETRUNCATE):(?P<length>\d+))?'
|
||||
rf'(?P<filters>(?:\|{filter_pattern})*)'
|
||||
r'\}\}',
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -292,6 +292,29 @@ export const downloadFileBlob = async (
|
|||
return { blob, filename };
|
||||
};
|
||||
|
||||
export const downloadFilePreview = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
path: string,
|
||||
sessionId?: string
|
||||
): Promise<{ blob: Blob; filename: string } | null> => {
|
||||
const url = `${baseUrl.replace(/\/$/, '')}/files/view?path=${encodeURIComponent(path)}&preview=true`;
|
||||
const headers: Record<string, string> = bearerHeaders(apiKey);
|
||||
if (sessionId) headers['X-Session-Id'] = sessionId;
|
||||
const res = await fetch(url, { headers }).catch(() => null);
|
||||
|
||||
if (!res) return null;
|
||||
if (!res.ok) return null;
|
||||
|
||||
const contentType = res.headers.get('content-type') ?? '';
|
||||
const filename = path.split('/').pop() ?? 'file';
|
||||
if (!contentType.includes('application/pdf')) return null;
|
||||
|
||||
const blob = await res.blob().catch(() => null);
|
||||
if (!blob) return null;
|
||||
return { blob, filename };
|
||||
};
|
||||
|
||||
export const archiveFromTerminal = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@
|
|||
scrollToTab(selectedTab);
|
||||
}
|
||||
|
||||
$: if (loaded && selectedTab) {
|
||||
loadCounts();
|
||||
}
|
||||
|
||||
const scrollToTab = (tabId) => {
|
||||
const tabElement = document.getElementById(tabId);
|
||||
if (tabElement) {
|
||||
|
|
@ -58,7 +62,6 @@
|
|||
await goto('/');
|
||||
}
|
||||
|
||||
await loadCounts();
|
||||
loaded = true;
|
||||
|
||||
const containerElement = document.getElementById('users-tabs-container');
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@
|
|||
return (b.member_count ?? 0) - (a.member_count ?? 0) || a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
$: if (loaded) {
|
||||
adminGroupCount.set(filteredGroups.length);
|
||||
}
|
||||
|
||||
/** @type {any} */
|
||||
let defaultPermissions = {};
|
||||
|
||||
|
|
@ -56,7 +60,6 @@
|
|||
|
||||
const setGroups = async () => {
|
||||
groups = await getGroups(localStorage.token);
|
||||
adminGroupCount.set(groups.length);
|
||||
};
|
||||
|
||||
/** @param {any} updatedGroup */
|
||||
|
|
|
|||
|
|
@ -129,7 +129,12 @@
|
|||
loading = true;
|
||||
try {
|
||||
const startNs = dateTimeToNs(startDate, allDay ? '00:00' : startTime);
|
||||
const endNs = endDate ? dateTimeToNs(endDate, allDay ? '23:59' : endTime) : undefined;
|
||||
let endNs = endDate ? dateTimeToNs(endDate, allDay ? '23:59' : endTime) : undefined;
|
||||
if (endNs !== undefined && endNs < startNs) {
|
||||
const duration =
|
||||
event?.end_at && event.end_at > event.start_at ? event.end_at - event.start_at : 0;
|
||||
endNs = startNs + duration;
|
||||
}
|
||||
|
||||
if (event && !event.meta?.automation_id) {
|
||||
const result = await updateCalendarEvent(localStorage.token, event.id, {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@
|
|||
const startDate = new Date(startMs);
|
||||
const endDate = new Date(endMs);
|
||||
const d = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
||||
const last = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()).getTime();
|
||||
const last = Math.max(
|
||||
d.getTime(),
|
||||
new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()).getTime()
|
||||
);
|
||||
while (d.getTime() <= last) {
|
||||
const key = d.getTime().toString();
|
||||
(map[key] ??= []).push(e);
|
||||
|
|
@ -90,7 +93,7 @@
|
|||
const dayEndMs = dayStartMs + 86_400_000;
|
||||
return filteredEvents.filter((e) => {
|
||||
const startMs = e.start_at / NS;
|
||||
const endMs = (e.end_at || e.start_at) / NS;
|
||||
const endMs = Math.max(startMs, (e.end_at || e.start_at) / NS);
|
||||
return startMs < dayEndMs && endMs >= dayStartMs;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
// Persists across mount/unmount cycles (module-level, not per-instance)
|
||||
let savedPath = '/';
|
||||
let savedFileRoot = null;
|
||||
const treeExpandedCache = new Map<string, string[]>();
|
||||
const treeContentsCache = new Map<string, [string, any[]][]>();
|
||||
/** @type {Map<string, string[]>} */
|
||||
const treeExpandedCache = new Map();
|
||||
/** @type {Map<string, [string, any[]][]>} */
|
||||
const treeContentsCache = new Map();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
@ -23,6 +25,7 @@
|
|||
listFiles,
|
||||
readFile,
|
||||
downloadFileBlob,
|
||||
downloadFilePreview,
|
||||
archiveFromTerminal,
|
||||
uploadToTerminal,
|
||||
createDirectory,
|
||||
|
|
@ -865,19 +868,38 @@
|
|||
);
|
||||
if (result) fileSqliteData = await result.blob.arrayBuffer();
|
||||
} else if (isOffice(filePath)) {
|
||||
const result = await downloadFileBlob(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
filePath,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (result) {
|
||||
const ext = getFileExt(filePath);
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
try {
|
||||
if (ext === 'docx') {
|
||||
const ext = getFileExt(filePath);
|
||||
try {
|
||||
if (ext === 'docx') {
|
||||
const preview = await downloadFilePreview(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
filePath,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (preview) {
|
||||
filePdfData = await preview.blob.arrayBuffer();
|
||||
} else {
|
||||
const result = await downloadFileBlob(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
filePath,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (!result) throw new Error('Preview failed');
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
fileDocxData = arrayBuffer;
|
||||
} else if (ext === 'xlsx') {
|
||||
toast.info($i18n.t('Preview may differ from download.'));
|
||||
}
|
||||
} else if (ext === 'xlsx') {
|
||||
const result = await downloadFileBlob(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
filePath,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (result) {
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const XLSX = await import('xlsx');
|
||||
const wb = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' });
|
||||
excelWorkbook = wb;
|
||||
|
|
@ -889,16 +911,35 @@
|
|||
const DOMPurify = (await import('dompurify')).default;
|
||||
fileOfficeHtml = DOMPurify.sanitize(result.html);
|
||||
}
|
||||
} else if (ext === 'pptx') {
|
||||
const { pptxToImages } = await import('$lib/utils/pptxToHtml');
|
||||
const result = await pptxToImages(arrayBuffer);
|
||||
fileOfficeSlides = result.images;
|
||||
currentSlide = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to render Office file:', e);
|
||||
fileContent = `Error previewing file: ${e instanceof Error ? e.message : 'Unknown error'}`;
|
||||
} else if (ext === 'pptx') {
|
||||
const preview = await downloadFilePreview(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
filePath,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (preview) {
|
||||
filePdfData = await preview.blob.arrayBuffer();
|
||||
} else {
|
||||
const result = await downloadFileBlob(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
filePath,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (!result) throw new Error('Preview failed');
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const { pptxToImages } = await import('$lib/utils/pptxToHtml');
|
||||
const fallback = await pptxToImages(arrayBuffer);
|
||||
fileOfficeSlides = fallback.images;
|
||||
currentSlide = 0;
|
||||
toast.info($i18n.t('Preview may differ from download.'));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to render Office file:', e);
|
||||
fileContent = `Error previewing file: ${e instanceof Error ? e.message : 'Unknown error'}`;
|
||||
}
|
||||
} else {
|
||||
fileContent = await readFile(terminal.url, terminal.key, filePath, chatId ?? undefined);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { isCodeFile } from '$lib/utils/codeHighlight';
|
||||
import { initMermaid, renderMermaidDiagram } from '$lib/utils';
|
||||
import Spinner from '../../common/Spinner.svelte';
|
||||
import PDFViewer from '../../common/PDFViewer.svelte';
|
||||
import PdfPagesPreview from '../../common/PdfPagesPreview.svelte';
|
||||
import PanzoomContainer from '../../common/PanzoomContainer.svelte';
|
||||
import DocxPreview from '../../common/DocxPreview.svelte';
|
||||
import PptxPreview from '../../common/PptxPreview.svelte';
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
import SqliteView from './SqliteView.svelte';
|
||||
import FileCodeEditor from './FileCodeEditor.svelte';
|
||||
|
||||
let pdfViewerRef: PDFViewer;
|
||||
let pdfPagesPreviewRef: PdfPagesPreview;
|
||||
let fileCodeEditorRef: FileCodeEditor;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -116,6 +116,7 @@
|
|||
$: isNotebook = getExt(selectedFile) === 'ipynb';
|
||||
$: isCode = isCodeFile(selectedFile);
|
||||
$: csvDelimiter = getExt(selectedFile) === 'tsv' ? '\t' : ',';
|
||||
$: isPptx = getExt(selectedFile) === 'pptx';
|
||||
|
||||
// For HTML files on system terminals (proxy URL), use path-based serving
|
||||
// so the iframe can resolve relative CSS/JS/image references via cookie auth.
|
||||
|
|
@ -279,13 +280,14 @@
|
|||
};
|
||||
|
||||
export const resetPdfView = () => {
|
||||
pdfViewerRef?.resetView();
|
||||
pdfPagesPreviewRef?.resetView();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex-1 {fileImageUrl !== null ||
|
||||
fileDocxData !== null ||
|
||||
filePdfData !== null ||
|
||||
(fileOfficeSlides !== null && fileOfficeSlides.length > 0)
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-y-auto'} min-h-0 min-w-0 relative h-full"
|
||||
|
|
@ -321,7 +323,16 @@
|
|||
</audio>
|
||||
</div>
|
||||
{:else if filePdfData !== null}
|
||||
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} {targetPage} className="w-full h-full" />
|
||||
<PdfPagesPreview
|
||||
bind:this={pdfPagesPreviewRef}
|
||||
data={filePdfData}
|
||||
bind:currentSlide
|
||||
{targetPage}
|
||||
singlePage={isPptx}
|
||||
itemLabel={isPptx ? 'Slide' : 'Page'}
|
||||
listLabel={isPptx ? 'Slides' : 'Pages'}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
{:else if fileSqliteData !== null}
|
||||
<SqliteView data={fileSqliteData} />
|
||||
{:else if fileDocxData !== null}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
showFileNavPath,
|
||||
terminalServers
|
||||
} from '$lib/stores';
|
||||
import { downloadFileBlob, readFile } from '$lib/apis/terminal';
|
||||
import { downloadFileBlob, downloadFilePreview, readFile } from '$lib/apis/terminal';
|
||||
import FilePreview from '$lib/components/chat/FileNav/FilePreview.svelte';
|
||||
import Icon from '$lib/components/chat/FileNav/Icon.svelte';
|
||||
import { fileIconName } from '$lib/components/chat/FileNav/fileIcon';
|
||||
|
|
@ -143,26 +143,61 @@
|
|||
else if (isVideo(path)) fileVideoUrl = url;
|
||||
else fileAudioUrl = url;
|
||||
} else if (isPdf(path) || isSqlite(path) || isOffice(path)) {
|
||||
const result = await blobForPreview();
|
||||
if (!result) throw new Error(t('Preview failed'));
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const ext = getExt(path);
|
||||
|
||||
if (isPdf(path)) {
|
||||
const result = await blobForPreview();
|
||||
if (!result) throw new Error(t('Preview failed'));
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
filePdfData = arrayBuffer;
|
||||
} else if (isSqlite(path)) {
|
||||
const result = await blobForPreview();
|
||||
if (!result) throw new Error(t('Preview failed'));
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
fileSqliteData = arrayBuffer;
|
||||
} else if (ext === 'docx') {
|
||||
fileDocxData = arrayBuffer;
|
||||
const preview = await downloadFilePreview(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
path,
|
||||
chatId || undefined
|
||||
);
|
||||
if (preview) {
|
||||
filePdfData = await preview.blob.arrayBuffer();
|
||||
} else {
|
||||
const result = await blobForPreview();
|
||||
if (!result) throw new Error(t('Preview failed'));
|
||||
fileDocxData = await result.blob.arrayBuffer();
|
||||
toast.info(t('Preview may differ from download.'));
|
||||
}
|
||||
} else if (ext === 'xlsx' || ext === 'xls') {
|
||||
const result = await blobForPreview();
|
||||
if (!result) throw new Error(t('Preview failed'));
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const XLSX = await import('xlsx');
|
||||
excelWorkbook = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' });
|
||||
excelSheetNames = excelWorkbook.SheetNames;
|
||||
if (excelSheetNames.length > 0) await loadExcelSheet(excelSheetNames[0]);
|
||||
if (excelSheetNames.length > 0) {
|
||||
await loadExcelSheet(excelSheetNames[0]);
|
||||
}
|
||||
} else if (ext === 'pptx') {
|
||||
const { pptxToImages } = await import('$lib/utils/pptxToHtml');
|
||||
const result = await pptxToImages(arrayBuffer);
|
||||
fileOfficeSlides = result.images;
|
||||
const preview = await downloadFilePreview(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
path,
|
||||
chatId || undefined
|
||||
);
|
||||
if (preview) {
|
||||
filePdfData = await preview.blob.arrayBuffer();
|
||||
} else {
|
||||
const result = await blobForPreview();
|
||||
if (!result) throw new Error(t('Preview failed'));
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const { pptxToImages } = await import('$lib/utils/pptxToHtml');
|
||||
const resultImages = await pptxToImages(arrayBuffer);
|
||||
fileOfficeSlides = resultImages.images;
|
||||
toast.info(t('Preview may differ from download.'));
|
||||
}
|
||||
}
|
||||
} else if (terminal) {
|
||||
fileContent = await readFile(terminal.url, terminal.key, path, chatId || undefined);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
export let data: ArrayBuffer | Uint8Array | null = null;
|
||||
export let className = 'w-full h-[70vh]';
|
||||
export let targetPage: number | null = null;
|
||||
export let singlePage = false;
|
||||
export let itemLabel = 'Page';
|
||||
export let onPageChange: ((page: number) => void) | null = null;
|
||||
|
||||
type PdfDocument = import('pdfjs-dist').PDFDocumentProxy;
|
||||
type PdfTextLayer = InstanceType<typeof import('pdfjs-dist').TextLayer>;
|
||||
|
|
@ -22,10 +25,28 @@
|
|||
let zoomLevel = 1;
|
||||
let rerenderTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let lastRenderedZoom = 1;
|
||||
let pageCount = 0;
|
||||
let renderedPage = 0;
|
||||
let activePage = 1;
|
||||
let loadToken = 0;
|
||||
let renderToken = 0;
|
||||
let scrollFrame: number | null = null;
|
||||
let mounted = false;
|
||||
let loadedSource: ArrayBuffer | Uint8Array | string | null = null;
|
||||
let wheelDelta = 0;
|
||||
let lastWheelNavigationAt = 0;
|
||||
const wheelNavigationThreshold = 80;
|
||||
const wheelNavigationCooldown = 450;
|
||||
const pageShortcutKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
||||
|
||||
$: selectedPage = singlePage ? (clampDocumentTargetPage(targetPage, pageCount) ?? 1) : activePage;
|
||||
|
||||
// Keep a reference to TextLayer instances so we can update/cancel them
|
||||
let textLayerInstances: PdfTextLayer[] = [];
|
||||
|
||||
const copyPdfData = (pdfData: ArrayBuffer | Uint8Array) =>
|
||||
pdfData instanceof Uint8Array ? pdfData.slice() : pdfData.slice(0);
|
||||
|
||||
const cancelTextLayers = () => {
|
||||
for (const tl of textLayerInstances) {
|
||||
try {
|
||||
|
|
@ -53,8 +74,11 @@
|
|||
}
|
||||
return false;
|
||||
},
|
||||
beforeMouseDown: () => {
|
||||
beforeMouseDown: (e) => {
|
||||
// Only allow drag-to-pan when zoomed in (not at default scale)
|
||||
if ((e?.target as HTMLElement | null)?.closest?.('.textLayer')) {
|
||||
return true;
|
||||
}
|
||||
const transform = pzInstance?.getTransform();
|
||||
if (transform && Math.abs(transform.scale - 1) < 0.01) {
|
||||
return true; // cancel panzoom mouse handling at 1x — allow text selection / normal interaction
|
||||
|
|
@ -100,16 +124,73 @@
|
|||
}
|
||||
};
|
||||
|
||||
export const scrollToPage = async (page: number | null) => {
|
||||
targetPage = page;
|
||||
if (singlePage) {
|
||||
if (targetPage) onPageChange?.(targetPage);
|
||||
} else {
|
||||
await scrollToTargetPage();
|
||||
}
|
||||
};
|
||||
|
||||
const selectPage = async (page: number) => {
|
||||
if (!pdfDoc) return;
|
||||
const nextPage = clampDocumentTargetPage(page, pdfDoc.numPages);
|
||||
if (!nextPage || nextPage === selectedPage) return;
|
||||
|
||||
targetPage = nextPage;
|
||||
activePage = nextPage;
|
||||
onPageChange?.(nextPage);
|
||||
if (!singlePage) await scrollToTargetPage();
|
||||
};
|
||||
|
||||
const scrollToTargetPage = async () => {
|
||||
if (!outerContainer || !sceneElement || !pdfDoc) return;
|
||||
const page = clampDocumentTargetPage(targetPage, pdfDoc.numPages);
|
||||
if (!page) return;
|
||||
|
||||
if (singlePage) return;
|
||||
|
||||
await tick();
|
||||
const pageWrapper = sceneElement.querySelectorAll('.pdf-page-wrapper')[page - 1] as
|
||||
| HTMLElement
|
||||
| undefined;
|
||||
pageWrapper?.scrollIntoView({ block: 'start' });
|
||||
activePage = page;
|
||||
onPageChange?.(page);
|
||||
};
|
||||
|
||||
const syncVisiblePage = () => {
|
||||
scrollFrame = null;
|
||||
if (singlePage || !outerContainer || !sceneElement || !pdfDoc) return;
|
||||
|
||||
const marker = outerContainer.getBoundingClientRect().top + outerContainer.clientHeight * 0.35;
|
||||
let bestPage = activePage;
|
||||
let bestDistance = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const wrapper of sceneElement.querySelectorAll('.pdf-page-wrapper')) {
|
||||
const el = wrapper as HTMLElement;
|
||||
const page = Number(el.dataset.pageNumber);
|
||||
if (!page) continue;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const distance =
|
||||
marker < rect.top ? rect.top - marker : marker > rect.bottom ? marker - rect.bottom : 0;
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestPage = page;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPage !== activePage) {
|
||||
activePage = bestPage;
|
||||
onPageChange?.(bestPage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (singlePage || scrollFrame !== null) return;
|
||||
scrollFrame = requestAnimationFrame(syncVisiblePage);
|
||||
};
|
||||
|
||||
// Re-render existing canvases at a new zoom level (preserves panzoom transform)
|
||||
|
|
@ -117,16 +198,15 @@
|
|||
if (!pdfDoc || !sceneElement) return;
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const containerWidth = outerContainer?.clientWidth || 800;
|
||||
|
||||
const pageWrappers = sceneElement.querySelectorAll('.pdf-page-wrapper');
|
||||
|
||||
cancelTextLayers();
|
||||
|
||||
for (let i = 0; i < pageWrappers.length; i++) {
|
||||
const page = await pdfDoc.getPage(i + 1);
|
||||
const page = await pdfDoc.getPage(singlePage ? selectedPage : i + 1);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const cssScale = containerWidth / viewport.width;
|
||||
const cssScale = getCssScale(viewport);
|
||||
const renderScale = cssScale * forZoom * dpr;
|
||||
const scaledViewport = page.getViewport({ scale: renderScale });
|
||||
const cssViewport = page.getViewport({ scale: cssScale });
|
||||
|
|
@ -162,8 +242,17 @@
|
|||
lastRenderedZoom = forZoom;
|
||||
};
|
||||
|
||||
const getCssScale = (viewport: { width: number; height: number }) => {
|
||||
if (!singlePage) return (outerContainer?.clientWidth || 800) / viewport.width;
|
||||
|
||||
const availableWidth = Math.max(320, (outerContainer?.clientWidth || 800) - 64);
|
||||
const availableHeight = Math.max(220, (outerContainer?.clientHeight || 600) - 64);
|
||||
return Math.min(1, availableWidth / viewport.width, availableHeight / viewport.height);
|
||||
};
|
||||
|
||||
const renderAllPages = async () => {
|
||||
if (!pdfDoc || !sceneElement) return;
|
||||
const token = ++renderToken;
|
||||
|
||||
// Clear previous content
|
||||
sceneElement.innerHTML = '';
|
||||
|
|
@ -172,14 +261,17 @@
|
|||
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const wrappers: HTMLElement[] = [];
|
||||
const firstPage = singlePage ? selectedPage : 1;
|
||||
const lastPage = singlePage ? selectedPage : pdfDoc.numPages;
|
||||
|
||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||
for (let i = firstPage; i <= lastPage; i++) {
|
||||
const page = await pdfDoc.getPage(i);
|
||||
if (token !== renderToken) return;
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
|
||||
// Scale to fit container width
|
||||
const containerWidth = outerContainer?.clientWidth || 800;
|
||||
const cssScale = containerWidth / viewport.width;
|
||||
const cssScale = getCssScale(viewport);
|
||||
const renderScale = cssScale * dpr;
|
||||
const scaledViewport = page.getViewport({ scale: renderScale });
|
||||
const cssViewport = page.getViewport({ scale: cssScale });
|
||||
|
|
@ -187,6 +279,7 @@
|
|||
// Create page wrapper (positioned container for canvas + text layer)
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'pdf-page-wrapper';
|
||||
wrapper.dataset.pageNumber = String(i);
|
||||
wrapper.style.position = 'relative';
|
||||
wrapper.style.width = `${Math.round(cssScale * viewport.width)}px`;
|
||||
wrapper.style.height = `${Math.round(cssScale * viewport.height)}px`;
|
||||
|
|
@ -217,6 +310,7 @@
|
|||
canvasContext: ctx,
|
||||
viewport: scaledViewport
|
||||
}).promise;
|
||||
if (token !== renderToken) return;
|
||||
|
||||
// Create text layer overlay — pdfjs setLayerDimensions handles its sizing
|
||||
const textLayerDiv = document.createElement('div');
|
||||
|
|
@ -230,21 +324,83 @@
|
|||
viewport: cssViewport
|
||||
});
|
||||
await textLayer.render();
|
||||
if (token !== renderToken) return;
|
||||
textLayerInstances.push(textLayer);
|
||||
|
||||
sceneElement.appendChild(wrapper);
|
||||
wrappers.push(wrapper);
|
||||
}
|
||||
|
||||
sceneElement.replaceChildren(...wrappers);
|
||||
lastRenderedZoom = 1;
|
||||
renderedPage = singlePage ? selectedPage : 0;
|
||||
initPanzoom();
|
||||
await scrollToTargetPage();
|
||||
syncVisiblePage();
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (!singlePage) return;
|
||||
if (e.ctrlKey || e.metaKey) return;
|
||||
|
||||
const transform = pzInstance?.getTransform();
|
||||
if (transform && Math.abs(transform.scale - 1) >= 0.01) {
|
||||
e.preventDefault();
|
||||
pzInstance?.moveBy(-e.deltaX, -e.deltaY, false);
|
||||
zoomLevel = pzInstance?.getTransform()?.scale ?? 1;
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
if (pageCount <= 1) return;
|
||||
|
||||
const multiplier = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? outerContainer.clientHeight : 1;
|
||||
const dominantDelta = Math.abs(e.deltaY) >= Math.abs(e.deltaX) ? e.deltaY : e.deltaX;
|
||||
wheelDelta += dominantDelta * multiplier;
|
||||
|
||||
const now = Date.now();
|
||||
if (Math.abs(wheelDelta) < wheelNavigationThreshold) return;
|
||||
if (now - lastWheelNavigationAt < wheelNavigationCooldown) {
|
||||
wheelDelta = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lastWheelNavigationAt = now;
|
||||
void selectPage(selectedPage + (wheelDelta > 0 ? 1 : -1));
|
||||
wheelDelta = 0;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (
|
||||
!singlePage ||
|
||||
e.defaultPrevented ||
|
||||
e.altKey ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
pageCount <= 1 ||
|
||||
!pageShortcutKeys.includes(e.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
void selectPage(selectedPage + (e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? -1 : 1));
|
||||
};
|
||||
|
||||
const loadPdf = async () => {
|
||||
if (!url && !data) return;
|
||||
|
||||
const source = data ?? url;
|
||||
if (source === loadedSource && pdfDoc) return;
|
||||
const token = ++loadToken;
|
||||
loadedSource = source;
|
||||
loading = true;
|
||||
error = '';
|
||||
renderedPage = 0;
|
||||
pageCount = 0;
|
||||
pzInstance?.dispose();
|
||||
cancelTextLayers();
|
||||
pdfDoc?.destroy();
|
||||
pdfDoc = null;
|
||||
|
||||
try {
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
|
|
@ -252,7 +408,7 @@
|
|||
|
||||
let pdfData: ArrayBuffer | Uint8Array;
|
||||
if (data) {
|
||||
pdfData = data;
|
||||
pdfData = copyPdfData(data);
|
||||
} else {
|
||||
// Fetch with credentials so auth cookies are sent
|
||||
const res = await fetch(url!, { credentials: 'include' });
|
||||
|
|
@ -260,24 +416,42 @@
|
|||
pdfData = await res.arrayBuffer();
|
||||
}
|
||||
pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;
|
||||
if (token !== loadToken) return;
|
||||
pageCount = pdfDoc.numPages;
|
||||
activePage = clampDocumentTargetPage(targetPage, pageCount) ?? 1;
|
||||
targetPage = clampDocumentTargetPage(targetPage, pageCount) ?? 1;
|
||||
await renderAllPages();
|
||||
} catch (e) {
|
||||
console.error('PDF render error:', e);
|
||||
error = 'Failed to load PDF.';
|
||||
if (token === loadToken) {
|
||||
console.error('PDF render error:', e);
|
||||
error = 'Failed to load PDF.';
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
if (token === loadToken) loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
loadPdf();
|
||||
});
|
||||
|
||||
$: if (!loading && pdfDoc && targetPage) {
|
||||
$: if (mounted && (data || url)) {
|
||||
void loadPdf();
|
||||
}
|
||||
|
||||
$: if (!loading && pdfDoc && singlePage && targetPage && selectedPage !== renderedPage) {
|
||||
void renderAllPages();
|
||||
}
|
||||
|
||||
$: if (!loading && pdfDoc && !singlePage && targetPage) {
|
||||
void scrollToTargetPage();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
loadToken++;
|
||||
renderToken++;
|
||||
if (scrollFrame !== null) cancelAnimationFrame(scrollFrame);
|
||||
if (rerenderTimer) clearTimeout(rerenderTimer);
|
||||
pzInstance?.dispose();
|
||||
cancelTextLayers();
|
||||
|
|
@ -288,6 +462,8 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<div class="relative {className}">
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
|
|
@ -299,14 +475,67 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="overflow-y-auto h-full" bind:this={outerContainer}>
|
||||
<div bind:this={sceneElement} class="w-full"></div>
|
||||
<div
|
||||
class={singlePage
|
||||
? 'overflow-hidden h-full flex items-center justify-center overscroll-contain'
|
||||
: 'overflow-y-auto h-full'}
|
||||
bind:this={outerContainer}
|
||||
on:scroll={handleScroll}
|
||||
on:wheel|nonpassive={handleWheel}
|
||||
>
|
||||
<div bind:this={sceneElement} class={singlePage ? '' : 'w-full'}></div>
|
||||
</div>
|
||||
|
||||
{#if !loading && !error && pdfDoc}
|
||||
{#if !error && pdfDoc}
|
||||
<div
|
||||
class="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 rounded-lg bg-white/90 dark:bg-gray-850/90 backdrop-blur-sm shadow-lg border border-gray-200/60 dark:border-gray-700/60 px-1 py-0.5"
|
||||
>
|
||||
{#if singlePage}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 disabled:opacity-30"
|
||||
disabled={selectedPage === 1}
|
||||
on:click={() => selectPage(selectedPage - 1)}
|
||||
aria-label={`Previous ${itemLabel.toLowerCase()}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
class="shrink-0 min-w-12 text-center text-[0.6875rem] text-gray-500 dark:text-gray-400 tabular-nums"
|
||||
>{selectedPage} / {pageCount}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 disabled:opacity-30"
|
||||
disabled={selectedPage === pageCount}
|
||||
on:click={() => selectPage(selectedPage + 1)}
|
||||
aria-label={`Next ${itemLabel.toLowerCase()}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
|
||||
|
|
|
|||
238
src/lib/components/common/PdfPagesPreview.svelte
Normal file
238
src/lib/components/common/PdfPagesPreview.svelte
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import PDFViewer from './PDFViewer.svelte';
|
||||
|
||||
export let data: ArrayBuffer | Uint8Array | null = null;
|
||||
export let currentSlide = 0;
|
||||
export let className = '';
|
||||
export let targetPage: number | null = null;
|
||||
export let singlePage = false;
|
||||
export let itemLabel = 'Page';
|
||||
export let listLabel = 'Pages';
|
||||
|
||||
type PdfDocument = import('pdfjs-dist').PDFDocumentProxy;
|
||||
|
||||
let rootEl: HTMLDivElement;
|
||||
let pdfViewerRef: PDFViewer;
|
||||
let viewerData: ArrayBuffer | Uint8Array | null = null;
|
||||
let pageTarget: number | null = null;
|
||||
let thumbsLoading = false;
|
||||
let thumbnails: string[] = [];
|
||||
let thumbnailButtons: Array<HTMLButtonElement | undefined> = [];
|
||||
let hideThumbs = false;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let pdfDoc: PdfDocument | null = null;
|
||||
let loadToken = 0;
|
||||
|
||||
$: safePage = Math.min(Math.max(0, currentSlide), Math.max(0, thumbnails.length - 1));
|
||||
|
||||
const copyPdfData = (pdfData: ArrayBuffer | Uint8Array) =>
|
||||
pdfData instanceof Uint8Array ? pdfData.slice() : pdfData.slice(0);
|
||||
|
||||
const updateLayout = () => {
|
||||
hideThumbs = (rootEl?.clientWidth ?? window.innerWidth) < 720;
|
||||
};
|
||||
|
||||
const trackThumbnail = (node: HTMLButtonElement, index: number) => {
|
||||
thumbnailButtons[index] = node;
|
||||
return {
|
||||
destroy: () => {
|
||||
if (thumbnailButtons[index] === node) thumbnailButtons[index] = undefined;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const scrollSelectedThumbnailIntoView = () => {
|
||||
if (hideThumbs) return;
|
||||
thumbnailButtons[safePage]?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
};
|
||||
|
||||
const selectPage = async (index: number) => {
|
||||
currentSlide = Math.min(Math.max(0, index), Math.max(0, thumbnails.length - 1));
|
||||
pageTarget = currentSlide + 1;
|
||||
await tick();
|
||||
await pdfViewerRef?.scrollToPage?.(pageTarget);
|
||||
scrollSelectedThumbnailIntoView();
|
||||
};
|
||||
|
||||
const loadThumbnails = async (pdfData: ArrayBuffer | Uint8Array) => {
|
||||
const token = ++loadToken;
|
||||
thumbsLoading = true;
|
||||
thumbnails = [];
|
||||
|
||||
try {
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl;
|
||||
|
||||
pdfDoc?.destroy();
|
||||
pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;
|
||||
|
||||
const rendered: string[] = [];
|
||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||
if (token !== loadToken) return;
|
||||
|
||||
const page = await pdfDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.28 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) continue;
|
||||
|
||||
await page.render({ canvas, canvasContext: ctx, viewport }).promise;
|
||||
rendered.push(canvas.toDataURL('image/png'));
|
||||
}
|
||||
|
||||
if (token === loadToken) thumbnails = rendered;
|
||||
} catch (e) {
|
||||
console.error('PDF thumbnail render error:', e);
|
||||
if (token === loadToken) thumbnails = [];
|
||||
} finally {
|
||||
if (token === loadToken) thumbsLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (data) {
|
||||
viewerData = copyPdfData(data);
|
||||
void loadThumbnails(copyPdfData(data));
|
||||
} else {
|
||||
viewerData = null;
|
||||
thumbnails = [];
|
||||
}
|
||||
|
||||
$: if (targetPage) {
|
||||
currentSlide = Math.max(0, targetPage - 1);
|
||||
pageTarget = targetPage;
|
||||
}
|
||||
|
||||
$: if (thumbnails.length > 0) {
|
||||
void tick().then(scrollSelectedThumbnailIntoView);
|
||||
}
|
||||
|
||||
export const resetView = () => {
|
||||
pdfViewerRef?.resetView();
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
currentSlide = page - 1;
|
||||
if (singlePage) pageTarget = page;
|
||||
void tick().then(scrollSelectedThumbnailIntoView);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
resizeObserver = new ResizeObserver(updateLayout);
|
||||
if (rootEl) resizeObserver.observe(rootEl);
|
||||
updateLayout();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
loadToken++;
|
||||
resizeObserver?.disconnect();
|
||||
pdfDoc?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={rootEl}
|
||||
class="relative grid {hideThumbs
|
||||
? 'grid-cols-[minmax(0,1fr)]'
|
||||
: 'grid-cols-[144px_minmax(0,1fr)]'} min-h-0 bg-transparent text-gray-900 dark:text-gray-100 {className}"
|
||||
>
|
||||
<aside
|
||||
class={hideThumbs
|
||||
? 'hidden'
|
||||
: 'thumbnail-sidebar overflow-y-auto px-2 pt-3 pb-16 border-r border-gray-50 dark:border-gray-850/30 bg-transparent'}
|
||||
aria-label={listLabel}
|
||||
>
|
||||
{#if thumbsLoading && thumbnails.length === 0}
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{:else}
|
||||
{#each thumbnails as thumbnail, index}
|
||||
<button
|
||||
use:trackThumbnail={index}
|
||||
type="button"
|
||||
class="grid grid-cols-[20px_minmax(0,1fr)] items-start gap-2 w-full mb-3 p-0 text-left text-gray-900 dark:text-gray-100"
|
||||
on:click={() => selectPage(index)}
|
||||
aria-label="{itemLabel} {index + 1}"
|
||||
aria-current={safePage === index ? 'true' : undefined}
|
||||
>
|
||||
<span
|
||||
class="pt-1.5 text-[0.6875rem] font-medium text-right {safePage === index
|
||||
? 'text-gray-400 dark:text-gray-500'
|
||||
: 'text-gray-300/70 dark:text-gray-700'}">{index + 1}</span
|
||||
>
|
||||
<span
|
||||
class="block overflow-hidden rounded-md bg-transparent {safePage === index
|
||||
? 'opacity-100'
|
||||
: 'opacity-55 hover:opacity-80'}"
|
||||
>
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt="{itemLabel} {index + 1} thumbnail"
|
||||
class="block w-full h-full object-contain"
|
||||
draggable="false"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<section class="min-w-0 min-h-0 overflow-hidden">
|
||||
{#if viewerData}
|
||||
<PDFViewer
|
||||
bind:this={pdfViewerRef}
|
||||
data={viewerData}
|
||||
targetPage={pageTarget}
|
||||
{singlePage}
|
||||
{itemLabel}
|
||||
onPageChange={handlePageChange}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.thumbnail-sidebar {
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.thumbnail-sidebar:hover,
|
||||
.thumbnail-sidebar:focus,
|
||||
.thumbnail-sidebar:focus-within,
|
||||
.thumbnail-sidebar:active {
|
||||
scrollbar-color: rgba(215, 215, 215, 0.6) transparent;
|
||||
}
|
||||
|
||||
:global(.dark) .thumbnail-sidebar:hover,
|
||||
:global(.dark) .thumbnail-sidebar:focus,
|
||||
:global(.dark) .thumbnail-sidebar:focus-within,
|
||||
:global(.dark) .thumbnail-sidebar:active {
|
||||
scrollbar-color: rgba(67, 67, 67, 0.6) transparent;
|
||||
}
|
||||
|
||||
.thumbnail-sidebar::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.thumbnail-sidebar:hover::-webkit-scrollbar-thumb,
|
||||
.thumbnail-sidebar:focus::-webkit-scrollbar-thumb,
|
||||
.thumbnail-sidebar:focus-within::-webkit-scrollbar-thumb,
|
||||
.thumbnail-sidebar:active::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.thumbnail-sidebar::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
export let currentSlide = 0;
|
||||
export let className = '';
|
||||
export let targetPage: number | null = null;
|
||||
export let itemLabel = 'Slide';
|
||||
export let listLabel = 'Slides';
|
||||
|
||||
let rootEl: HTMLDivElement;
|
||||
let stageEl: HTMLElement;
|
||||
|
|
@ -236,8 +238,8 @@
|
|||
<aside
|
||||
class={hideThumbs
|
||||
? 'hidden'
|
||||
: 'pptx-slide-rail overflow-y-auto px-2 pt-3 pb-16 border-r border-gray-50 dark:border-gray-850/30 bg-transparent'}
|
||||
aria-label="Slides"
|
||||
: 'thumbnail-sidebar overflow-y-auto px-2 pt-3 pb-16 border-r border-gray-50 dark:border-gray-850/30 bg-transparent'}
|
||||
aria-label={listLabel}
|
||||
>
|
||||
{#each slides as slide, index}
|
||||
<button
|
||||
|
|
@ -245,7 +247,7 @@
|
|||
type="button"
|
||||
class="grid grid-cols-[20px_minmax(0,1fr)] items-start gap-2 w-full mb-3 p-0 text-left text-gray-900 dark:text-gray-100"
|
||||
on:click={() => selectSlide(index)}
|
||||
aria-label="Slide {index + 1}"
|
||||
aria-label="{itemLabel} {index + 1}"
|
||||
aria-current={safeSlide === index ? 'true' : undefined}
|
||||
>
|
||||
<span
|
||||
|
|
@ -260,7 +262,7 @@
|
|||
>
|
||||
<img
|
||||
src={slide}
|
||||
alt="Slide {index + 1} thumbnail"
|
||||
alt="{itemLabel} {index + 1} thumbnail"
|
||||
class="block w-full h-full object-contain"
|
||||
draggable="false"
|
||||
/>
|
||||
|
|
@ -283,7 +285,7 @@
|
|||
<img
|
||||
bind:this={slideImgEl}
|
||||
src={selectedSlide}
|
||||
alt="Slide {safeSlide + 1}"
|
||||
alt="{itemLabel} {safeSlide + 1}"
|
||||
class="block w-full h-full object-contain rounded"
|
||||
draggable="false"
|
||||
on:load={onSlideLoad}
|
||||
|
|
@ -303,7 +305,7 @@
|
|||
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 disabled:opacity-30"
|
||||
disabled={safeSlide === 0}
|
||||
on:click={() => selectSlide(safeSlide - 1)}
|
||||
aria-label="Previous slide"
|
||||
aria-label={`Previous ${itemLabel.toLowerCase()}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -327,7 +329,7 @@
|
|||
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 disabled:opacity-30"
|
||||
disabled={safeSlide === slides.length - 1}
|
||||
on:click={() => selectSlide(safeSlide + 1)}
|
||||
aria-label="Next slide"
|
||||
aria-label={`Next ${itemLabel.toLowerCase()}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -391,36 +393,36 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.pptx-slide-rail {
|
||||
.thumbnail-sidebar {
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.pptx-slide-rail:hover,
|
||||
.pptx-slide-rail:focus,
|
||||
.pptx-slide-rail:focus-within,
|
||||
.pptx-slide-rail:active {
|
||||
.thumbnail-sidebar:hover,
|
||||
.thumbnail-sidebar:focus,
|
||||
.thumbnail-sidebar:focus-within,
|
||||
.thumbnail-sidebar:active {
|
||||
scrollbar-color: rgba(215, 215, 215, 0.6) transparent;
|
||||
}
|
||||
|
||||
:global(.dark) .pptx-slide-rail:hover,
|
||||
:global(.dark) .pptx-slide-rail:focus,
|
||||
:global(.dark) .pptx-slide-rail:focus-within,
|
||||
:global(.dark) .pptx-slide-rail:active {
|
||||
:global(.dark) .thumbnail-sidebar:hover,
|
||||
:global(.dark) .thumbnail-sidebar:focus,
|
||||
:global(.dark) .thumbnail-sidebar:focus-within,
|
||||
:global(.dark) .thumbnail-sidebar:active {
|
||||
scrollbar-color: rgba(67, 67, 67, 0.6) transparent;
|
||||
}
|
||||
|
||||
.pptx-slide-rail::-webkit-scrollbar-thumb {
|
||||
.thumbnail-sidebar::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pptx-slide-rail:hover::-webkit-scrollbar-thumb,
|
||||
.pptx-slide-rail:focus::-webkit-scrollbar-thumb,
|
||||
.pptx-slide-rail:focus-within::-webkit-scrollbar-thumb,
|
||||
.pptx-slide-rail:active::-webkit-scrollbar-thumb {
|
||||
.thumbnail-sidebar:hover::-webkit-scrollbar-thumb,
|
||||
.thumbnail-sidebar:focus::-webkit-scrollbar-thumb,
|
||||
.thumbnail-sidebar:focus-within::-webkit-scrollbar-thumb,
|
||||
.thumbnail-sidebar:active::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.pptx-slide-rail::-webkit-scrollbar-corner {
|
||||
.thumbnail-sidebar::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@
|
|||
export let orderBy = 'updated_at';
|
||||
export let direction = 'desc'; // 'asc' or 'desc'
|
||||
|
||||
$: if (!show) {
|
||||
query = '';
|
||||
}
|
||||
|
||||
export let chatList = null;
|
||||
export let allChatsLoaded = false;
|
||||
export let chatListLoading = false;
|
||||
|
|
|
|||
|
|
@ -735,14 +735,14 @@
|
|||
<Tooltip content={model.name} className="min-w-0" placement="top-start">
|
||||
<a
|
||||
href={`/?model=${encodeURIComponent(model.id)}`}
|
||||
class="truncate text-[0.8125rem] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
class="block truncate text-[0.8125rem] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{model.name}
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
class="min-w-0 max-w-[40%] shrink-0 truncate text-[0.6875rem] leading-5 text-gray-500"
|
||||
class="hidden min-w-0 max-w-[40%] shrink-0 truncate text-[0.6875rem] leading-5 text-gray-500 sm:block"
|
||||
>
|
||||
{model.id}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
"Advanced MinerU parsing parameters in JSON format.": "Paràmetres avançats d'anàlisi de MinerU en format JSON.",
|
||||
"Advanced Parameters": "Paràmetres avançats",
|
||||
"Advanced Params": "Paràmetres avançats",
|
||||
"After changing the embedding model, reindex knowledge, knowledge search, and memory vectors for changes to take effect.": "",
|
||||
"After changing the embedding model, reindex knowledge, knowledge search, and memory vectors for changes to take effect.": "Després de canviar el model d'incrustació, torna a indexar el coneixement, la cerca de coneixement i els vectors de memòria perquè els canvis tinguin efecte.",
|
||||
"All": "Tots",
|
||||
"All Chats": "Tots els xats",
|
||||
"All chats have been unarchived.": "Tots els xats han estat desarxivats.",
|
||||
|
|
@ -161,8 +161,8 @@
|
|||
"All time": "Sempre",
|
||||
"All Users": "Tots els usuaris",
|
||||
"All users and system events": "Tots els usuaris i esdeveniments del sistema",
|
||||
"Allow": "",
|
||||
"Allow {{NAME}}?": "",
|
||||
"Allow": "Permetre",
|
||||
"Allow {{NAME}}?": "Vols permetre {{NAME}}?",
|
||||
"Allow artifacts to access same-origin browser APIs inside the sandbox.": "Permetre que els artefactes accedeixin a les API del navegador del mateix origen dins del sandbox.",
|
||||
"Allow automatic names for new chats.": "Permetre noms automàtics per a xats nous.",
|
||||
"Allow browser notifications for completed responses.": "Permetre les notificacions del navegador per a les respostes completades.",
|
||||
|
|
@ -179,7 +179,7 @@
|
|||
"Allow Continue Response": "Permetre continuar la resposta",
|
||||
"Allow delegated sub-agents to keep running while the parent chat continues.": "Permetre que els subagents delegats continuïn funcionant mentre continua el xat principal.",
|
||||
"Allow Delete Messages": "Permetre eliminar missatges",
|
||||
"Allow downloads inside sandboxed iframes.": "",
|
||||
"Allow downloads inside sandboxed iframes.": "Permet descàrregues dins d'iframes sandbox.",
|
||||
"Allow File Upload": "Permetre la pujada d'arxius",
|
||||
"Allow forms inside sandboxed artifact iframes.": "Permetre formularis dins d'iframes d'artefactes en sandbox.",
|
||||
"Allow Google Drive as a document source.": "Permetre Google Drive com a font de documents.",
|
||||
|
|
@ -192,15 +192,15 @@
|
|||
"Allow public write access": "Permetre accés públic d'escriptura",
|
||||
"Allow Rate Response": "Permetre valorar les respostes",
|
||||
"Allow Regenerate Response": "Permetre regenerar respostes",
|
||||
"Allow scripts inside sandboxed iframes.": "",
|
||||
"Allow scripts inside sandboxed iframes.": "Permetre scripts dins d'iframes en sandbox.",
|
||||
"Allow Sharing With Groups": "Permetre compartir amb grups",
|
||||
"Allow Sharing With Users": "Permetre compartir amb usuaris",
|
||||
"Allow Speech to Text": "Permetre Parla a Text",
|
||||
"Allow Temporary Chat": "Permetre el xat temporal",
|
||||
"Allow terminal previews to access same-origin browser APIs.": "",
|
||||
"Allow terminal previews to access same-origin browser APIs.": "Permet que les previsualitzacions del terminal accedeixin a les API del navegador del mateix origen.",
|
||||
"Allow Text to Speech": "Permetre Text a Parla",
|
||||
"Allow the AI to delegate tasks to sub-agents. Each sub-agent creates a real chat with full tool access. Uses additional LLM calls.": "Permetre que l'IA delegui tasques als subagents. Cada subagent crea un xat real amb accés complet a l'eina. Utilitza trucades addicionals de LLM.",
|
||||
"Allow Tool Call": "",
|
||||
"Allow Tool Call": "Permetre la crida a l'eina",
|
||||
"Allow User Location": "Permetre la ubicació de l'usuari",
|
||||
"Allow users to access calendar features.": "Permetre als usuaris accedir a les funcions del calendari.",
|
||||
"Allow users to authenticate with an LDAP directory.": "Permetre als usuaris autenticar-se amb un directori LDAP.",
|
||||
|
|
@ -244,7 +244,7 @@
|
|||
"API Key": "clau API",
|
||||
"API Key / Token": "Clau de l'API/Token",
|
||||
"API Key created.": "clau API creada.",
|
||||
"API Key deleted.": "",
|
||||
"API Key deleted.": "S'ha suprimit la clau de l'API.",
|
||||
"API Key Endpoint Restrictions": "Restriccions del punt d'accés de la Clau API",
|
||||
"API key for embedding requests.": "Clau API per incrustar sol·licituds.",
|
||||
"API key sent to Docling.": "Clau de l'API enviada a Docling.",
|
||||
|
|
@ -298,7 +298,7 @@
|
|||
"Ask a question": "Fer una pregunta",
|
||||
"Ask a test question": "Fes una pregunta de prova",
|
||||
"Ask this knowledge source a test question": "Fes una pregunta de prova a aquesta font de coneixement",
|
||||
"Ask User": "",
|
||||
"Ask User": "Pregunta a l'usuari",
|
||||
"Assistant": "Assistent",
|
||||
"Assistant messages": "Missatges de l'assistent",
|
||||
"Async Embedding Processing": "Procés d'incrustat asíncron",
|
||||
|
|
@ -320,7 +320,7 @@
|
|||
"Auth required": "Es requereix l'autenticació",
|
||||
"Authenticate": "Autenticar",
|
||||
"Authentication": "Autenticació",
|
||||
"Authorize OAuth": "",
|
||||
"Authorize OAuth": "Autoritzar OAuth",
|
||||
"Auto": "Automàtic",
|
||||
"Auto (Random)": "Automàtic (aleatori)",
|
||||
"Auto Redirect": "Redirecció automàtica",
|
||||
|
|
@ -424,7 +424,7 @@
|
|||
"Change User Role": "Canviar el rol d'usuari",
|
||||
"Channel": "Canal",
|
||||
"Channel deleted successfully": "Canal suprimit correctament",
|
||||
"Channel is required": "",
|
||||
"Channel is required": "El canal és obligatori",
|
||||
"Channel Name": "Nom del canal",
|
||||
"Channel name cannot be empty.": "El nom del canal no pot estar buit.",
|
||||
"Channel name must be less than 128 characters": "El nom del canal ha de tenir menys de 128 caràcters",
|
||||
|
|
@ -434,7 +434,7 @@
|
|||
"Character": "Personatge",
|
||||
"Chat": "Xat",
|
||||
"Chat actions": "Accions de xat",
|
||||
"Chat archived.": "Xar arxivat",
|
||||
"Chat archived.": "Xat arxivat",
|
||||
"Chat Background Image": "Imatge de fons del xat",
|
||||
"Chat Bubble UI": "Chat Bubble UI",
|
||||
"Chat Completions": "Completacions de xat",
|
||||
|
|
@ -445,14 +445,14 @@
|
|||
"Chat forked": "Xat bifurcat",
|
||||
"Chat history": "Historial de xats",
|
||||
"Chat History": "Historial de xats",
|
||||
"Chat Hover Previews": "",
|
||||
"Chat Hover Previews": "Previsualitzacions del xat",
|
||||
"Chat ID": "ID del xat",
|
||||
"Chat is too short to compact": "El xat és massa curt per compactar",
|
||||
"Chat moved successfully": "El xat s'ha mogut correctament",
|
||||
"Chat Permissions": "Permisos del xat",
|
||||
"Chat Tags Auto-Generation": "Generació automàtica d'etiquetes del xat",
|
||||
"Chat unshared successfully.": "El xat s'ha deixat de compartir correctament",
|
||||
"Chat Uploads": "",
|
||||
"Chat Uploads": "Càrregues de xat",
|
||||
"Chat Variables": "Variables de xat",
|
||||
"Chat Variables have conflicting model definitions": "Les variables de xat tenen definicions de model contradictòries",
|
||||
"chats": "xats",
|
||||
|
|
@ -461,12 +461,12 @@
|
|||
"Chats Public Sharing": "Compartició pública dels xats",
|
||||
"Check Again": "Comprovar-ho de nou",
|
||||
"Check for updates": "Comprovar si hi ha actualitzacions",
|
||||
"Check OAuth Discovery": "",
|
||||
"Check OAuth Discovery": "Comprovar OAuth Discovery",
|
||||
"Checking for updates...": "Comprovant actualitzacions...",
|
||||
"Choose a dedicated model for context compaction summaries. Current Model follows the active chat model.": "Escull un model dedicat per als resums de compactació de context. El model actual segueix el model de xat actiu.",
|
||||
"Choose a model before saving...": "Triar un model abans de desar...",
|
||||
"Choose automatic, left-to-right, or right-to-left text flow.": "Escull el flux de text automàtic, d'esquerra a dreta o de dreta a esquerra.",
|
||||
"Choose channel": "",
|
||||
"Choose channel": "Tria el canal",
|
||||
"Choose fallback models for background tasks. Current Model follows the active chat model.": "Escull models alternatius per a tasques en segon pla. El model actual segueix el model de xat actiu.",
|
||||
"Choose folder": "Escull la carpeta",
|
||||
"Choose how extracted text is split before indexing.": "Escull com es divideix el text extret abans de la indexació.",
|
||||
|
|
@ -489,7 +489,7 @@
|
|||
"Choose the speech provider used for assistant audio output.": "Escull el proveïdor de veu utilitzat per a la sortida d'àudio de l'assistent.",
|
||||
"Choose the transcription provider used for audio input.": "Escull el proveïdor de transcripció utilitzat per a l'entrada d'àudio.",
|
||||
"Choose where model responses to root-level channel mentions are posted.": "Escull on es publiquen les respostes del model a les mencions del canal arrel.",
|
||||
"Choose where terminal display_file results appear by default.": "",
|
||||
"Choose where terminal display_file results appear by default.": "Tria on apareixen els resultats del fitxer_visualització del terminal per defecte.",
|
||||
"Choose whether Enter sends immediately or uses Ctrl+Enter.": "Escull si Enter envia immediatament o utilitza Ctrl+Enter.",
|
||||
"Choose whether the app opens to the default home or chat view.": "Escull si l'aplicació s'obre a la vista d'inici o de xat predeterminada.",
|
||||
"Chunk Min Size Target": "Objectiu de mida mínima del fragment",
|
||||
|
|
@ -510,7 +510,7 @@
|
|||
"Clear Memory": "Esborrar la memòria",
|
||||
"Clear search": "Esborrar la cerca",
|
||||
"Clear status": "Esborrar l'estat",
|
||||
"Clear vector storage and knowledge indexing data.": "Esboarrar l'emmagatzematge de vectors i dades d'indexació de coneixement.",
|
||||
"Clear vector storage and knowledge indexing data.": "Esborrar l'emmagatzematge de vectors i dades d'indexació de coneixement.",
|
||||
"click here": "prem aquí",
|
||||
"Click here for filter guides.": "Clica aquí per l'ajuda dels filtres.",
|
||||
"Click here for help.": "Clica aquí per obtenir ajuda.",
|
||||
|
|
@ -535,14 +535,14 @@
|
|||
"Close": "Tancar",
|
||||
"Close Banner": "Tancar el bàner",
|
||||
"Close chat controls": "Tancar els controls de xat",
|
||||
"Close citation modal": "Tancar el model de citatció",
|
||||
"Close citation modal": "Tancar la finestra modal de citació",
|
||||
"Close Configure Connection Modal": "Tancar la finestra de configuració de la connexió",
|
||||
"Close feedback": "Tancar el retorn",
|
||||
"Close modal": "Tancar el modal",
|
||||
"Close Modal": "Tancar el modal",
|
||||
"Close Sidebar": "Tancar la barra lateral",
|
||||
"cloud": "núvol",
|
||||
"CMU ARCTIC speaker embedding name": "Nom de l'altaveu d'incrustació CMU ARCTIC",
|
||||
"CMU ARCTIC speaker embedding name": "Nom del parlant d'incrustació CMU ARCTIC",
|
||||
"Code Block": "Bloc de codi",
|
||||
"Code Editor": "Editor de codi",
|
||||
"Code execution": "Execució de codi",
|
||||
|
|
@ -558,7 +558,7 @@
|
|||
"Collapse code blocks by default.": "Col·lapsar els blocs de codi per defecte.",
|
||||
"Collection": "Col·lecció",
|
||||
"Collection Field": "Camp de col·lecció",
|
||||
"Collections": "Col·lecccions",
|
||||
"Collections": "Col·leccions",
|
||||
"Color": "Color",
|
||||
"Combine semantic and keyword retrieval.": "Combinar la recuperació semàntica i de paraules clau.",
|
||||
"ComfyUI": "ComfyUI",
|
||||
|
|
@ -627,7 +627,7 @@
|
|||
"Content Extraction Engine": "Motor d'extracció de contingut",
|
||||
"Content Field": "Camp de contingut",
|
||||
"Content lengths (character counts only)": "Mida del contingut (només els caràcters)",
|
||||
"Content matches": "",
|
||||
"Content matches": "Coincidència de contingut",
|
||||
"Context": "Context",
|
||||
"Context compacted": "Context compactat",
|
||||
"Context Compaction": "Compactació del context",
|
||||
|
|
@ -652,7 +652,7 @@
|
|||
"Conversation saved successfully": "Conversa desada correctament",
|
||||
"Convert long pasted text into a file attachment.": "Converteix el text enganxat llargament en un fitxer adjunt.",
|
||||
"Copied": "Copiat",
|
||||
"Copied link to clipboard": "Enllaç copiat al portaretalls",
|
||||
"Copied link to clipboard": "Enllaç copiat al porta-retalls",
|
||||
"Copied shared chat URL to clipboard!": "S'ha copiat la URL compartida al porta-retalls!",
|
||||
"Copied to clipboard": "Copiat al porta-retalls",
|
||||
"Copy": "Copiar",
|
||||
|
|
@ -708,7 +708,7 @@
|
|||
"Credential used for Document Intelligence.": "Credencial utilitzada per a Document Intelligence.",
|
||||
"Credentials used to authenticate with the Jupyter server.": "Credencials utilitzades per autenticar-se amb el servidor Jupyter.",
|
||||
"CSV Import": "Importar CSV",
|
||||
"CSV: Name,Email": "",
|
||||
"CSV: Name,Email": "CSV: Name,Email",
|
||||
"Ctrl+Enter to Send": "Ctrl+Enter per enviar",
|
||||
"Current": "Actual",
|
||||
"Current Model": "Model actual",
|
||||
|
|
@ -733,7 +733,7 @@
|
|||
"Database": "Base de dades",
|
||||
"Datalab Marker API": "API de Datalab Marker",
|
||||
"Datalab Marker service endpoint used for document parsing.": "Punt final del servei Datalab Marker utilitzat per a l'anàlisi de documents.",
|
||||
"Date is required": "",
|
||||
"Date is required": "La data és necessària",
|
||||
"Date Modified": "Data de modificació",
|
||||
"Day": "Dia",
|
||||
"DD/MM/YYYY": "DD/MM/YYYY",
|
||||
|
|
@ -747,9 +747,9 @@
|
|||
"Default action buttons will be used.": "S'utilitzaran els botons d'acció per defecte",
|
||||
"Default description enabled": "Descripcions per defecte habilitades",
|
||||
"Default Features": "Característiques per defecte",
|
||||
"Default Filters": "Filres per defecte",
|
||||
"Default Filters": "Filtres per defecte",
|
||||
"Default Group": "Grup per defecte",
|
||||
"Default Interface Settings": "",
|
||||
"Default Interface Settings": "Configuració de la interfície per defecte",
|
||||
"Default Model": "Model per defecte",
|
||||
"Default model updated": "Model per defecte actualitzat",
|
||||
"Default permissions": "Permisos per defecte",
|
||||
|
|
@ -771,7 +771,7 @@
|
|||
"Delete All Chats": "Eliminar tots els xats",
|
||||
"Delete all contents inside this directory": "Eliminar tot el contingut d'aquesta carpeta",
|
||||
"Delete all contents inside this folder": "Eliminar tot el contingut d'aquesta carpeta",
|
||||
"Delete API Key?": "",
|
||||
"Delete API Key?": "Vols suprimir la clau de l'API?",
|
||||
"Delete automation?": "Eliminar l'automatització",
|
||||
"Delete calendar": "Eliminar el calendari",
|
||||
"Delete Calendar": "Eliminar el calendari",
|
||||
|
|
@ -799,9 +799,9 @@
|
|||
"Deleted {{name}}": "S'ha eliminat {{name}}",
|
||||
"Deleted {{ok}} of {{total}} items": "S'han eliminat {{ok}} de {{total}} ítems",
|
||||
"Deleted User": "Usuari eliminat",
|
||||
"Denied {{NAME}}": "",
|
||||
"Deny": "",
|
||||
"Deny Tool Call": "",
|
||||
"Denied {{NAME}}": "S'ha denegat {{NAME}}",
|
||||
"Deny": "Denegar",
|
||||
"Deny Tool Call": "Denegar la crida a l'eina",
|
||||
"Deployment names are required for Azure OpenAI": "Els noms de desplegament són requerits per Azure OpenAI",
|
||||
"Desc": "Descendent",
|
||||
"Describe the edit...": "Descriu l'edició...",
|
||||
|
|
@ -810,7 +810,7 @@
|
|||
"Describe your knowledge base and objectives": "Descriu la teva base de coneixement i objectius",
|
||||
"Description": "Descripció",
|
||||
"Deselect": "Deseleccionar",
|
||||
"Destination": "",
|
||||
"Destination": "Destinació",
|
||||
"Detect Artifacts Automatically": "Detectar automàticament els artefactes",
|
||||
"Detect generated artifacts and show them in the artifact workspace.": "Detectar els artefactes generats i mostrar-los a l'espai de treball d'artefactes.",
|
||||
"Detected Variables": "Variables detectades",
|
||||
|
|
@ -877,27 +877,27 @@
|
|||
"Don't like the style": "No t'agrada l'estil?",
|
||||
"Done": "Fet",
|
||||
"Download": "Descarregar",
|
||||
"Download \"{{searchValue}}\"": "",
|
||||
"Download \"{{searchValue}}\" from {{provider}}": "",
|
||||
"Download \"{{searchValue}}\"": "Descarrega \"{{searchValue}}\"",
|
||||
"Download \"{{searchValue}}\" from {{provider}}": "Descarrega \"{{searchValue}}\" de {{provider}}",
|
||||
"Download & Delete": "Descarregar i suprimir",
|
||||
"Download all users as CSV.": "Descarregar tots els usuaris com a CSV.",
|
||||
"Download as JSON": "Descarregar com a JSON",
|
||||
"Download as SVG": "Descarrega com a SVG",
|
||||
"Download canceled": "Descàrrega cancel·lada",
|
||||
"Download CSV Template": "",
|
||||
"Download CSV Template": "Descarregar la plantilla CSV",
|
||||
"Download every user's chat history as JSON.": "Descarregar l'historial de xat de cada usuari com a JSON.",
|
||||
"Download failed": "La descarrega ha fallat",
|
||||
"Download Model": "",
|
||||
"Download Model": "Descarregar model",
|
||||
"Download the application database when supported.": "Descarregar la base de dades de l'aplicació quan sigui compatible.",
|
||||
"Download the current admin configuration as JSON.": "Descarregar la configuració actual de l'administració com a JSON.",
|
||||
"Download your chat history as a JSON export.": "Descarregar el teu historial de xat com a exportació JSON.",
|
||||
"Downloading \"{{searchValue}}\"": "",
|
||||
"Downloading \"{{searchValue}}\"": "Descarregant \"{{searchValue}}\"",
|
||||
"Downloading stats...": "Descarregant estadístiques...",
|
||||
"Drag to reorder": "Arrossegar per reordenar",
|
||||
"Draw": "Dibuixar",
|
||||
"Drop any files here to upload": "Arrossega aquí qualsevol fitxer per pujar-lo",
|
||||
"Drop files here": "Arrossega els arxius aquí",
|
||||
"Drop to upload": "",
|
||||
"Drop to upload": "Arrossega per carregar",
|
||||
"DuckDuckGo": "DuckDuckGo",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
|
||||
"e.g. 'low', 'medium', 'high'": "p. ex. 'baix', 'mitjà', 'alt'",
|
||||
|
|
@ -915,7 +915,7 @@
|
|||
"e.g. my_tools": "p. ex. les_meves_eines",
|
||||
"e.g. pdf, docx, txt": "p. ex. pdf, docx, txt",
|
||||
"e.g. Step-by-step instructions for code reviews": "p. ex. instruccions pas a pas per a revisions de codi",
|
||||
"e.g. Tell me a fun fact": "p. ex. digues-me quelcome divertit",
|
||||
"e.g. Tell me a fun fact": "p. ex. digues-me quelcom divertit",
|
||||
"e.g. Tell me a fun fact about the Roman Empire": "p. ex. digues-me quelcom divertit sobre l'imperi Romà",
|
||||
"e.g. Tools for performing various operations": "p. ex. Eines per dur a terme operacions",
|
||||
"e.g., 3, 4, 5 (leave blank for default)": "p. ex. 3, 4, 5 (deixa-ho en blanc per utilitzar el per defecte)",
|
||||
|
|
@ -956,7 +956,7 @@
|
|||
"Emojis": "Emojis",
|
||||
"Empty": "Buit",
|
||||
"Empty message": "Missatge buit",
|
||||
"Enable accessibility-focused visual enhancements.": "",
|
||||
"Enable accessibility-focused visual enhancements.": "Activa les millores visuals centrades en l'accessibilitat.",
|
||||
"Enable All": "Habilitar tot",
|
||||
"Enable background sub-agents": "Activar els subagents en segon pla",
|
||||
"Enable Code Execution": "Permetre l'execució de codi",
|
||||
|
|
@ -1049,7 +1049,7 @@
|
|||
"Enter New Password": "Introdueix un nova contrasenya",
|
||||
"Enter Number of Steps (e.g. 50)": "Introdueix el nombre de passos (p. ex. 50)",
|
||||
"Enter Ollama Cloud API Key": "Introdueix la clau API de Ollama Cloud",
|
||||
"Enter OpenSERP Base URL": "",
|
||||
"Enter OpenSERP Base URL": "Introduïu l'URL base d'OpenSERP",
|
||||
"Enter PaddleOCR-vl API Base URL": "Introdueix la URL Base de PaddleOCR-vl API",
|
||||
"Enter PaddleOCR-vl API Token": "Introdueix el Token de PaddleOCR-vl API",
|
||||
"Enter Perplexity API Key": "Introdueix la clau API de Perplexity",
|
||||
|
|
@ -1218,7 +1218,7 @@
|
|||
"Failed to load DOCX file. Please try downloading it instead.": "No s'ha pogut carregar el fitxer DOCX. Si us plau, prova de descarregar-lo.",
|
||||
"Failed to load Excel/CSV file. Please try downloading it instead.": "No s'ha pogut carregar el fitxer Excel/CSV. Si us plau, prova de descarregar-lo.",
|
||||
"Failed to load file content.": "No s'ha pogut carregar el contingut del fitxer",
|
||||
"Failed to load folder": "",
|
||||
"Failed to load folder": "No s'ha pogut carregar la carpeta",
|
||||
"Failed to load Interface settings": "No s'ha pogut carregar la configuració de la interfície",
|
||||
"Failed to load policy: {{error}}": "No s'ha pogut carregar la política: {{error}}",
|
||||
"Failed to load PPTX file. Please try downloading it instead.": "No s'ha pogut carregar el fitxer PPTX. Si us plau, prova de descarregar-lo.",
|
||||
|
|
@ -1237,7 +1237,7 @@
|
|||
"Failed to save policy: {{error}}": "No s'ha pogut desar la política: {{error}}",
|
||||
"Failed to save terminal servers": "No s'han pogut desar els servidors de terminal",
|
||||
"Failed to save webhook": "No s'ha pogut desar el webhook",
|
||||
"Failed to search files": "",
|
||||
"Failed to search files": "No s'han pogut cercar els fitxers",
|
||||
"Failed to unshare chat.": "No s'ha pogut deixar de compartir el xat.",
|
||||
"Failed to update settings": "No s'han pogut actualitzar les preferències",
|
||||
"Failed to update status": "No s'ha pogut actualitzar l'estat",
|
||||
|
|
@ -1272,9 +1272,9 @@
|
|||
"File Upload": "Pujar arxiu",
|
||||
"File uploaded successfully": "Arxiu pujat satisfactòriament",
|
||||
"Filename": "Nom de l'arxiu",
|
||||
"Filename matches": "",
|
||||
"Filename matches": "El nom del fitxer coincideix",
|
||||
"Files": "Arxius",
|
||||
"Filesystem": "",
|
||||
"Filesystem": "Sistema de fitxers",
|
||||
"Fill the required fields first.": "Omple primer els camps obligatoris.",
|
||||
"Fill the source fields and test query first.": "Omple primer els camps de la font i la consulta de prova.",
|
||||
"Filter": "Filtre",
|
||||
|
|
@ -1408,7 +1408,7 @@
|
|||
"Hide": "Amaga",
|
||||
"Hide All": "Amagar tots",
|
||||
"Hide from Sidebar": "Amagar de la barra lateral",
|
||||
"Hide Hidden Files": "",
|
||||
"Hide Hidden Files": "Amaga els fitxers ocults",
|
||||
"Hide Model": "Amagar el model",
|
||||
"High": "Alt",
|
||||
"High Contrast Mode": "Mode d'alt contrast",
|
||||
|
|
@ -1426,14 +1426,14 @@
|
|||
"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Afirmo que he llegit i entenc les implicacions de la meva acció. Soc conscient dels riscos associats a l'execució de codi arbitrari i he verificat la fiabilitat de la font.",
|
||||
"ID": "ID",
|
||||
"ID cannot contain \":\" or \"|\" characters": "L'ID no pot contenir caràcters \":\" ni \"|\"",
|
||||
"ID copied to clipboard": "L'ID s'ha copiat al portaretalls",
|
||||
"ID copied to clipboard": "L'ID s'ha copiat al porta-retalls",
|
||||
"Identity audit": "Auditoria d'identitat",
|
||||
"Idle only": "Només inactiu",
|
||||
"Idle Timeout": "Temps d'espera d'inactivitat",
|
||||
"iframe Sandbox Allow Downloads": "",
|
||||
"iframe Sandbox Allow Downloads": "Permetre descàrregues iframe Sandbox",
|
||||
"iframe Sandbox Allow Forms": "Permetre formularis sandbox iframe",
|
||||
"iframe Sandbox Allow Same Origin": "Permetre same-origin sandbox iframe",
|
||||
"iframe Sandbox Allow Scripts": "",
|
||||
"iframe Sandbox Allow Scripts": "Permetre scripts sandbox iframe",
|
||||
"Image": "Imatge",
|
||||
"Image Compression": "Compressió d'imatges",
|
||||
"Image Compression Height": "Alçada de la compressió d'imatges",
|
||||
|
|
@ -1448,7 +1448,7 @@
|
|||
"Image Prompt Generation": "Generació d'indicacions d'imatge",
|
||||
"Image Prompt Generation Prompt": "Indicació per a la generació d'indicacions d'imatge",
|
||||
"Image Size": "Mida de la imatge",
|
||||
"Image unavailable": "",
|
||||
"Image unavailable": "Imatge no disponible",
|
||||
"image/*, video/*": "imatge/*, vídeo/*",
|
||||
"Images": "Imatges",
|
||||
"Import": "Importar",
|
||||
|
|
@ -1456,7 +1456,7 @@
|
|||
"Import chat history from a JSON export file.": "Importar l'historial de xats a partir d'un fitxer JSON",
|
||||
"Import Chats": "Importar xats",
|
||||
"Import Config": "Importar la configuració",
|
||||
"Import CSV": "",
|
||||
"Import CSV": "Importar CSV",
|
||||
"Import From Link": "Importar des d'un enllaç",
|
||||
"Import JSON": "Importar JSON",
|
||||
"Import Models": "Importar models",
|
||||
|
|
@ -1468,7 +1468,7 @@
|
|||
"Important Update": "Actualització important",
|
||||
"Imported automations successfully": "Automatitzacions importades correctament",
|
||||
"Imported notes successfully": "Notes importades correctament",
|
||||
"Importing...": "",
|
||||
"Importing...": "S'està important...",
|
||||
"Inactive": "No actiu",
|
||||
"Include": "Incloure",
|
||||
"Include saved memories in the system context.": "Incloure les memòries desades al context del sistema.",
|
||||
|
|
@ -1482,10 +1482,10 @@
|
|||
"Inject stored memories into conversation context": "Injectar la memòria emmagatzemats al context de la conversa",
|
||||
"Inject the entire content as context for comprehensive processing, this is recommended for complex queries.": "Injectar tot el contingut com a context per a un processament complet, això es recomana per a consultes complexes.",
|
||||
"Inject the entire content as context for comprehensive processing.": "Injectar tot el contingut com a context per a un processament complet.",
|
||||
"Inline": "",
|
||||
"Inline": "En línia",
|
||||
"Input": "Entrada",
|
||||
"Input Key (e.g. text, unet_name, steps)": "Entra la clau (p. ex. text, unet_name, steps)",
|
||||
"Input needed": "",
|
||||
"Input needed": "Es necessita entrada",
|
||||
"Input Variables": "Variables d'entrada",
|
||||
"Insert": "Inserir",
|
||||
"Insert Follow-Up Prompt to Input": "Inserir un missatge de continuació per a l'entrada",
|
||||
|
|
@ -1502,7 +1502,7 @@
|
|||
"Interface": "Interfície",
|
||||
"Interface Settings Access": "Accés a la configuració de la interfície",
|
||||
"Invalid file format.": "Format d'arxiu no vàlid.",
|
||||
"Invalid format.": "",
|
||||
"Invalid format.": "Format no vàlid.",
|
||||
"Invalid JSON file": "Arxiu JSON no vàlid",
|
||||
"Invalid JSON format": "Format JSON no vàlid",
|
||||
"Invalid JSON format for ComfyUI Edit Workflow.": "Format JSON no vàlid per al Workflow de ComfyUI d'edició",
|
||||
|
|
@ -1543,7 +1543,7 @@
|
|||
"Knowledge Access": "Accés al coneixement",
|
||||
"Knowledge Base": "Base de coneixement",
|
||||
"Knowledge base has been reset": "La base de coneixement s'ha reiniciat",
|
||||
"Knowledge base not found.": "",
|
||||
"Knowledge base not found.": "No s'ha trobat la base de coneixement.",
|
||||
"Knowledge created successfully.": "Coneixement creat correctament.",
|
||||
"Knowledge deleted successfully.": "Coneixement eliminat correctament.",
|
||||
"Knowledge Description": "Descripció del coneixement",
|
||||
|
|
@ -1617,8 +1617,8 @@
|
|||
"Live": "Live",
|
||||
"llama.cpp": "llama.cpp",
|
||||
"LLMs can make mistakes. Verify important information.": "Els models de llenguatge poden cometre errors. Verifica la informació important.",
|
||||
"LM Studio": "",
|
||||
"Load Model": "",
|
||||
"LM Studio": "LM Studio",
|
||||
"Load Model": "Carregar model",
|
||||
"Loaded": "Carregat",
|
||||
"Loader": "Carregador",
|
||||
"Loading Kokoro.js...": "Carregant Kokoro.js",
|
||||
|
|
@ -1664,7 +1664,7 @@
|
|||
"Max concurrent": "Màxim concurrents",
|
||||
"Max iterations": "Màxim d'iteracions",
|
||||
"Max output": "Sortida màxima",
|
||||
"Max Speakers": "Nombre màxim d'altaveus",
|
||||
"Max Speakers": "Nombre màxim de parlants",
|
||||
"Max tokens to retrieve (1024-32768, default 8192)": "Nombre màxim de tokens a obtenir (1024-32768, per defecte 8192)",
|
||||
"Max Upload Count": "Nombre màxim de càrregues",
|
||||
"Max Upload Size": "Mida màxima de càrrega",
|
||||
|
|
@ -1747,18 +1747,18 @@
|
|||
"Model Capabilities": "Capacitats del model",
|
||||
"Model created successfully!": "Model creat correctament",
|
||||
"Model Defaults": "Models per defecte",
|
||||
"Model deleted successfully": "",
|
||||
"Model download started": "",
|
||||
"Model deleted successfully": "El model s'ha suprimit correctament",
|
||||
"Model download started": "S'ha iniciat la descàrrega del model",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "S'ha detectat el camí del sistema de fitxers del model. És necessari un nom curt del model per actualitzar, no es pot continuar.",
|
||||
"Model Filtering": "Filtrat de models",
|
||||
"Model ID": "Identificador del model",
|
||||
"Model ID is already added": "",
|
||||
"Model ID is already added": "L'identificador del model ja s'ha afegit",
|
||||
"Model ID is required.": "L'ID del model és necessari",
|
||||
"Model IDs": "Identificadors del model",
|
||||
"Model is now private": "El model ara és privat",
|
||||
"Model is now public": "El model ara és públic",
|
||||
"Model list refreshed": "",
|
||||
"Model loaded successfully": "",
|
||||
"Model list refreshed": "S'ha actualitzat la llista de models",
|
||||
"Model loaded successfully": "El model s'ha carregat correctament",
|
||||
"Model Name": "Nom del model",
|
||||
"Model name already exists, please choose a different one": "Aquest nom de model ja existeix, si us plau, tria'n un altre",
|
||||
"Model Name is required.": "El nom del model és necessari",
|
||||
|
|
@ -1771,7 +1771,7 @@
|
|||
"Model Parameters": "Paràmetres del model",
|
||||
"Model Params": "Paràmetres del model",
|
||||
"Model Permissions": "Permisos dels models",
|
||||
"Model providers": "",
|
||||
"Model providers": "Proveïdors de models",
|
||||
"Model removed from pinned models": "Model eliminat dels models fixats",
|
||||
"Model removed from selected models": "S'ha eliminat el model dels models seleccionats",
|
||||
"Model Response Mode": "Mode de resposta del model",
|
||||
|
|
@ -1837,7 +1837,7 @@
|
|||
"New Tool": "Nova eina",
|
||||
"New Webhook": "Nou webhook",
|
||||
"new-channel": "nou-canal",
|
||||
"Next": "",
|
||||
"Next": "Següent",
|
||||
"Next message": "Missatge següent",
|
||||
"Next month": "El mes vinent",
|
||||
"Next page": "Pàgina següent",
|
||||
|
|
@ -1849,12 +1849,12 @@
|
|||
"No additional headers are sent unless configured.": "No s'envia cap capçalera addicional tret que estigui configurada.",
|
||||
"No authentication": "Sense autenticació",
|
||||
"No automations found": "No s'ha trobat cap automatització",
|
||||
"No channels": "",
|
||||
"No channels": "Sense canals",
|
||||
"No chat alerts": "No hi ha alertes de xat",
|
||||
"No chat history": "No hi ha historial de xat",
|
||||
"No chat to compact": "No hi ha xat per compactar",
|
||||
"No chat to fork": "No hi ha cap xat a bifurcar",
|
||||
"No chats": "",
|
||||
"No chats": "No hi ha xats",
|
||||
"No chats found": "No s'han trobat xats",
|
||||
"No chats found for this user.": "No s'han trobat xats per a aquest usuari.",
|
||||
"No chats found.": "No s'ha trobat xats.",
|
||||
|
|
@ -1946,7 +1946,7 @@
|
|||
"OAuth 2.1 (Static)": "OAuth 2.1 (Estàtic)",
|
||||
"OAuth client identifier from the provider.": "Identificador de client OAuth del proveïdor.",
|
||||
"OAuth client secret from the provider.": "Secret del client OAuth del proveïdor.",
|
||||
"OAuth discovery successful": "",
|
||||
"OAuth discovery successful": "La descoberta d'OAuth ha estat correcta",
|
||||
"OAuth ID": "ID OAuth",
|
||||
"OAuth Resource Parameter": "Paràmetre de recurs OAuth",
|
||||
"OAuth Scopes": "Àmbits d'OAuth",
|
||||
|
|
@ -1971,7 +1971,7 @@
|
|||
"OneDrive": "OneDrive",
|
||||
"Only active during Voice Mode.": "Només actiu en el mode de Veu.",
|
||||
"Only active when \"Paste Large Text as File\" setting is toggled on.": "Només està actiu quan l'opció \"Enganxa text gran com a fitxer\" està activada.",
|
||||
"Only active when a tool call is waiting for approval.": "",
|
||||
"Only active when a tool call is waiting for approval.": "Només està actiu quan una crida d'eina està esperant aprovació.",
|
||||
"Only active when the chat input is in focus and an LLM is generating a response.": "Només s'activa quan l'entrada del xat està en focus i un LLM està generant una resposta.",
|
||||
"Only active when the chat input is in focus.": "Només actiu quan l'entrada del xat està en focus.",
|
||||
"Only alphanumeric characters and hyphens are allowed": "Només es permeten caràcters alfanumèrics i guions",
|
||||
|
|
@ -1993,7 +1993,7 @@
|
|||
"Open create menu": "Obrir el menú de creació",
|
||||
"Open detail blocks by default.": "Obrir blocs de detalls per defecte.",
|
||||
"Open file": "Obrir arxiu",
|
||||
"Open Folder": "",
|
||||
"Open Folder": "Obre la carpeta",
|
||||
"Open in full screen": "Obrir en pantalla complerta",
|
||||
"Open in new tab": "Obrir en una nova pestanya",
|
||||
"Open link": "Obrir l'enllaç",
|
||||
|
|
@ -2024,12 +2024,12 @@
|
|||
"OpenAPI Spec": "Especificació OpenAPI",
|
||||
"openapi.json URL or Path": "Camí o URL a openapi.json",
|
||||
"OpenID discovery URL for this provider.": "URL de descobriment d'OpenID per a aquest proveïdor.",
|
||||
"OpenSERP URL": "",
|
||||
"OpenSERP URL": "URL d'OpenSERP",
|
||||
"optional": "opcional",
|
||||
"Optional": "Opcional",
|
||||
"Optional API key for Ollama requests.": "Clau API opcional per a peticions a Ollama.",
|
||||
"or": "o",
|
||||
"Orchestrator": "",
|
||||
"Orchestrator": "Orquestrador",
|
||||
"Ordered List": "Llista ordenada",
|
||||
"Other": "Altres",
|
||||
"out of": "de",
|
||||
|
|
@ -2053,13 +2053,13 @@
|
|||
"Passthrough params": "Paràmetres de pas",
|
||||
"Password": "Contrasenya",
|
||||
"Password for the bind DN.": "Contrasenya per al DN d'enllaç.",
|
||||
"Password updated. Please sign in again.": "",
|
||||
"Password updated. Please sign in again.": "Contrasenya actualitzada. Si us plau, torna a iniciar la sessió.",
|
||||
"Passwords do not match.": "Les contrasenyes no coincideixen",
|
||||
"Paste inserted prompts as rich text when possible.": "Enganxar les sol·licituds inserides com a text enriquit quan sigui possible.",
|
||||
"Paste Large Text as File": "Enganxa un text llarg com a fitxer",
|
||||
"Path": "Camí",
|
||||
"Path copied": "Camí copiat",
|
||||
"Pause a response to ask the user a clarifying question": "",
|
||||
"Pause a response to ask the user a clarifying question": "Posa en pausa una resposta per fer una pregunta d'aclariment a l'usuari",
|
||||
"Paused": "Pausat",
|
||||
"PDF document (.pdf)": "Document PDF (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
|
||||
|
|
@ -2071,8 +2071,8 @@
|
|||
"Pending Accounts": "Comptes pendents",
|
||||
"Pending User Overlay Content": "Contingut de la finestra d'usuari pendent",
|
||||
"Pending User Overlay Title": "Títol de la finestra d'usuari pendent",
|
||||
"Per automation": "",
|
||||
"Per chat": "",
|
||||
"Per automation": "Per automatització",
|
||||
"Per chat": "Per xat",
|
||||
"Percentage of recent messages to keep after older messages are summarized.": "Percentatge de missatges recents que cal conservar després de resumir els missatges més antics.",
|
||||
"Permanently delete every chat after confirmation.": "Suprimeix permanentment cada xat després de la confirmació.",
|
||||
"Permission denied when accessing media devices": "Permís denegat en accedir a dispositius multimèdia",
|
||||
|
|
@ -2120,7 +2120,7 @@
|
|||
"Please enter Client ID and Client Secret": "Si us plau, introdueix l'ID de client i el secret del client.",
|
||||
"Please fill in all fields.": "Emplena tots els camps, si us plau.",
|
||||
"Please register the OAuth client": "Si us plau, registra el client OAuth",
|
||||
"Please save the connection before authorizing OAuth": "",
|
||||
"Please save the connection before authorizing OAuth": "Si us plau, desa la connexió abans d'autoritzar OAuth",
|
||||
"Please save the connection to persist the OAuth client information and do not change the ID": "Si us plau, desa la connexió per conservar la informació del client OAuth i no canviïs l'ID.",
|
||||
"Please select a model.": "Si us plau, selecciona un model.",
|
||||
"Please select a reason": "Si us plau, selecciona una raó",
|
||||
|
|
@ -2138,14 +2138,14 @@
|
|||
"Preferred transcript language codes, separated by commas.": "Codis de llengua de transcripció preferits, separats per comes.",
|
||||
"Prefix ID": "Identificador del prefix",
|
||||
"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "L'identificador de prefix s'utilitza per evitar conflictes amb altres connexions afegint un prefix als ID de model; deixa'l en blanc per desactivar-lo.",
|
||||
"Preparing {{NAME}}...": "",
|
||||
"Preparing {{NAME}}...": "S'està preparant {{NAME}}...",
|
||||
"Preparing download...": "Preparant la descàrrega...",
|
||||
"Press keys...": "Prem una tecla...",
|
||||
"Prevent File Creation": "Prevenir la creació d'arxius",
|
||||
"Preview": "Previsualització",
|
||||
"Preview Access": "Previsualitza l'accés",
|
||||
"Preview failed": "",
|
||||
"Previous": "",
|
||||
"Preview failed": "La previsualització ha fallat",
|
||||
"Previous": "Anterior",
|
||||
"Previous 30 days": "30 dies anteriors",
|
||||
"Previous 7 days": "7 dies anteriors",
|
||||
"Previous message": "Missatge anterior",
|
||||
|
|
@ -2205,13 +2205,13 @@
|
|||
"Reasoning Effort": "Esforç de raonament",
|
||||
"Reasoning Tags": "Etiqueta de raonament",
|
||||
"Reasoning text...": "Text de raonament...",
|
||||
"Rebuild knowledge file, knowledge search, and memory vectors using the current embedding model.": "",
|
||||
"Rebuild vectors for existing knowledge files, knowledge search, and memories.": "",
|
||||
"Rebuild knowledge file, knowledge search, and memory vectors using the current embedding model.": "Reconstruir el fitxer de coneixement, la cerca de coneixement i els vectors de memòria utilitzant el model d'inserció actual.",
|
||||
"Rebuild vectors for existing knowledge files, knowledge search, and memories.": "Reconstruir vectors per a fitxers de coneixement existents, cerca de coneixement i memòries.",
|
||||
"Receives matching events across the instance, including system/config events and events associated with any user.": "Rep esdeveniments coincidents a tota la instància, inclosos els esdeveniments del sistema/configuració i els esdeveniments associats a qualsevol usuari.",
|
||||
"Receives matching events that are not associated with a user.": "Rep esdeveniments coincidents que no estan associats a un usuari.",
|
||||
"Receives matching user-associated events only when the actor, user subject, or user data matches these users or current group members. System/config events are not sent.": "Rep esdeveniments coincidents associats a l'usuari només quan l'actor, el subjecte de l'usuari o les dades de l'usuari coincideixen amb aquests usuaris o membres del grup actual. No s'envien esdeveniments de sistema/configuració.",
|
||||
"Recently Used": "Recentment utilitzat",
|
||||
"Recommended": "",
|
||||
"Recommended": "Recomanat",
|
||||
"Reconnected": "Reconnectat",
|
||||
"Record": "Enregistrar",
|
||||
"Record voice": "Enregistrar la veu",
|
||||
|
|
@ -2238,8 +2238,8 @@
|
|||
"Registration failed": "El registre ha fallat",
|
||||
"Registration successful": "Registre correcte",
|
||||
"Reindex": "Reindexar",
|
||||
"Reindex Embedding Data": "",
|
||||
"Reindex Knowledge and Memory Vectors": "",
|
||||
"Reindex Embedding Data": "Reindexar les dades d'incrustació",
|
||||
"Reindex Knowledge and Memory Vectors": "Reindexar els vectors de coneixement i memòria",
|
||||
"Release Notes": "Notes de la versió",
|
||||
"Releases": "Versions",
|
||||
"Relevance": "Rellevància",
|
||||
|
|
@ -2289,7 +2289,7 @@
|
|||
"Reset Upload Directory": "Restableix el directori de pujades",
|
||||
"Reset Vector Storage/Knowledge": "Restableix el Repositori de vectors/Coneixement",
|
||||
"Reset view": "Netejar la vista",
|
||||
"Reset zoom": "",
|
||||
"Reset zoom": "Restableix el zoom",
|
||||
"Response": "Resposta",
|
||||
"Response Auto-Scroll": "Desplaçament automàtic de resposta",
|
||||
"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notificacions de resposta no es poden activar perquè els permisos del lloc web han estat rebutjats. Comprova les preferències del navegador per donar l'accés necessari.",
|
||||
|
|
@ -2357,12 +2357,12 @@
|
|||
"Search and view user chat history": "Cerca i mostra l'historial de xats",
|
||||
"Search Automations": "Cercar automatitzacions",
|
||||
"Search Base": "Base de cerca",
|
||||
"Search channels": "",
|
||||
"Search channels": "Cerca canals",
|
||||
"Search channels and channel messages": "Cerca els canals i els missatges als canals",
|
||||
"Search Chats": "Cercar xats",
|
||||
"Search Collection": "Cercar col·leccions",
|
||||
"Search Files": "Cerca arxius",
|
||||
"Search files and contents": "",
|
||||
"Search files and contents": "Cerca fitxers i continguts",
|
||||
"Search filters": "Filtres de cerca",
|
||||
"Search Filters": "Filtres de cerca",
|
||||
"Search folders": "Cercar carpetes",
|
||||
|
|
@ -2429,7 +2429,7 @@
|
|||
"Select an embedding model engine": "Seleccionar un motor d'incrustació",
|
||||
"Select an engine": "Seleccionar un motor",
|
||||
"Select an installed pipeline to configure or remove.": "Seleccionar una pipeline instal·lada per configurar o eliminar",
|
||||
"Select an instance": "",
|
||||
"Select an instance": "Selecciona una instància",
|
||||
"Select an Ollama instance": "Seleccionar una instància d'Ollama",
|
||||
"Select an option": "Seleccionar una opció",
|
||||
"Select an output format": "Seleccionar un format de sortida",
|
||||
|
|
@ -2448,7 +2448,7 @@
|
|||
"Select the Gemini endpoint method to call.": "Seleccionar el punt de connexió de Gemini a cridar.",
|
||||
"Select the local model precision used by Kokoro.js.": "Seleccionar la precisió del model local utilitzat per Kohoro.js.",
|
||||
"Select the Pipelines server to manage.": "Seleccionar el servidor de Pipelines a gestionar.",
|
||||
"Select the Tika server API version.": "",
|
||||
"Select the Tika server API version.": "Selecciona la versió de l'API del servidor Tika.",
|
||||
"Select Tool": "Selecionar eina",
|
||||
"Select view": "Seleccionar una vista",
|
||||
"Selected": "Seleccionat",
|
||||
|
|
@ -2494,7 +2494,7 @@
|
|||
"Set embedding model": "Establir el model d'incrustació",
|
||||
"Set embedding model (e.g. {{model}})": "Establir el model d'incrustació (p. ex. {{model}})",
|
||||
"Set reranking model (e.g. {{model}})": "Establir el model de reavaluació (p. ex. {{model}})",
|
||||
"Set system-wide interface defaults for every account. Personal settings override these defaults.": "",
|
||||
"Set system-wide interface defaults for every account. Personal settings override these defaults.": "Establir els valors predeterminats de la interfície a tot el sistema per a cada compte. La configuració personal anul·la aquests valors predeterminats.",
|
||||
"Set the birth date saved with your profile.": "Establir la data de naixement desada amb el teu perfil.",
|
||||
"Set the default system prompt for new chats.": "Establir la sol·licitud predeterminada del sistema per a nous xats.",
|
||||
"Set the display name shown across your account.": "Establir el nom de visualització que es mostra al teu compte.",
|
||||
|
|
@ -2512,13 +2512,13 @@
|
|||
"Sets the stop sequences to use. When this pattern is encountered, the LLM will stop generating text and return. Multiple stop patterns may be set by specifying multiple separate stop parameters in a modelfile.": "Establir les seqüències d'aturada a utilitzar. Quan es trobi aquest patró, el LLM deixarà de generar text. Es poden establir diversos patrons de parada especificant diversos paràmetres de parada separats en un fitxer model.",
|
||||
"Setting": "Preferència",
|
||||
"Settings": "Preferències",
|
||||
"settings configured": "",
|
||||
"settings configured": "configuració establerta",
|
||||
"Settings Permissions": "Preferències de permisos",
|
||||
"Settings saved successfully!": "Les preferències s'han desat correctament",
|
||||
"Shapes the short label generated for each chat.": "Donar forma a l'etiqueta curta generada per a cada xat.",
|
||||
"Share": "Compartir",
|
||||
"Share Chat": "Compartir el xat",
|
||||
"Share link copied to clipboard.": "Compartir l'enllaç copiat al portaretalls",
|
||||
"Share link copied to clipboard.": "Compartir l'enllaç copiat al porta-retalls",
|
||||
"Share to Open WebUI Community": "Compartir amb la comunitat OpenWebUI",
|
||||
"Share your background and interests": "Compartir la teva informació i interessos",
|
||||
"Share your current location with features that can use it.": "Compartir la teva ubicació actual amb funcions que la puguin utilitzar.",
|
||||
|
|
@ -2529,7 +2529,7 @@
|
|||
"Sharing Permissions": "Compartir els permisos",
|
||||
"Show": "Mostrar",
|
||||
"Show \"What's New\" Modal on Login": "Veure 'Què hi ha de nou' a l'entrada",
|
||||
"Show a floating preview of recent messages when hovering a chat in the sidebar.": "",
|
||||
"Show a floating preview of recent messages when hovering a chat in the sidebar.": "Mostra una vista prèvia flotant dels missatges recents quan passes el cursor per un xat a la barra lateral.",
|
||||
"Show admin contact details while an account waits for approval.": "Mostrar les dades de contacte de l'administrador mentre un compte espera l'aprovació.",
|
||||
"Show All": "Mostrar tot",
|
||||
"Show all ({{COUNT}} characters)": "Mostra tot ({{COUNT}} caràcters)",
|
||||
|
|
@ -2538,8 +2538,8 @@
|
|||
"Show Files on Terminal Select": "Mostra fitxers en seleccionar terminal",
|
||||
"Show formatting controls in the rich text composer.": "Mostrar els controls de format a l'editor de text enriquit.",
|
||||
"Show Formatting Toolbar": "Mostrar la barra de format",
|
||||
"Show Full access and Ask for approval in the chat input menu.": "",
|
||||
"Show Hidden Files": "",
|
||||
"Show Full access and Ask for approval in the chat input menu.": "Mostra «Accés complet» i «Demana aprovació» al menú d'entrada del xat.",
|
||||
"Show Hidden Files": "Mostra els fitxers ocults",
|
||||
"Show image preview": "Mostrar la previsualització de la imatge",
|
||||
"Show Model": "Mostrar el model",
|
||||
"Show more": "Mostrar-ne més",
|
||||
|
|
@ -2554,7 +2554,7 @@
|
|||
"Show your support!": "Mostra el teu suport!",
|
||||
"Show your username label instead of You in chat bubbles.": "Mostrar l'etiqueta del teu nom d'usuari en lloc de Tu a les bombolles de xat.",
|
||||
"Showcased creativity": "Creativitat mostrada",
|
||||
"Sidebar": "",
|
||||
"Sidebar": "Barra lateral",
|
||||
"Sign in": "Iniciar sessió",
|
||||
"Sign in to {{WEBUI_NAME}}": "Iniciar sessió a {{WEBUI_NAME}}",
|
||||
"Sign in to {{WEBUI_NAME}} with LDAP": "Iniciar sessió a {{WEBUI_NAME}} amb LDAP",
|
||||
|
|
@ -2565,7 +2565,7 @@
|
|||
"simultaneous sub-agents": "subagents simultanis",
|
||||
"Single": "Únic",
|
||||
"Sink List": "Enfonsar la llista",
|
||||
"Size": "",
|
||||
"Size": "Mida",
|
||||
"sk-1234": "sk-1234",
|
||||
"Skill created successfully": "Habilitat creada correctament",
|
||||
"Skill deleted successfully": "Habilitat suprimida correctament",
|
||||
|
|
@ -2605,7 +2605,7 @@
|
|||
"Start new chats as temporary unless changed.": "Iniciar xats nous com a temporals tret que es modifiqui.",
|
||||
"Start of the channel": "Inici del canal",
|
||||
"Start Tag": "Etiqueta d'inici",
|
||||
"Start the chat to use this terminal.": "",
|
||||
"Start the chat to use this terminal.": "Inicia el xat per utilitzar aquest terminal.",
|
||||
"Starting in {{count}} minutes_one": "Començant en {{count}} minut",
|
||||
"Starting in {{count}} minutes_many": "Començant en {{count}} minuts",
|
||||
"Starting in {{count}} minutes_other": "Començant en {{count}} minuts",
|
||||
|
|
@ -2635,7 +2635,7 @@
|
|||
"Su_day_of_week": "Diumenge",
|
||||
"Sub Claim": "Reclamació sub",
|
||||
"Sub-agents": "Subagents",
|
||||
"Submit answers": "",
|
||||
"Submit answers": "Envia les respostes",
|
||||
"Submit question": "Enviar la pregunta",
|
||||
"Submit suggestion": "Enviar un suggeriment",
|
||||
"Subtitle": "Subtítol",
|
||||
|
|
@ -2683,7 +2683,7 @@
|
|||
"Task List": "Llista de tasques",
|
||||
"Task Management": "Gestió de tasques",
|
||||
"Task Model": "Model de tasques",
|
||||
"Task Model Parameters": "",
|
||||
"Task Model Parameters": "Paràmetres del model de tasca",
|
||||
"Tasks": "Tasques",
|
||||
"tasks completed": "tasques completades",
|
||||
"Tavily API Key": "Clau API de Tavily",
|
||||
|
|
@ -2693,9 +2693,9 @@
|
|||
"Temporary Chat": "Xat temporal",
|
||||
"Temporary Chat by Default": "Xat temporal per defecte",
|
||||
"Terminal": "Terminal",
|
||||
"Terminal Contexts": "",
|
||||
"Terminal File Display": "",
|
||||
"Terminal Preview Allow Same Origin": "",
|
||||
"Terminal Contexts": "Contextos del terminal",
|
||||
"Terminal File Display": "Visualització de fitxers de terminal",
|
||||
"Terminal Preview Allow Same Origin": "Vista prèvia del terminal permet el mateix origen",
|
||||
"Terminal servers saved": "Servidors de terminal desats",
|
||||
"Test": "Prova",
|
||||
"Test must pass before a source is created.": "El test s'ha de passar abans de crear una font.",
|
||||
|
|
@ -2727,7 +2727,7 @@
|
|||
"The temperature of the model. Increasing the temperature will make the model answer more creatively.": "La temperatura del model. Augmentar la temperatura farà que el model respongui de manera més creativa.",
|
||||
"Theme": "Tema",
|
||||
"There was an error syncing your stats. Please try again.": "S'ha produït un error en sincronitzar les estadístiques. Torna-ho a provar.",
|
||||
"These settings are read from environment variables and cannot be edited here while {{ENV_VAR}} is disabled.": "",
|
||||
"These settings are read from environment variables and cannot be edited here while {{ENV_VAR}} is disabled.": "Aquests paràmetres es llegeixen des de variables d'entorn i no es poden editar aquí mentre {{ENV_VAR}} estigui desactivat.",
|
||||
"thinking, output_config": "pensant, output_config",
|
||||
"Thinking...": "Pensant...",
|
||||
"This action cannot be undone. Do you wish to continue?": "Aquesta acció no es pot desfer. Vols continuar?",
|
||||
|
|
@ -2748,10 +2748,10 @@
|
|||
"This will delete": "Això eliminarà",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "Això eliminarà <strong>{{NAME}}</strong> i <strong>tots els continguts</strong>.",
|
||||
"This will delete all models including custom models and cannot be undone.": "Això eliminarà tots els models incloent els personalitzats i no es pot desfer",
|
||||
"This will delete the cached model and cannot be undone.": "",
|
||||
"This will delete the cached model and cannot be undone.": "Això suprimirà el model a la memòria cau i no es pot desfer.",
|
||||
"This will permanently delete the calendar \"{{name}}\" and all its events. This action cannot be undone.": "Això eliminarà permanentment el calendari \"{{name}}\" i tots els seus esdeveniments. Aquesta acció no es pot desfer.",
|
||||
"This will remove all files and directories from this knowledge base. This action cannot be undone.": "Això eliminarà tots els fitxers i carpetes d'aquesta base de coneixement. Aquesta acció no es pot desfer.",
|
||||
"This will revoke the current API key.": "",
|
||||
"This will revoke the current API key.": "Això revocarà la clau de l'API actual.",
|
||||
"Thorough explanation": "Explicació en detall",
|
||||
"Thought": "Pensament",
|
||||
"Thought for {{DURATION}}": "He pensat durant {{DURATION}}",
|
||||
|
|
@ -2761,12 +2761,12 @@
|
|||
"Through": "A través de",
|
||||
"Thumbs up/down ratings from users on model responses": "Valoracions de 'm'agrada'/'no' dels usuaris sobre les respostes del model",
|
||||
"Tika": "Tika",
|
||||
"Tika 3.x": "",
|
||||
"Tika 4.x": "",
|
||||
"Tika 3.x": "Tika 3.x",
|
||||
"Tika 4.x": "Tika 4.x",
|
||||
"Tika server endpoint used for content extraction.": "Punt final del servidor Tika utilitzat per a l'extracció de contingut.",
|
||||
"Tika Server URL": "URL del servidor Tika",
|
||||
"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
|
||||
"Tika Server Version": "",
|
||||
"Tika Server Version": "Versió del servidor Tika",
|
||||
"Tiktoken": "Tiktoken",
|
||||
"Time": "Temps",
|
||||
"Time & Calculation": "Temps i càlculs",
|
||||
|
|
@ -2786,7 +2786,7 @@
|
|||
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Per adjuntar la base de coneixement aquí, afegeix-la primer a l'espai de treball \"Coneixement\".",
|
||||
"To learn more about available endpoints, visit our documentation.": "Per obtenir més informació sobre els punts d'accés disponibles, visita la nostra documentació.",
|
||||
"To learn more about SpeechT5,": "Per obtenir més informació sobre SpeechT5,",
|
||||
"To see the available CMU Arctic speaker embeddings,": "Per veure les incrustacions d'altaveus CMU Arctic disponibles,",
|
||||
"To see the available CMU Arctic speaker embeddings,": "Per veure les incrustacions dels parlants CMU Arctic disponibles,",
|
||||
"To select actions here, add them to the \"Functions\" workspace first.": "Per seleccionar accions aquí, afegeix-les primer a l'espai de treball \"Funcions\".",
|
||||
"To select default filters here, enable toggleable filters for this model first.": "Per seleccionar els filtres predeterminats aquí, primer activa els filtres que es poden canviar per a aquest model.",
|
||||
"To select filters here, add them to the \"Functions\" workspace first.": "Per seleccionar filtres aquí, afegeix-los primer a l'espai de treball \"Funcions\".",
|
||||
|
|
@ -2814,7 +2814,7 @@
|
|||
"tokens": "tokens",
|
||||
"Tokens": "Tokens",
|
||||
"Too verbose": "Massa explícit",
|
||||
"tool": "",
|
||||
"tool": "eina",
|
||||
"Tool created successfully": "Eina creada correctament",
|
||||
"Tool deleted successfully": "Eina eliminada correctament",
|
||||
"Tool Description": "Descripció de l'eina",
|
||||
|
|
@ -2823,7 +2823,7 @@
|
|||
"tool loops per sub-agent": "bucles d'eina per subagent",
|
||||
"Tool Menu": "Menú d'eines",
|
||||
"Tool Name": "Nom de l'eina",
|
||||
"Tool Permissions": "",
|
||||
"Tool Permissions": "Permisos d'eina",
|
||||
"Tool Servers": "Servidors d'eines",
|
||||
"Tool updated successfully": "Eina actualitzada correctament",
|
||||
"Tools": "Eines",
|
||||
|
|
@ -2848,10 +2848,10 @@
|
|||
"TTS Voice": "Veu TTS",
|
||||
"Tu_day_of_week": "Dimarts",
|
||||
"Type": "Tipus",
|
||||
"Type a model ref": "",
|
||||
"Type a model ref": "Escriu un model de referència",
|
||||
"Type here...": "Escriu aquí...",
|
||||
"Type Hugging Face Resolve (Download) URL": "Escriu la URL de Resolució (Descàrrega) de Hugging Face",
|
||||
"Type your answer": "",
|
||||
"Type your answer": "Escriu la teva resposta",
|
||||
"Uh-oh! There was an issue with the response.": "Vaja! Hi ha hagut una incidència amb la resposta.",
|
||||
"UI": "UI",
|
||||
"UI Scale": "Mida de la interfície gràfica",
|
||||
|
|
@ -2862,7 +2862,7 @@
|
|||
"unknown": "desconegut",
|
||||
"Unknown": "Desconegut",
|
||||
"Unknown User": "Usuari desconegut",
|
||||
"Unload Model": "",
|
||||
"Unload Model": "Descarregar model",
|
||||
"Unloads {{FROM_NOW}}": "Es descarrega {{FROM_NOW}}",
|
||||
"Unmute": "No silenciar",
|
||||
"Unpin": "Alliberar",
|
||||
|
|
@ -2896,7 +2896,7 @@
|
|||
"Upload a workflow.json file exported as API format from ComfyUI.": "Carregar un fitxer workflow.json exportat com a format API des de ComfyUI.",
|
||||
"Upload Audio": "Pujar àudio",
|
||||
"Upload directory": "Pujar directori",
|
||||
"Upload failed": "",
|
||||
"Upload failed": "La càrrega ha fallat",
|
||||
"Upload files": "Pujar fitxers",
|
||||
"Upload Files": "Pujar fitxers",
|
||||
"Upload Model": "Pujar model",
|
||||
|
|
@ -2946,7 +2946,7 @@
|
|||
"User location successfully retrieved.": "Ubicació de l'usuari obtinguda correctament",
|
||||
"User menu": "Menú d'usuari",
|
||||
"User messages": "Missatges d'usuari",
|
||||
"User not found.": "",
|
||||
"User not found.": "No s'ha trobat l'usuari.",
|
||||
"User Preview": "Previsualitzar l'usuari",
|
||||
"User ratings (thumbs up/down)": "Valoracions dels usuaris (polze amunt/avall)",
|
||||
"User Status": "Estats d'usuari",
|
||||
|
|
@ -2981,7 +2981,7 @@
|
|||
"Version": "Versió",
|
||||
"Version {{selectedVersion}} of {{totalVersions}}": "Versió {{selectedVersion}} de {{totalVersions}}",
|
||||
"Version deleted": "Versió eliminada",
|
||||
"View channel": "",
|
||||
"View channel": "Veure canal",
|
||||
"View chat": "Veure xat",
|
||||
"View Replies": "Veure les respostes",
|
||||
"View Result from {{NAME}}": "Mostra el resultat de {{NAME}}",
|
||||
|
|
@ -3001,7 +3001,7 @@
|
|||
"Voice Mode Prompt": "Indicació per al mode de veu",
|
||||
"Wait for the current response to finish before compacting.": "Esperar que finalitzi la resposta actual abans de compactar.",
|
||||
"Wait for the current response to finish before forking.": "Esperar que finalitzi la resposta actual abans de bifurcar.",
|
||||
"Waiting for upload": "",
|
||||
"Waiting for upload": "Esperant la pujada",
|
||||
"Waiting for upload...": "Esperant per pujar...",
|
||||
"Warning": "Avís",
|
||||
"Warning:": "Avís:",
|
||||
|
|
@ -3102,6 +3102,6 @@
|
|||
"YouTube": "YouTube",
|
||||
"Youtube Language": "Idioma de YouTube",
|
||||
"Youtube Proxy URL": "URL de Proxy de Youtube",
|
||||
"Zoom in": "",
|
||||
"Zoom out": ""
|
||||
"Zoom in": "Apropa",
|
||||
"Zoom out": "Allunya"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue