Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c869652ef4 | ||
|
|
240a30147d | ||
|
|
91fdb86fcc | ||
|
|
0e5a56e2cf | ||
|
|
40ecc2563a | ||
|
|
7e473f194d | ||
|
|
0c231fd387 | ||
|
|
7f3ebcaa91 | ||
|
|
5904ef86f2 | ||
|
|
d692649bac | ||
|
|
0d019a00c9 | ||
|
|
e6b3de310b | ||
|
|
d19b96d0c2 | ||
|
|
7cbc94592e | ||
|
|
9747f1e841 | ||
|
|
42bc24a646 | ||
|
|
35115957d8 | ||
|
|
b193eb1d82 | ||
|
|
ff90b125ee | ||
|
|
a140d319fe | ||
|
|
9581bf5dde | ||
|
|
76dcabdce6 | ||
|
|
dc262a5f79 | ||
|
|
db5830dc28 | ||
|
|
ecf3d567cf | ||
|
|
e8375e9acd | ||
|
|
8c84c74197 | ||
|
|
f7d1225c23 | ||
|
|
3dc90b854c | ||
|
|
ac3c657315 | ||
|
|
37b117a84c | ||
|
|
ab6346ea1c | ||
|
|
6e6f9862e6 | ||
|
|
7d42a79177 | ||
|
|
05bbca5b07 | ||
|
|
5afe0be5b4 | ||
|
|
d8aa2cc05f | ||
|
|
7b071d403f | ||
|
|
d5b1e5e0c0 | ||
|
|
d8498aa2b0 | ||
|
|
e593d3aee3 | ||
|
|
323cfaf2d7 | ||
|
|
637f0c6fb2 | ||
|
|
2756403102 | ||
|
|
92861d095a | ||
|
|
bf6c6627e8 | ||
|
|
b217cb76f0 | ||
|
|
b414fde6ca | ||
|
|
d8c39569be | ||
|
|
5ce15bb0df | ||
|
|
88086935af | ||
|
|
ace3552e15 | ||
|
|
003ceff7e4 | ||
|
|
209ccdf668 | ||
|
|
e5dd7e65d4 | ||
|
|
b2999ad590 | ||
|
|
1871a7a2fa | ||
|
|
49677e9c9d | ||
|
|
73954f4a63 | ||
|
|
a084938d9c | ||
|
|
1bf042ac84 | ||
|
|
a2f9f7c975 | ||
|
|
565f40c642 | ||
|
|
4441338574 | ||
|
|
389d650ee3 | ||
|
|
080d45239a | ||
|
|
91851114e4 | ||
|
|
5b11099a0b | ||
|
|
2996d1e096 | ||
|
|
be82175f86 | ||
|
|
05977bd1c7 | ||
|
|
8074289a8f | ||
|
|
f8ba0334e8 | ||
|
|
7c9bed83d2 | ||
|
|
482a7723b2 | ||
|
|
948d29b676 | ||
|
|
3fa6c41303 | ||
|
|
220a4bb535 | ||
|
|
287559e834 | ||
|
|
cdac0cd1df | ||
|
|
3653126179 | ||
|
|
2eb662df85 | ||
|
|
cdcf214455 | ||
|
|
a75a9c953a | ||
|
|
800ba206f7 | ||
|
|
8d5aa98297 | ||
|
|
283234d51d | ||
|
|
f9e1a933a9 | ||
|
|
ebd8ef1a9e | ||
|
|
ed691e26a6 | ||
|
|
067d76fece | ||
|
|
534c18c94c | ||
|
|
af9e0cc33a | ||
|
|
6847c2fc8c | ||
|
|
028eb7b351 | ||
|
|
12c21fac22 | ||
|
|
774defd184 | ||
|
|
6657f28694 | ||
|
|
9d7507236f | ||
|
|
8f49429228 | ||
|
|
f21e9dbd9a | ||
|
|
d3146d20ad | ||
|
|
24a177a149 | ||
|
|
a58dfccb7d | ||
|
|
b35293228e | ||
|
|
963e250654 | ||
|
|
ca8b766c09 | ||
|
|
21eca7f1c1 | ||
|
|
8062866973 | ||
|
|
99530358fd | ||
|
|
621393ca62 | ||
|
|
0c6284be02 | ||
|
|
9d5e3e2a91 | ||
|
|
7f260938db | ||
|
|
c416444e24 | ||
|
|
b559bc84a7 | ||
|
|
3f53abb233 | ||
|
|
67efd0dd39 | ||
|
|
dcd32faa83 | ||
|
|
8fcb5cee66 | ||
|
|
13b757d847 | ||
|
|
c6d7f24542 | ||
|
|
4c1a9380e6 | ||
|
|
ad6e8edcd3 | ||
|
|
95c742ba9b | ||
|
|
546d442626 | ||
|
|
ed8c0b23a0 | ||
|
|
c8f44b73f1 | ||
|
|
bf6b149b8b | ||
|
|
f334c834e5 | ||
|
|
7c850bb78a | ||
|
|
a280fed4be | ||
|
|
cf9b5241ae | ||
|
|
dfa25af271 | ||
|
|
b538c5a206 | ||
|
|
4fa3fcbb3b | ||
|
|
b049ae269b | ||
|
|
e5bf27e716 | ||
|
|
a9a6ed8b71 | ||
|
|
e6c64282fc | ||
|
|
64b41655bb | ||
|
|
6ac40552b4 | ||
|
|
039c5c540b | ||
|
|
c9ed934d0b | ||
|
|
06a64219bc | ||
|
|
ef36b21684 | ||
|
|
c44e51ae58 | ||
|
|
2c4bc7a2b2 | ||
|
|
62dc486c85 | ||
|
|
d11961626c | ||
|
|
519375b4c0 | ||
|
|
6ecb7c80f1 | ||
|
|
380cb93e46 | ||
|
|
36e7662706 | ||
|
|
49199819db | ||
|
|
aa8d26496c | ||
|
|
4619e32b6a | ||
|
|
0352e956b6 | ||
|
|
f999956997 | ||
|
|
bfd268c5e6 | ||
|
|
b7ad47017d | ||
|
|
502b722902 | ||
|
|
c89b34fd75 | ||
|
|
b9b1fdd1a1 | ||
|
|
2e0fa1c6a0 | ||
|
|
f8726719ef | ||
|
|
3569fe9c73 | ||
|
|
4ffcabfb3f | ||
|
|
a7ed74744f | ||
|
|
ae0bb8f1eb | ||
|
|
034411e47e | ||
|
|
baf58ef396 | ||
|
|
006fc3495e | ||
|
|
29a3b82336 | ||
|
|
22a5e196c9 | ||
|
|
deec41d29a | ||
|
|
3978efd710 | ||
|
|
7a80e732df | ||
|
|
6681df29d2 | ||
|
|
9d58bb1c66 | ||
|
|
acbdb05bd0 | ||
|
|
25f5259e84 | ||
|
|
0ced153f86 | ||
|
|
faca8c8b53 | ||
|
|
4ad8181088 | ||
|
|
4f3ff4a69d | ||
|
|
fc0f2f2b3a | ||
|
|
b4cd084117 | ||
|
|
c1fd55bb04 | ||
|
|
77b2d2dbee | ||
|
|
f92aed1b91 | ||
|
|
90a6be8001 | ||
|
|
2e4de209fe | ||
|
|
287147687e | ||
|
|
a53f5d01aa | ||
|
|
1aaa2e8219 | ||
|
|
02b104f56b | ||
|
|
e691839771 | ||
|
|
4c22d130d9 | ||
|
|
065d23d0b1 | ||
|
|
4b0a232ae1 | ||
|
|
81130c205c | ||
|
|
36b94ca5f5 | ||
|
|
9e4326a582 | ||
|
|
8193c2f847 | ||
|
|
fbef731a04 | ||
|
|
dbf88a2eca | ||
|
|
413a9031eb | ||
|
|
23e69bcdb4 | ||
|
|
edff071cd2 | ||
|
|
3b487cfae9 | ||
|
|
a8d2072e9f | ||
|
|
260e42a691 | ||
|
|
596a9c60d7 | ||
|
|
a47e4a1148 | ||
|
|
312c8b9e7a | ||
|
|
1d99508943 | ||
|
|
b42fda0971 | ||
|
|
cf377d2e1c | ||
|
|
ec9d0b8a6d | ||
|
|
14cac8ed22 | ||
|
|
4ecf9dd62d | ||
|
|
b56dcf155c | ||
|
|
63ffdb38aa | ||
|
|
f9289d3079 | ||
|
|
709b56bc40 | ||
|
|
eb38d382ba | ||
|
|
5beaa5d892 | ||
|
|
01b24f6282 | ||
|
|
cbe3694b81 | ||
|
|
f531a51e91 | ||
|
|
a0667dfd1b | ||
|
|
2b78e613a4 | ||
|
|
b34ab10b5e | ||
|
|
f07cc37939 | ||
|
|
957b0ecd15 | ||
|
|
659bc246c9 | ||
|
|
1502af7e94 | ||
|
|
26c5a533ec | ||
|
|
4274016491 | ||
|
|
c6c298b412 | ||
|
|
63eda0fe42 | ||
|
|
c3693c91b3 | ||
|
|
e426067e46 | ||
|
|
2e482eca6d | ||
|
|
fdea1afc7f | ||
|
|
41fecb800d | ||
|
|
52cf06b36c | ||
|
|
ebce006974 | ||
|
|
ff60695a0f | ||
|
|
3aa9685f5a | ||
|
|
6cecb964a9 | ||
|
|
0216ee101c | ||
|
|
d3c6bfb764 | ||
|
|
ea6a5121b3 | ||
|
|
d6a4fe00b3 | ||
|
|
36318e8a1b | ||
|
|
79562a2531 | ||
|
|
d2664ab2a6 | ||
|
|
e664a429a1 | ||
|
|
b09bd1ed34 | ||
|
|
f83c80aaec | ||
|
|
bbc8adca94 | ||
|
|
20c71d127b | ||
|
|
e35df904a0 | ||
|
|
62682a8c4c | ||
|
|
253c02cc3c | ||
|
|
175d1cc8f2 | ||
|
|
23c9122458 | ||
|
|
b42d2886a2 | ||
|
|
c3f90ba347 | ||
|
|
c6eba8c0a1 | ||
|
|
51bdb4cbea | ||
|
|
376b2d0297 | ||
|
|
7e03624408 | ||
|
|
133f223df6 | ||
|
|
8dc73fdbdb | ||
|
|
8ec3ed1830 | ||
|
|
8b71ce23bb | ||
|
|
82079e644a | ||
|
|
1e7a364c68 | ||
|
|
c74e7df6a0 | ||
|
|
29d8f8d91b | ||
|
|
0d1553562d | ||
|
|
8e3312c032 | ||
|
|
878d12da24 | ||
|
|
c1dfafcc9e | ||
|
|
6668d79519 | ||
|
|
775a3dc359 | ||
|
|
98f935b8f7 | ||
|
|
86359ef374 | ||
|
|
ef8fbaae28 | ||
|
|
9e48aeca45 | ||
|
|
9518111ed3 | ||
|
|
4fea55bac3 | ||
|
|
027dd848eb | ||
|
|
98928fc5f0 | ||
|
|
6e843ab563 | ||
|
|
b3a0d47a67 | ||
|
|
d492d29153 | ||
|
|
ed21f9f906 | ||
|
|
bdadfc958c | ||
|
|
3bbd238d7d | ||
|
|
4eecdbadd3 | ||
|
|
a33b0abbe0 | ||
|
|
3d0f457306 | ||
|
|
ca5f1c1efb | ||
|
|
e72592eac3 | ||
|
|
72e9cecb17 | ||
|
|
27e7494853 | ||
|
|
f0a8aca0e3 | ||
|
|
4514595811 | ||
|
|
e0bcbf0b79 | ||
|
|
285725eaab | ||
|
|
9a4770eb17 | ||
|
|
8a11985fce | ||
|
|
731b3654d8 | ||
|
|
da4e976d5c | ||
|
|
b74739f91b | ||
|
|
6b5c452a3c | ||
|
|
40075f69f9 | ||
|
|
7702803e5c | ||
|
|
3731f1950a | ||
|
|
9ab47cf628 | ||
|
|
676688aabf | ||
|
|
5958d8775c | ||
|
|
dfd5de9381 | ||
|
|
ac0164c4cc | ||
|
|
9e258ea466 | ||
|
|
0aacb95700 | ||
|
|
3668179dfe | ||
|
|
fa26b8d7cf | ||
|
|
07283ca0cf | ||
|
|
917dde7edf | ||
|
|
8102347296 | ||
|
|
736d8586f1 | ||
|
|
c97d4bbb50 | ||
|
|
553dcd208c | ||
|
|
e19c327279 | ||
|
|
6d0e4852cc | ||
|
|
d258523a3c | ||
|
|
dbc352f01b | ||
|
|
5e8a74ef74 | ||
|
|
3cc3671e74 | ||
|
|
d4d7c3d8b6 | ||
|
|
69083b6485 | ||
|
|
feba1f2e3c | ||
|
|
bb20251b43 | ||
|
|
1ec2e74b07 | ||
|
|
db6f6e62b9 | ||
|
|
a99743c1c8 | ||
|
|
04dd0f2b72 | ||
|
|
b4eb630fc2 | ||
|
|
8494b97e05 | ||
|
|
a98ff3c151 | ||
|
|
b16967a29f | ||
|
|
73afa73286 | ||
|
|
63ab526bfb | ||
|
|
cf92909d9a | ||
|
|
ca6a71749c | ||
|
|
79778fa221 | ||
|
|
6806b01327 | ||
|
|
6f06577f18 | ||
|
|
bc4bb0b4e5 | ||
|
|
f40aee9d2e | ||
|
|
26bf1ef621 | ||
|
|
03618fd2e0 | ||
|
|
fa3721bcde | ||
|
|
a049b38ab5 | ||
|
|
540a669dae | ||
|
|
e266504463 | ||
|
|
f07172bfe6 | ||
|
|
856845e5f2 | ||
|
|
14c0efe300 | ||
|
|
eb10001eb7 | ||
|
|
90c3d68f00 | ||
|
|
97098edfeb | ||
|
|
9f9122b6d7 | ||
|
|
8dcb3d78dc | ||
|
|
4dd77b785a | ||
|
|
7d7a29cfb9 | ||
|
|
f462744fc8 | ||
|
|
9ab97b834a | ||
|
|
7ba7b959a8 | ||
|
|
b094153af2 | ||
|
|
0ef27bfc5e | ||
|
|
a62b0924df | ||
|
|
eff736acd2 | ||
|
|
38cc0da990 | ||
|
|
37ff651723 | ||
|
|
fae7db89e6 | ||
|
|
ef21397f0d | ||
|
|
2011cbd210 | ||
|
|
ebcc5be1bf | ||
|
|
3661dd16b8 | ||
|
|
2aa5d26735 | ||
|
|
d50639971a | ||
|
|
fede1e9e3b | ||
|
|
f7777f489e | ||
|
|
93ae6084d6 | ||
|
|
3b39334bae | ||
|
|
da8ee43481 | ||
|
|
a843e81aaf |
@@ -8,36 +8,43 @@ assignees: ''
|
||||
|
||||
# Bug Report
|
||||
|
||||
## Description
|
||||
## Installation Method
|
||||
|
||||
**Bug Summary:**
|
||||
[Provide a brief but clear summary of the bug]
|
||||
|
||||
**Steps to Reproduce:**
|
||||
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
||||
|
||||
**Expected Behavior:**
|
||||
[Describe what you expected to happen.]
|
||||
|
||||
**Actual Behavior:**
|
||||
[Describe what actually happened.]
|
||||
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
|
||||
|
||||
## Environment
|
||||
|
||||
- **Open WebUI Version:** [e.g., 0.1.120]
|
||||
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
|
||||
- **Open WebUI Version:** [e.g., v0.3.11]
|
||||
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
|
||||
|
||||
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
||||
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
||||
|
||||
## Reproduction Details
|
||||
|
||||
**Confirmation:**
|
||||
|
||||
- [ ] I have read and followed all the instructions provided in the README.md.
|
||||
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
||||
- [ ] I have included the browser console logs.
|
||||
- [ ] I have included the Docker container logs.
|
||||
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
|
||||
|
||||
## Expected Behavior:
|
||||
|
||||
[Describe what you expected to happen.]
|
||||
|
||||
## Actual Behavior:
|
||||
|
||||
[Describe what actually happened.]
|
||||
|
||||
## Description
|
||||
|
||||
**Bug Summary:**
|
||||
[Provide a brief but clear summary of the bug]
|
||||
|
||||
## Reproduction Details
|
||||
|
||||
**Steps to Reproduce:**
|
||||
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
||||
|
||||
## Logs and Screenshots
|
||||
|
||||
@@ -47,13 +54,9 @@ assignees: ''
|
||||
**Docker Container Logs:**
|
||||
[Include relevant Docker container logs, if applicable]
|
||||
|
||||
**Screenshots (if applicable):**
|
||||
**Screenshots/Screen Recordings (if applicable):**
|
||||
[Attach any relevant screenshots to help illustrate the issue]
|
||||
|
||||
## Installation Method
|
||||
|
||||
[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.]
|
||||
|
||||
## Additional Information
|
||||
|
||||
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
|
||||
|
||||
@@ -26,6 +26,10 @@ jobs:
|
||||
--file docker-compose.a1111-test.yaml \
|
||||
up --detach --build
|
||||
|
||||
- name: Delete Docker build cache
|
||||
run: |
|
||||
docker builder prune --all --force
|
||||
|
||||
- name: Wait for Ollama to be up
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
@@ -35,10 +39,6 @@ jobs:
|
||||
done
|
||||
echo "Service is up!"
|
||||
|
||||
- name: Delete Docker build cache
|
||||
run: |
|
||||
docker builder prune --all --force
|
||||
|
||||
- name: Preload Ollama model
|
||||
run: |
|
||||
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
||||
|
||||
@@ -5,6 +5,96 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.12] - 2024-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- **🔄 Sidebar Infinite Scroll**: Added an infinite scroll feature in the sidebar for more efficient chat navigation, reducing load times and enhancing user experience.
|
||||
- **🚀 Enhanced Markdown Rendering**: Support for rendering all code blocks and making images clickable for preview; codespan styling is also enhanced to improve readability and user interaction.
|
||||
- **🔒 Admin Shared Chat Visibility**: Admins no longer have default visibility over shared chats when ENABLE_ADMIN_CHAT_ACCESS is set to false, tightening security and privacy settings for users.
|
||||
- **🌍 Language Updates**: Added Malay (Bahasa Malaysia) translation and updated Catalan and Traditional Chinese translations to improve accessibility for more users.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📊 Markdown Rendering Issues**: Resolved issues with markdown rendering to ensure consistent and correct display across components.
|
||||
- **🛠️ Styling Issues**: Multiple fixes applied to styling throughout the application, improving the overall visual experience and interface consistency.
|
||||
- **🗃️ Modal Handling**: Fixed an issue where modals were not closing correctly in various model chat scenarios, enhancing usability and interface reliability.
|
||||
- **📄 Missing OpenAI Usage Information**: Resolved issues where usage statistics for OpenAI services were not being correctly displayed, ensuring users have access to crucial data for managing and monitoring their API consumption.
|
||||
- **🔧 Non-Streaming Support for Functions Plugin**: Fixed a functionality issue with the Functions plugin where non-streaming operations were not functioning as intended, restoring full capabilities for async and sync integration within the platform.
|
||||
- **🔄 Environment Variable Type Correction (COMFYUI_FLUX_FP8_CLIP)**: Corrected the data type of the 'COMFYUI_FLUX_FP8_CLIP' environment variable from string to boolean, ensuring environment settings apply correctly and enhance configuration management.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔧 Backend Dependency Updates**: Updated several backend dependencies such as boto3, pypdf, python-pptx, validators, and black, ensuring up-to-date security and performance optimizations.
|
||||
|
||||
## [0.3.11] - 2024-08-02
|
||||
|
||||
### Added
|
||||
|
||||
- **📊 Model Information Display**: Added visuals for model selection, including images next to model names for more intuitive navigation.
|
||||
- **🗣 ElevenLabs Voice Adaptations**: Voice enhancements including support for ElevenLabs voice ID by name for personalized vocal interactions.
|
||||
- **⌨️ Arrow Keys Model Selection**: Users can now use arrow keys for quicker model selection, enhancing accessibility.
|
||||
- **🔍 Fuzzy Search in Model Selector**: Enhanced model selector with fuzzy search to locate models swiftly, including descriptions.
|
||||
- **🕹️ ComfyUI Flux Image Generation**: Added support for the new Flux image gen model; introduces environment controls like weight precision and CLIP model options in Settings.
|
||||
- **💾 Display File Size for Uploads**: Enhanced file interface now displays file size, preparing for upcoming upload restrictions.
|
||||
- **🎚️ Advanced Params "Min P"**: Added 'Min P' parameter in the advanced settings for customized model precision control.
|
||||
- **🔒 Enhanced OAuth**: Introduced custom redirect URI support for OAuth behind reverse proxies, enabling safer authentication processes.
|
||||
- **🖥 Enhanced Latex Rendering**: Adjustments made to latex rendering processes, now accurately detecting and presenting latex inputs from text.
|
||||
- **🌐 Internationalization**: Enhanced with new Romanian and updated Vietnamese and Ukrainian translations, helping broaden accessibility for international users.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔧 Tags Handling in Document Upload**: Tags are now properly sent to the upload document handler, resolving issues with missing metadata.
|
||||
- **🖥️ Sensitive Input Fields**: Corrected browser misinterpretation of secure input fields, preventing misclassification as password fields.
|
||||
- **📂 Static Path Resolution in PDF Generation**: Fixed static paths that adjust dynamically to prevent issues across various environments.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🎨 UI/UX Styling Enhancements**: Multiple minor styling updates for a cleaner and more intuitive user interface.
|
||||
- **🚧 Refactoring Various Components**: Numerous refactoring changes across styling, file handling, and function simplifications for clarity and performance.
|
||||
- **🎛️ User Valves Management**: Moved user valves from settings to direct chat controls for more user-friendly access during interactions.
|
||||
|
||||
### Removed
|
||||
|
||||
- **⚙️ Health Check Logging**: Removed verbose logging from the health checking processes to declutter logs and improve backend performance.
|
||||
|
||||
## [0.3.10] - 2024-07-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔄 Improved File Upload**: Addressed the issue where file uploads lacked animation.
|
||||
- **💬 Chat Continuity**: Fixed a problem where existing chats were not functioning properly in some instances.
|
||||
- **🗂️ Chat File Reset**: Resolved the issue of chat files not resetting for new conversations, now ensuring a clean slate for each chat session.
|
||||
- **📁 Document Workspace Uploads**: Corrected the handling of document uploads in the workspace using the Files API.
|
||||
|
||||
## [0.3.9] - 2024-07-17
|
||||
|
||||
### Added
|
||||
|
||||
- **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed.
|
||||
- **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon.
|
||||
- **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access.
|
||||
- **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly.
|
||||
- **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface.
|
||||
- **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings.
|
||||
- **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly.
|
||||
- **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required.
|
||||
- **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat.
|
||||
- **✅ Boolean Values in Params**: Added support for boolean values in parameters.
|
||||
- **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay.
|
||||
|
||||
### Changed
|
||||
|
||||
- **⬆️ Dependency Updates**
|
||||
- Upgraded 'pydantic' from version 2.7.1 to 2.8.2.
|
||||
- Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31.
|
||||
- Upgraded 'unstructured' from version 0.14.9 to 0.14.10.
|
||||
- Upgraded 'chromadb' from version 0.5.3 to 0.5.4.
|
||||
|
||||
## [0.3.8] - 2024-07-09
|
||||
|
||||
### Added
|
||||
@@ -27,6 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
|
||||
- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
|
||||
- **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version.
|
||||
- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
|
||||
- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
|
||||
- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
|
||||
|
||||
+5
-3
@@ -97,6 +97,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
||||
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
# install helper tools
|
||||
@@ -107,8 +108,9 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
else \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
|
||||
# Install pandoc, netcat and gcc
|
||||
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
|
||||
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
# cleanup
|
||||
@@ -149,7 +151,7 @@ COPY --chown=$UID:$GID ./backend .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
|
||||
HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
|
||||
|
||||
USER $UID:$GID
|
||||
|
||||
|
||||
+171
-42
@@ -10,12 +10,12 @@ from fastapi import (
|
||||
File,
|
||||
Form,
|
||||
)
|
||||
|
||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
from typing import List
|
||||
import uuid
|
||||
import requests
|
||||
import hashlib
|
||||
@@ -31,6 +31,7 @@ from utils.utils import (
|
||||
)
|
||||
from utils.misc import calculate_sha256
|
||||
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
CACHE_DIR,
|
||||
@@ -43,6 +44,7 @@ from config import (
|
||||
AUDIO_STT_OPENAI_API_KEY,
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||
AUDIO_TTS_OPENAI_API_KEY,
|
||||
AUDIO_TTS_API_KEY,
|
||||
AUDIO_STT_ENGINE,
|
||||
AUDIO_STT_MODEL,
|
||||
AUDIO_TTS_ENGINE,
|
||||
@@ -75,6 +77,7 @@ app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
||||
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
|
||||
|
||||
# setting device type for whisper model
|
||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||
@@ -87,6 +90,7 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
class TTSConfigForm(BaseModel):
|
||||
OPENAI_API_BASE_URL: str
|
||||
OPENAI_API_KEY: str
|
||||
API_KEY: str
|
||||
ENGINE: str
|
||||
MODEL: str
|
||||
VOICE: str
|
||||
@@ -137,6 +141,7 @@ async def get_audio_config(user=Depends(get_admin_user)):
|
||||
"tts": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||
"API_KEY": app.state.config.TTS_API_KEY,
|
||||
"ENGINE": app.state.config.TTS_ENGINE,
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
@@ -156,6 +161,7 @@ async def update_audio_config(
|
||||
):
|
||||
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
||||
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
|
||||
app.state.config.TTS_API_KEY = form_data.tts.API_KEY
|
||||
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
|
||||
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
||||
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
||||
@@ -169,6 +175,7 @@ async def update_audio_config(
|
||||
"tts": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||
"API_KEY": app.state.config.TTS_API_KEY,
|
||||
"ENGINE": app.state.config.TTS_ENGINE,
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
@@ -194,55 +201,111 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
if file_path.is_file():
|
||||
return FileResponse(file_path)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
if app.state.config.TTS_ENGINE == "openai":
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
try:
|
||||
body = body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
body["model"] = app.state.config.TTS_MODEL
|
||||
body = json.dumps(body).encode("utf-8")
|
||||
except Exception as e:
|
||||
pass
|
||||
try:
|
||||
body = body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
body["model"] = app.state.config.TTS_MODEL
|
||||
body = json.dumps(body).encode("utf-8")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
)
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
r.raise_for_status()
|
||||
|
||||
# Save the streaming content to a file
|
||||
with open(file_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
# Save the streaming content to a file
|
||||
with open(file_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump(json.loads(body.decode("utf-8")), f)
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump(json.loads(body.decode("utf-8")), f)
|
||||
|
||||
# Return the saved file
|
||||
return FileResponse(file_path)
|
||||
# Return the saved file
|
||||
return FileResponse(file_path)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r != None else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r != None else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||
payload = None
|
||||
try:
|
||||
payload = json.loads(body.decode("utf-8"))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON payload")
|
||||
|
||||
voice_id = payload.get("voice", "")
|
||||
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
||||
|
||||
headers = {
|
||||
"Accept": "audio/mpeg",
|
||||
"Content-Type": "application/json",
|
||||
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||
}
|
||||
|
||||
data = {
|
||||
"text": payload["input"],
|
||||
"model_id": app.state.config.TTS_MODEL,
|
||||
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.post(url, json=data, headers=headers)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Save the streaming content to a file
|
||||
with open(file_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump(json.loads(body.decode("utf-8")), f)
|
||||
|
||||
# Return the saved file
|
||||
return FileResponse(file_path)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r != None else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/transcriptions")
|
||||
@@ -373,3 +436,69 @@ def transcribe(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
|
||||
def get_available_models() -> List[dict]:
|
||||
if app.state.config.TTS_ENGINE == "openai":
|
||||
return [{"id": "tts-1"}, {"id": "tts-1-hd"}]
|
||||
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||
headers = {
|
||||
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.elevenlabs.io/v1/models", headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
models = response.json()
|
||||
return [
|
||||
{"name": model["name"], "id": model["model_id"]} for model in models
|
||||
]
|
||||
except requests.RequestException as e:
|
||||
log.error(f"Error fetching voices: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
@app.get("/models")
|
||||
async def get_models(user=Depends(get_verified_user)):
|
||||
return {"models": get_available_models()}
|
||||
|
||||
|
||||
def get_available_voices() -> List[dict]:
|
||||
if app.state.config.TTS_ENGINE == "openai":
|
||||
return [
|
||||
{"name": "alloy", "id": "alloy"},
|
||||
{"name": "echo", "id": "echo"},
|
||||
{"name": "fable", "id": "fable"},
|
||||
{"name": "onyx", "id": "onyx"},
|
||||
{"name": "nova", "id": "nova"},
|
||||
{"name": "shimmer", "id": "shimmer"},
|
||||
]
|
||||
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||
headers = {
|
||||
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.elevenlabs.io/v1/voices", headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
voices_data = response.json()
|
||||
|
||||
voices = []
|
||||
for voice in voices_data.get("voices", []):
|
||||
voices.append({"name": voice["name"], "id": voice["voice_id"]})
|
||||
return voices
|
||||
except requests.RequestException as e:
|
||||
log.error(f"Error fetching voices: {str(e)}")
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@app.get("/voices")
|
||||
async def get_voices(user=Depends(get_verified_user)):
|
||||
return {"voices": get_available_voices()}
|
||||
|
||||
@@ -42,6 +42,9 @@ from config import (
|
||||
COMFYUI_SAMPLER,
|
||||
COMFYUI_SCHEDULER,
|
||||
COMFYUI_SD3,
|
||||
COMFYUI_FLUX,
|
||||
COMFYUI_FLUX_WEIGHT_DTYPE,
|
||||
COMFYUI_FLUX_FP8_CLIP,
|
||||
IMAGES_OPENAI_API_BASE_URL,
|
||||
IMAGES_OPENAI_API_KEY,
|
||||
IMAGE_GENERATION_MODEL,
|
||||
@@ -85,6 +88,9 @@ app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
|
||||
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
|
||||
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
|
||||
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
|
||||
app.state.config.COMFYUI_FLUX = COMFYUI_FLUX
|
||||
app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE = COMFYUI_FLUX_WEIGHT_DTYPE
|
||||
app.state.config.COMFYUI_FLUX_FP8_CLIP = COMFYUI_FLUX_FP8_CLIP
|
||||
|
||||
|
||||
def get_automatic1111_api_auth():
|
||||
@@ -421,7 +427,7 @@ def save_url_image(url):
|
||||
|
||||
|
||||
@app.post("/generations")
|
||||
def generate_image(
|
||||
async def image_generations(
|
||||
form_data: GenerateImageForm,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
@@ -497,6 +503,15 @@ def generate_image(
|
||||
if app.state.config.COMFYUI_SD3 is not None:
|
||||
data["sd3"] = app.state.config.COMFYUI_SD3
|
||||
|
||||
if app.state.config.COMFYUI_FLUX is not None:
|
||||
data["flux"] = app.state.config.COMFYUI_FLUX
|
||||
|
||||
if app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE is not None:
|
||||
data["flux_weight_dtype"] = app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE
|
||||
|
||||
if app.state.config.COMFYUI_FLUX_FP8_CLIP is not None:
|
||||
data["flux_fp8_clip"] = app.state.config.COMFYUI_FLUX_FP8_CLIP
|
||||
|
||||
data = ImageGenerationPayload(**data)
|
||||
|
||||
res = comfyui_generate_image(
|
||||
|
||||
@@ -125,6 +125,135 @@ COMFYUI_DEFAULT_PROMPT = """
|
||||
}
|
||||
"""
|
||||
|
||||
FLUX_DEFAULT_PROMPT = """
|
||||
{
|
||||
"5": {
|
||||
"inputs": {
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage"
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"text": "Input Text Here",
|
||||
"clip": [
|
||||
"11",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"13",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"10",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode"
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
"10": {
|
||||
"inputs": {
|
||||
"vae_name": "ae.sft"
|
||||
},
|
||||
"class_type": "VAELoader"
|
||||
},
|
||||
"11": {
|
||||
"inputs": {
|
||||
"clip_name1": "clip_l.safetensors",
|
||||
"clip_name2": "t5xxl_fp16.safetensors",
|
||||
"type": "flux"
|
||||
},
|
||||
"class_type": "DualCLIPLoader"
|
||||
},
|
||||
"12": {
|
||||
"inputs": {
|
||||
"unet_name": "flux1-dev.sft",
|
||||
"weight_dtype": "default"
|
||||
},
|
||||
"class_type": "UNETLoader"
|
||||
},
|
||||
"13": {
|
||||
"inputs": {
|
||||
"noise": [
|
||||
"25",
|
||||
0
|
||||
],
|
||||
"guider": [
|
||||
"22",
|
||||
0
|
||||
],
|
||||
"sampler": [
|
||||
"16",
|
||||
0
|
||||
],
|
||||
"sigmas": [
|
||||
"17",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SamplerCustomAdvanced"
|
||||
},
|
||||
"16": {
|
||||
"inputs": {
|
||||
"sampler_name": "euler"
|
||||
},
|
||||
"class_type": "KSamplerSelect"
|
||||
},
|
||||
"17": {
|
||||
"inputs": {
|
||||
"scheduler": "simple",
|
||||
"steps": 20,
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"12",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicScheduler"
|
||||
},
|
||||
"22": {
|
||||
"inputs": {
|
||||
"model": [
|
||||
"12",
|
||||
0
|
||||
],
|
||||
"conditioning": [
|
||||
"6",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicGuider"
|
||||
},
|
||||
"25": {
|
||||
"inputs": {
|
||||
"noise_seed": 778937779713005
|
||||
},
|
||||
"class_type": "RandomNoise"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def queue_prompt(prompt, client_id, base_url):
|
||||
log.info("queue_prompt")
|
||||
@@ -194,6 +323,9 @@ class ImageGenerationPayload(BaseModel):
|
||||
sampler: Optional[str] = None
|
||||
scheduler: Optional[str] = None
|
||||
sd3: Optional[bool] = None
|
||||
flux: Optional[bool] = None
|
||||
flux_weight_dtype: Optional[str] = None
|
||||
flux_fp8_clip: Optional[bool] = None
|
||||
|
||||
|
||||
def comfyui_generate_image(
|
||||
@@ -215,21 +347,46 @@ def comfyui_generate_image(
|
||||
if payload.sd3:
|
||||
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
|
||||
|
||||
if payload.steps:
|
||||
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
||||
|
||||
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
||||
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
||||
comfyui_prompt["3"]["inputs"]["seed"] = (
|
||||
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||
)
|
||||
|
||||
# as Flux uses a completely different workflow, we must treat it specially
|
||||
if payload.flux:
|
||||
comfyui_prompt = json.loads(FLUX_DEFAULT_PROMPT)
|
||||
comfyui_prompt["12"]["inputs"]["unet_name"] = model
|
||||
comfyui_prompt["25"]["inputs"]["noise_seed"] = (
|
||||
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||
)
|
||||
|
||||
if payload.sampler:
|
||||
comfyui_prompt["16"]["inputs"]["sampler_name"] = payload.sampler
|
||||
|
||||
if payload.steps:
|
||||
comfyui_prompt["17"]["inputs"]["steps"] = payload.steps
|
||||
|
||||
if payload.scheduler:
|
||||
comfyui_prompt["17"]["inputs"]["scheduler"] = payload.scheduler
|
||||
|
||||
if payload.flux_weight_dtype:
|
||||
comfyui_prompt["12"]["inputs"]["weight_dtype"] = payload.flux_weight_dtype
|
||||
|
||||
if payload.flux_fp8_clip:
|
||||
comfyui_prompt["11"]["inputs"][
|
||||
"clip_name2"
|
||||
] = "t5xxl_fp8_e4m3fn.safetensors"
|
||||
|
||||
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
||||
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
||||
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
||||
|
||||
# set the text prompt for our positive CLIPTextEncode
|
||||
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
||||
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
||||
|
||||
if payload.steps:
|
||||
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
||||
|
||||
comfyui_prompt["3"]["inputs"]["seed"] = (
|
||||
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||
)
|
||||
|
||||
try:
|
||||
ws = websocket.WebSocket()
|
||||
|
||||
+86
-26
@@ -728,8 +728,10 @@ async def generate_chat_completion(
|
||||
)
|
||||
|
||||
payload = {
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
**form_data.model_dump(exclude_none=True, exclude=["metadata"]),
|
||||
}
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
model_id = form_data.model
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
@@ -741,52 +743,85 @@ async def generate_chat_completion(
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
payload["options"] = {}
|
||||
if payload.get("options") is None:
|
||||
payload["options"] = {}
|
||||
|
||||
if model_info.params.get("mirostat", None):
|
||||
if (
|
||||
model_info.params.get("mirostat", None)
|
||||
and payload["options"].get("mirostat") is None
|
||||
):
|
||||
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
||||
|
||||
if model_info.params.get("mirostat_eta", None):
|
||||
if (
|
||||
model_info.params.get("mirostat_eta", None)
|
||||
and payload["options"].get("mirostat_eta") is None
|
||||
):
|
||||
payload["options"]["mirostat_eta"] = model_info.params.get(
|
||||
"mirostat_eta", None
|
||||
)
|
||||
|
||||
if model_info.params.get("mirostat_tau", None):
|
||||
|
||||
if (
|
||||
model_info.params.get("mirostat_tau", None)
|
||||
and payload["options"].get("mirostat_tau") is None
|
||||
):
|
||||
payload["options"]["mirostat_tau"] = model_info.params.get(
|
||||
"mirostat_tau", None
|
||||
)
|
||||
|
||||
if model_info.params.get("num_ctx", None):
|
||||
if (
|
||||
model_info.params.get("num_ctx", None)
|
||||
and payload["options"].get("num_ctx") is None
|
||||
):
|
||||
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
||||
|
||||
if model_info.params.get("num_batch", None):
|
||||
if (
|
||||
model_info.params.get("num_batch", None)
|
||||
and payload["options"].get("num_batch") is None
|
||||
):
|
||||
payload["options"]["num_batch"] = model_info.params.get(
|
||||
"num_batch", None
|
||||
)
|
||||
|
||||
if model_info.params.get("num_keep", None):
|
||||
if (
|
||||
model_info.params.get("num_keep", None)
|
||||
and payload["options"].get("num_keep") is None
|
||||
):
|
||||
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
|
||||
|
||||
if model_info.params.get("repeat_last_n", None):
|
||||
if (
|
||||
model_info.params.get("repeat_last_n", None)
|
||||
and payload["options"].get("repeat_last_n") is None
|
||||
):
|
||||
payload["options"]["repeat_last_n"] = model_info.params.get(
|
||||
"repeat_last_n", None
|
||||
)
|
||||
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
if (
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
and payload["options"].get("frequency_penalty") is None
|
||||
):
|
||||
payload["options"]["repeat_penalty"] = model_info.params.get(
|
||||
"frequency_penalty", None
|
||||
)
|
||||
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
if (
|
||||
model_info.params.get("temperature", None) is not None
|
||||
and payload["options"].get("temperature") is None
|
||||
):
|
||||
payload["options"]["temperature"] = model_info.params.get(
|
||||
"temperature", None
|
||||
)
|
||||
|
||||
if model_info.params.get("seed", None):
|
||||
if (
|
||||
model_info.params.get("seed", None) is not None
|
||||
and payload["options"].get("seed") is None
|
||||
):
|
||||
payload["options"]["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
if (
|
||||
model_info.params.get("stop", None)
|
||||
and payload["options"].get("stop") is None
|
||||
):
|
||||
payload["options"]["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
@@ -796,37 +831,62 @@ async def generate_chat_completion(
|
||||
else None
|
||||
)
|
||||
|
||||
if model_info.params.get("tfs_z", None):
|
||||
if (
|
||||
model_info.params.get("tfs_z", None)
|
||||
and payload["options"].get("tfs_z") is None
|
||||
):
|
||||
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
||||
|
||||
if model_info.params.get("max_tokens", None):
|
||||
if (
|
||||
model_info.params.get("max_tokens", None)
|
||||
and payload["options"].get("max_tokens") is None
|
||||
):
|
||||
payload["options"]["num_predict"] = model_info.params.get(
|
||||
"max_tokens", None
|
||||
)
|
||||
|
||||
if model_info.params.get("top_k", None):
|
||||
if (
|
||||
model_info.params.get("top_k", None)
|
||||
and payload["options"].get("top_k") is None
|
||||
):
|
||||
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
||||
|
||||
if model_info.params.get("top_p", None):
|
||||
if (
|
||||
model_info.params.get("top_p", None)
|
||||
and payload["options"].get("top_p") is None
|
||||
):
|
||||
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
||||
|
||||
if model_info.params.get("use_mmap", None):
|
||||
if (
|
||||
model_info.params.get("min_p", None)
|
||||
and payload["options"].get("min_p") is None
|
||||
):
|
||||
payload["options"]["min_p"] = model_info.params.get("min_p", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("use_mmap", None)
|
||||
and payload["options"].get("use_mmap") is None
|
||||
):
|
||||
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
||||
|
||||
if model_info.params.get("use_mlock", None):
|
||||
if (
|
||||
model_info.params.get("use_mlock", None)
|
||||
and payload["options"].get("use_mlock") is None
|
||||
):
|
||||
payload["options"]["use_mlock"] = model_info.params.get(
|
||||
"use_mlock", None
|
||||
)
|
||||
|
||||
if model_info.params.get("num_thread", None):
|
||||
if (
|
||||
model_info.params.get("num_thread", None)
|
||||
and payload["options"].get("num_thread") is None
|
||||
):
|
||||
payload["options"]["num_thread"] = model_info.params.get(
|
||||
"num_thread", None
|
||||
)
|
||||
|
||||
system = model_info.params.get("system", None)
|
||||
if system:
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
system = prompt_template(
|
||||
system,
|
||||
**(
|
||||
@@ -893,10 +953,10 @@ async def generate_openai_chat_completion(
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
form_data = OpenAIChatCompletionForm(**form_data)
|
||||
payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])}
|
||||
|
||||
payload = {
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
}
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
model_id = form_data.model
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
+73
-137
@@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
||||
from fastapi import FastAPI, Request, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||
from fastapi.responses import StreamingResponse, FileResponse
|
||||
|
||||
import requests
|
||||
import aiohttp
|
||||
@@ -12,15 +12,12 @@ from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from apps.webui.models.models import Models
|
||||
from apps.webui.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
get_verified_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from utils.task import prompt_template
|
||||
from utils.misc import apply_model_params_to_body, apply_model_system_prompt_to_body
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
@@ -33,7 +30,7 @@ from config import (
|
||||
MODEL_FILTER_LIST,
|
||||
AppConfig,
|
||||
)
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Literal, overload
|
||||
|
||||
|
||||
import hashlib
|
||||
@@ -68,8 +65,6 @@ app.state.MODELS = {}
|
||||
async def check_url(request: Request, call_next):
|
||||
if len(app.state.MODELS) == 0:
|
||||
await get_all_models()
|
||||
else:
|
||||
pass
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
@@ -174,7 +169,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -233,64 +228,68 @@ def merge_models_lists(model_lists):
|
||||
return merged_list
|
||||
|
||||
|
||||
async def get_all_models(raw: bool = False):
|
||||
def is_openai_api_disabled():
|
||||
api_keys = app.state.config.OPENAI_API_KEYS
|
||||
no_keys = len(api_keys) == 1 and api_keys[0] == ""
|
||||
return no_keys or not app.state.config.ENABLE_OPENAI_API
|
||||
|
||||
|
||||
async def get_all_models_raw() -> list:
|
||||
if is_openai_api_disabled():
|
||||
return []
|
||||
|
||||
# Check if API KEYS length is same than API URLS length
|
||||
num_urls = len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
num_keys = len(app.state.config.OPENAI_API_KEYS)
|
||||
|
||||
if num_keys != num_urls:
|
||||
# if there are more keys than urls, remove the extra keys
|
||||
if num_keys > num_urls:
|
||||
new_keys = app.state.config.OPENAI_API_KEYS[:num_urls]
|
||||
app.state.config.OPENAI_API_KEYS = new_keys
|
||||
# if there are more urls than keys, add empty keys
|
||||
else:
|
||||
app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys)
|
||||
|
||||
tasks = [
|
||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
log.debug(f"get_all_models:responses() {responses}")
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
@overload
|
||||
async def get_all_models(raw: Literal[True]) -> list: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
|
||||
|
||||
|
||||
async def get_all_models(raw=False) -> dict[str, list] | list:
|
||||
log.info("get_all_models()")
|
||||
if is_openai_api_disabled():
|
||||
return [] if raw else {"data": []}
|
||||
|
||||
if (
|
||||
len(app.state.config.OPENAI_API_KEYS) == 1
|
||||
and app.state.config.OPENAI_API_KEYS[0] == ""
|
||||
) or not app.state.config.ENABLE_OPENAI_API:
|
||||
models = {"data": []}
|
||||
else:
|
||||
# Check if API KEYS length is same than API URLS length
|
||||
if len(app.state.config.OPENAI_API_KEYS) != len(
|
||||
app.state.config.OPENAI_API_BASE_URLS
|
||||
):
|
||||
# if there are more keys than urls, remove the extra keys
|
||||
if len(app.state.config.OPENAI_API_KEYS) > len(
|
||||
app.state.config.OPENAI_API_BASE_URLS
|
||||
):
|
||||
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
|
||||
: len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
# if there are more urls than keys, add empty keys
|
||||
else:
|
||||
app.state.config.OPENAI_API_KEYS += [
|
||||
""
|
||||
for _ in range(
|
||||
len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
- len(app.state.config.OPENAI_API_KEYS)
|
||||
)
|
||||
]
|
||||
responses = await get_all_models_raw()
|
||||
if raw:
|
||||
return responses
|
||||
|
||||
tasks = [
|
||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
def extract_data(response):
|
||||
if response and "data" in response:
|
||||
return response["data"]
|
||||
if isinstance(response, list):
|
||||
return response
|
||||
return None
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
log.debug(f"get_all_models:responses() {responses}")
|
||||
models = {"data": merge_models_lists(map(extract_data, responses))}
|
||||
|
||||
if raw:
|
||||
return responses
|
||||
|
||||
models = {
|
||||
"data": merge_models_lists(
|
||||
list(
|
||||
map(
|
||||
lambda response: (
|
||||
response["data"]
|
||||
if (response and "data" in response)
|
||||
else (response if isinstance(response, list) else None)
|
||||
),
|
||||
responses,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
log.debug(f"models: {models}")
|
||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||
log.debug(f"models: {models}")
|
||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||
|
||||
return models
|
||||
|
||||
@@ -298,7 +297,7 @@ async def get_all_models(raw: bool = False):
|
||||
@app.get("/models")
|
||||
@app.get("/models/{url_idx}")
|
||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
@@ -339,7 +338,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -357,6 +356,7 @@ async def generate_chat_completion(
|
||||
):
|
||||
idx = 0
|
||||
payload = {**form_data}
|
||||
payload.pop("metadata")
|
||||
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
@@ -365,69 +365,9 @@ async def generate_chat_completion(
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
payload["temperature"] = float(model_info.params.get("temperature"))
|
||||
|
||||
if model_info.params.get("top_p", None):
|
||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
||||
|
||||
if model_info.params.get("max_tokens", None):
|
||||
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
payload["frequency_penalty"] = int(
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
)
|
||||
|
||||
if model_info.params.get("seed", None):
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
system = model_info.params.get("system", None)
|
||||
if system:
|
||||
system = prompt_template(
|
||||
system,
|
||||
**(
|
||||
{
|
||||
"user_name": user.name,
|
||||
"user_location": (
|
||||
user.info.get("location") if user.info else None
|
||||
),
|
||||
}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = system + message["content"]
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": system,
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
pass
|
||||
params = model_info.params.model_dump()
|
||||
payload = apply_model_params_to_body(params, payload)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||
|
||||
model = app.state.MODELS[payload.get("model")]
|
||||
idx = model["urlIdx"]
|
||||
@@ -440,13 +380,6 @@ async def generate_chat_completion(
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||
# This is a workaround until OpenAI fixes the issue with this model
|
||||
if payload.get("model") == "gpt-4-vision-preview":
|
||||
if "max_tokens" not in payload:
|
||||
payload["max_tokens"] = 4000
|
||||
log.debug("Modified payload:", payload)
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
@@ -458,6 +391,9 @@ async def generate_chat_completion(
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
|
||||
headers["HTTP-Referer"] = "https://openwebui.com/"
|
||||
headers["X-Title"] = "Open WebUI"
|
||||
|
||||
r = None
|
||||
session = None
|
||||
@@ -499,7 +435,7 @@ async def generate_chat_completion(
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
@@ -562,7 +498,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
|
||||
@@ -930,7 +930,9 @@ def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
|
||||
)
|
||||
|
||||
|
||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||
def store_data_in_vector_db(
|
||||
data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
|
||||
) -> bool:
|
||||
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=app.state.config.CHUNK_SIZE,
|
||||
@@ -942,7 +944,7 @@ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> b
|
||||
|
||||
if len(docs) > 0:
|
||||
log.info(f"store_data_in_vector_db {docs}")
|
||||
return store_docs_in_vector_db(docs, collection_name, overwrite), None
|
||||
return store_docs_in_vector_db(docs, collection_name, metadata, overwrite), None
|
||||
else:
|
||||
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
||||
|
||||
@@ -956,14 +958,16 @@ def store_text_in_vector_db(
|
||||
add_start_index=True,
|
||||
)
|
||||
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
||||
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
||||
return store_docs_in_vector_db(docs, collection_name, overwrite=overwrite)
|
||||
|
||||
|
||||
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
||||
def store_docs_in_vector_db(
|
||||
docs, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
|
||||
) -> bool:
|
||||
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
|
||||
|
||||
texts = [doc.page_content for doc in docs]
|
||||
metadatas = [doc.metadata for doc in docs]
|
||||
metadatas = [{**doc.metadata, **(metadata if metadata else {})} for doc in docs]
|
||||
|
||||
# ChromaDB does not like datetime formats
|
||||
# for meta-data so convert them to string.
|
||||
@@ -1095,6 +1099,13 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
"vue",
|
||||
"svelte",
|
||||
"msg",
|
||||
"ex",
|
||||
"exs",
|
||||
"erl",
|
||||
"tsx",
|
||||
"jsx",
|
||||
"hs",
|
||||
"lhs",
|
||||
]
|
||||
|
||||
if (
|
||||
@@ -1237,13 +1248,21 @@ def process_doc(
|
||||
data = loader.load()
|
||||
|
||||
try:
|
||||
result = store_data_in_vector_db(data, collection_name)
|
||||
result = store_data_in_vector_db(
|
||||
data,
|
||||
collection_name,
|
||||
{
|
||||
"file_id": form_data.file_id,
|
||||
"name": file.meta.get("name", file.filename),
|
||||
},
|
||||
)
|
||||
|
||||
if result:
|
||||
return {
|
||||
"status": True,
|
||||
"collection_name": collection_name,
|
||||
"known_type": known_type,
|
||||
"filename": file.meta.get("name", file.filename),
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
||||
+46
-14
@@ -44,24 +44,26 @@ async def user_join(sid, data):
|
||||
print("user-join", sid, data)
|
||||
|
||||
auth = data["auth"] if "auth" in data else None
|
||||
if not auth or "token" not in auth:
|
||||
return
|
||||
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
data = decode_token(auth["token"])
|
||||
if data is None or "id" not in data:
|
||||
return
|
||||
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
if not user:
|
||||
return
|
||||
|
||||
if user:
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
@@ -80,7 +82,6 @@ def get_models_in_use():
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
|
||||
model_id = data["model"]
|
||||
|
||||
# Cancel previous callback if there is one
|
||||
@@ -137,3 +138,34 @@ async def disconnect(sid):
|
||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||
else:
|
||||
print(f"Unknown session ID {sid} disconnected")
|
||||
|
||||
|
||||
def get_event_emitter(request_info):
|
||||
async def __event_emitter__(event_data):
|
||||
await sio.emit(
|
||||
"chat-events",
|
||||
{
|
||||
"chat_id": request_info["chat_id"],
|
||||
"message_id": request_info["message_id"],
|
||||
"data": event_data,
|
||||
},
|
||||
to=request_info["session_id"],
|
||||
)
|
||||
|
||||
return __event_emitter__
|
||||
|
||||
|
||||
def get_event_call(request_info):
|
||||
async def __event_call__(event_data):
|
||||
response = await sio.call(
|
||||
"chat-events",
|
||||
{
|
||||
"chat_id": request_info["chat_id"],
|
||||
"message_id": request_info["message_id"],
|
||||
"data": event_data,
|
||||
},
|
||||
to=request_info["session_id"],
|
||||
)
|
||||
return response
|
||||
|
||||
return __event_call__
|
||||
|
||||
+204
-259
@@ -1,9 +1,6 @@
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
from apps.webui.routers import (
|
||||
auths,
|
||||
users,
|
||||
@@ -20,15 +17,17 @@ from apps.webui.routers import (
|
||||
)
|
||||
from apps.webui.models.functions import Functions
|
||||
from apps.webui.models.models import Models
|
||||
|
||||
from apps.webui.utils import load_function_module_by_id
|
||||
|
||||
from utils.misc import stream_message_template
|
||||
from utils.task import prompt_template
|
||||
from utils.misc import (
|
||||
openai_chat_chunk_message_template,
|
||||
openai_chat_completion_message_template,
|
||||
apply_model_params_to_body,
|
||||
apply_model_system_prompt_to_body,
|
||||
)
|
||||
|
||||
|
||||
from config import (
|
||||
WEBUI_BUILD_HASH,
|
||||
SHOW_ADMIN_DETAILS,
|
||||
ADMIN_EMAIL,
|
||||
WEBUI_AUTH,
|
||||
@@ -36,6 +35,7 @@ from config import (
|
||||
DEFAULT_PROMPT_SUGGESTIONS,
|
||||
DEFAULT_USER_ROLE,
|
||||
ENABLE_SIGNUP,
|
||||
ENABLE_LOGIN_FORM,
|
||||
USER_PERMISSIONS,
|
||||
WEBHOOK_URL,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
@@ -48,12 +48,12 @@ from config import (
|
||||
OAUTH_PICTURE_CLAIM,
|
||||
)
|
||||
|
||||
from apps.socket.main import get_event_call, get_event_emitter
|
||||
|
||||
import inspect
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
|
||||
from typing import Iterator, Generator
|
||||
from typing import Iterator, Generator, AsyncGenerator
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
@@ -63,6 +63,7 @@ origins = ["*"]
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
|
||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
||||
@@ -124,57 +125,63 @@ async def get_status():
|
||||
}
|
||||
|
||||
|
||||
def get_function_module(pipe_id: str):
|
||||
# Check if function is already loaded
|
||||
if pipe_id not in app.state.FUNCTIONS:
|
||||
function_module, _, _ = load_function_module_by_id(pipe_id)
|
||||
app.state.FUNCTIONS[pipe_id] = function_module
|
||||
else:
|
||||
function_module = app.state.FUNCTIONS[pipe_id]
|
||||
|
||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||
valves = Functions.get_function_valves_by_id(pipe_id)
|
||||
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
||||
return function_module
|
||||
|
||||
|
||||
async def get_pipe_models():
|
||||
pipes = Functions.get_functions_by_type("pipe", active_only=True)
|
||||
pipe_models = []
|
||||
|
||||
for pipe in pipes:
|
||||
# Check if function is already loaded
|
||||
if pipe.id not in app.state.FUNCTIONS:
|
||||
function_module, function_type, frontmatter = load_function_module_by_id(
|
||||
pipe.id
|
||||
)
|
||||
app.state.FUNCTIONS[pipe.id] = function_module
|
||||
else:
|
||||
function_module = app.state.FUNCTIONS[pipe.id]
|
||||
|
||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||
valves = Functions.get_function_valves_by_id(pipe.id)
|
||||
function_module.valves = function_module.Valves(
|
||||
**(valves if valves else {})
|
||||
)
|
||||
function_module = get_function_module(pipe.id)
|
||||
|
||||
# Check if function is a manifold
|
||||
if hasattr(function_module, "type"):
|
||||
if function_module.type == "manifold":
|
||||
manifold_pipes = []
|
||||
if hasattr(function_module, "pipes"):
|
||||
manifold_pipes = []
|
||||
|
||||
# Check if pipes is a function or a list
|
||||
if callable(function_module.pipes):
|
||||
manifold_pipes = function_module.pipes()
|
||||
else:
|
||||
manifold_pipes = function_module.pipes
|
||||
# Check if pipes is a function or a list
|
||||
if callable(function_module.pipes):
|
||||
manifold_pipes = function_module.pipes()
|
||||
else:
|
||||
manifold_pipes = function_module.pipes
|
||||
|
||||
for p in manifold_pipes:
|
||||
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
|
||||
manifold_pipe_name = p["name"]
|
||||
for p in manifold_pipes:
|
||||
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
|
||||
manifold_pipe_name = p["name"]
|
||||
|
||||
if hasattr(function_module, "name"):
|
||||
manifold_pipe_name = (
|
||||
f"{function_module.name}{manifold_pipe_name}"
|
||||
)
|
||||
if hasattr(function_module, "name"):
|
||||
manifold_pipe_name = f"{function_module.name}{manifold_pipe_name}"
|
||||
|
||||
pipe_models.append(
|
||||
{
|
||||
"id": manifold_pipe_id,
|
||||
"name": manifold_pipe_name,
|
||||
"object": "model",
|
||||
"created": pipe.created_at,
|
||||
"owned_by": "openai",
|
||||
"pipe": {"type": pipe.type},
|
||||
}
|
||||
)
|
||||
pipe_flag = {"type": pipe.type}
|
||||
if hasattr(function_module, "ChatValves"):
|
||||
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||
|
||||
pipe_models.append(
|
||||
{
|
||||
"id": manifold_pipe_id,
|
||||
"name": manifold_pipe_name,
|
||||
"object": "model",
|
||||
"created": pipe.created_at,
|
||||
"owned_by": "openai",
|
||||
"pipe": pipe_flag,
|
||||
}
|
||||
)
|
||||
else:
|
||||
pipe_flag = {"type": "pipe"}
|
||||
if hasattr(function_module, "ChatValves"):
|
||||
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||
|
||||
pipe_models.append(
|
||||
{
|
||||
"id": pipe.id,
|
||||
@@ -182,239 +189,177 @@ async def get_pipe_models():
|
||||
"object": "model",
|
||||
"created": pipe.created_at,
|
||||
"owned_by": "openai",
|
||||
"pipe": {"type": "pipe"},
|
||||
"pipe": pipe_flag,
|
||||
}
|
||||
)
|
||||
|
||||
return pipe_models
|
||||
|
||||
|
||||
async def execute_pipe(pipe, params):
|
||||
if inspect.iscoroutinefunction(pipe):
|
||||
return await pipe(**params)
|
||||
else:
|
||||
return pipe(**params)
|
||||
|
||||
|
||||
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
|
||||
if isinstance(res, str):
|
||||
return res
|
||||
if isinstance(res, Generator):
|
||||
return "".join(map(str, res))
|
||||
if isinstance(res, AsyncGenerator):
|
||||
return "".join([str(stream) async for stream in res])
|
||||
|
||||
|
||||
def process_line(form_data: dict, line):
|
||||
if isinstance(line, BaseModel):
|
||||
line = line.model_dump_json()
|
||||
line = f"data: {line}"
|
||||
if isinstance(line, dict):
|
||||
line = f"data: {json.dumps(line)}"
|
||||
|
||||
try:
|
||||
line = line.decode("utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if line.startswith("data:"):
|
||||
return f"{line}\n\n"
|
||||
else:
|
||||
line = openai_chat_chunk_message_template(form_data["model"], line)
|
||||
return f"data: {json.dumps(line)}\n\n"
|
||||
|
||||
|
||||
def get_pipe_id(form_data: dict) -> str:
|
||||
pipe_id = form_data["model"]
|
||||
if "." in pipe_id:
|
||||
pipe_id, _ = pipe_id.split(".", 1)
|
||||
print(pipe_id)
|
||||
return pipe_id
|
||||
|
||||
|
||||
def get_function_params(function_module, form_data, user, extra_params={}):
|
||||
pipe_id = get_pipe_id(form_data)
|
||||
# Get the signature of the function
|
||||
sig = inspect.signature(function_module.pipe)
|
||||
params = {"body": form_data}
|
||||
|
||||
for key, value in extra_params.items():
|
||||
if key in sig.parameters:
|
||||
params[key] = value
|
||||
|
||||
if "__user__" in sig.parameters:
|
||||
__user__ = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
try:
|
||||
if hasattr(function_module, "UserValves"):
|
||||
__user__["valves"] = function_module.UserValves(
|
||||
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
params["__user__"] = __user__
|
||||
return params
|
||||
|
||||
|
||||
async def generate_function_chat_completion(form_data, user):
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
metadata = form_data.pop("metadata", None)
|
||||
|
||||
__event_emitter__ = None
|
||||
__event_call__ = None
|
||||
__task__ = None
|
||||
|
||||
if metadata:
|
||||
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
|
||||
__event_emitter__ = get_event_emitter(metadata)
|
||||
__event_call__ = get_event_call(metadata)
|
||||
__task__ = metadata.get("task", None)
|
||||
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
form_data["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
params = model_info.params.model_dump()
|
||||
form_data = apply_model_params_to_body(params, form_data)
|
||||
form_data = apply_model_system_prompt_to_body(params, form_data, user)
|
||||
|
||||
if model_info.params:
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
form_data["temperature"] = float(model_info.params.get("temperature"))
|
||||
pipe_id = get_pipe_id(form_data)
|
||||
function_module = get_function_module(pipe_id)
|
||||
|
||||
if model_info.params.get("top_p", None):
|
||||
form_data["top_p"] = int(model_info.params.get("top_p", None))
|
||||
pipe = function_module.pipe
|
||||
params = get_function_params(
|
||||
function_module,
|
||||
form_data,
|
||||
user,
|
||||
{
|
||||
"__event_emitter__": __event_emitter__,
|
||||
"__event_call__": __event_call__,
|
||||
"__task__": __task__,
|
||||
},
|
||||
)
|
||||
|
||||
if model_info.params.get("max_tokens", None):
|
||||
form_data["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
form_data["frequency_penalty"] = int(
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
)
|
||||
|
||||
if model_info.params.get("seed", None):
|
||||
form_data["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
form_data["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
system = model_info.params.get("system", None)
|
||||
if system:
|
||||
system = prompt_template(
|
||||
system,
|
||||
**(
|
||||
{
|
||||
"user_name": user.name,
|
||||
"user_location": (
|
||||
user.info.get("location") if user.info else None
|
||||
),
|
||||
}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if form_data.get("messages"):
|
||||
for message in form_data["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = system + message["content"]
|
||||
break
|
||||
else:
|
||||
form_data["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": system,
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
async def job():
|
||||
pipe_id = form_data["model"]
|
||||
if "." in pipe_id:
|
||||
pipe_id, sub_pipe_id = pipe_id.split(".", 1)
|
||||
print(pipe_id)
|
||||
|
||||
# Check if function is already loaded
|
||||
if pipe_id not in app.state.FUNCTIONS:
|
||||
function_module, function_type, frontmatter = load_function_module_by_id(
|
||||
pipe_id
|
||||
)
|
||||
app.state.FUNCTIONS[pipe_id] = function_module
|
||||
else:
|
||||
function_module = app.state.FUNCTIONS[pipe_id]
|
||||
|
||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||
|
||||
valves = Functions.get_function_valves_by_id(pipe_id)
|
||||
function_module.valves = function_module.Valves(
|
||||
**(valves if valves else {})
|
||||
)
|
||||
|
||||
pipe = function_module.pipe
|
||||
|
||||
# Get the signature of the function
|
||||
sig = inspect.signature(pipe)
|
||||
params = {"body": form_data}
|
||||
|
||||
if "__user__" in sig.parameters:
|
||||
__user__ = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
}
|
||||
if form_data["stream"]:
|
||||
|
||||
async def stream_content():
|
||||
try:
|
||||
if hasattr(function_module, "UserValves"):
|
||||
__user__["valves"] = function_module.UserValves(
|
||||
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
res = await execute_pipe(pipe, params)
|
||||
|
||||
params = {**params, "__user__": __user__}
|
||||
|
||||
if form_data["stream"]:
|
||||
|
||||
async def stream_content():
|
||||
try:
|
||||
if inspect.iscoroutinefunction(pipe):
|
||||
res = await pipe(**params)
|
||||
else:
|
||||
res = pipe(**params)
|
||||
|
||||
# Directly return if the response is a StreamingResponse
|
||||
if isinstance(res, StreamingResponse):
|
||||
async for data in res.body_iterator:
|
||||
yield data
|
||||
return
|
||||
if isinstance(res, dict):
|
||||
yield f"data: {json.dumps(res)}\n\n"
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
||||
# Directly return if the response is a StreamingResponse
|
||||
if isinstance(res, StreamingResponse):
|
||||
async for data in res.body_iterator:
|
||||
yield data
|
||||
return
|
||||
if isinstance(res, dict):
|
||||
yield f"data: {json.dumps(res)}\n\n"
|
||||
return
|
||||
|
||||
if isinstance(res, str):
|
||||
message = stream_message_template(form_data["model"], res)
|
||||
yield f"data: {json.dumps(message)}\n\n"
|
||||
|
||||
if isinstance(res, Iterator):
|
||||
for line in res:
|
||||
if isinstance(line, BaseModel):
|
||||
line = line.model_dump_json()
|
||||
line = f"data: {line}"
|
||||
if isinstance(line, dict):
|
||||
line = f"data: {json.dumps(line)}"
|
||||
|
||||
try:
|
||||
line = line.decode("utf-8")
|
||||
except:
|
||||
pass
|
||||
|
||||
if line.startswith("data:"):
|
||||
yield f"{line}\n\n"
|
||||
else:
|
||||
line = stream_message_template(form_data["model"], line)
|
||||
yield f"data: {json.dumps(line)}\n\n"
|
||||
|
||||
if isinstance(res, str) or isinstance(res, Generator):
|
||||
finish_message = {
|
||||
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": form_data["model"],
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"logprobs": None,
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
yield f"data: {json.dumps(finish_message)}\n\n"
|
||||
yield f"data: [DONE]"
|
||||
|
||||
return StreamingResponse(stream_content(), media_type="text/event-stream")
|
||||
else:
|
||||
|
||||
try:
|
||||
if inspect.iscoroutinefunction(pipe):
|
||||
res = await pipe(**params)
|
||||
else:
|
||||
res = pipe(**params)
|
||||
|
||||
if isinstance(res, StreamingResponse):
|
||||
return res
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return {"error": {"detail": str(e)}}
|
||||
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
||||
return
|
||||
|
||||
if isinstance(res, dict):
|
||||
return res
|
||||
elif isinstance(res, BaseModel):
|
||||
return res.model_dump()
|
||||
else:
|
||||
message = ""
|
||||
if isinstance(res, str):
|
||||
message = res
|
||||
if isinstance(res, Generator):
|
||||
for stream in res:
|
||||
message = f"{message}{stream}"
|
||||
if isinstance(res, str):
|
||||
message = openai_chat_chunk_message_template(form_data["model"], res)
|
||||
yield f"data: {json.dumps(message)}\n\n"
|
||||
|
||||
return {
|
||||
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": form_data["model"],
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": message,
|
||||
},
|
||||
"logprobs": None,
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
}
|
||||
if isinstance(res, Iterator):
|
||||
for line in res:
|
||||
yield process_line(form_data, line)
|
||||
|
||||
return await job()
|
||||
if isinstance(res, AsyncGenerator):
|
||||
async for line in res:
|
||||
yield process_line(form_data, line)
|
||||
|
||||
if isinstance(res, str) or isinstance(res, Generator):
|
||||
finish_message = openai_chat_chunk_message_template(
|
||||
form_data["model"], ""
|
||||
)
|
||||
finish_message["choices"][0]["finish_reason"] = "stop"
|
||||
yield f"data: {json.dumps(finish_message)}\n\n"
|
||||
yield "data: [DONE]"
|
||||
|
||||
return StreamingResponse(stream_content(), media_type="text/event-stream")
|
||||
else:
|
||||
try:
|
||||
res = await execute_pipe(pipe, params)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return {"error": {"detail": str(e)}}
|
||||
|
||||
if isinstance(res, StreamingResponse) or isinstance(res, dict):
|
||||
return res
|
||||
if isinstance(res, BaseModel):
|
||||
return res.model_dump()
|
||||
|
||||
message = await get_message_content(res)
|
||||
return openai_chat_completion_message_template(form_data["model"], message)
|
||||
|
||||
@@ -159,7 +159,7 @@ class AuthsTable:
|
||||
log.info(f"authenticate_user_by_trusted_header: {email}")
|
||||
try:
|
||||
with get_db() as db:
|
||||
auth = db.query(Auth).filter(email=email, active=True).first()
|
||||
auth = db.query(Auth).filter_by(email=email, active=True).first()
|
||||
if auth:
|
||||
user = Users.get_user_by_id(auth.id)
|
||||
return user
|
||||
|
||||
@@ -245,6 +245,39 @@ class ChatTable:
|
||||
)
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chat_title_id_list_by_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
include_archived: bool = False,
|
||||
skip: int = 0,
|
||||
limit: int = -1,
|
||||
) -> List[ChatTitleIdResponse]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
if not include_archived:
|
||||
query = query.filter_by(archived=False)
|
||||
|
||||
all_chats = (
|
||||
query.order_by(Chat.updated_at.desc())
|
||||
# limit cols
|
||||
.with_entities(Chat.id, Chat.title, Chat.updated_at, Chat.created_at)
|
||||
.limit(limit)
|
||||
.offset(skip)
|
||||
.all()
|
||||
)
|
||||
# result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
|
||||
return [
|
||||
ChatTitleIdResponse.model_validate(
|
||||
{
|
||||
"id": chat[0],
|
||||
"title": chat[1],
|
||||
"updated_at": chat[2],
|
||||
"created_at": chat[3],
|
||||
}
|
||||
)
|
||||
for chat in all_chats
|
||||
]
|
||||
|
||||
def get_chat_list_by_chat_ids(
|
||||
self, chat_ids: List[str], skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
|
||||
@@ -167,6 +167,15 @@ class FunctionsTable:
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_global_action_functions(self) -> List[FunctionModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function)
|
||||
.filter_by(type="action", is_active=True, is_global=True)
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
|
||||
with get_db() as db:
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import String, Column, BigInteger, Text
|
||||
from sqlalchemy import Column, BigInteger, Text
|
||||
|
||||
from apps.webui.internal.db import Base, JSONField, get_db
|
||||
|
||||
from typing import List, Union, Optional
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
import time
|
||||
@@ -113,7 +111,6 @@ class ModelForm(BaseModel):
|
||||
|
||||
|
||||
class ModelsTable:
|
||||
|
||||
def insert_new_model(
|
||||
self, form_data: ModelForm, user_id: str
|
||||
) -> Optional[ModelModel]:
|
||||
@@ -126,9 +123,7 @@ class ModelsTable:
|
||||
}
|
||||
)
|
||||
try:
|
||||
|
||||
with get_db() as db:
|
||||
|
||||
result = Model(**model.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
@@ -144,13 +139,11 @@ class ModelsTable:
|
||||
|
||||
def get_all_models(self) -> List[ModelModel]:
|
||||
with get_db() as db:
|
||||
|
||||
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
|
||||
|
||||
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
||||
model = db.get(Model, id)
|
||||
return ModelModel.model_validate(model)
|
||||
except:
|
||||
@@ -178,7 +171,6 @@ class ModelsTable:
|
||||
def delete_model_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
||||
db.query(Model).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class TagTable:
|
||||
) -> Optional[TagModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
tag = db.query(Tag).filter(name=name, user_id=user_id).first()
|
||||
tag = db.query(Tag).filter_by(name=name, user_id=user_id).first()
|
||||
return TagModel.model_validate(tag)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
@@ -28,7 +28,7 @@ from apps.webui.models.tags import (
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
|
||||
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_CHAT_ACCESS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
@@ -43,9 +43,15 @@ router = APIRouter()
|
||||
@router.get("/", response_model=List[ChatTitleIdResponse])
|
||||
@router.get("/list", response_model=List[ChatTitleIdResponse])
|
||||
async def get_session_user_chat_list(
|
||||
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
|
||||
user=Depends(get_verified_user), page: Optional[int] = None
|
||||
):
|
||||
return Chats.get_chat_list_by_user_id(user.id, skip, limit)
|
||||
if page is not None:
|
||||
limit = 60
|
||||
skip = (page - 1) * limit
|
||||
|
||||
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
|
||||
else:
|
||||
return Chats.get_chat_title_id_list_by_user_id(user.id)
|
||||
|
||||
|
||||
############################
|
||||
@@ -81,6 +87,11 @@ async def get_user_chat_list_by_user_id(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
):
|
||||
if not ENABLE_ADMIN_CHAT_ACCESS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
return Chats.get_chat_list_by_user_id(
|
||||
user_id, include_archived=True, skip=skip, limit=limit
|
||||
)
|
||||
@@ -181,9 +192,9 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if user.role == "user":
|
||||
if user.role == "user" or (user.role == "admin" and not ENABLE_ADMIN_CHAT_ACCESS):
|
||||
chat = Chats.get_chat_by_share_id(share_id)
|
||||
elif user.role == "admin":
|
||||
elif user.role == "admin" and ENABLE_ADMIN_CHAT_ACCESS:
|
||||
chat = Chats.get_chat_by_id(share_id)
|
||||
|
||||
if chat:
|
||||
|
||||
@@ -58,6 +58,7 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
|
||||
|
||||
# replace filename with uuid
|
||||
id = str(uuid.uuid4())
|
||||
name = filename
|
||||
filename = f"{id}_{filename}"
|
||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||
|
||||
@@ -73,6 +74,7 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
|
||||
"id": id,
|
||||
"filename": filename,
|
||||
"meta": {
|
||||
"name": name,
|
||||
"content_type": file.content_type,
|
||||
"size": len(contents),
|
||||
"path": file_path,
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
||||
from apps.webui.utils import load_toolkit_module_by_id
|
||||
|
||||
@@ -14,7 +10,6 @@ from utils.utils import get_admin_user, get_verified_user
|
||||
from utils.tools import get_tools_specs
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from importlib import util
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -69,7 +64,7 @@ async def create_new_toolkit(
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||
if toolkit == None:
|
||||
if toolkit is None:
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
@@ -98,7 +93,7 @@ async def create_new_toolkit(
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -170,7 +165,7 @@ async def update_toolkit_by_id(
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
|
||||
|
||||
@@ -210,7 +205,7 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -233,7 +228,7 @@ async def get_toolkit_valves_spec_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "Valves"):
|
||||
@@ -261,7 +256,7 @@ async def update_toolkit_valves_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "Valves"):
|
||||
@@ -276,7 +271,7 @@ async def update_toolkit_valves_by_id(
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -306,7 +301,7 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -324,7 +319,7 @@ async def get_toolkit_user_valves_spec_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "UserValves"):
|
||||
@@ -348,7 +343,7 @@ async def update_toolkit_user_valves_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "UserValves"):
|
||||
@@ -365,7 +360,7 @@ async def update_toolkit_user_valves_by_id(
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from pathlib import Path
|
||||
import site
|
||||
|
||||
from fastapi import APIRouter, UploadFile, File, Response
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from starlette.responses import StreamingResponse, FileResponse
|
||||
@@ -64,8 +67,18 @@ async def download_chat_as_pdf(
|
||||
pdf = FPDF()
|
||||
pdf.add_page()
|
||||
|
||||
STATIC_DIR = "./static"
|
||||
FONTS_DIR = f"{STATIC_DIR}/fonts"
|
||||
# When running in docker, workdir is /app/backend, so fonts is in /app/backend/static/fonts
|
||||
FONTS_DIR = Path("./static/fonts")
|
||||
|
||||
# Non Docker Installation
|
||||
|
||||
# When running using `pip install` the static directory is in the site packages.
|
||||
if not FONTS_DIR.exists():
|
||||
FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
|
||||
# When running using `pip install -e .` the static directory is in the site packages.
|
||||
# This path only works if `open-webui serve` is run from the root of this project.
|
||||
if not FONTS_DIR.exists():
|
||||
FONTS_DIR = Path("./backend/static/fonts")
|
||||
|
||||
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
|
||||
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
|
||||
|
||||
@@ -79,6 +79,8 @@ def load_function_module_by_id(function_id):
|
||||
return module.Pipe(), "pipe", frontmatter
|
||||
elif hasattr(module, "Filter"):
|
||||
return module.Filter(), "filter", frontmatter
|
||||
elif hasattr(module, "Action"):
|
||||
return module.Action(), "action", frontmatter
|
||||
else:
|
||||
raise Exception("No Function class found")
|
||||
except Exception as e:
|
||||
|
||||
@@ -77,6 +77,16 @@ for source in log_sources:
|
||||
|
||||
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
||||
|
||||
|
||||
class EndpointFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
return record.getMessage().find("/health") == -1
|
||||
|
||||
|
||||
# Filter out /endpoint
|
||||
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
|
||||
|
||||
|
||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||
if WEBUI_NAME != "Open WebUI":
|
||||
WEBUI_NAME += " (Open WebUI)"
|
||||
@@ -339,6 +349,12 @@ GOOGLE_OAUTH_SCOPE = PersistentConfig(
|
||||
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
|
||||
)
|
||||
|
||||
GOOGLE_REDIRECT_URI = PersistentConfig(
|
||||
"GOOGLE_REDIRECT_URI",
|
||||
"oauth.google.redirect_uri",
|
||||
os.environ.get("GOOGLE_REDIRECT_URI", ""),
|
||||
)
|
||||
|
||||
MICROSOFT_CLIENT_ID = PersistentConfig(
|
||||
"MICROSOFT_CLIENT_ID",
|
||||
"oauth.microsoft.client_id",
|
||||
@@ -363,6 +379,12 @@ MICROSOFT_OAUTH_SCOPE = PersistentConfig(
|
||||
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
|
||||
)
|
||||
|
||||
MICROSOFT_REDIRECT_URI = PersistentConfig(
|
||||
"MICROSOFT_REDIRECT_URI",
|
||||
"oauth.microsoft.redirect_uri",
|
||||
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
|
||||
)
|
||||
|
||||
OAUTH_CLIENT_ID = PersistentConfig(
|
||||
"OAUTH_CLIENT_ID",
|
||||
"oauth.oidc.client_id",
|
||||
@@ -381,6 +403,12 @@ OPENID_PROVIDER_URL = PersistentConfig(
|
||||
os.environ.get("OPENID_PROVIDER_URL", ""),
|
||||
)
|
||||
|
||||
OPENID_REDIRECT_URI = PersistentConfig(
|
||||
"OPENID_REDIRECT_URI",
|
||||
"oauth.oidc.redirect_uri",
|
||||
os.environ.get("OPENID_REDIRECT_URI", ""),
|
||||
)
|
||||
|
||||
OAUTH_SCOPES = PersistentConfig(
|
||||
"OAUTH_SCOPES",
|
||||
"oauth.oidc.scopes",
|
||||
@@ -414,6 +442,7 @@ def load_oauth_providers():
|
||||
"client_secret": GOOGLE_CLIENT_SECRET.value,
|
||||
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
||||
"scope": GOOGLE_OAUTH_SCOPE.value,
|
||||
"redirect_uri": GOOGLE_REDIRECT_URI.value,
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -426,6 +455,7 @@ def load_oauth_providers():
|
||||
"client_secret": MICROSOFT_CLIENT_SECRET.value,
|
||||
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
|
||||
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
||||
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -439,6 +469,7 @@ def load_oauth_providers():
|
||||
"server_metadata_url": OPENID_PROVIDER_URL.value,
|
||||
"scope": OAUTH_SCOPES.value,
|
||||
"name": OAUTH_PROVIDER_NAME.value,
|
||||
"redirect_uri": OPENID_REDIRECT_URI.value,
|
||||
}
|
||||
|
||||
|
||||
@@ -709,6 +740,12 @@ ENABLE_SIGNUP = PersistentConfig(
|
||||
),
|
||||
)
|
||||
|
||||
ENABLE_LOGIN_FORM = PersistentConfig(
|
||||
"ENABLE_LOGIN_FORM",
|
||||
"ui.ENABLE_LOGIN_FORM",
|
||||
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
|
||||
)
|
||||
|
||||
DEFAULT_LOCALE = PersistentConfig(
|
||||
"DEFAULT_LOCALE",
|
||||
"ui.default_locale",
|
||||
@@ -787,6 +824,10 @@ WEBHOOK_URL = PersistentConfig(
|
||||
|
||||
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||
|
||||
ENABLE_ADMIN_CHAT_ACCESS = (
|
||||
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
|
||||
)
|
||||
|
||||
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
||||
"ENABLE_COMMUNITY_SHARING",
|
||||
"ui.enable_community_sharing",
|
||||
@@ -1265,6 +1306,24 @@ COMFYUI_SD3 = PersistentConfig(
|
||||
os.environ.get("COMFYUI_SD3", "").lower() == "true",
|
||||
)
|
||||
|
||||
COMFYUI_FLUX = PersistentConfig(
|
||||
"COMFYUI_FLUX",
|
||||
"image_generation.comfyui.flux",
|
||||
os.environ.get("COMFYUI_FLUX", "").lower() == "true",
|
||||
)
|
||||
|
||||
COMFYUI_FLUX_WEIGHT_DTYPE = PersistentConfig(
|
||||
"COMFYUI_FLUX_WEIGHT_DTYPE",
|
||||
"image_generation.comfyui.flux_weight_dtype",
|
||||
os.getenv("COMFYUI_FLUX_WEIGHT_DTYPE", ""),
|
||||
)
|
||||
|
||||
COMFYUI_FLUX_FP8_CLIP = PersistentConfig(
|
||||
"COMFYUI_FLUX_FP8_CLIP",
|
||||
"image_generation.comfyui.flux_fp8_clip",
|
||||
os.environ.get("COMFYUI_FLUX_FP8_CLIP", "").lower() == "true",
|
||||
)
|
||||
|
||||
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"IMAGES_OPENAI_API_BASE_URL",
|
||||
"image_generation.openai.api_base_url",
|
||||
@@ -1329,6 +1388,11 @@ AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
||||
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
AUDIO_TTS_API_KEY = PersistentConfig(
|
||||
"AUDIO_TTS_API_KEY",
|
||||
"audio.tts.api_key",
|
||||
os.getenv("AUDIO_TTS_API_KEY", ""),
|
||||
)
|
||||
|
||||
AUDIO_TTS_ENGINE = PersistentConfig(
|
||||
"AUDIO_TTS_ENGINE",
|
||||
|
||||
@@ -95,8 +95,8 @@ class TASKS(str, Enum):
|
||||
def __str__(self) -> str:
|
||||
return super().__str__()
|
||||
|
||||
DEFAULT = lambda task="": f"{task if task else 'default'}"
|
||||
TITLE_GENERATION = "Title Generation"
|
||||
EMOJI_GENERATION = "Emoji Generation"
|
||||
QUERY_GENERATION = "Query Generation"
|
||||
FUNCTION_CALLING = "Function Calling"
|
||||
DEFAULT = lambda task="": f"{task if task else 'generation'}"
|
||||
TITLE_GENERATION = "title_generation"
|
||||
EMOJI_GENERATION = "emoji_generation"
|
||||
QUERY_GENERATION = "query_generation"
|
||||
FUNCTION_CALLING = "function_calling"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 0,
|
||||
"ui": {
|
||||
"default_locale": "en-US",
|
||||
"default_locale": "",
|
||||
"prompt_suggestions": [
|
||||
{
|
||||
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||
|
||||
+252
-87
@@ -13,8 +13,6 @@ import aiohttp
|
||||
import requests
|
||||
import mimetypes
|
||||
import shutil
|
||||
import os
|
||||
import uuid
|
||||
import inspect
|
||||
|
||||
from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form
|
||||
@@ -29,7 +27,7 @@ from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import StreamingResponse, Response, RedirectResponse
|
||||
|
||||
|
||||
from apps.socket.main import sio, app as socket_app
|
||||
from apps.socket.main import app as socket_app, get_event_emitter, get_event_call
|
||||
from apps.ollama.main import (
|
||||
app as ollama_app,
|
||||
get_all_models as get_ollama_models,
|
||||
@@ -79,6 +77,7 @@ from utils.task import (
|
||||
from utils.misc import (
|
||||
get_last_user_message,
|
||||
add_or_update_system_message,
|
||||
prepend_to_first_user_message_content,
|
||||
parse_duration,
|
||||
)
|
||||
|
||||
@@ -117,6 +116,7 @@ from config import (
|
||||
WEBUI_SECRET_KEY,
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
ENABLE_ADMIN_CHAT_ACCESS,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
@@ -317,7 +317,7 @@ async def get_function_call_response(
|
||||
{"role": "user", "content": f"Query: {prompt}"},
|
||||
],
|
||||
"stream": False,
|
||||
"task": TASKS.FUNCTION_CALLING,
|
||||
"task": str(TASKS.FUNCTION_CALLING),
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -618,38 +618,15 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
# Extract session_id, chat_id and message_id from the request body
|
||||
session_id = None
|
||||
if "session_id" in body:
|
||||
session_id = body["session_id"]
|
||||
del body["session_id"]
|
||||
chat_id = None
|
||||
if "chat_id" in body:
|
||||
chat_id = body["chat_id"]
|
||||
del body["chat_id"]
|
||||
message_id = None
|
||||
if "id" in body:
|
||||
message_id = body["id"]
|
||||
del body["id"]
|
||||
metadata = {
|
||||
"chat_id": body.pop("chat_id", None),
|
||||
"message_id": body.pop("id", None),
|
||||
"session_id": body.pop("session_id", None),
|
||||
"valves": body.pop("valves", None),
|
||||
}
|
||||
|
||||
async def __event_emitter__(data):
|
||||
await sio.emit(
|
||||
"chat-events",
|
||||
{
|
||||
"chat_id": chat_id,
|
||||
"message_id": message_id,
|
||||
"data": data,
|
||||
},
|
||||
to=session_id,
|
||||
)
|
||||
|
||||
async def __event_call__(data):
|
||||
response = await sio.call(
|
||||
"chat-events",
|
||||
{"chat_id": chat_id, "message_id": message_id, "data": data},
|
||||
to=session_id,
|
||||
)
|
||||
return response
|
||||
__event_emitter__ = get_event_emitter(metadata)
|
||||
__event_call__ = get_event_call(metadata)
|
||||
|
||||
# Initialize data_items to store additional data to be sent to the client
|
||||
data_items = []
|
||||
@@ -692,17 +669,29 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||
if len(contexts) > 0:
|
||||
context_string = "/n".join(contexts).strip()
|
||||
prompt = get_last_user_message(body["messages"])
|
||||
body["messages"] = add_or_update_system_message(
|
||||
rag_template(
|
||||
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
),
|
||||
body["messages"],
|
||||
)
|
||||
|
||||
# Workaround for Ollama 2.0+ system prompt issue
|
||||
# TODO: replace with add_or_update_system_message
|
||||
if model["owned_by"] == "ollama":
|
||||
body["messages"] = prepend_to_first_user_message_content(
|
||||
rag_template(
|
||||
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
),
|
||||
body["messages"],
|
||||
)
|
||||
else:
|
||||
body["messages"] = add_or_update_system_message(
|
||||
rag_template(
|
||||
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
),
|
||||
body["messages"],
|
||||
)
|
||||
|
||||
# If there are citations, add them to the data_items
|
||||
if len(citations) > 0:
|
||||
data_items.append({"citations": citations})
|
||||
|
||||
body["metadata"] = metadata
|
||||
modified_body_bytes = json.dumps(body).encode("utf-8")
|
||||
# Replace the request body with the modified one
|
||||
request._body = modified_body_bytes
|
||||
@@ -823,9 +812,6 @@ def filter_pipeline(payload, user):
|
||||
if "detail" in res:
|
||||
raise Exception(r.status_code, res["detail"])
|
||||
|
||||
if "pipeline" not in app.state.MODELS[model_id] and "task" in payload:
|
||||
del payload["task"]
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@@ -935,6 +921,7 @@ webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
|
||||
|
||||
|
||||
async def get_all_models():
|
||||
# TODO: Optimize this function
|
||||
pipe_models = []
|
||||
openai_models = []
|
||||
ollama_models = []
|
||||
@@ -961,9 +948,17 @@ async def get_all_models():
|
||||
|
||||
models = pipe_models + openai_models + ollama_models
|
||||
|
||||
global_action_ids = [
|
||||
function.id for function in Functions.get_global_action_functions()
|
||||
]
|
||||
enabled_action_ids = [
|
||||
function.id
|
||||
for function in Functions.get_functions_by_type("action", active_only=True)
|
||||
]
|
||||
|
||||
custom_models = Models.get_all_models()
|
||||
for custom_model in custom_models:
|
||||
if custom_model.base_model_id == None:
|
||||
if custom_model.base_model_id is None:
|
||||
for model in models:
|
||||
if (
|
||||
custom_model.id == model["id"]
|
||||
@@ -971,9 +966,16 @@ async def get_all_models():
|
||||
):
|
||||
model["name"] = custom_model.name
|
||||
model["info"] = custom_model.model_dump()
|
||||
|
||||
action_ids = []
|
||||
if "info" in model and "meta" in model["info"]:
|
||||
action_ids.extend(model["info"]["meta"].get("actionIds", []))
|
||||
|
||||
model["action_ids"] = action_ids
|
||||
else:
|
||||
owned_by = "openai"
|
||||
pipe = None
|
||||
action_ids = []
|
||||
|
||||
for model in models:
|
||||
if (
|
||||
@@ -983,6 +985,9 @@ async def get_all_models():
|
||||
owned_by = model["owned_by"]
|
||||
if "pipe" in model:
|
||||
pipe = model["pipe"]
|
||||
|
||||
if "info" in model and "meta" in model["info"]:
|
||||
action_ids.extend(model["info"]["meta"].get("actionIds", []))
|
||||
break
|
||||
|
||||
models.append(
|
||||
@@ -995,9 +1000,59 @@ async def get_all_models():
|
||||
"info": custom_model.model_dump(),
|
||||
"preset": True,
|
||||
**({"pipe": pipe} if pipe is not None else {}),
|
||||
"action_ids": action_ids,
|
||||
}
|
||||
)
|
||||
|
||||
for model in models:
|
||||
action_ids = []
|
||||
if "action_ids" in model:
|
||||
action_ids = model["action_ids"]
|
||||
del model["action_ids"]
|
||||
|
||||
action_ids = action_ids + global_action_ids
|
||||
action_ids = list(set(action_ids))
|
||||
action_ids = [
|
||||
action_id for action_id in action_ids if action_id in enabled_action_ids
|
||||
]
|
||||
|
||||
model["actions"] = []
|
||||
for action_id in action_ids:
|
||||
action = Functions.get_function_by_id(action_id)
|
||||
|
||||
if action_id in webui_app.state.FUNCTIONS:
|
||||
function_module = webui_app.state.FUNCTIONS[action_id]
|
||||
else:
|
||||
function_module, _, _ = load_function_module_by_id(action_id)
|
||||
webui_app.state.FUNCTIONS[action_id] = function_module
|
||||
|
||||
if hasattr(function_module, "actions"):
|
||||
actions = function_module.actions
|
||||
model["actions"].extend(
|
||||
[
|
||||
{
|
||||
"id": f"{action_id}.{_action['id']}",
|
||||
"name": _action.get(
|
||||
"name", f"{action.name} ({_action['id']})"
|
||||
),
|
||||
"description": action.meta.description,
|
||||
"icon_url": _action.get(
|
||||
"icon_url", action.meta.manifest.get("icon_url", None)
|
||||
),
|
||||
}
|
||||
for _action in actions
|
||||
]
|
||||
)
|
||||
else:
|
||||
model["actions"].append(
|
||||
{
|
||||
"id": action_id,
|
||||
"name": action.name,
|
||||
"description": action.meta.description,
|
||||
"icon_url": action.meta.manifest.get("icon_url", None),
|
||||
}
|
||||
)
|
||||
|
||||
app.state.MODELS = {model["id"]: model for model in models}
|
||||
webui_app.state.MODELS = app.state.MODELS
|
||||
|
||||
@@ -1036,13 +1091,24 @@ async def generate_chat_completions(form_data: dict, user=Depends(get_verified_u
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
pipe = model.get("pipe")
|
||||
if pipe:
|
||||
# `task` field is used to determine the type of the request, e.g. `title_generation`, `query_generation`, etc.
|
||||
task = None
|
||||
if "task" in form_data:
|
||||
task = form_data["task"]
|
||||
del form_data["task"]
|
||||
|
||||
if task:
|
||||
if "metadata" in form_data:
|
||||
form_data["metadata"]["task"] = task
|
||||
else:
|
||||
form_data["metadata"] = {"task": task}
|
||||
|
||||
if model.get("pipe"):
|
||||
return await generate_function_chat_completion(form_data, user=user)
|
||||
if model["owned_by"] == "ollama":
|
||||
print("generate_ollama_chat_completion")
|
||||
return await generate_ollama_chat_completion(form_data, user=user)
|
||||
else:
|
||||
return await generate_openai_chat_completion(form_data, user=user)
|
||||
@@ -1101,30 +1167,27 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
||||
status_code=r.status_code,
|
||||
content=res,
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
async def __event_emitter__(event_data):
|
||||
await sio.emit(
|
||||
"chat-events",
|
||||
{
|
||||
"chat_id": data["chat_id"],
|
||||
"message_id": data["id"],
|
||||
"data": event_data,
|
||||
},
|
||||
to=data["session_id"],
|
||||
)
|
||||
__event_emitter__ = get_event_emitter(
|
||||
{
|
||||
"chat_id": data["chat_id"],
|
||||
"message_id": data["id"],
|
||||
"session_id": data["session_id"],
|
||||
}
|
||||
)
|
||||
|
||||
async def __event_call__(event_data):
|
||||
response = await sio.call(
|
||||
"chat-events",
|
||||
{"chat_id": data["chat_id"], "message_id": data["id"], "data": event_data},
|
||||
to=data["session_id"],
|
||||
)
|
||||
return response
|
||||
__event_call__ = get_event_call(
|
||||
{
|
||||
"chat_id": data["chat_id"],
|
||||
"message_id": data["id"],
|
||||
"session_id": data["session_id"],
|
||||
}
|
||||
)
|
||||
|
||||
def get_priority(function_id):
|
||||
function = Functions.get_function_by_id(function_id)
|
||||
@@ -1222,6 +1285,110 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
||||
return data
|
||||
|
||||
|
||||
@app.post("/api/chat/actions/{action_id}")
|
||||
async def chat_action(action_id: str, form_data: dict, user=Depends(get_verified_user)):
|
||||
if "." in action_id:
|
||||
action_id, sub_action_id = action_id.split(".")
|
||||
else:
|
||||
sub_action_id = None
|
||||
|
||||
action = Functions.get_function_by_id(action_id)
|
||||
if not action:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Action not found",
|
||||
)
|
||||
|
||||
data = form_data
|
||||
model_id = data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
__event_emitter__ = get_event_emitter(
|
||||
{
|
||||
"chat_id": data["chat_id"],
|
||||
"message_id": data["id"],
|
||||
"session_id": data["session_id"],
|
||||
}
|
||||
)
|
||||
__event_call__ = get_event_call(
|
||||
{
|
||||
"chat_id": data["chat_id"],
|
||||
"message_id": data["id"],
|
||||
"session_id": data["session_id"],
|
||||
}
|
||||
)
|
||||
|
||||
if action_id in webui_app.state.FUNCTIONS:
|
||||
function_module = webui_app.state.FUNCTIONS[action_id]
|
||||
else:
|
||||
function_module, _, _ = load_function_module_by_id(action_id)
|
||||
webui_app.state.FUNCTIONS[action_id] = function_module
|
||||
|
||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||
valves = Functions.get_function_valves_by_id(action_id)
|
||||
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
||||
|
||||
if hasattr(function_module, "action"):
|
||||
try:
|
||||
action = function_module.action
|
||||
|
||||
# Get the signature of the function
|
||||
sig = inspect.signature(action)
|
||||
params = {"body": data}
|
||||
|
||||
# Extra parameters to be passed to the function
|
||||
extra_params = {
|
||||
"__model__": model,
|
||||
"__id__": sub_action_id if sub_action_id is not None else action_id,
|
||||
"__event_emitter__": __event_emitter__,
|
||||
"__event_call__": __event_call__,
|
||||
}
|
||||
|
||||
# Add extra params in contained in function signature
|
||||
for key, value in extra_params.items():
|
||||
if key in sig.parameters:
|
||||
params[key] = value
|
||||
|
||||
if "__user__" in sig.parameters:
|
||||
__user__ = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
try:
|
||||
if hasattr(function_module, "UserValves"):
|
||||
__user__["valves"] = function_module.UserValves(
|
||||
**Functions.get_user_valves_by_id_and_user_id(
|
||||
action_id, user.id
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
params = {**params, "__user__": __user__}
|
||||
|
||||
if inspect.iscoroutinefunction(action):
|
||||
data = await action(**params)
|
||||
else:
|
||||
data = action(**params)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
##################################
|
||||
#
|
||||
# Task Endpoints
|
||||
@@ -1314,7 +1481,7 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
"stream": False,
|
||||
"max_tokens": 50,
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"task": TASKS.TITLE_GENERATION,
|
||||
"task": str(TASKS.TITLE_GENERATION),
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
@@ -1367,7 +1534,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 30,
|
||||
"task": TASKS.QUERY_GENERATION,
|
||||
"task": str(TASKS.QUERY_GENERATION),
|
||||
}
|
||||
|
||||
print(payload)
|
||||
@@ -1424,7 +1591,7 @@ Message: """{{prompt}}"""
|
||||
"stream": False,
|
||||
"max_tokens": 4,
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"task": TASKS.EMOJI_GENERATION,
|
||||
"task": str(TASKS.EMOJI_GENERATION),
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
@@ -1496,7 +1663,7 @@ async def get_pipelines_list(user=Depends(get_admin_user)):
|
||||
urlIdxs = [
|
||||
idx
|
||||
for idx, response in enumerate(responses)
|
||||
if response != None and "pipelines" in response
|
||||
if response is not None and "pipelines" in response
|
||||
]
|
||||
|
||||
return {
|
||||
@@ -1557,7 +1724,7 @@ async def upload_pipeline(
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1577,7 +1744,6 @@ class AddPipelineForm(BaseModel):
|
||||
|
||||
@app.post("/api/pipelines/add")
|
||||
async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)):
|
||||
|
||||
r = None
|
||||
try:
|
||||
urlIdx = form_data.urlIdx
|
||||
@@ -1604,7 +1770,7 @@ async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user))
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1620,7 +1786,6 @@ class DeletePipelineForm(BaseModel):
|
||||
|
||||
@app.delete("/api/pipelines/delete")
|
||||
async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)):
|
||||
|
||||
r = None
|
||||
try:
|
||||
urlIdx = form_data.urlIdx
|
||||
@@ -1647,7 +1812,7 @@ async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1680,7 +1845,7 @@ async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_use
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1695,10 +1860,8 @@ async def get_pipeline_valves(
|
||||
pipeline_id: str,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
models = await get_all_models()
|
||||
r = None
|
||||
try:
|
||||
|
||||
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||
|
||||
@@ -1735,8 +1898,6 @@ async def get_pipeline_valves_spec(
|
||||
pipeline_id: str,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
models = await get_all_models()
|
||||
|
||||
r = None
|
||||
try:
|
||||
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||
@@ -1759,7 +1920,7 @@ async def get_pipeline_valves_spec(
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1775,8 +1936,6 @@ async def update_pipeline_valves(
|
||||
form_data: dict,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
models = await get_all_models()
|
||||
|
||||
r = None
|
||||
try:
|
||||
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||
@@ -1804,7 +1963,7 @@ async def update_pipeline_valves(
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1833,10 +1992,12 @@ async def get_app_config():
|
||||
"auth": WEBUI_AUTH,
|
||||
"auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||
"enable_signup": webui_app.state.config.ENABLE_SIGNUP,
|
||||
"enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
|
||||
"enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"enable_image_generation": images_app.state.config.ENABLED,
|
||||
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
||||
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
||||
},
|
||||
"audio": {
|
||||
"tts": {
|
||||
@@ -1904,7 +2065,7 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||
|
||||
|
||||
@app.get("/api/version")
|
||||
async def get_app_config():
|
||||
async def get_app_version():
|
||||
return {
|
||||
"version": VERSION,
|
||||
}
|
||||
@@ -1927,7 +2088,7 @@ async def get_app_latest_release_version():
|
||||
latest_version = data["tag_name"]
|
||||
|
||||
return {"current": VERSION, "latest": latest_version[1:]}
|
||||
except aiohttp.ClientError as e:
|
||||
except aiohttp.ClientError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
|
||||
@@ -1949,6 +2110,7 @@ for provider_name, provider_config in OAUTH_PROVIDERS.items():
|
||||
client_kwargs={
|
||||
"scope": provider_config["scope"],
|
||||
},
|
||||
redirect_uri=provider_config["redirect_uri"],
|
||||
)
|
||||
|
||||
# SessionMiddleware is used by authlib for oauth
|
||||
@@ -1966,7 +2128,10 @@ if len(OAUTH_PROVIDERS) > 0:
|
||||
async def oauth_login(provider: str, request: Request):
|
||||
if provider not in OAUTH_PROVIDERS:
|
||||
raise HTTPException(404)
|
||||
redirect_uri = request.url_for("oauth_callback", provider=provider)
|
||||
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
|
||||
redirect_uri = OAUTH_PROVIDERS[provider].get("redirect_uri") or request.url_for(
|
||||
"oauth_callback", provider=provider
|
||||
)
|
||||
return await oauth.create_client(provider).authorize_redirect(request, redirect_uri)
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ config = context.config
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
fileConfig(config.config_file_name, disable_existing_loggers=False)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
|
||||
+19
-16
@@ -1,6 +1,6 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.22.0
|
||||
pydantic==2.7.1
|
||||
pydantic==2.8.2
|
||||
python-multipart==0.0.9
|
||||
|
||||
Flask==3.0.3
|
||||
@@ -12,17 +12,18 @@ passlib[bcrypt]==1.7.4
|
||||
|
||||
requests==2.32.3
|
||||
aiohttp==3.9.5
|
||||
sqlalchemy==2.0.30
|
||||
|
||||
sqlalchemy==2.0.31
|
||||
alembic==1.13.2
|
||||
peewee==3.17.6
|
||||
peewee-migrate==1.12.2
|
||||
psycopg2-binary==2.9.9
|
||||
PyMySQL==1.1.1
|
||||
bcrypt==4.1.3
|
||||
SQLAlchemy
|
||||
|
||||
pymongo
|
||||
redis
|
||||
boto3==1.34.110
|
||||
boto3==1.34.153
|
||||
|
||||
argon2-cffi==23.1.0
|
||||
APScheduler==3.10.4
|
||||
@@ -30,29 +31,31 @@ APScheduler==3.10.4
|
||||
# AI libraries
|
||||
openai
|
||||
anthropic
|
||||
google-generativeai==0.5.4
|
||||
google-generativeai==0.7.2
|
||||
tiktoken
|
||||
|
||||
langchain==0.2.6
|
||||
langchain-community==0.2.6
|
||||
langchain==0.2.11
|
||||
langchain-community==0.2.10
|
||||
langchain-chroma==0.1.2
|
||||
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.5.3
|
||||
chromadb==0.5.4
|
||||
sentence-transformers==3.0.1
|
||||
pypdf==4.2.0
|
||||
pypdf==4.3.1
|
||||
docx2txt==0.8
|
||||
python-pptx==0.6.23
|
||||
unstructured==0.14.9
|
||||
python-pptx==1.0.0
|
||||
unstructured==0.15.0
|
||||
Markdown==3.6
|
||||
pypandoc==1.13
|
||||
pandas==2.2.2
|
||||
openpyxl==3.1.5
|
||||
pyxlsb==1.0.10
|
||||
xlrd==2.0.1
|
||||
validators==0.28.1
|
||||
validators==0.33.0
|
||||
psutil
|
||||
|
||||
opencv-python-headless==4.10.0.84
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
rapidocr-onnxruntime==1.3.24
|
||||
|
||||
fpdf2==2.7.9
|
||||
rank-bm25==0.2.2
|
||||
@@ -62,14 +65,14 @@ faster-whisper==1.0.2
|
||||
PyJWT[crypto]==2.8.0
|
||||
authlib==1.3.1
|
||||
|
||||
black==24.4.2
|
||||
langfuse==2.38.0
|
||||
black==24.8.0
|
||||
langfuse==2.39.2
|
||||
youtube-transcript-api==0.6.2
|
||||
pytube==15.0.0
|
||||
|
||||
extract_msg
|
||||
pydub
|
||||
duckduckgo-search~=6.1.7
|
||||
duckduckgo-search~=6.2.1
|
||||
|
||||
## Tests
|
||||
docker~=7.1.0
|
||||
|
||||
+98
-32
@@ -1,44 +1,47 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Optional, List, Tuple
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from utils.task import prompt_template
|
||||
|
||||
def get_last_user_message_item(messages: List[dict]) -> str:
|
||||
|
||||
def get_last_user_message_item(messages: List[dict]) -> Optional[dict]:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "user":
|
||||
return message
|
||||
return None
|
||||
|
||||
|
||||
def get_last_user_message(messages: List[dict]) -> str:
|
||||
message = get_last_user_message_item(messages)
|
||||
|
||||
if message is not None:
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
def get_content_from_message(message: dict) -> Optional[str]:
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
else:
|
||||
return message["content"]
|
||||
return None
|
||||
|
||||
|
||||
def get_last_assistant_message(messages: List[dict]) -> str:
|
||||
def get_last_user_message(messages: List[dict]) -> Optional[str]:
|
||||
message = get_last_user_message_item(messages)
|
||||
if message is None:
|
||||
return None
|
||||
|
||||
return get_content_from_message(message)
|
||||
|
||||
|
||||
def get_last_assistant_message(messages: List[dict]) -> Optional[str]:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "assistant":
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
return message["content"]
|
||||
return get_content_from_message(message)
|
||||
return None
|
||||
|
||||
|
||||
def get_system_message(messages: List[dict]) -> dict:
|
||||
def get_system_message(messages: List[dict]) -> Optional[dict]:
|
||||
for message in messages:
|
||||
if message["role"] == "system":
|
||||
return message
|
||||
@@ -49,10 +52,25 @@ def remove_system_message(messages: List[dict]) -> List[dict]:
|
||||
return [message for message in messages if message["role"] != "system"]
|
||||
|
||||
|
||||
def pop_system_message(messages: List[dict]) -> Tuple[dict, List[dict]]:
|
||||
def pop_system_message(messages: List[dict]) -> Tuple[Optional[dict], List[dict]]:
|
||||
return get_system_message(messages), remove_system_message(messages)
|
||||
|
||||
|
||||
def prepend_to_first_user_message_content(
|
||||
content: str, messages: List[dict]
|
||||
) -> List[dict]:
|
||||
for message in messages:
|
||||
if message["role"] == "user":
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
item["text"] = f"{content}\n{item['text']}"
|
||||
else:
|
||||
message["content"] = f"{content}\n{message['content']}"
|
||||
break
|
||||
return messages
|
||||
|
||||
|
||||
def add_or_update_system_message(content: str, messages: List[dict]):
|
||||
"""
|
||||
Adds a new system message at the beginning of the messages list
|
||||
@@ -72,23 +90,71 @@ def add_or_update_system_message(content: str, messages: List[dict]):
|
||||
return messages
|
||||
|
||||
|
||||
def stream_message_template(model: str, message: str):
|
||||
def openai_chat_message_template(model: str):
|
||||
return {
|
||||
"id": f"{model}-{str(uuid.uuid4())}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {"content": message},
|
||||
"logprobs": None,
|
||||
"finish_reason": None,
|
||||
}
|
||||
],
|
||||
"choices": [{"index": 0, "logprobs": None, "finish_reason": None}],
|
||||
}
|
||||
|
||||
|
||||
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion.chunk"
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
return template
|
||||
|
||||
|
||||
def openai_chat_completion_message_template(model: str, message: str) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion"
|
||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
return template
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||
system = params.get("system", None)
|
||||
if not system:
|
||||
return form_data
|
||||
|
||||
if user:
|
||||
template_params = {
|
||||
"user_name": user.name,
|
||||
"user_location": user.info.get("location") if user.info else None,
|
||||
}
|
||||
else:
|
||||
template_params = {}
|
||||
system = prompt_template(system, **template_params)
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
system, form_data.get("messages", [])
|
||||
)
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body(params: dict, form_data: dict) -> dict:
|
||||
if not params:
|
||||
return form_data
|
||||
|
||||
mappings = {
|
||||
"temperature": float,
|
||||
"top_p": int,
|
||||
"max_tokens": int,
|
||||
"frequency_penalty": int,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
}
|
||||
|
||||
for key, cast_func in mappings.items():
|
||||
if (value := params.get(key)) is not None:
|
||||
form_data[key] = cast_func(value)
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
def get_gravatar_url(email):
|
||||
# Trim leading and trailing whitespace from
|
||||
# an email address and force all characters
|
||||
@@ -159,7 +225,7 @@ def extract_folders_after_data_docs(path):
|
||||
tags = []
|
||||
|
||||
folders = parts[index_docs:-1]
|
||||
for idx, part in enumerate(folders):
|
||||
for idx, _ in enumerate(folders):
|
||||
tags.append("/".join(folders[: idx + 1]))
|
||||
|
||||
return tags
|
||||
@@ -255,11 +321,11 @@ def parse_ollama_modelfile(model_text):
|
||||
value = param_match.group(1)
|
||||
|
||||
try:
|
||||
if param_type == int:
|
||||
if param_type is int:
|
||||
value = int(value)
|
||||
elif param_type == float:
|
||||
elif param_type is float:
|
||||
value = float(value)
|
||||
elif param_type == bool:
|
||||
elif param_type is bool:
|
||||
value = value.lower() == "true"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Optional
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: str = None, user_location: str = None
|
||||
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
|
||||
) -> str:
|
||||
# Get the current date
|
||||
current_date = datetime.now()
|
||||
@@ -83,7 +83,6 @@ def title_generation_template(
|
||||
def search_query_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi import HTTPException, status, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Union, Optional
|
||||
from constants import ERROR_MESSAGES
|
||||
from passlib.context import CryptContext
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
import jwt
|
||||
import uuid
|
||||
import logging
|
||||
@@ -54,7 +51,7 @@ def decode_token(token: str) -> Optional[dict]:
|
||||
try:
|
||||
decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM])
|
||||
return decoded
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@@ -71,7 +68,7 @@ def get_http_authorization_cred(auth_header: str):
|
||||
try:
|
||||
scheme, credentials = auth_header.split(" ")
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
|
||||
|
||||
|
||||
@@ -96,7 +93,7 @@ def get_current_user(
|
||||
|
||||
# auth by jwt token
|
||||
data = decode_token(token)
|
||||
if data != None and "id" in data:
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pellentesque elit eget gravida cum sociis natoque. Morbi tristique senectus et netus et malesuada. Sapien nec sagittis aliquam malesuada bibendum. Amet consectetur adipiscing elit duis tristique sollicitudin. Non pulvinar neque laoreet suspendisse interdum consectetur libero. Arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque. Nec feugiat nisl pretium fusce id velit. Imperdiet proin fermentum leo vel. Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Pellentesque sit amet porttitor eget dolor morbi non arcu risus. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Et ultrices neque ornare aenean euismod.
|
||||
|
||||
Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Viverra nibh cras pulvinar mattis nunc. Lacinia at quis risus sed vulputate. Ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus. Bibendum arcu vitae elementum curabitur vitae nunc. Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet commodo. Velit egestas dui id ornare arcu odio ut. Et malesuada fames ac turpis egestas integer eget aliquet. Lacus suspendisse faucibus interdum posuere lorem ipsum dolor sit. Morbi tristique senectus et netus. Pretium viverra suspendisse potenti nullam ac tortor vitae. Parturient montes nascetur ridiculus mus mauris vitae. Quis viverra nibh cras pulvinar mattis nunc sed blandit libero. Euismod nisi porta lorem mollis aliquam ut porttitor leo. Mauris in aliquam sem fringilla ut morbi. Faucibus pulvinar elementum integer enim neque. Neque sodales ut etiam sit. Consectetur a erat nam at.
|
||||
|
||||
Sed nisi lacus sed viverra tellus in hac habitasse. Proin sagittis nisl rhoncus mattis rhoncus. Risus commodo viverra maecenas accumsan lacus. Morbi quis commodo odio aenean sed adipiscing. Mollis nunc sed id semper risus in. Ultricies mi eget mauris pharetra et ultrices neque. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Eget magna fermentum iaculis eu non diam phasellus. Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Id donec ultrices tincidunt arcu non sodales. Sed cras ornare arcu dui vivamus arcu felis bibendum ut. Urna duis convallis convallis tellus id interdum velit. Rhoncus mattis rhoncus urna neque viverra justo nec. Purus semper eget duis at tellus at urna condimentum. Et odio pellentesque diam volutpat commodo sed egestas. Blandit volutpat maecenas volutpat blandit. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Est ullamcorper eget nulla facilisi etiam dignissim.
|
||||
|
||||
Justo nec ultrices dui sapien eget mi proin sed. Purus gravida quis blandit turpis cursus in hac. Placerat orci nulla pellentesque dignissim enim sit. Morbi tristique senectus et netus et malesuada fames ac. Consequat mauris nunc congue nisi. Eu lobortis elementum nibh tellus molestie nunc non blandit. Viverra justo nec ultrices dui. Morbi non arcu risus quis. Elementum sagittis vitae et leo duis. Lectus mauris ultrices eros in cursus. Neque laoreet suspendisse interdum consectetur.
|
||||
|
||||
Facilisis gravida neque convallis a cras. Nisl rhoncus mattis rhoncus urna neque viverra justo. Faucibus purus in massa tempor. Lacus laoreet non curabitur gravida arcu ac tortor. Tincidunt eget nullam non nisi est sit amet. Ornare lectus sit amet est placerat in egestas. Sollicitudin tempor id eu nisl nunc mi. Scelerisque viverra mauris in aliquam sem fringilla ut. Ullamcorper sit amet risus nullam. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar. Velit euismod in pellentesque massa placerat duis ultricies lacus. Pharetra magna ac placerat vestibulum lectus mauris ultrices eros in. Lorem ipsum dolor sit amet. Sit amet mauris commodo quis imperdiet. Quam pellentesque nec nam aliquam sem et tortor. Amet nisl purus in mollis nunc. Sed risus pretium quam vulputate dignissim suspendisse in est. Nisl condimentum id venenatis a condimentum. Velit euismod in pellentesque massa. Quam id leo in vitae turpis massa sed.
|
||||
@@ -0,0 +1,46 @@
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../support/index.d.ts" />
|
||||
|
||||
describe('Documents', () => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
before(() => {
|
||||
cy.uploadTestDocument(timestamp);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.deleteTestDocument(timestamp);
|
||||
});
|
||||
|
||||
context('Admin', () => {
|
||||
beforeEach(() => {
|
||||
// Login as the admin user
|
||||
cy.loginAdmin();
|
||||
// Visit the home page
|
||||
cy.visit('/workspace/documents');
|
||||
cy.get('button').contains('#cypress-test').click();
|
||||
});
|
||||
|
||||
it('can see documents', () => {
|
||||
cy.get('div').contains(`document-test-initial-${timestamp}.txt`).should('have.length', 1);
|
||||
});
|
||||
|
||||
it('can see edit button', () => {
|
||||
cy.get('div')
|
||||
.contains(`document-test-initial-${timestamp}.txt`)
|
||||
.get("button[aria-label='Edit Doc']")
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('can see delete button', () => {
|
||||
cy.get('div')
|
||||
.contains(`document-test-initial-${timestamp}.txt`)
|
||||
.get("button[aria-label='Delete Doc']")
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('can see upload button', () => {
|
||||
cy.get("button[aria-label='Add Docs']").should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../support/index.d.ts" />
|
||||
|
||||
export const adminUser = {
|
||||
name: 'Admin User',
|
||||
@@ -10,6 +12,9 @@ const login = (email: string, password: string) => {
|
||||
return cy.session(
|
||||
email,
|
||||
() => {
|
||||
// Make sure to test against us english to have stable tests,
|
||||
// regardless on local language preferences
|
||||
localStorage.setItem('locale', 'en-US');
|
||||
// Visit auth page
|
||||
cy.visit('/auth');
|
||||
// Fill out the form
|
||||
@@ -68,6 +73,50 @@ Cypress.Commands.add('register', (name, email, password) => register(name, email
|
||||
Cypress.Commands.add('registerAdmin', () => registerAdmin());
|
||||
Cypress.Commands.add('loginAdmin', () => loginAdmin());
|
||||
|
||||
Cypress.Commands.add('uploadTestDocument', (suffix: any) => {
|
||||
// Login as admin
|
||||
cy.loginAdmin();
|
||||
// upload example document
|
||||
cy.visit('/workspace/documents');
|
||||
// Create a document
|
||||
cy.get("button[aria-label='Add Docs']").click();
|
||||
cy.readFile('cypress/data/example-doc.txt').then((text) => {
|
||||
// select file
|
||||
cy.get('#upload-doc-input').selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from(text + Date.now()),
|
||||
fileName: `document-test-initial-${suffix}.txt`,
|
||||
mimeType: 'text/plain',
|
||||
lastModified: Date.now()
|
||||
},
|
||||
{
|
||||
force: true
|
||||
}
|
||||
);
|
||||
// open tag input
|
||||
cy.get("button[aria-label='Add Tag']").click();
|
||||
cy.get("input[placeholder='Add a tag']").type('cypress-test');
|
||||
cy.get("button[aria-label='Save Tag']").click();
|
||||
|
||||
// submit to upload
|
||||
cy.get("button[type='submit']").click();
|
||||
|
||||
// wait for upload to finish
|
||||
cy.get('button').contains('#cypress-test').should('exist');
|
||||
cy.get('div').contains(`document-test-initial-${suffix}.txt`).should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteTestDocument', (suffix: any) => {
|
||||
cy.loginAdmin();
|
||||
cy.visit('/workspace/documents');
|
||||
// clean up uploaded documents
|
||||
cy.get('div')
|
||||
.contains(`document-test-initial-${suffix}.txt`)
|
||||
.find("button[aria-label='Delete Doc']")
|
||||
.click();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
cy.registerAdmin();
|
||||
});
|
||||
|
||||
Vendored
+2
@@ -7,5 +7,7 @@ declare namespace Cypress {
|
||||
register(name: string, email: string, password: string): Chainable<Element>;
|
||||
registerAdmin(): Chainable<Element>;
|
||||
loginAdmin(): Chainable<Element>;
|
||||
uploadTestDocument(suffix: any): Chainable<Element>;
|
||||
deleteTestDocument(suffix: any): Chainable<Element>;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -11,10 +11,26 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord.
|
||||
We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines:
|
||||
|
||||
1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted.
|
||||
|
||||
2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts.
|
||||
|
||||
3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded.
|
||||
|
||||
4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
|
||||
|
||||
5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security.
|
||||
|
||||
Submissions that do not meet these criteria will be closed, and repeat offenders may face a ban from future submissions. We aim to create a respectful and constructive reporting environment, where high-quality submissions foster better security for everyone.
|
||||
|
||||
## Product Security
|
||||
|
||||
We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques.
|
||||
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
|
||||
|
||||
We are planning on implementing SAST and SCA scans in our project soon.
|
||||
For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s).
|
||||
|
||||
---
|
||||
|
||||
_Last updated on **2024-08-06**._
|
||||
|
||||
Generated
+11
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.12",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
@@ -20,6 +20,7 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
@@ -4820,6 +4821,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
|
||||
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gc-hook": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -60,6 +60,7 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
|
||||
+36
-21
@@ -8,50 +8,61 @@ license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"uvicorn[standard]==0.22.0",
|
||||
"pydantic==2.7.1",
|
||||
"pydantic==2.8.2",
|
||||
"python-multipart==0.0.9",
|
||||
|
||||
"Flask==3.0.3",
|
||||
"Flask-Cors==4.0.1",
|
||||
|
||||
"python-socketio==5.11.2",
|
||||
"python-socketio==5.11.3",
|
||||
"python-jose==3.3.0",
|
||||
"passlib[bcrypt]==1.7.4",
|
||||
|
||||
"requests==2.32.2",
|
||||
"requests==2.32.3",
|
||||
"aiohttp==3.9.5",
|
||||
"peewee==3.17.5",
|
||||
|
||||
"sqlalchemy==2.0.31",
|
||||
"alembic==1.13.2",
|
||||
"peewee==3.17.6",
|
||||
"peewee-migrate==1.12.2",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"PyMySQL==1.1.0",
|
||||
"PyMySQL==1.1.1",
|
||||
"bcrypt==4.1.3",
|
||||
|
||||
"boto3==1.34.110",
|
||||
"pymongo",
|
||||
"redis",
|
||||
"boto3==1.34.153",
|
||||
|
||||
"argon2-cffi==23.1.0",
|
||||
"APScheduler==3.10.4",
|
||||
"google-generativeai==0.5.4",
|
||||
|
||||
"langchain==0.2.0",
|
||||
"langchain-community==0.2.0",
|
||||
"langchain-chroma==0.1.1",
|
||||
"openai",
|
||||
"anthropic",
|
||||
"google-generativeai==0.7.2",
|
||||
"tiktoken",
|
||||
|
||||
"langchain==0.2.11",
|
||||
"langchain-community==0.2.10",
|
||||
"langchain-chroma==0.1.2",
|
||||
|
||||
"fake-useragent==1.5.1",
|
||||
"chromadb==0.5.0",
|
||||
"sentence-transformers==2.7.0",
|
||||
"pypdf==4.2.0",
|
||||
"chromadb==0.5.4",
|
||||
"sentence-transformers==3.0.1",
|
||||
"pypdf==4.3.1",
|
||||
"docx2txt==0.8",
|
||||
"unstructured==0.14.0",
|
||||
"python-pptx==1.0.0",
|
||||
"unstructured==0.15.0",
|
||||
"Markdown==3.6",
|
||||
"pypandoc==1.13",
|
||||
"pandas==2.2.2",
|
||||
"openpyxl==3.1.2",
|
||||
"openpyxl==3.1.5",
|
||||
"pyxlsb==1.0.10",
|
||||
"xlrd==2.0.1",
|
||||
"validators==0.28.1",
|
||||
"validators==0.33.0",
|
||||
"psutil",
|
||||
|
||||
"opencv-python-headless==4.9.0.80",
|
||||
"rapidocr-onnxruntime==1.3.22",
|
||||
"opencv-python-headless==4.10.0.84",
|
||||
"rapidocr-onnxruntime==1.3.24",
|
||||
|
||||
"fpdf2==2.7.9",
|
||||
"rank-bm25==0.2.2",
|
||||
@@ -61,14 +72,18 @@ dependencies = [
|
||||
"PyJWT[crypto]==2.8.0",
|
||||
"authlib==1.3.1",
|
||||
|
||||
"black==24.4.2",
|
||||
"langfuse==2.33.0",
|
||||
"black==24.8.0",
|
||||
"langfuse==2.39.2",
|
||||
"youtube-transcript-api==0.6.2",
|
||||
"pytube==15.0.0",
|
||||
|
||||
"extract_msg",
|
||||
"pydub",
|
||||
"duckduckgo-search~=6.1.5"
|
||||
"duckduckgo-search~=6.2.1",
|
||||
|
||||
"docker~=7.1.0",
|
||||
"pytest~=8.2.2",
|
||||
"pytest-docker~=3.1.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11, < 3.12.0a1"
|
||||
|
||||
+99
-35
@@ -16,10 +16,17 @@ aiohttp==3.9.5
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
alembic==1.13.2
|
||||
# via open-webui
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
anthropic==0.32.0
|
||||
# via open-webui
|
||||
anyio==4.4.0
|
||||
# via anthropic
|
||||
# via httpx
|
||||
# via langfuse
|
||||
# via openai
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
@@ -32,6 +39,7 @@ asgiref==3.8.1
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
# via pytest-docker
|
||||
authlib==1.3.1
|
||||
# via open-webui
|
||||
av==11.0.0
|
||||
@@ -49,13 +57,13 @@ beautifulsoup4==4.12.3
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
black==24.4.2
|
||||
black==24.8.0
|
||||
# via open-webui
|
||||
blinker==1.8.2
|
||||
# via flask
|
||||
boto3==1.34.110
|
||||
boto3==1.34.153
|
||||
# via open-webui
|
||||
botocore==1.34.110
|
||||
botocore==1.34.155
|
||||
# via boto3
|
||||
# via s3transfer
|
||||
build==1.2.1
|
||||
@@ -76,9 +84,9 @@ chardet==5.2.0
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
chroma-hnswlib==0.7.3
|
||||
chroma-hnswlib==0.7.5
|
||||
# via chromadb
|
||||
chromadb==0.5.0
|
||||
chromadb==0.5.4
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
@@ -102,7 +110,6 @@ cryptography==42.0.7
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
dataclasses-json==0.6.6
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -113,11 +120,17 @@ defusedxml==0.7.1
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
distro==1.9.0
|
||||
# via anthropic
|
||||
# via openai
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
# via pymongo
|
||||
docker==7.1.0
|
||||
# via open-webui
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
duckduckgo-search==6.2.6
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
@@ -166,7 +179,7 @@ frozenlist==1.4.1
|
||||
fsspec==2024.3.1
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
google-ai-generativelanguage==0.6.4
|
||||
google-ai-generativelanguage==0.6.6
|
||||
# via google-generativeai
|
||||
google-api-core==2.19.0
|
||||
# via google-ai-generativelanguage
|
||||
@@ -183,7 +196,7 @@ google-auth==2.29.0
|
||||
# via kubernetes
|
||||
google-auth-httplib2==0.2.0
|
||||
# via google-api-python-client
|
||||
google-generativeai==0.5.4
|
||||
google-generativeai==0.7.2
|
||||
# via open-webui
|
||||
googleapis-common-protos==1.63.0
|
||||
# via google-api-core
|
||||
@@ -208,8 +221,11 @@ httplib2==0.22.0
|
||||
httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via langfuse
|
||||
# via openai
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
@@ -229,12 +245,16 @@ importlib-metadata==7.0.0
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via torch
|
||||
jiter==0.5.0
|
||||
# via anthropic
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
# via botocore
|
||||
@@ -249,14 +269,14 @@ jsonpointer==2.4
|
||||
# via jsonpatch
|
||||
kubernetes==29.0.0
|
||||
# via chromadb
|
||||
langchain==0.2.0
|
||||
langchain==0.2.11
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
langchain-chroma==0.1.1
|
||||
langchain-chroma==0.1.2
|
||||
# via open-webui
|
||||
langchain-community==0.2.0
|
||||
langchain-community==0.2.10
|
||||
# via open-webui
|
||||
langchain-core==0.2.1
|
||||
langchain-core==0.2.28
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
@@ -265,22 +285,26 @@ langchain-text-splitters==0.2.0
|
||||
# via langchain
|
||||
langdetect==1.0.9
|
||||
# via unstructured
|
||||
langfuse==2.33.0
|
||||
langfuse==2.39.2
|
||||
# via open-webui
|
||||
langsmith==0.1.57
|
||||
langsmith==0.1.96
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via python-pptx
|
||||
# via unstructured
|
||||
mako==1.3.5
|
||||
# via alembic
|
||||
markdown==3.6
|
||||
# via open-webui
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
# via mako
|
||||
# via werkzeug
|
||||
marshmallow==3.21.2
|
||||
# via dataclasses-json
|
||||
@@ -339,11 +363,13 @@ onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
openai==1.38.0
|
||||
# via open-webui
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
opencv-python-headless==4.10.0.84
|
||||
# via open-webui
|
||||
openpyxl==3.1.2
|
||||
openpyxl==3.1.5
|
||||
# via open-webui
|
||||
opentelemetry-api==1.24.0
|
||||
# via chromadb
|
||||
@@ -380,7 +406,6 @@ ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
overrides==7.7.0
|
||||
@@ -393,6 +418,7 @@ packaging==23.2
|
||||
# via langfuse
|
||||
# via marshmallow
|
||||
# via onnxruntime
|
||||
# via pytest
|
||||
# via transformers
|
||||
# via unstructured-client
|
||||
pandas==2.2.2
|
||||
@@ -403,19 +429,24 @@ pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
peewee==3.17.6
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
peewee-migrate==1.12.2
|
||||
# via open-webui
|
||||
pillow==10.3.0
|
||||
# via fpdf2
|
||||
# via python-pptx
|
||||
# via rapidocr-onnxruntime
|
||||
# via sentence-transformers
|
||||
platformdirs==4.2.1
|
||||
# via black
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
posthog==3.5.0
|
||||
# via chromadb
|
||||
primp==0.5.5
|
||||
# via duckduckgo-search
|
||||
proto-plus==1.23.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
@@ -428,6 +459,9 @@ protobuf==4.25.3
|
||||
# via onnxruntime
|
||||
# via opentelemetry-proto
|
||||
# via proto-plus
|
||||
psutil==6.0.0
|
||||
# via open-webui
|
||||
# via unstructured
|
||||
psycopg2-binary==2.9.9
|
||||
# via open-webui
|
||||
pyasn1==0.6.0
|
||||
@@ -440,7 +474,8 @@ pyclipper==1.3.0.post5
|
||||
# via rapidocr-onnxruntime
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.7.1
|
||||
pydantic==2.8.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
@@ -449,7 +484,8 @@ pydantic==2.7.1
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
pydantic-core==2.18.2
|
||||
# via openai
|
||||
pydantic-core==2.20.1
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
@@ -457,22 +493,27 @@ pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
pymongo==4.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.1
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
pypdf==4.3.1
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
pytest==8.2.2
|
||||
# via open-webui
|
||||
# via pytest-docker
|
||||
pytest-docker==3.1.1
|
||||
# via open-webui
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
@@ -492,7 +533,9 @@ python-magic==0.4.27
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
python-pptx==1.0.0
|
||||
# via open-webui
|
||||
python-socketio==5.11.3
|
||||
# via open-webui
|
||||
pytube==15.0.0
|
||||
# via open-webui
|
||||
@@ -516,15 +559,18 @@ rank-bm25==0.2.2
|
||||
# via open-webui
|
||||
rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
rapidocr-onnxruntime==1.3.24
|
||||
# via open-webui
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
redis==5.0.8
|
||||
# via open-webui
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
requests==2.32.3
|
||||
# via docker
|
||||
# via google-api-core
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
@@ -534,6 +580,7 @@ requests==2.32.2
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -556,12 +603,12 @@ scikit-learn==1.4.2
|
||||
scipy==1.13.0
|
||||
# via scikit-learn
|
||||
# via sentence-transformers
|
||||
sentence-transformers==2.7.0
|
||||
sentence-transformers==3.0.1
|
||||
# via open-webui
|
||||
setuptools==69.5.1
|
||||
# via ctranslate2
|
||||
# via opentelemetry-instrumentation
|
||||
shapely==2.0.4
|
||||
shapely==2.0.5
|
||||
# via rapidocr-onnxruntime
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
@@ -577,13 +624,17 @@ six==1.16.0
|
||||
# via rapidocr-onnxruntime
|
||||
# via unstructured-client
|
||||
sniffio==1.3.1
|
||||
# via anthropic
|
||||
# via anyio
|
||||
# via httpx
|
||||
# via openai
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
sqlalchemy==2.0.31
|
||||
# via alembic
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
starlette==0.37.2
|
||||
# via fastapi
|
||||
sympy==1.12
|
||||
@@ -598,7 +649,10 @@ tenacity==8.3.0
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tiktoken==0.7.0
|
||||
# via open-webui
|
||||
tokenizers==0.15.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via transformers
|
||||
@@ -609,21 +663,28 @@ tqdm==4.66.4
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via openai
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
# via unstructured
|
||||
transformers==4.39.3
|
||||
# via sentence-transformers
|
||||
typer==0.12.3
|
||||
# via chromadb
|
||||
# via fastapi-cli
|
||||
typing-extensions==4.11.0
|
||||
# via alembic
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via openai
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
# via python-pptx
|
||||
# via sqlalchemy
|
||||
# via torch
|
||||
# via typer
|
||||
@@ -640,7 +701,7 @@ tzlocal==5.2
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
unstructured==0.15.0
|
||||
# via open-webui
|
||||
unstructured-client==0.22.0
|
||||
# via unstructured
|
||||
@@ -648,6 +709,7 @@ uritemplate==4.1.1
|
||||
# via google-api-python-client
|
||||
urllib3==2.2.1
|
||||
# via botocore
|
||||
# via docker
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
@@ -657,7 +719,7 @@ uvicorn==0.22.0
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
validators==0.28.1
|
||||
validators==0.33.0
|
||||
# via open-webui
|
||||
watchfiles==0.21.0
|
||||
# via uvicorn
|
||||
@@ -676,6 +738,8 @@ wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
xlrd==2.0.1
|
||||
# via open-webui
|
||||
xlsxwriter==3.2.0
|
||||
# via python-pptx
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
youtube-transcript-api==0.6.2
|
||||
|
||||
+99
-35
@@ -16,10 +16,17 @@ aiohttp==3.9.5
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
alembic==1.13.2
|
||||
# via open-webui
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
anthropic==0.32.0
|
||||
# via open-webui
|
||||
anyio==4.4.0
|
||||
# via anthropic
|
||||
# via httpx
|
||||
# via langfuse
|
||||
# via openai
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
@@ -32,6 +39,7 @@ asgiref==3.8.1
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
# via pytest-docker
|
||||
authlib==1.3.1
|
||||
# via open-webui
|
||||
av==11.0.0
|
||||
@@ -49,13 +57,13 @@ beautifulsoup4==4.12.3
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
black==24.4.2
|
||||
black==24.8.0
|
||||
# via open-webui
|
||||
blinker==1.8.2
|
||||
# via flask
|
||||
boto3==1.34.110
|
||||
boto3==1.34.153
|
||||
# via open-webui
|
||||
botocore==1.34.110
|
||||
botocore==1.34.155
|
||||
# via boto3
|
||||
# via s3transfer
|
||||
build==1.2.1
|
||||
@@ -76,9 +84,9 @@ chardet==5.2.0
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
chroma-hnswlib==0.7.3
|
||||
chroma-hnswlib==0.7.5
|
||||
# via chromadb
|
||||
chromadb==0.5.0
|
||||
chromadb==0.5.4
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
@@ -102,7 +110,6 @@ cryptography==42.0.7
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
dataclasses-json==0.6.6
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -113,11 +120,17 @@ defusedxml==0.7.1
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
distro==1.9.0
|
||||
# via anthropic
|
||||
# via openai
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
# via pymongo
|
||||
docker==7.1.0
|
||||
# via open-webui
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
duckduckgo-search==6.2.6
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
@@ -166,7 +179,7 @@ frozenlist==1.4.1
|
||||
fsspec==2024.3.1
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
google-ai-generativelanguage==0.6.4
|
||||
google-ai-generativelanguage==0.6.6
|
||||
# via google-generativeai
|
||||
google-api-core==2.19.0
|
||||
# via google-ai-generativelanguage
|
||||
@@ -183,7 +196,7 @@ google-auth==2.29.0
|
||||
# via kubernetes
|
||||
google-auth-httplib2==0.2.0
|
||||
# via google-api-python-client
|
||||
google-generativeai==0.5.4
|
||||
google-generativeai==0.7.2
|
||||
# via open-webui
|
||||
googleapis-common-protos==1.63.0
|
||||
# via google-api-core
|
||||
@@ -208,8 +221,11 @@ httplib2==0.22.0
|
||||
httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via langfuse
|
||||
# via openai
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
@@ -229,12 +245,16 @@ importlib-metadata==7.0.0
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via torch
|
||||
jiter==0.5.0
|
||||
# via anthropic
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
# via botocore
|
||||
@@ -249,14 +269,14 @@ jsonpointer==2.4
|
||||
# via jsonpatch
|
||||
kubernetes==29.0.0
|
||||
# via chromadb
|
||||
langchain==0.2.0
|
||||
langchain==0.2.11
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
langchain-chroma==0.1.1
|
||||
langchain-chroma==0.1.2
|
||||
# via open-webui
|
||||
langchain-community==0.2.0
|
||||
langchain-community==0.2.10
|
||||
# via open-webui
|
||||
langchain-core==0.2.1
|
||||
langchain-core==0.2.28
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
@@ -265,22 +285,26 @@ langchain-text-splitters==0.2.0
|
||||
# via langchain
|
||||
langdetect==1.0.9
|
||||
# via unstructured
|
||||
langfuse==2.33.0
|
||||
langfuse==2.39.2
|
||||
# via open-webui
|
||||
langsmith==0.1.57
|
||||
langsmith==0.1.96
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via python-pptx
|
||||
# via unstructured
|
||||
mako==1.3.5
|
||||
# via alembic
|
||||
markdown==3.6
|
||||
# via open-webui
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
# via mako
|
||||
# via werkzeug
|
||||
marshmallow==3.21.2
|
||||
# via dataclasses-json
|
||||
@@ -339,11 +363,13 @@ onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
openai==1.38.0
|
||||
# via open-webui
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
opencv-python-headless==4.10.0.84
|
||||
# via open-webui
|
||||
openpyxl==3.1.2
|
||||
openpyxl==3.1.5
|
||||
# via open-webui
|
||||
opentelemetry-api==1.24.0
|
||||
# via chromadb
|
||||
@@ -380,7 +406,6 @@ ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
overrides==7.7.0
|
||||
@@ -393,6 +418,7 @@ packaging==23.2
|
||||
# via langfuse
|
||||
# via marshmallow
|
||||
# via onnxruntime
|
||||
# via pytest
|
||||
# via transformers
|
||||
# via unstructured-client
|
||||
pandas==2.2.2
|
||||
@@ -403,19 +429,24 @@ pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
peewee==3.17.6
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
peewee-migrate==1.12.2
|
||||
# via open-webui
|
||||
pillow==10.3.0
|
||||
# via fpdf2
|
||||
# via python-pptx
|
||||
# via rapidocr-onnxruntime
|
||||
# via sentence-transformers
|
||||
platformdirs==4.2.1
|
||||
# via black
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
posthog==3.5.0
|
||||
# via chromadb
|
||||
primp==0.5.5
|
||||
# via duckduckgo-search
|
||||
proto-plus==1.23.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
@@ -428,6 +459,9 @@ protobuf==4.25.3
|
||||
# via onnxruntime
|
||||
# via opentelemetry-proto
|
||||
# via proto-plus
|
||||
psutil==6.0.0
|
||||
# via open-webui
|
||||
# via unstructured
|
||||
psycopg2-binary==2.9.9
|
||||
# via open-webui
|
||||
pyasn1==0.6.0
|
||||
@@ -440,7 +474,8 @@ pyclipper==1.3.0.post5
|
||||
# via rapidocr-onnxruntime
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.7.1
|
||||
pydantic==2.8.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
@@ -449,7 +484,8 @@ pydantic==2.7.1
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
pydantic-core==2.18.2
|
||||
# via openai
|
||||
pydantic-core==2.20.1
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
@@ -457,22 +493,27 @@ pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
pymongo==4.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.1
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
pypdf==4.3.1
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
pytest==8.2.2
|
||||
# via open-webui
|
||||
# via pytest-docker
|
||||
pytest-docker==3.1.1
|
||||
# via open-webui
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
@@ -492,7 +533,9 @@ python-magic==0.4.27
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
python-pptx==1.0.0
|
||||
# via open-webui
|
||||
python-socketio==5.11.3
|
||||
# via open-webui
|
||||
pytube==15.0.0
|
||||
# via open-webui
|
||||
@@ -516,15 +559,18 @@ rank-bm25==0.2.2
|
||||
# via open-webui
|
||||
rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
rapidocr-onnxruntime==1.3.24
|
||||
# via open-webui
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
redis==5.0.8
|
||||
# via open-webui
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
requests==2.32.3
|
||||
# via docker
|
||||
# via google-api-core
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
@@ -534,6 +580,7 @@ requests==2.32.2
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -556,12 +603,12 @@ scikit-learn==1.4.2
|
||||
scipy==1.13.0
|
||||
# via scikit-learn
|
||||
# via sentence-transformers
|
||||
sentence-transformers==2.7.0
|
||||
sentence-transformers==3.0.1
|
||||
# via open-webui
|
||||
setuptools==69.5.1
|
||||
# via ctranslate2
|
||||
# via opentelemetry-instrumentation
|
||||
shapely==2.0.4
|
||||
shapely==2.0.5
|
||||
# via rapidocr-onnxruntime
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
@@ -577,13 +624,17 @@ six==1.16.0
|
||||
# via rapidocr-onnxruntime
|
||||
# via unstructured-client
|
||||
sniffio==1.3.1
|
||||
# via anthropic
|
||||
# via anyio
|
||||
# via httpx
|
||||
# via openai
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
sqlalchemy==2.0.31
|
||||
# via alembic
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
starlette==0.37.2
|
||||
# via fastapi
|
||||
sympy==1.12
|
||||
@@ -598,7 +649,10 @@ tenacity==8.3.0
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tiktoken==0.7.0
|
||||
# via open-webui
|
||||
tokenizers==0.15.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via transformers
|
||||
@@ -609,21 +663,28 @@ tqdm==4.66.4
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via openai
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
# via unstructured
|
||||
transformers==4.39.3
|
||||
# via sentence-transformers
|
||||
typer==0.12.3
|
||||
# via chromadb
|
||||
# via fastapi-cli
|
||||
typing-extensions==4.11.0
|
||||
# via alembic
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via openai
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
# via python-pptx
|
||||
# via sqlalchemy
|
||||
# via torch
|
||||
# via typer
|
||||
@@ -640,7 +701,7 @@ tzlocal==5.2
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
unstructured==0.15.0
|
||||
# via open-webui
|
||||
unstructured-client==0.22.0
|
||||
# via unstructured
|
||||
@@ -648,6 +709,7 @@ uritemplate==4.1.1
|
||||
# via google-api-python-client
|
||||
urllib3==2.2.1
|
||||
# via botocore
|
||||
# via docker
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
@@ -657,7 +719,7 @@ uvicorn==0.22.0
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
validators==0.28.1
|
||||
validators==0.33.0
|
||||
# via open-webui
|
||||
watchfiles==0.21.0
|
||||
# via uvicorn
|
||||
@@ -676,6 +738,8 @@ wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
xlrd==2.0.1
|
||||
# via open-webui
|
||||
xlsxwriter==3.2.0
|
||||
# via python-pptx
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
youtube-transcript-api==0.6.2
|
||||
|
||||
+13
@@ -154,3 +154,16 @@ input[type='number'] {
|
||||
.tippy-box[data-theme~='dark'] {
|
||||
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
|
||||
}
|
||||
|
||||
.password {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
.codespan {
|
||||
color: #eb5757;
|
||||
border-width: 0px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
||||
}
|
||||
|
||||
+5
-2
@@ -65,6 +65,7 @@
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
||||
@@ -86,7 +87,7 @@
|
||||
height: 6rem;
|
||||
top: 44%;
|
||||
left: 50%;
|
||||
margin-left: -3rem;
|
||||
transform: translateX(-50%);
|
||||
"
|
||||
src="/static/splash.png"
|
||||
/>
|
||||
@@ -98,7 +99,7 @@
|
||||
left: 50%;
|
||||
|
||||
width: 24rem;
|
||||
margin-left: -12rem;
|
||||
transform: translateX(-50%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -154,6 +155,7 @@
|
||||
#splash-screen {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
html.dark #splash-screen {
|
||||
background: #000;
|
||||
}
|
||||
@@ -210,6 +212,7 @@
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-fast {
|
||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@@ -131,3 +131,59 @@ export const synthesizeOpenAISpeech = async (
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getModels = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${AUDIO_API_BASE_URL}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getVoices = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${AUDIO_API_BASE_URL}/voices`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -32,10 +32,15 @@ export const createNewChat = async (token: string, chat: object) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChatList = async (token: string = '') => {
|
||||
export const getChatList = async (token: string = '', page: number | null = null) => {
|
||||
let error = null;
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, {
|
||||
if (page !== null) {
|
||||
searchParams.append('page', `${page}`);
|
||||
}
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -104,6 +104,45 @@ export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
type ChatActionForm = {
|
||||
model: string;
|
||||
messages: string[];
|
||||
chat_id: string;
|
||||
};
|
||||
|
||||
export const chatAction = async (token: string, action_id: string, body: ChatActionForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/chat/actions/${action_id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = err;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getTaskConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ export const resetUploadDir = async (token: string) => {
|
||||
export const resetVectorDB = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reset`, {
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reset/db`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import {
|
||||
getAudioConfig,
|
||||
updateAudioConfig,
|
||||
getModels as _getModels,
|
||||
getVoices as _getVoices
|
||||
} from '$lib/apis/audio';
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
@@ -16,6 +22,7 @@
|
||||
|
||||
let TTS_OPENAI_API_BASE_URL = '';
|
||||
let TTS_OPENAI_API_KEY = '';
|
||||
let TTS_API_KEY = '';
|
||||
let TTS_ENGINE = '';
|
||||
let TTS_MODEL = '';
|
||||
let TTS_VOICE = '';
|
||||
@@ -29,30 +36,41 @@
|
||||
let models = [];
|
||||
let nonLocalVoices = false;
|
||||
|
||||
const getOpenAIVoices = () => {
|
||||
voices = [
|
||||
{ name: 'alloy' },
|
||||
{ name: 'echo' },
|
||||
{ name: 'fable' },
|
||||
{ name: 'onyx' },
|
||||
{ name: 'nova' },
|
||||
{ name: 'shimmer' }
|
||||
];
|
||||
};
|
||||
const getModels = async () => {
|
||||
if (TTS_ENGINE === '') {
|
||||
models = [];
|
||||
} else {
|
||||
const res = await _getModels(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
});
|
||||
|
||||
const getOpenAIModels = () => {
|
||||
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
|
||||
};
|
||||
|
||||
const getWebAPIVoices = () => {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
if (res) {
|
||||
console.log(res);
|
||||
models = res.models;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const getVoices = async () => {
|
||||
if (TTS_ENGINE === '') {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
voices = res.voices;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfigHandler = async () => {
|
||||
@@ -60,6 +78,7 @@
|
||||
tts: {
|
||||
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
|
||||
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
|
||||
API_KEY: TTS_API_KEY,
|
||||
ENGINE: TTS_ENGINE,
|
||||
MODEL: TTS_MODEL,
|
||||
VOICE: TTS_VOICE
|
||||
@@ -86,6 +105,7 @@
|
||||
console.log(res);
|
||||
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
|
||||
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
|
||||
TTS_API_KEY = res.tts.API_KEY;
|
||||
|
||||
TTS_ENGINE = res.tts.ENGINE;
|
||||
TTS_MODEL = res.tts.MODEL;
|
||||
@@ -98,12 +118,8 @@
|
||||
STT_MODEL = res.stt.MODEL;
|
||||
}
|
||||
|
||||
if (TTS_ENGINE === 'openai') {
|
||||
getOpenAIVoices();
|
||||
getOpenAIModels();
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
}
|
||||
await getVoices();
|
||||
await getModels();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -138,7 +154,7 @@
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={STT_OPENAI_API_BASE_URL}
|
||||
required
|
||||
@@ -182,19 +198,23 @@
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={TTS_ENGINE}
|
||||
placeholder="Select a mode"
|
||||
on:change={(e) => {
|
||||
on:change={async (e) => {
|
||||
await updateConfigHandler();
|
||||
await getVoices();
|
||||
await getModels();
|
||||
|
||||
if (e.target.value === 'openai') {
|
||||
getOpenAIVoices();
|
||||
TTS_VOICE = 'alloy';
|
||||
TTS_MODEL = 'tts-1';
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
TTS_VOICE = '';
|
||||
TTS_MODEL = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">{$i18n.t('Web API')}</option>
|
||||
<option value="openai">{$i18n.t('OpenAI')}</option>
|
||||
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +223,7 @@
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||
required
|
||||
@@ -212,6 +232,17 @@
|
||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'elevenlabs'}
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={TTS_API_KEY}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
@@ -252,7 +283,7 @@
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.name} />
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
@@ -263,15 +294,56 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
|
||||
<datalist id="model-list">
|
||||
<datalist id="tts-model-list">
|
||||
{#each models as model}
|
||||
<option value={model.name} />
|
||||
<option value={model.id} />
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'elevenlabs'}
|
||||
<div class=" flex gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_VOICE}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
|
||||
<datalist id="tts-model-list">
|
||||
{#each models as model}
|
||||
<option value={model.id} />
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
<div class=" ">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
||||
|
||||
<div class=" flex">
|
||||
<div class=" flex gap-1">
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
|
||||
|
||||
@@ -632,7 +632,7 @@
|
||||
</div>
|
||||
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class="flex w-full">
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium min-w-fit">
|
||||
{$i18n.t('Minimum Score')}
|
||||
</div>
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
|
||||
bind:value={AUTOMATIC1111_API_AUTH}
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
user,
|
||||
socket,
|
||||
showCallOverlay,
|
||||
tools
|
||||
tools,
|
||||
currentChatPage
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
@@ -52,7 +53,7 @@
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
import { queryMemory } from '$lib/apis/memories';
|
||||
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
||||
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
|
||||
import { chatCompleted, generateTitle, generateSearchQuery, chatAction } from '$lib/apis';
|
||||
|
||||
import Banner from '../common/Banner.svelte';
|
||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||
@@ -78,6 +79,9 @@
|
||||
let showEventConfirmation = false;
|
||||
let eventConfirmationTitle = '';
|
||||
let eventConfirmationMessage = '';
|
||||
let eventConfirmationInput = false;
|
||||
let eventConfirmationInputPlaceholder = '';
|
||||
let eventConfirmationInputValue = '';
|
||||
let eventCallback = null;
|
||||
|
||||
let showModelSelector = true;
|
||||
@@ -96,6 +100,8 @@
|
||||
|
||||
let title = '';
|
||||
let prompt = '';
|
||||
|
||||
let chatFiles = [];
|
||||
let files = [];
|
||||
let messages = [];
|
||||
let history = {
|
||||
@@ -156,12 +162,28 @@
|
||||
} else {
|
||||
message.citations = [data];
|
||||
}
|
||||
} else if (type === 'message') {
|
||||
message.content += data.content;
|
||||
} else if (type === 'replace') {
|
||||
message.content = data.content;
|
||||
} else if (type === 'confirmation') {
|
||||
eventCallback = cb;
|
||||
|
||||
eventConfirmationInput = false;
|
||||
showEventConfirmation = true;
|
||||
|
||||
eventConfirmationTitle = data.title;
|
||||
eventConfirmationMessage = data.message;
|
||||
} else if (type === 'input') {
|
||||
eventCallback = cb;
|
||||
|
||||
eventConfirmationInput = true;
|
||||
showEventConfirmation = true;
|
||||
|
||||
eventConfirmationTitle = data.title;
|
||||
eventConfirmationMessage = data.message;
|
||||
eventConfirmationInputPlaceholder = data.placeholder;
|
||||
eventConfirmationInputValue = data?.value ?? '';
|
||||
} else {
|
||||
console.log('Unknown message type', data);
|
||||
}
|
||||
@@ -244,6 +266,8 @@
|
||||
messages: {},
|
||||
currentId: null
|
||||
};
|
||||
|
||||
chatFiles = [];
|
||||
params = {};
|
||||
|
||||
if ($page.url.searchParams.get('models')) {
|
||||
@@ -259,6 +283,10 @@
|
||||
|
||||
if ($page.url.searchParams.get('q')) {
|
||||
prompt = $page.url.searchParams.get('q') ?? '';
|
||||
selectedToolIds = ($page.url.searchParams.get('tool_ids') ?? '')
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id);
|
||||
|
||||
if (prompt) {
|
||||
await tick();
|
||||
@@ -315,6 +343,7 @@
|
||||
}
|
||||
|
||||
params = chatContent?.params ?? {};
|
||||
chatFiles = chatContent?.files ?? [];
|
||||
|
||||
autoScroll = true;
|
||||
await tick();
|
||||
@@ -347,7 +376,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const chatCompletedHandler = async (modelId, responseMessageId, messages) => {
|
||||
const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
|
||||
await mermaid.run({
|
||||
querySelector: '.mermaid'
|
||||
});
|
||||
@@ -361,7 +390,7 @@
|
||||
info: m.info ? m.info : undefined,
|
||||
timestamp: m.timestamp
|
||||
})),
|
||||
chat_id: $chatId,
|
||||
chat_id: chatId,
|
||||
session_id: $socket?.id,
|
||||
id: responseMessageId
|
||||
}).catch((error) => {
|
||||
@@ -383,6 +412,69 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ($chatId == chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await updateChatById(localStorage.token, chatId, {
|
||||
models: selectedModels,
|
||||
messages: messages,
|
||||
history: history,
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId) => {
|
||||
const res = await chatAction(localStorage.token, actionId, {
|
||||
model: modelId,
|
||||
messages: messages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
info: m.info ? m.info : undefined,
|
||||
timestamp: m.timestamp
|
||||
})),
|
||||
chat_id: chatId,
|
||||
session_id: $socket?.id,
|
||||
id: responseMessageId
|
||||
}).catch((error) => {
|
||||
toast.error(error);
|
||||
messages.at(-1).error = { content: error };
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res !== null) {
|
||||
// Update chat history with the new messages
|
||||
for (const message of res.messages) {
|
||||
history.messages[message.id] = {
|
||||
...history.messages[message.id],
|
||||
...(history.messages[message.id].content !== message.content
|
||||
? { originalContent: history.messages[message.id].content }
|
||||
: {}),
|
||||
...message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ($chatId == chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await updateChatById(localStorage.token, chatId, {
|
||||
models: selectedModels,
|
||||
messages: messages,
|
||||
history: history,
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
|
||||
@@ -439,6 +531,13 @@
|
||||
}
|
||||
|
||||
const _files = JSON.parse(JSON.stringify(files));
|
||||
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
|
||||
chatFiles = chatFiles.filter(
|
||||
// Remove duplicates
|
||||
(item, index, array) =>
|
||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||
);
|
||||
|
||||
files = [];
|
||||
|
||||
prompt = '';
|
||||
@@ -533,7 +632,9 @@
|
||||
tags: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await chatId.set(chat.id);
|
||||
} else {
|
||||
await chatId.set('local');
|
||||
@@ -609,7 +710,9 @@
|
||||
})
|
||||
);
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
return _responses;
|
||||
};
|
||||
|
||||
@@ -617,6 +720,7 @@
|
||||
let _response = null;
|
||||
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
const userMessage = history.messages[responseMessage.parentId];
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
@@ -679,24 +783,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
let files = [];
|
||||
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||
if (model?.info?.meta?.knowledge ?? false) {
|
||||
files = model.info.meta.knowledge;
|
||||
files.push(...model.info.meta.knowledge);
|
||||
}
|
||||
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
|
||||
|
||||
files = [
|
||||
...files,
|
||||
...(lastUserMessage?.files?.filter((item) =>
|
||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
||||
) ?? []),
|
||||
...(responseMessage?.files?.filter((item) =>
|
||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
||||
) ?? [])
|
||||
].filter(
|
||||
// Remove duplicates
|
||||
(item, index, array) =>
|
||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||
files.push(
|
||||
...(userMessage?.files ?? []).filter((item) =>
|
||||
['doc', 'file', 'collection'].includes(item.type)
|
||||
),
|
||||
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
|
||||
);
|
||||
|
||||
eventTarget.dispatchEvent(
|
||||
@@ -717,8 +812,8 @@
|
||||
...(params ?? $settings.params ?? {}),
|
||||
stop:
|
||||
params?.stop ?? $settings?.params?.stop ?? undefined
|
||||
? (params?.stop ?? $settings.params.stop).map((str) =>
|
||||
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
|
||||
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
: undefined,
|
||||
num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
|
||||
@@ -752,7 +847,7 @@
|
||||
controller.abort('User: Stop Response');
|
||||
} else {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
await chatCompletedHandler(model.id, responseMessageId, messages);
|
||||
await chatCompletedHandler(_chatId, model.id, responseMessageId, messages);
|
||||
}
|
||||
|
||||
_response = responseMessage.content;
|
||||
@@ -860,9 +955,12 @@
|
||||
messages: messages,
|
||||
history: history,
|
||||
models: selectedModels,
|
||||
params: params
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -914,7 +1012,7 @@
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
if (messages.length == 2 && messages.at(1).content !== '') {
|
||||
if (messages.length == 2 && messages.at(1).content !== '' && selectedModels[0] === model.id) {
|
||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||
const _title = await generateChatTitle(userPrompt);
|
||||
await setChatTitle(_chatId, _title);
|
||||
@@ -925,25 +1023,19 @@
|
||||
|
||||
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||
let _response = null;
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
|
||||
let files = [];
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
const userMessage = history.messages[responseMessage.parentId];
|
||||
|
||||
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||
if (model?.info?.meta?.knowledge ?? false) {
|
||||
files = model.info.meta.knowledge;
|
||||
files.push(...model.info.meta.knowledge);
|
||||
}
|
||||
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
|
||||
files = [
|
||||
...files,
|
||||
...(lastUserMessage?.files?.filter((item) =>
|
||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
||||
) ?? []),
|
||||
...(responseMessage?.files?.filter((item) =>
|
||||
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
|
||||
) ?? [])
|
||||
].filter(
|
||||
// Remove duplicates
|
||||
(item, index, array) =>
|
||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||
files.push(
|
||||
...(userMessage?.files ?? []).filter((item) =>
|
||||
['doc', 'file', 'collection'].includes(item.type)
|
||||
),
|
||||
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
|
||||
);
|
||||
|
||||
scrollToBottom();
|
||||
@@ -1022,8 +1114,8 @@
|
||||
seed: params?.seed ?? $settings?.params?.seed ?? undefined,
|
||||
stop:
|
||||
params?.stop ?? $settings?.params?.stop ?? undefined
|
||||
? (params?.stop ?? $settings.params.stop).map((str) =>
|
||||
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
|
||||
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
: undefined,
|
||||
temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
|
||||
@@ -1047,7 +1139,6 @@
|
||||
|
||||
if (res && res.ok && res.body) {
|
||||
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
|
||||
let lastUsage = null;
|
||||
|
||||
for await (const update of textStream) {
|
||||
const { value, done, citations, error, usage } = update;
|
||||
@@ -1064,7 +1155,7 @@
|
||||
} else {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
|
||||
await chatCompletedHandler(model.id, responseMessageId, messages);
|
||||
await chatCompletedHandler(_chatId, model.id, responseMessageId, messages);
|
||||
}
|
||||
|
||||
_response = responseMessage.content;
|
||||
@@ -1073,7 +1164,7 @@
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
lastUsage = usage;
|
||||
responseMessage.info = { ...usage, openai: true };
|
||||
}
|
||||
|
||||
if (citations) {
|
||||
@@ -1127,19 +1218,18 @@
|
||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
responseMessage.info = { ...lastUsage, openai: true };
|
||||
}
|
||||
|
||||
if ($chatId == _chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
models: selectedModels,
|
||||
messages: messages,
|
||||
history: history,
|
||||
params: params
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1175,7 +1265,7 @@
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
if (messages.length == 2) {
|
||||
if (messages.length == 2 && selectedModels[0] === model.id) {
|
||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||
|
||||
const _title = await generateChatTitle(userPrompt);
|
||||
@@ -1304,7 +1394,9 @@
|
||||
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1408,8 +1500,15 @@
|
||||
bind:show={showEventConfirmation}
|
||||
title={eventConfirmationTitle}
|
||||
message={eventConfirmationMessage}
|
||||
input={eventConfirmationInput}
|
||||
inputPlaceholder={eventConfirmationInputPlaceholder}
|
||||
inputValue={eventConfirmationInputValue}
|
||||
on:confirm={(e) => {
|
||||
eventCallback(true);
|
||||
if (e.detail) {
|
||||
eventCallback(e.detail);
|
||||
} else {
|
||||
eventCallback(true);
|
||||
}
|
||||
}}
|
||||
on:cancel={() => {
|
||||
eventCallback(false);
|
||||
@@ -1511,6 +1610,7 @@
|
||||
{sendPrompt}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
{chatActionHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1539,6 +1639,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChatControls bind:show={showControls} bind:params />
|
||||
<ChatControls
|
||||
models={selectedModelIds.reduce((a, e, i, arr) => {
|
||||
const model = $models.find((m) => m.id === e);
|
||||
if (model) {
|
||||
return [...a, model];
|
||||
}
|
||||
return a;
|
||||
}, [])}
|
||||
bind:show={showControls}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
export let show = false;
|
||||
|
||||
export let models = [];
|
||||
|
||||
export let chatId = null;
|
||||
export let chatFiles = [];
|
||||
export let params = {};
|
||||
|
||||
let largeScreen = false;
|
||||
@@ -43,6 +46,8 @@
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
</div>
|
||||
@@ -56,6 +61,8 @@
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
|
||||
import Valves from '$lib/components/chat/Controls/Valves.svelte';
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
|
||||
export let models = [];
|
||||
|
||||
export let chatFiles = [];
|
||||
export let params = {};
|
||||
</script>
|
||||
|
||||
@@ -22,28 +28,59 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" dark:text-gray-200 text-sm font-primary">
|
||||
<div>
|
||||
<div class="mb-1.5 font-medium">System Prompt</div>
|
||||
<div class=" dark:text-gray-200 text-sm font-primary py-0.5">
|
||||
{#if chatFiles.length > 0}
|
||||
<Collapsible title={$i18n.t('Files')} open={true}>
|
||||
<div class="flex flex-col gap-1 mt-1.5" slot="content">
|
||||
{#each chatFiles as file, fileIdx}
|
||||
<FileItem
|
||||
className="w-full"
|
||||
url={`${file?.url}`}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
size={file?.size}
|
||||
dismissible={true}
|
||||
on:dismiss={() => {
|
||||
// Remove the file from the chatFiles array
|
||||
|
||||
<div>
|
||||
<textarea
|
||||
bind:value={params.system}
|
||||
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
|
||||
rows="3"
|
||||
placeholder="Enter system prompt"
|
||||
/>
|
||||
chatFiles.splice(fileIdx, 1);
|
||||
chatFiles = chatFiles;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<hr class="my-2 border-gray-100 dark:border-gray-800" />
|
||||
{/if}
|
||||
|
||||
<Collapsible title={$i18n.t('Valves')}>
|
||||
<div class="text-sm mt-1.5" slot="content">
|
||||
<Valves />
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<hr class="my-2 border-gray-100 dark:border-gray-800" />
|
||||
|
||||
<div>
|
||||
<div class="mb-1.5 font-medium">Advanced Params</div>
|
||||
|
||||
<div>
|
||||
<AdvancedParams bind:params />
|
||||
<Collapsible title={$i18n.t('System Prompt')} open={true}>
|
||||
<div class=" mt-1.5" slot="content">
|
||||
<textarea
|
||||
bind:value={params.system}
|
||||
class="w-full rounded-lg px-3.5 py-2.5 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<hr class="my-2 border-gray-100 dark:border-gray-800" />
|
||||
|
||||
<Collapsible title={$i18n.t('Advanced Params')} open={true}>
|
||||
<div class="text-sm mt-1.5" slot="content">
|
||||
<div>
|
||||
<AdvancedParams bind:params />
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { config, functions, models, settings, tools, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
|
||||
import {
|
||||
getUserValvesSpecById as getToolUserValvesSpecById,
|
||||
getUserValvesById as getToolUserValvesById,
|
||||
updateUserValvesById as updateToolUserValvesById
|
||||
} from '$lib/apis/tools';
|
||||
import {
|
||||
getUserValvesSpecById as getFunctionUserValvesSpecById,
|
||||
getUserValvesById as getFunctionUserValvesById,
|
||||
updateUserValvesById as updateFunctionUserValvesById
|
||||
} from '$lib/apis/functions';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Valves from '$lib/components/common/Valves.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let tab = 'tools';
|
||||
let selectedId = '';
|
||||
|
||||
let loading = false;
|
||||
|
||||
let valvesSpec = null;
|
||||
let valves = {};
|
||||
|
||||
let debounceTimer;
|
||||
|
||||
const debounceSubmitHandler = async () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
// Set a new timer
|
||||
debounceTimer = setTimeout(() => {
|
||||
submitHandler();
|
||||
}, 500); // 0.5 second debounce
|
||||
};
|
||||
|
||||
const getUserValves = async () => {
|
||||
loading = true;
|
||||
if (tab === 'tools') {
|
||||
valves = await getToolUserValvesById(localStorage.token, selectedId);
|
||||
valvesSpec = await getToolUserValvesSpecById(localStorage.token, selectedId);
|
||||
} else if (tab === 'functions') {
|
||||
valves = await getFunctionUserValvesById(localStorage.token, selectedId);
|
||||
valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, selectedId);
|
||||
}
|
||||
|
||||
if (valvesSpec) {
|
||||
// Convert array to string
|
||||
for (const property in valvesSpec.properties) {
|
||||
if (valvesSpec.properties[property]?.type === 'array') {
|
||||
valves[property] = (valves[property] ?? []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (valvesSpec) {
|
||||
// Convert string to array
|
||||
for (const property in valvesSpec.properties) {
|
||||
if (valvesSpec.properties[property]?.type === 'array') {
|
||||
valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (tab === 'tools') {
|
||||
const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Valves updated'));
|
||||
valves = res;
|
||||
}
|
||||
} else if (tab === 'functions') {
|
||||
const res = await updateFunctionUserValvesById(
|
||||
localStorage.token,
|
||||
selectedId,
|
||||
valves
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Valves updated'));
|
||||
valves = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$: if (tab) {
|
||||
selectedId = '';
|
||||
}
|
||||
|
||||
$: if (selectedId) {
|
||||
getUserValves();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="space-y-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
|
||||
bind:value={tab}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="tools" class="bg-gray-100 dark:bg-gray-800">{$i18n.t('Tools')}</option>
|
||||
<option value="functions" class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Functions')}</option
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
|
||||
bind:value={selectedId}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
}}
|
||||
>
|
||||
{#if tab === 'tools'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Select a tool')}</option
|
||||
>
|
||||
|
||||
{#each $tools as tool, toolIdx}
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
||||
{/each}
|
||||
{:else if tab === 'functions'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Select a function')}</option
|
||||
>
|
||||
|
||||
{#each $functions as func, funcIdx}
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedId}
|
||||
<hr class="dark:border-gray-800 my-1 w-full" />
|
||||
|
||||
<div class="my-2 text-xs">
|
||||
{#if !loading}
|
||||
<Valves
|
||||
{valvesSpec}
|
||||
bind:valves
|
||||
on:change={() => {
|
||||
debounceSubmitHandler();
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
@@ -40,6 +40,8 @@
|
||||
import Headphone from '../icons/Headphone.svelte';
|
||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -96,6 +98,7 @@
|
||||
|
||||
const uploadFileHandler = async (file) => {
|
||||
console.log(file);
|
||||
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
@@ -110,40 +113,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the file to the server
|
||||
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
const fileItem = {
|
||||
type: 'file',
|
||||
file: '',
|
||||
id: null,
|
||||
url: '',
|
||||
name: file.name,
|
||||
collection_name: '',
|
||||
status: '',
|
||||
size: file.size,
|
||||
error: ''
|
||||
};
|
||||
files = [...files, fileItem];
|
||||
|
||||
if (uploadedFile) {
|
||||
const fileItem = {
|
||||
type: 'file',
|
||||
file: uploadedFile,
|
||||
id: uploadedFile.id,
|
||||
url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`,
|
||||
name: file.name,
|
||||
collection_name: '',
|
||||
status: 'uploaded',
|
||||
error: ''
|
||||
};
|
||||
files = [...files, fileItem];
|
||||
try {
|
||||
const uploadedFile = await uploadFile(localStorage.token, file);
|
||||
|
||||
// TODO: Check if tools & functions have files support to skip this step to delegate file processing
|
||||
// Default Upload to VectorDB
|
||||
if (
|
||||
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
||||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
||||
) {
|
||||
processFileItem(fileItem);
|
||||
if (uploadedFile) {
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.file = uploadedFile;
|
||||
fileItem.id = uploadedFile.id;
|
||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
||||
|
||||
// TODO: Check if tools & functions have files support to skip this step to delegate file processing
|
||||
// Default Upload to VectorDB
|
||||
if (
|
||||
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
||||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
||||
) {
|
||||
processFileItem(fileItem);
|
||||
} else {
|
||||
toast.error(
|
||||
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
|
||||
file_type: file['type']
|
||||
})
|
||||
);
|
||||
processFileItem(fileItem);
|
||||
}
|
||||
} else {
|
||||
toast.error(
|
||||
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
|
||||
file_type: file['type']
|
||||
})
|
||||
);
|
||||
processFileItem(fileItem);
|
||||
files = files.filter((item) => item.status !== null);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
files = files.filter((item) => item.status !== null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -160,7 +172,6 @@
|
||||
// Remove the failed doc from the files array
|
||||
// files = files.filter((f) => f.id !== fileItem.id);
|
||||
toast.error(e);
|
||||
|
||||
fileItem.status = 'processed';
|
||||
files = files;
|
||||
}
|
||||
@@ -297,24 +308,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if dragged}
|
||||
<div
|
||||
class="fixed {$showSidebar
|
||||
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
|
||||
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
|
||||
id="dropzone"
|
||||
role="region"
|
||||
aria-label="Drag and Drop Container"
|
||||
>
|
||||
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<AddFilesPlaceholder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<FilesOverlay show={dragged} />
|
||||
|
||||
<div class="w-full font-primary">
|
||||
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||
@@ -390,7 +384,7 @@
|
||||
|
||||
{#if atSelectedModel !== undefined}
|
||||
<div
|
||||
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
|
||||
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-50"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
|
||||
<img
|
||||
@@ -500,10 +494,10 @@
|
||||
dir={$settings?.chatDirection ?? 'LTR'}
|
||||
>
|
||||
{#if files.length > 0}
|
||||
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
|
||||
<div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
|
||||
{#each files as file, fileIdx}
|
||||
<div class=" relative group">
|
||||
{#if file.type === 'image'}
|
||||
{#if file.type === 'image'}
|
||||
<div class=" relative group">
|
||||
<div class="relative">
|
||||
<img
|
||||
src={file.url}
|
||||
@@ -534,137 +528,41 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if ['doc', 'file'].includes(file.type)}
|
||||
<div
|
||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
{#if file.status === 'processed'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class=" w-6 h-6 translate-y-[0.5px]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle
|
||||
class="spinner_qM83"
|
||||
cx="4"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle
|
||||
class="spinner_qM83 spinner_ZTLf"
|
||||
cx="20"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if file.type === 'collection'}
|
||||
<div
|
||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
files = files;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-6 h-6"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
|
||||
/>
|
||||
<path
|
||||
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
||||
{file?.title ?? `#${file.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
files = files;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FileItem
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
size={file?.size}
|
||||
status={file.status}
|
||||
dismissible={true}
|
||||
on:dismiss={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
files = files;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { showSidebar } from '$lib/stores';
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
|
||||
export let show = false;
|
||||
let overlayElement = null;
|
||||
|
||||
$: if (show && overlayElement) {
|
||||
document.body.appendChild(overlayElement);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else if (overlayElement) {
|
||||
document.body.removeChild(overlayElement);
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
bind:this={overlayElement}
|
||||
class="fixed {$showSidebar
|
||||
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
|
||||
: 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none"
|
||||
id="dropzone"
|
||||
role="region"
|
||||
aria-label="Drag and Drop Container"
|
||||
>
|
||||
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<AddFilesPlaceholder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -147,8 +147,8 @@
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -159,13 +159,18 @@
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt={model?.name ?? model.id}
|
||||
class="rounded-full size-6 items-center mr-2"
|
||||
/>
|
||||
{model.name}
|
||||
</div>
|
||||
|
||||
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
||||
{doc.title}
|
||||
</div> -->
|
||||
</div> -->
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { chats, config, settings, user as _user, mobile } from '$lib/stores';
|
||||
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
|
||||
import { tick, getContext, onMount } from 'svelte';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -22,6 +22,7 @@
|
||||
export let sendPrompt: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let chatActionHandler: Function;
|
||||
|
||||
export let user = $_user;
|
||||
export let prompt;
|
||||
@@ -89,7 +90,8 @@
|
||||
history: history
|
||||
});
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
};
|
||||
|
||||
const confirmEditResponseMessage = async (messageId, content) => {
|
||||
@@ -145,12 +147,14 @@
|
||||
|
||||
await tick();
|
||||
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
if ($settings?.scrollOnBranchChange ?? true) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const showNextMessage = async (message) => {
|
||||
@@ -194,12 +198,14 @@
|
||||
|
||||
await tick();
|
||||
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
if ($settings?.scrollOnBranchChange ?? true) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async (messageId) => {
|
||||
@@ -335,6 +341,9 @@
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:action={async (e) => {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -54,9 +53,25 @@
|
||||
<div class="text-sm font-medium dark:text-gray-300">
|
||||
{$i18n.t('Source')}
|
||||
</div>
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
{document.source?.name ?? $i18n.t('No source available')}
|
||||
</div>
|
||||
|
||||
{#if document.source?.name}
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
<a
|
||||
href={document?.metadata?.file_id
|
||||
? `/api/v1/files/${document?.metadata?.file_id}/content`
|
||||
: document.source.name.includes('http')
|
||||
? document.source.name
|
||||
: `#`}
|
||||
target="_blank"
|
||||
>
|
||||
{document?.metadata?.name ?? document.source.name}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
{$i18n.t('No source available')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" text-sm font-medium dark:text-gray-300">
|
||||
|
||||
@@ -218,7 +218,7 @@ __builtins__.input = input`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-4" dir="ltr">
|
||||
<div class="my-2" dir="ltr">
|
||||
<div
|
||||
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
|
||||
>
|
||||
|
||||
@@ -118,47 +118,47 @@
|
||||
currentMessageId = message.id;
|
||||
let messageId = message.id;
|
||||
console.log(messageId);
|
||||
|
||||
//
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ResponseMessage
|
||||
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
||||
isLastMessage={true}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
showPreviousMessage={() => showPreviousMessage(model)}
|
||||
showNextMessage={() => showNextMessage(model)}
|
||||
{readOnly}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
await tick();
|
||||
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
{#key history.currentId}
|
||||
<ResponseMessage
|
||||
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
||||
isLastMessage={true}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
showPreviousMessage={() => showPreviousMessage(model)}
|
||||
showNextMessage={() => showNextMessage(model)}
|
||||
{readOnly}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
await tick();
|
||||
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import CodeBlock from './CodeBlock.svelte';
|
||||
|
||||
/* The html content of the tag */
|
||||
export let html; //: string;
|
||||
let parsedHTML = [html];
|
||||
|
||||
export let images;
|
||||
export let codes;
|
||||
|
||||
// all images are in {{IMAGE_0}}, {{IMAGE_1}}.... format
|
||||
// all codes are in {{CODE_0}}, {{CODE_1}}.... format
|
||||
|
||||
const rules = [];
|
||||
rules.forEach((rule) => {
|
||||
parsedHTML = parsedHTML.map((substr) => substr.split(rule.regex)).flat();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each parsedHTML as part}
|
||||
{@const match = rules.find((rule) => rule.regex.test(part))}
|
||||
{#if match}
|
||||
<svelte:component this={match.component} {...match.props}>
|
||||
{@html part}
|
||||
</svelte:component>
|
||||
{:else}
|
||||
{@html part}
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { Token } from 'marked';
|
||||
import { unescapeHtml } from '$lib/utils';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
|
||||
export let id: string;
|
||||
export let tokens: Token[];
|
||||
</script>
|
||||
|
||||
{#each tokens as token}
|
||||
{#if token.type === 'escape'}
|
||||
{unescapeHtml(token.text)}
|
||||
{:else if token.type === 'html'}
|
||||
{@html token.text}
|
||||
{:else if token.type === 'link'}
|
||||
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
|
||||
{:else if token.type === 'image'}
|
||||
<Image src={token.href} alt={token.text} />
|
||||
{:else if token.type === 'strong'}
|
||||
<strong>
|
||||
<svelte:self id={`${id}-strong`} tokens={token.tokens} />
|
||||
</strong>
|
||||
{:else if token.type === 'em'}
|
||||
<em>
|
||||
<svelte:self id={`${id}-em`} tokens={token.tokens} />
|
||||
</em>
|
||||
{:else if token.type === 'codespan'}
|
||||
<code class="codespan">{unescapeHtml(token.text.replaceAll('&', '&'))}</code>
|
||||
{:else if token.type === 'br'}
|
||||
<br />
|
||||
{:else if token.type === 'del'}
|
||||
<del>
|
||||
<svelte:self id={`${id}-del`} tokens={token.tokens} />
|
||||
</del>
|
||||
{:else if token.type === 'text'}
|
||||
{unescapeHtml(token.text)}
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import type { Token } from 'marked';
|
||||
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
||||
|
||||
import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
|
||||
|
||||
export let id: string;
|
||||
export let tokens: Token[];
|
||||
export let top = true;
|
||||
|
||||
let containerElement;
|
||||
|
||||
const headerComponent = (depth: number) => {
|
||||
return 'h' + depth;
|
||||
};
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
// For code blocks with simple backticks
|
||||
renderer.codespan = (code) => {
|
||||
return `<code class="codespan">${code.replaceAll('&', '&')}</code>`;
|
||||
};
|
||||
|
||||
let codes = [];
|
||||
renderer.code = (code, lang) => {
|
||||
codes.push({
|
||||
code: code,
|
||||
lang: lang
|
||||
});
|
||||
codes = codes;
|
||||
const codeId = `${id}-${codes.length}`;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const codeElement = document.getElementById(`code-${codeId}`);
|
||||
if (codeElement) {
|
||||
clearInterval(interval);
|
||||
// If the code is already loaded, don't load it again
|
||||
if (codeElement.innerHTML) {
|
||||
return;
|
||||
}
|
||||
|
||||
new CodeBlock({
|
||||
target: codeElement,
|
||||
props: {
|
||||
id: `${id}-${codes.length}`,
|
||||
lang: lang,
|
||||
code: revertSanitizedResponseContent(code)
|
||||
},
|
||||
hydrate: true,
|
||||
$$inline: true
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return `<div id="code-${id}-${codes.length}"></div>`;
|
||||
};
|
||||
|
||||
let images = [];
|
||||
renderer.image = (href, title, text) => {
|
||||
images.push({
|
||||
href: href,
|
||||
title: title,
|
||||
text: text
|
||||
});
|
||||
images = images;
|
||||
|
||||
const imageId = `${id}-${images.length}`;
|
||||
const interval = setInterval(() => {
|
||||
const imageElement = document.getElementById(`image-${imageId}`);
|
||||
if (imageElement) {
|
||||
clearInterval(interval);
|
||||
|
||||
// If the image is already loaded, don't load it again
|
||||
if (imageElement.innerHTML) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('image', href, text);
|
||||
new Image({
|
||||
target: imageElement,
|
||||
props: {
|
||||
src: href,
|
||||
alt: text
|
||||
},
|
||||
$$inline: true
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return `<div id="image-${id}-${images.length}"></div>`;
|
||||
};
|
||||
|
||||
// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
|
||||
const origLinkRenderer = renderer.link;
|
||||
renderer.link = (href, title, text) => {
|
||||
const html = origLinkRenderer.call(renderer, href, title, text);
|
||||
return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
|
||||
};
|
||||
|
||||
const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
extensions: any;
|
||||
};
|
||||
|
||||
$: if (tokens) {
|
||||
images = [];
|
||||
codes = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={containerElement} class="flex flex-col">
|
||||
{#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)}
|
||||
{#if token.type === 'code'}
|
||||
{#if token.lang === 'mermaid'}
|
||||
<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
|
||||
{:else}
|
||||
<CodeBlock
|
||||
id={`${id}-${tokenIdx}`}
|
||||
lang={token?.lang ?? ''}
|
||||
code={revertSanitizedResponseContent(token?.text ?? '')}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
{@html marked.parse(token.raw, {
|
||||
...defaults,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
renderer
|
||||
})}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,10 @@
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content">
|
||||
<div
|
||||
class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl mb-1.5"
|
||||
slot="content"
|
||||
>
|
||||
{#if status?.query}
|
||||
<a
|
||||
href="https://www.google.com/search?q={status.query}"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { user as _user } from '$lib/stores';
|
||||
import { getFileContentById } from '$lib/apis/files';
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -98,107 +99,14 @@
|
||||
<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
|
||||
{#if file.type === 'image'}
|
||||
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
|
||||
{:else if file.type === 'file'}
|
||||
<button
|
||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (file?.url) {
|
||||
window.open(`${file?.url}/content`, '_blank').focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else if file.type === 'doc'}
|
||||
<button
|
||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (file?.url) {
|
||||
window.open(file?.url, '_blank').focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else if file.type === 'collection'}
|
||||
<button
|
||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
type="button"
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
|
||||
/>
|
||||
<path
|
||||
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
||||
{file?.title ?? `#${file.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<FileItem
|
||||
url={file.url}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
size={file?.size}
|
||||
colorClassName="bg-white dark:bg-gray-850 "
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { marked } from 'marked';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
@@ -33,7 +34,7 @@
|
||||
[key: string]: any;
|
||||
} = [];
|
||||
|
||||
export let className = 'w-[30rem]';
|
||||
export let className = 'w-[32rem]';
|
||||
|
||||
let show = false;
|
||||
|
||||
@@ -43,17 +44,31 @@
|
||||
let searchValue = '';
|
||||
let ollamaVersion = null;
|
||||
|
||||
$: filteredItems = items.filter(
|
||||
(item) =>
|
||||
(searchValue
|
||||
? item.value.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||
item.label.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||
(item.model?.info?.meta?.tags ?? []).some((tag) =>
|
||||
tag.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||
)
|
||||
: true) && !(item.model?.info?.meta?.hidden ?? false)
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
const fuse = new Fuse(
|
||||
items
|
||||
.filter((item) => !item.model?.info?.meta?.hidden)
|
||||
.map((item) => {
|
||||
const _item = {
|
||||
...item,
|
||||
modelName: item.model?.name,
|
||||
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
|
||||
desc: item.model?.info?.meta?.description
|
||||
};
|
||||
return _item;
|
||||
}),
|
||||
{
|
||||
keys: ['value', 'label', 'tags', 'desc', 'modelName']
|
||||
}
|
||||
);
|
||||
|
||||
$: filteredItems = searchValue
|
||||
? fuse.search(searchValue).map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
: items.filter((item) => !item.model?.info?.meta?.hidden);
|
||||
|
||||
const pullModelHandler = async () => {
|
||||
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||
|
||||
@@ -202,6 +217,7 @@
|
||||
bind:open={show}
|
||||
onOpenChange={async () => {
|
||||
searchValue = '';
|
||||
selectedModelIdx = 0;
|
||||
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||
}}
|
||||
closeFocus={false}
|
||||
@@ -238,19 +254,41 @@
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
placeholder={searchPlaceholder}
|
||||
autocomplete="off"
|
||||
on:keydown={(e) => {
|
||||
if (e.code === 'Enter' && filteredItems.length > 0) {
|
||||
value = filteredItems[selectedModelIdx].value;
|
||||
show = false;
|
||||
return; // dont need to scroll on selection
|
||||
} else if (e.code === 'ArrowDown') {
|
||||
selectedModelIdx = Math.min(selectedModelIdx + 1, filteredItems.length - 1);
|
||||
} else if (e.code === 'ArrowUp') {
|
||||
selectedModelIdx = Math.max(selectedModelIdx - 1, 0);
|
||||
} else {
|
||||
// if the user types something, reset to the top selection.
|
||||
selectedModelIdx = 0;
|
||||
}
|
||||
|
||||
const item = document.querySelector(`[data-arrow-selected="true"]`);
|
||||
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800" />
|
||||
{/if}
|
||||
|
||||
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden">
|
||||
{#each filteredItems as item}
|
||||
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
|
||||
{#each filteredItems as item, index}
|
||||
<button
|
||||
aria-label="model-item"
|
||||
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted {index ===
|
||||
selectedModelIdx
|
||||
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
|
||||
: ''}"
|
||||
data-arrow-selected={index === selectedModelIdx}
|
||||
on:click={() => {
|
||||
value = item.value;
|
||||
selectedModelIdx = index;
|
||||
|
||||
show = false;
|
||||
}}
|
||||
@@ -270,7 +308,14 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center min-w-fit">
|
||||
<div class="line-clamp-1">
|
||||
{item.label}
|
||||
<div class="flex items-center min-w-fit">
|
||||
<img
|
||||
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt="Model"
|
||||
class="rounded-full size-5 flex items-center mr-2"
|
||||
/>
|
||||
{item.label}
|
||||
</div>
|
||||
</div>
|
||||
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
|
||||
<div class="flex ml-1 items-center translate-y-[0.5px]">
|
||||
@@ -295,23 +340,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
|
||||
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
|
||||
{#each item.model?.info?.meta.tags as tag}
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- {JSON.stringify(item.info)} -->
|
||||
|
||||
{#if item.model.owned_by === 'openai'}
|
||||
<Tooltip content={`${'External'}`}>
|
||||
<div class="">
|
||||
<div class="translate-y-[1px]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -342,7 +375,7 @@
|
||||
)
|
||||
)}`}
|
||||
>
|
||||
<div class="">
|
||||
<div class=" translate-y-[1px]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -360,6 +393,20 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
|
||||
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
|
||||
{#each item.model?.info?.meta.tags as tag}
|
||||
<Tooltip content={tag.name}>
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -19,6 +20,7 @@
|
||||
mirostat_tau: null,
|
||||
top_k: null,
|
||||
top_p: null,
|
||||
min_p: null,
|
||||
tfs_z: null,
|
||||
num_ctx: null,
|
||||
num_batch: null,
|
||||
@@ -384,6 +386,52 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Min P')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.min_p = (params?.min_p ?? null) === null ? 0.0 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.min_p ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.min_p ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
bind:value={params.min_p}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
bind:value={params.min_p}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
|
||||
@@ -720,10 +768,22 @@
|
||||
{#if (params?.use_mmap ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.use_mmap ?? null) !== null}
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
{params.use_mmap ? 'Enabled' : 'Disabled'}
|
||||
</div>
|
||||
|
||||
<div class=" pr-2">
|
||||
<Switch bind:state={params.use_mmap} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
@@ -740,10 +800,22 @@
|
||||
{#if (params?.use_mlock ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (params?.use_mlock ?? null) !== null}
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
{params.use_mlock ? 'Enabled' : 'Disabled'}
|
||||
</div>
|
||||
|
||||
<div class=" pr-2">
|
||||
<Switch bind:state={params.use_mlock} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { getVoices as _getVoices } from '$lib/apis/audio';
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -20,26 +23,26 @@
|
||||
let voices = [];
|
||||
let voice = '';
|
||||
|
||||
const getOpenAIVoices = () => {
|
||||
voices = [
|
||||
{ name: 'alloy' },
|
||||
{ name: 'echo' },
|
||||
{ name: 'fable' },
|
||||
{ name: 'onyx' },
|
||||
{ name: 'nova' },
|
||||
{ name: 'shimmer' }
|
||||
];
|
||||
};
|
||||
const getVoices = async () => {
|
||||
if ($config.audio.tts.engine === '') {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
|
||||
const getWebAPIVoices = () => {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
});
|
||||
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
if (res) {
|
||||
console.log(res);
|
||||
voices = res.voices;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleResponseAutoPlayback = async () => {
|
||||
@@ -58,14 +61,16 @@
|
||||
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
|
||||
|
||||
STTEngine = $settings?.audio?.stt?.engine ?? '';
|
||||
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
|
||||
|
||||
if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
|
||||
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
|
||||
} else {
|
||||
voice = $config.audio.tts.voice ?? '';
|
||||
}
|
||||
|
||||
nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
|
||||
|
||||
if ($config.audio.tts.engine === 'openai') {
|
||||
getOpenAIVoices();
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
}
|
||||
await getVoices();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -79,6 +84,7 @@
|
||||
},
|
||||
tts: {
|
||||
voice: voice !== '' ? voice : undefined,
|
||||
defaultVoice: $config?.audio?.tts?.voice ?? '',
|
||||
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
|
||||
}
|
||||
}
|
||||
@@ -181,7 +187,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $config.audio.tts.engine === 'openai'}
|
||||
{:else if $config.audio.tts.engine !== ''}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
@@ -195,7 +201,7 @@
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.name} />
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { chats, user, settings } from '$lib/stores';
|
||||
import { chats, user, settings, scrollPaginationEnabled, currentChatPage } from '$lib/stores';
|
||||
|
||||
import {
|
||||
archiveAllChats,
|
||||
@@ -62,7 +62,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
|
||||
const exportChats = async () => {
|
||||
@@ -77,7 +79,10 @@
|
||||
await archiveAllChats(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
|
||||
const deleteAllChatsHandler = async () => {
|
||||
@@ -85,7 +90,10 @@
|
||||
await deleteAllChats(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
|
||||
const toggleSaveChatHistory = async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
let responseAutoCopy = false;
|
||||
let widescreenMode = false;
|
||||
let splitLargeChunks = false;
|
||||
let scrollOnBranchChange = true;
|
||||
let userLocation = false;
|
||||
|
||||
// Interface
|
||||
@@ -39,6 +40,11 @@
|
||||
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||
};
|
||||
|
||||
const togglesScrollOnBranchChange = async () => {
|
||||
scrollOnBranchChange = !scrollOnBranchChange;
|
||||
saveSettings({ scrollOnBranchChange: scrollOnBranchChange });
|
||||
};
|
||||
|
||||
const togglewidescreenMode = async () => {
|
||||
widescreenMode = !widescreenMode;
|
||||
saveSettings({ widescreenMode: widescreenMode });
|
||||
@@ -141,10 +147,14 @@
|
||||
chatBubble = $settings.chatBubble ?? true;
|
||||
widescreenMode = $settings.widescreenMode ?? false;
|
||||
splitLargeChunks = $settings.splitLargeChunks ?? false;
|
||||
scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
|
||||
chatDirection = $settings.chatDirection ?? 'LTR';
|
||||
userLocation = $settings.userLocation ?? false;
|
||||
|
||||
defaultModelId = ($settings?.models ?? ['']).at(0);
|
||||
defaultModelId = $settings?.models?.at(0) ?? '';
|
||||
if ($config?.default_models) {
|
||||
defaultModelId = $config.default_models.split(',')[0];
|
||||
}
|
||||
|
||||
backgroundImageUrl = $settings.backgroundImageUrl ?? null;
|
||||
});
|
||||
@@ -315,6 +325,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Scroll to bottom when switching between branches')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
togglesScrollOnBranchChange();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if scrollOnBranchChange === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { config, functions, models, settings, tools, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
|
||||
import {
|
||||
getUserValvesSpecById as getToolUserValvesSpecById,
|
||||
getUserValvesById as getToolUserValvesById,
|
||||
updateUserValvesById as updateToolUserValvesById
|
||||
} from '$lib/apis/tools';
|
||||
import {
|
||||
getUserValvesSpecById as getFunctionUserValvesSpecById,
|
||||
getUserValvesById as getFunctionUserValvesById,
|
||||
updateUserValvesById as updateFunctionUserValvesById
|
||||
} from '$lib/apis/functions';
|
||||
|
||||
import ManageModal from './Personalization/ManageModal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
let tab = 'tools';
|
||||
let selectedId = '';
|
||||
|
||||
let loading = false;
|
||||
|
||||
let valvesSpec = null;
|
||||
let valves = {};
|
||||
|
||||
const getUserValves = async () => {
|
||||
loading = true;
|
||||
if (tab === 'tools') {
|
||||
valves = await getToolUserValvesById(localStorage.token, selectedId);
|
||||
valvesSpec = await getToolUserValvesSpecById(localStorage.token, selectedId);
|
||||
} else if (tab === 'functions') {
|
||||
valves = await getFunctionUserValvesById(localStorage.token, selectedId);
|
||||
valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, selectedId);
|
||||
}
|
||||
|
||||
if (valvesSpec) {
|
||||
// Convert array to string
|
||||
for (const property in valvesSpec.properties) {
|
||||
if (valvesSpec.properties[property]?.type === 'array') {
|
||||
valves[property] = (valves[property] ?? []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (valvesSpec) {
|
||||
// Convert string to array
|
||||
for (const property in valvesSpec.properties) {
|
||||
if (valvesSpec.properties[property]?.type === 'array') {
|
||||
valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (tab === 'tools') {
|
||||
const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Valves updated'));
|
||||
valves = res;
|
||||
}
|
||||
} else if (tab === 'functions') {
|
||||
const res = await updateFunctionUserValvesById(
|
||||
localStorage.token,
|
||||
selectedId,
|
||||
valves
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Valves updated'));
|
||||
valves = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$: if (tab) {
|
||||
selectedId = '';
|
||||
}
|
||||
|
||||
$: if (selectedId) {
|
||||
getUserValves();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col pr-1.5 overflow-y-scroll max-h-[25rem]">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<Tooltip content="">
|
||||
<div class="text-sm font-medium">
|
||||
{$i18n.t('Manage Valves')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class=" self-end">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded text-xs bg-transparent outline-none text-right"
|
||||
bind:value={tab}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="tools">{$i18n.t('Tools')}</option>
|
||||
<option value="functions">{$i18n.t('Functions')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={selectedId}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
}}
|
||||
>
|
||||
{#if tab === 'tools'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
|
||||
>{$i18n.t('Select a tool')}</option
|
||||
>
|
||||
|
||||
{#each $tools as tool, toolIdx}
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-700">{tool.name}</option>
|
||||
{/each}
|
||||
{:else if tab === 'functions'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
|
||||
>{$i18n.t('Select a function')}</option
|
||||
>
|
||||
|
||||
{#each $functions as func, funcIdx}
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-700">{func.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedId}
|
||||
<hr class="dark:border-gray-800 my-3 w-full" />
|
||||
|
||||
<div>
|
||||
{#if !loading}
|
||||
{#if valvesSpec}
|
||||
{#each Object.keys(valvesSpec.properties) as property, idx}
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{valvesSpec.properties[property].title}
|
||||
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
<span class=" text-gray-500">*required</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
valves[property] =
|
||||
(valves[property] ?? null) === null
|
||||
? valvesSpec.properties[property]?.default ?? ''
|
||||
: null;
|
||||
}}
|
||||
>
|
||||
{#if (valves[property] ?? null) === null}
|
||||
<span class="ml-2 self-center">
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
{$i18n.t('None')}
|
||||
{:else}
|
||||
{$i18n.t('Default')}
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (valves[property] ?? null) !== null}
|
||||
<!-- {valves[property]} -->
|
||||
<div class="flex mt-0.5 mb-1.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
{#if valvesSpec.properties[property]?.enum ?? null}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={valves[property]}
|
||||
>
|
||||
{#each valvesSpec.properties[property].enum as option}
|
||||
<option value={option} selected={option === valves[property]}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'}
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-xs text-gray-500">
|
||||
{valves[property] ? 'Enabled' : 'Disabled'}
|
||||
</div>
|
||||
|
||||
<div class=" pr-2">
|
||||
<Switch bind:state={valves[property]} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={valvesSpec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
|
||||
<div class="text-xs text-gray-500">
|
||||
{valvesSpec.properties[property].description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div>No valves</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -15,7 +15,6 @@
|
||||
import Chats from './Settings/Chats.svelte';
|
||||
import User from '../icons/User.svelte';
|
||||
import Personalization from './Settings/Personalization.svelte';
|
||||
import Valves from './Settings/Valves.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -188,30 +187,6 @@
|
||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'valves'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'valves';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Valves')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'chats'
|
||||
@@ -349,13 +324,6 @@
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'valves'}
|
||||
<Valves
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'chats'}
|
||||
<Chats {saveSettings} />
|
||||
{:else if selectedTab === 'account'}
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
getTagsById,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { tags as _tags, chats, pinnedChats } from '$lib/stores';
|
||||
import {
|
||||
tags as _tags,
|
||||
chats,
|
||||
pinnedChats,
|
||||
currentChatPage,
|
||||
scrollPaginationEnabled
|
||||
} from '$lib/stores';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -46,11 +52,7 @@
|
||||
tags: tags
|
||||
});
|
||||
|
||||
console.log($_tags);
|
||||
await _tags.set(await getAllChatTags(localStorage.token));
|
||||
|
||||
console.log($_tags);
|
||||
|
||||
if ($_tags.map((t) => t.name).includes(tagName)) {
|
||||
if (tagName === 'pinned') {
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
@@ -62,8 +64,11 @@
|
||||
dispatch('close');
|
||||
}
|
||||
} else {
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
// if the tag we deleted is no longer a valid tag, return to main chat list view
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
await scrollPaginationEnabled.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
|
||||
export let open = false;
|
||||
export let className = '';
|
||||
export let title = null;
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
<button on:click={() => (open = !open)}>
|
||||
<slot />
|
||||
</button>
|
||||
{#if title !== null}
|
||||
<button class="w-full" on:click={() => (open = !open)}>
|
||||
<div class=" w-full font-medium transition flex items-center justify-between gap-2">
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if open}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
|
||||
{:else}
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => (open = !open)}>
|
||||
<div
|
||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if open}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
export let cancelLabel = $i18n.t('Cancel');
|
||||
export let confirmLabel = $i18n.t('Confirm');
|
||||
|
||||
export let input = false;
|
||||
export let inputPlaceholder = '';
|
||||
export let inputValue = '';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
|
||||
@@ -73,6 +78,16 @@
|
||||
{:else}
|
||||
{$i18n.t('This action cannot be undone. Do you wish to continue?')}
|
||||
{/if}
|
||||
|
||||
{#if input}
|
||||
<textarea
|
||||
bind:value={inputValue}
|
||||
placeholder={inputPlaceholder ? inputPlaceholder : $i18n.t('Enter your message')}
|
||||
class="w-full mt-2 rounded-lg px-4 py-2 text-sm dark:text-gray-300 dark:bg-gray-900 outline-none resize-none"
|
||||
rows="3"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
@@ -91,7 +106,7 @@
|
||||
class="bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2.5 rounded-lg transition"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
dispatch('confirm');
|
||||
dispatch('confirm', inputValue);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let className = 'w-72';
|
||||
export let colorClassName = 'bg-white dark:bg-gray-800';
|
||||
export let url: string | null = null;
|
||||
|
||||
export let clickHandler: Function | null = null;
|
||||
|
||||
export let dismissible = false;
|
||||
export let status = 'processed';
|
||||
|
||||
export let name: string;
|
||||
export let type: string;
|
||||
export let size: number;
|
||||
|
||||
function formatSize(size) {
|
||||
if (size == null) return 'Unknown size';
|
||||
if (typeof size !== 'number' || size < 0) return 'Invalid size';
|
||||
if (size === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative group">
|
||||
<button
|
||||
class="h-14 {className} flex items-center space-x-3 {colorClassName} rounded-xl border border-gray-100 dark:border-gray-800 text-left"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (clickHandler === null) {
|
||||
if (url) {
|
||||
if (type === 'file') {
|
||||
window.open(`${url}/content`, '_blank').focus();
|
||||
} else {
|
||||
window.open(`${url}`, '_blank').focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clickHandler();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="p-4 py-[1.1rem] bg-red-400 text-white rounded-l-xl">
|
||||
{#if status === 'processed'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class=" size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class=" size-5 translate-y-[0.5px]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5 pl-1.5 pr-4 w-full">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-between text-gray-500 text-xs">
|
||||
{#if type === 'file'}
|
||||
{$i18n.t('File')}
|
||||
{:else if type === 'doc'}
|
||||
{$i18n.t('Document')}
|
||||
{:else if type === 'collection'}
|
||||
{$i18n.t('Collection')}
|
||||
{:else}
|
||||
<span class=" capitalize">{type}</span>
|
||||
{/if}
|
||||
{#if size}
|
||||
<span class="capitalize">{formatSize(size)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if dismissible}
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
dispatch('dismiss');
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -5,19 +5,21 @@
|
||||
export let src = '';
|
||||
export let alt = '';
|
||||
|
||||
let _src = '';
|
||||
export let className = '';
|
||||
|
||||
let _src = '';
|
||||
$: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src;
|
||||
|
||||
let showImagePreview = false;
|
||||
</script>
|
||||
|
||||
<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
|
||||
<button
|
||||
class={className}
|
||||
on:click={() => {
|
||||
console.log('image preview');
|
||||
showImagePreview = true;
|
||||
}}
|
||||
>
|
||||
<img src={_src} {alt} class=" max-h-96 rounded-lg" draggable="false" data-cy="image" />
|
||||
<img src={_src} {alt} class=" rounded-lg cursor-pointer" draggable="false" data-cy="image" />
|
||||
</button>
|
||||
|
||||
<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
let mounted = false;
|
||||
|
||||
let previewElement = null;
|
||||
|
||||
const downloadImage = (url, filename) => {
|
||||
fetch(url)
|
||||
.then((response) => response.blob())
|
||||
@@ -34,14 +36,14 @@
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$: if (mounted) {
|
||||
if (show) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
$: if (show && previewElement) {
|
||||
document.body.appendChild(previewElement);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else if (previewElement) {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.removeChild(previewElement);
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -49,9 +51,10 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
|
||||
bind:this={previewElement}
|
||||
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
|
||||
>
|
||||
<div class=" absolute left-0 w-full flex justify-between">
|
||||
<div class=" absolute left-0 w-full flex justify-between select-none">
|
||||
<div>
|
||||
<button
|
||||
class=" p-5"
|
||||
@@ -95,6 +98,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<img {src} {alt} class=" mx-auto h-full object-scale-down" />
|
||||
<img {src} {alt} class=" mx-auto h-full object-scale-down select-none" draggable="false" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let loaderElement: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
dispatch('visible');
|
||||
// observer.unobserve(loaderElement); // Stop observing until content is loaded
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
root: null, // viewport
|
||||
rootMargin: '0px',
|
||||
threshold: 0.1 // When 10% of the loader is visible
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(loaderElement);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={loaderElement}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let value: string = '';
|
||||
export let placeholder = '';
|
||||
export let required = true;
|
||||
export let readOnly = false;
|
||||
export let outerClassName = 'flex flex-1';
|
||||
export let inputClassName =
|
||||
@@ -12,13 +13,13 @@
|
||||
|
||||
<div class={outerClassName}>
|
||||
<input
|
||||
class={inputClassName}
|
||||
class={`${inputClassName} ${show ? '' : 'password'}`}
|
||||
{placeholder}
|
||||
bind:value
|
||||
required={!readOnly}
|
||||
required={required && !readOnly}
|
||||
disabled={readOnly}
|
||||
autocomplete="off"
|
||||
{...{ type: show ? 'text' : 'password' }}
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
class={showButtonClassName}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
{/each}
|
||||
</datalist>
|
||||
|
||||
<button type="button" on:click={addTagHandler}>
|
||||
<button type="button" aria-label={$i18n.t('Save Tag')} on:click={addTagHandler}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -63,6 +63,7 @@
|
||||
<button
|
||||
class=" cursor-pointer self-center p-0.5 flex h-fit items-center dark:hover:bg-gray-700 rounded-full transition border dark:border-gray-600 border-dashed"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add Tag')}
|
||||
on:click={() => {
|
||||
showTagInput = !showTagInput;
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import { onMount, getContext, createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Switch from './Switch.svelte';
|
||||
|
||||
export let valvesSpec = null;
|
||||
export let valves = {};
|
||||
</script>
|
||||
|
||||
{#if valvesSpec && Object.keys(valvesSpec?.properties ?? {}).length}
|
||||
{#each Object.keys(valvesSpec.properties) as property, idx}
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{valvesSpec.properties[property].title}
|
||||
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
<span class=" text-gray-500">*required</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
valves[property] =
|
||||
(valves[property] ?? null) === null
|
||||
? valvesSpec.properties[property]?.default ?? ''
|
||||
: null;
|
||||
|
||||
dispatch('change');
|
||||
}}
|
||||
>
|
||||
{#if (valves[property] ?? null) === null}
|
||||
<span class="ml-2 self-center">
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
{$i18n.t('None')}
|
||||
{:else}
|
||||
{$i18n.t('Default')}
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (valves[property] ?? null) !== null}
|
||||
<!-- {valves[property]} -->
|
||||
<div class="flex mt-0.5 mb-1.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
{#if valvesSpec.properties[property]?.enum ?? null}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none border border-gray-100 dark:border-gray-800"
|
||||
bind:value={valves[property]}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
>
|
||||
{#each valvesSpec.properties[property].enum as option}
|
||||
<option value={option} selected={option === valves[property]}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'}
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-xs text-gray-500">
|
||||
{valves[property] ? 'Enabled' : 'Disabled'}
|
||||
</div>
|
||||
|
||||
<div class=" pr-2">
|
||||
<Switch
|
||||
bind:state={valves[property]}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none border border-gray-100 dark:border-gray-800"
|
||||
type="text"
|
||||
placeholder={valvesSpec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
required
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
|
||||
<div class="text-xs text-gray-500">
|
||||
{valvesSpec.properties[property].description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-xs">No valves</div>
|
||||
{/if}
|
||||
@@ -16,6 +16,7 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let uploadDoc: Function;
|
||||
let uploadDocInputElement: HTMLInputElement;
|
||||
let inputFiles;
|
||||
let tags = [];
|
||||
@@ -26,32 +27,6 @@
|
||||
content: null
|
||||
};
|
||||
|
||||
const uploadDoc = async (file) => {
|
||||
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await createNewDoc(
|
||||
localStorage.token,
|
||||
res.collection_name,
|
||||
res.filename,
|
||||
transformFileName(res.filename),
|
||||
res.filename,
|
||||
tags.length > 0
|
||||
? {
|
||||
tags: tags
|
||||
}
|
||||
: null
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
await documents.set(await getDocs(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
for (const file of inputFiles) {
|
||||
@@ -60,12 +35,12 @@
|
||||
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
||||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
||||
) {
|
||||
uploadDoc(file);
|
||||
uploadDoc(file, tags);
|
||||
} else {
|
||||
toast.error(
|
||||
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
||||
);
|
||||
uploadDoc(file);
|
||||
uploadDoc(file, tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -11,7 +11,9 @@
|
||||
showSidebar,
|
||||
mobile,
|
||||
showArchivedChats,
|
||||
pinnedChats
|
||||
pinnedChats,
|
||||
scrollPaginationEnabled,
|
||||
currentChatPage
|
||||
} from '$lib/stores';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
|
||||
@@ -34,6 +36,8 @@
|
||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||
import ChatItem from './Sidebar/ChatItem.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
@@ -50,6 +54,10 @@
|
||||
|
||||
let filteredChatList = [];
|
||||
|
||||
// Pagination variables
|
||||
let chatListLoading = false;
|
||||
let allChatsLoaded = false;
|
||||
|
||||
$: filteredChatList = $chats.filter((chat) => {
|
||||
if (search === '') {
|
||||
return true;
|
||||
@@ -70,6 +78,29 @@
|
||||
}
|
||||
});
|
||||
|
||||
const enablePagination = async () => {
|
||||
// Reset pagination variables
|
||||
currentChatPage.set(1);
|
||||
allChatsLoaded = false;
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
// Enable pagination
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
|
||||
const loadMoreChats = async () => {
|
||||
chatListLoading = true;
|
||||
|
||||
currentChatPage.set($currentChatPage + 1);
|
||||
const newChatList = await getChatList(localStorage.token, $currentChatPage);
|
||||
|
||||
// once the bottom of the list has been reached (no results) there is no need to continue querying
|
||||
allChatsLoaded = newChatList.length === 0;
|
||||
await chats.set([...$chats, ...newChatList]);
|
||||
|
||||
chatListLoading = false;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
mobile.subscribe((e) => {
|
||||
if ($showSidebar && e) {
|
||||
@@ -82,9 +113,8 @@
|
||||
});
|
||||
|
||||
showSidebar.set(window.innerWidth > BREAKPOINT);
|
||||
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await enablePagination();
|
||||
|
||||
let touchstart;
|
||||
let touchend;
|
||||
@@ -185,7 +215,11 @@
|
||||
await tick();
|
||||
goto('/');
|
||||
}
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
allChatsLoaded = false;
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}
|
||||
};
|
||||
@@ -238,7 +272,7 @@
|
||||
<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
id="sidebar-new-chat-button"
|
||||
class="flex flex-1 justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
class="flex flex-1 justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
href="/"
|
||||
draggable="false"
|
||||
on:click={async () => {
|
||||
@@ -282,7 +316,7 @@
|
||||
</a>
|
||||
|
||||
<button
|
||||
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showSidebar.set(!$showSidebar);
|
||||
}}
|
||||
@@ -410,7 +444,10 @@
|
||||
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
|
||||
placeholder={$i18n.t('Search')}
|
||||
bind:value={search}
|
||||
on:focus={() => {
|
||||
on:focus={async () => {
|
||||
// TODO: migrate backend for more scalable search mechanism
|
||||
scrollPaginationEnabled.set(false);
|
||||
await chats.set(await getChatList(localStorage.token)); // when searching, load all chats
|
||||
enrichChatsWithContent($chats);
|
||||
}}
|
||||
/>
|
||||
@@ -422,7 +459,7 @@
|
||||
<button
|
||||
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||
on:click={async () => {
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await enablePagination();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('all')}
|
||||
@@ -431,12 +468,17 @@
|
||||
<button
|
||||
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||
on:click={async () => {
|
||||
scrollPaginationEnabled.set(false);
|
||||
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
|
||||
if (chatIds.length === 0) {
|
||||
await tags.set(await getAllChatTags(localStorage.token));
|
||||
chatIds = await getChatList(localStorage.token);
|
||||
|
||||
// if the tag we deleted is no longer a valid tag, return to main chat list view
|
||||
await enablePagination();
|
||||
}
|
||||
await chats.set(chatIds);
|
||||
|
||||
chatListLoading = false;
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
@@ -527,6 +569,21 @@
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if $scrollPaginationEnabled && !allChatsLoaded}
|
||||
<Loader
|
||||
on:visible={(e) => {
|
||||
if (!chatListLoading) {
|
||||
loadMoreChats();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
||||
<Spinner className=" size-4" />
|
||||
<div class=" ">Loading...</div>
|
||||
</div>
|
||||
</Loader>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
getChatListByTagName,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { chatId, chats, mobile, pinnedChats, showSidebar } from '$lib/stores';
|
||||
import { chatId, chats, mobile, pinnedChats, showSidebar, currentChatPage } from '$lib/stores';
|
||||
|
||||
import ChatMenu from './ChatMenu.svelte';
|
||||
import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
|
||||
@@ -40,7 +40,9 @@
|
||||
await updateChatById(localStorage.token, id, {
|
||||
title: _title
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}
|
||||
};
|
||||
@@ -53,14 +55,18 @@
|
||||
|
||||
if (res) {
|
||||
goto(`/c/${res.id}`);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}
|
||||
};
|
||||
|
||||
const archiveChatHandler = async (id) => {
|
||||
await archiveChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
};
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
|
||||
|
||||
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
|
||||
import { uploadDocToVectorDB } from '$lib/apis/rag';
|
||||
import { transformFileName } from '$lib/utils';
|
||||
import { processDocToVectorDB, uploadDocToVectorDB } from '$lib/apis/rag';
|
||||
import { blobToFile, transformFileName } from '$lib/utils';
|
||||
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
|
||||
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import { uploadFile } from '$lib/apis/files';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -49,8 +51,29 @@
|
||||
await documents.set(await getDocs(localStorage.token));
|
||||
};
|
||||
|
||||
const uploadDoc = async (file) => {
|
||||
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
|
||||
const uploadDoc = async (file, tags?: object) => {
|
||||
console.log(file);
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
|
||||
const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (transcribeRes) {
|
||||
console.log(transcribeRes);
|
||||
const blob = new Blob([transcribeRes.text], { type: 'text/plain' });
|
||||
file = blobToFile(blob, `${file.name}.txt`);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the file to the server
|
||||
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
const res = await processDocToVectorDB(localStorage.token, uploadedFile.id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
@@ -61,7 +84,12 @@
|
||||
res.collection_name,
|
||||
res.filename,
|
||||
transformFileName(res.filename),
|
||||
res.filename
|
||||
res.filename,
|
||||
tags?.length > 0
|
||||
? {
|
||||
tags: tags
|
||||
}
|
||||
: null
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
@@ -182,7 +210,7 @@
|
||||
<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
|
||||
{/key}
|
||||
|
||||
<AddDocModal bind:show={showAddDocModal} />
|
||||
<AddDocModal bind:show={showAddDocModal} {uploadDoc} />
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -216,6 +244,7 @@
|
||||
<div>
|
||||
<button
|
||||
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
|
||||
aria-label={$i18n.t('Add Docs')}
|
||||
on:click={() => {
|
||||
showAddDocModal = true;
|
||||
}}
|
||||
@@ -418,6 +447,7 @@
|
||||
<button
|
||||
class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Edit Doc')}
|
||||
on:click={async (e) => {
|
||||
e.stopPropagation();
|
||||
showEditDocModal = !showEditDocModal;
|
||||
@@ -465,6 +495,7 @@
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete Doc')}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -518,7 +549,8 @@
|
||||
doc.collection_name,
|
||||
doc.filename,
|
||||
doc.name,
|
||||
doc.title
|
||||
doc.title,
|
||||
doc.content
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
|
||||
@@ -122,12 +122,17 @@
|
||||
|
||||
if (res) {
|
||||
if (func.is_global) {
|
||||
toast.success($i18n.t('Filter is now globally enabled'));
|
||||
func.type === 'filter'
|
||||
? toast.success($i18n.t('Filter is now globally enabled'))
|
||||
: toast.success($i18n.t('Function is now globally enabled'));
|
||||
} else {
|
||||
toast.success($i18n.t('Filter is now globally disabled'));
|
||||
func.type === 'filter'
|
||||
? toast.success($i18n.t('Filter is now globally disabled'))
|
||||
: toast.success($i18n.t('Function is now globally disabled'));
|
||||
}
|
||||
|
||||
functions.set(await getFunctions(localStorage.token));
|
||||
models.set(await getModels(localStorage.token));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -230,7 +235,7 @@
|
||||
</a>
|
||||
<div class="flex flex-row gap-0.5 self-center">
|
||||
{#if func?.meta?.manifest?.funding_url ?? false}
|
||||
<Tooltip content="Support">
|
||||
<Tooltip content={$i18n.t('Support')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
@@ -244,7 +249,7 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Valves">
|
||||
<Tooltip content={$i18n.t('Valves')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
@@ -294,7 +299,7 @@
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
toggleGlobalHandler={() => {
|
||||
if (func.type === 'filter') {
|
||||
if (['filter', 'action'].includes(func.type)) {
|
||||
toggleGlobalHandler(func);
|
||||
}
|
||||
}}
|
||||
@@ -309,7 +314,7 @@
|
||||
</FunctionMenu>
|
||||
|
||||
<div class=" self-center mx-1">
|
||||
<Tooltip content={func.is_active ? 'Enabled' : 'Disabled'}>
|
||||
<Tooltip content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||
<Switch
|
||||
bind:state={func.is_active}
|
||||
on:change={async (e) => {
|
||||
@@ -489,15 +494,15 @@
|
||||
<div>Please carefully review the following warnings:</div>
|
||||
|
||||
<ul class=" mt-1 list-disc pl-4 text-xs">
|
||||
<li>Functions allow arbitrary code execution.</li>
|
||||
<li>Do not install functions from sources you do not fully trust.</li>
|
||||
<li>{$i18n.t('Functions allow arbitrary code execution.')}</li>
|
||||
<li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
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.
|
||||
{$i18n.t(
|
||||
'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.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -309,7 +309,7 @@ class Pipe:
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Function Name (e.g. My Filter)"
|
||||
placeholder={$i18n.t('Function Name (e.g. My Filter)')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
@@ -317,7 +317,7 @@ class Pipe:
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Function ID (e.g. my_filter)"
|
||||
placeholder={$i18n.t('Function ID (e.g. my_filter)')}
|
||||
bind:value={id}
|
||||
required
|
||||
disabled={edit}
|
||||
@@ -326,7 +326,9 @@ class Pipe:
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Function Description (e.g. A filter to remove profanity from text)"
|
||||
placeholder={$i18n.t(
|
||||
'Function Description (e.g. A filter to remove profanity from text)'
|
||||
)}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
/>
|
||||
@@ -348,10 +350,10 @@ class Pipe:
|
||||
<div class="pb-3 flex justify-between">
|
||||
<div class="flex-1 pr-3">
|
||||
<div class="text-xs text-gray-500 line-clamp-2">
|
||||
<span class=" font-semibold dark:text-gray-200">Warning:</span> Functions allow
|
||||
arbitrary code execution <br />—
|
||||
<span class=" font-semibold dark:text-gray-200">{$i18n.t('Warning:')}</span>
|
||||
{$i18n.t('Functions allow arbitrary code execution')} <br />—
|
||||
<span class=" font-medium dark:text-gray-400"
|
||||
>don't install random functions from sources you don't trust.</span
|
||||
>{$i18n.t(`don't install random functions from sources you don't trust.`)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,18 +378,18 @@ class Pipe:
|
||||
>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
|
||||
<div>Please carefully review the following warnings:</div>
|
||||
<div>{$i18n.t('Please carefully review the following warnings:')}</div>
|
||||
|
||||
<ul class=" mt-1 list-disc pl-4 text-xs">
|
||||
<li>Functions allow arbitrary code execution.</li>
|
||||
<li>Do not install functions from sources you do not fully trust.</li>
|
||||
<li>{$i18n.t('Functions allow arbitrary code execution.')}</li>
|
||||
<li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
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.
|
||||
{$i18n.t(
|
||||
'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.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
{#if func.type === 'filter'}
|
||||
{#if ['filter', 'action'].includes(func.type)}
|
||||
<div
|
||||
class="flex gap-2 justify-between items-center px-3 py-2 text-sm font-medium cursor-pointerrounded-md"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let actions = [];
|
||||
export let selectedActionIds = [];
|
||||
|
||||
let _actions = {};
|
||||
|
||||
onMount(() => {
|
||||
_actions = actions.reduce((acc, action) => {
|
||||
acc[action.id] = {
|
||||
...action,
|
||||
selected: selectedActionIds.includes(action.id)
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex w-full justify-between mb-1">
|
||||
<div class=" self-center text-sm font-semibold">{$i18n.t('Actions')}</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-xs dark:text-gray-500">
|
||||
{$i18n.t('To select actions here, add them to the "Functions" workspace first.')}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if actions.length > 0}
|
||||
<div class=" flex items-center mt-2 flex-wrap">
|
||||
{#each Object.keys(_actions) as action, actionIdx}
|
||||
<div class=" flex items-center gap-2 mr-3">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
state={_actions[action].selected ? 'checked' : 'unchecked'}
|
||||
on:change={(e) => {
|
||||
_actions[action].selected = e.detail === 'checked';
|
||||
selectedActionIds = Object.keys(_actions).filter((t) => _actions[t].selected);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
||||
<Tooltip content={_actions[action].meta.description}>
|
||||
{_actions[action].name}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,7 +221,7 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Valves">
|
||||
<Tooltip content={$i18n.t('Valves')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
@@ -436,18 +436,20 @@
|
||||
>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
|
||||
<div>Please carefully review the following warnings:</div>
|
||||
<div>{$i18n.t('Please carefully review the following warnings:')}</div>
|
||||
|
||||
<ul class=" mt-1 list-disc pl-4 text-xs">
|
||||
<li>Tools have a function calling system that allows arbitrary code execution.</li>
|
||||
<li>Do not install tools from sources you do not fully trust.</li>
|
||||
<li>
|
||||
{$i18n.t('Tools have a function calling system that allows arbitrary code execution')}.
|
||||
</li>
|
||||
<li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
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.
|
||||
{$i18n.t(
|
||||
'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.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -197,7 +197,7 @@ class Tools:
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Toolkit Name (e.g. My ToolKit)"
|
||||
placeholder={$i18n.t('Toolkit Name (e.g. My ToolKit)')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
@@ -205,7 +205,7 @@ class Tools:
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Toolkit ID (e.g. my_toolkit)"
|
||||
placeholder={$i18n.t('Toolkit ID (e.g. my_toolkit)')}
|
||||
bind:value={id}
|
||||
required
|
||||
disabled={edit}
|
||||
@@ -214,7 +214,9 @@ class Tools:
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder="Toolkit Description (e.g. A toolkit for performing various operations)"
|
||||
placeholder={$i18n.t(
|
||||
'Toolkit Description (e.g. A toolkit for performing various operations)'
|
||||
)}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
/>
|
||||
@@ -236,10 +238,10 @@ class Tools:
|
||||
<div class="pb-3 flex justify-between">
|
||||
<div class="flex-1 pr-3">
|
||||
<div class="text-xs text-gray-500 line-clamp-2">
|
||||
<span class=" font-semibold dark:text-gray-200">Warning:</span> Tools are a function
|
||||
calling system with arbitrary code execution <br />—
|
||||
<span class=" font-semibold dark:text-gray-200">{$i18n.t('Warning:')}</span>
|
||||
{$i18n.t('Tools are a function calling system with arbitrary code execution')} <br />—
|
||||
<span class=" font-medium dark:text-gray-400"
|
||||
>don't install random tools from sources you don't trust.</span
|
||||
>{$i18n.t(`don't install random tools from sources you don't trust.`)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,18 +266,20 @@ class Tools:
|
||||
>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
|
||||
<div>Please carefully review the following warnings:</div>
|
||||
<div>{$i18n.t('Please carefully review the following warnings:')}</div>
|
||||
|
||||
<ul class=" mt-1 list-disc pl-4 text-xs">
|
||||
<li>Tools have a function calling system that allows arbitrary code execution.</li>
|
||||
<li>Do not install tools from sources you do not fully trust.</li>
|
||||
<li>
|
||||
{$i18n.t('Tools have a function calling system that allows arbitrary code execution.')}
|
||||
</li>
|
||||
<li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
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.
|
||||
{$i18n.t(
|
||||
'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.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -44,23 +44,25 @@
|
||||
}}
|
||||
>
|
||||
<div class="px-1 text-sm">
|
||||
<div class=" my-2">
|
||||
The developers behind this plugin are passionate volunteers from the community. If you
|
||||
find this plugin helpful, please consider contributing to its development.
|
||||
<div class="my-2">
|
||||
{$i18n.t(
|
||||
'The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class=" my-2">
|
||||
Your entire contribution will go directly to the plugin developer; Open WebUI does not
|
||||
take any percentage. However, the chosen funding platform might have its own fees.
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800 my-3" />
|
||||
|
||||
<div class="my-2">
|
||||
Support this plugin: <a
|
||||
{$i18n.t(
|
||||
'Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr class="dark:border-gray-800 my-3" />
|
||||
<div class="my-2">
|
||||
{$i18n.t('Support this plugin:')}
|
||||
<a
|
||||
href={manifest.funding_url}
|
||||
target="_blank"
|
||||
class=" underline text-blue-400 hover:text-blue-300">{manifest.funding_url}</a
|
||||
class="underline text-blue-400 hover:text-blue-300">{manifest.funding_url}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { getToolValvesById, getToolValvesSpecById, updateToolValvesById } from '$lib/apis/tools';
|
||||
import Spinner from '../../common/Spinner.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Valves from '$lib/components/common/Valves.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -127,91 +128,7 @@
|
||||
>
|
||||
<div class="px-1">
|
||||
{#if !loading}
|
||||
{#if valvesSpec}
|
||||
{#each Object.keys(valvesSpec.properties) as property, idx}
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{valvesSpec.properties[property].title}
|
||||
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
<span class=" text-gray-500">*required</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
valves[property] =
|
||||
(valves[property] ?? null) === null
|
||||
? valvesSpec.properties[property]?.default ?? ''
|
||||
: null;
|
||||
}}
|
||||
>
|
||||
{#if (valves[property] ?? null) === null}
|
||||
<span class="ml-2 self-center">
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
{$i18n.t('None')}
|
||||
{:else}
|
||||
{$i18n.t('Default')}
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (valves[property] ?? null) !== null}
|
||||
<!-- {valves[property]} -->
|
||||
<div class="flex mt-0.5 mb-1.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
{#if valvesSpec.properties[property]?.enum ?? null}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={valves[property]}
|
||||
>
|
||||
{#each valvesSpec.properties[property].enum as option}
|
||||
<option value={option} selected={option === valves[property]}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'}
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-xs text-gray-500">
|
||||
{valves[property] ? 'Enabled' : 'Disabled'}
|
||||
</div>
|
||||
|
||||
<div class=" pr-2">
|
||||
<Switch bind:state={valves[property]} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={valvesSpec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
|
||||
<div class="text-xs text-gray-500">
|
||||
{valvesSpec.properties[property].description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-sm">No valves</div>
|
||||
{/if}
|
||||
<Valves {valvesSpec} bind:valves />
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Account": "الحساب",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "معلومات دقيقة",
|
||||
"Actions": "",
|
||||
"Active Users": "",
|
||||
"Add": "أضف",
|
||||
"Add a model id": "إضافة معرف نموذج",
|
||||
@@ -27,6 +28,7 @@
|
||||
"Add Memory": "إضافة ذكرايات",
|
||||
"Add message": "اضافة رسالة",
|
||||
"Add Model": "اضافة موديل",
|
||||
"Add Tag": "",
|
||||
"Add Tags": "اضافة تاق",
|
||||
"Add User": "اضافة مستخدم",
|
||||
"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين",
|
||||
@@ -168,6 +170,7 @@
|
||||
"Delete chat": "حذف المحادثه",
|
||||
"Delete Chat": "حذف المحادثه.",
|
||||
"Delete chat?": "",
|
||||
"Delete Doc": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "أحذف هذا الرابط",
|
||||
@@ -177,6 +180,7 @@
|
||||
"Deleted {{name}}": "حذف {{name}}",
|
||||
"Description": "وصف",
|
||||
"Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل",
|
||||
"Disabled": "",
|
||||
"Discover a function": "",
|
||||
"Discover a model": "اكتشف نموذجا",
|
||||
"Discover a prompt": "اكتشاف موجه",
|
||||
@@ -188,6 +192,8 @@
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
||||
"Do not install functions from sources you do not fully trust.": "",
|
||||
"Do not install tools from sources you do not fully trust.": "",
|
||||
"Document": "المستند",
|
||||
"Document Settings": "أعدادات المستند",
|
||||
"Documentation": "",
|
||||
@@ -195,6 +201,8 @@
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.",
|
||||
"Don't Allow": "لا تسمح بذلك",
|
||||
"Don't have an account?": "ليس لديك حساب؟",
|
||||
"don't install random functions from sources you don't trust.": "",
|
||||
"don't install random tools from sources you don't trust.": "",
|
||||
"Don't like the style": "لا أحب النمط",
|
||||
"Done": "",
|
||||
"Download": "تحميل",
|
||||
@@ -206,6 +214,7 @@
|
||||
"Edit Doc": "تعديل الملف",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "تعديل المستخدم",
|
||||
"ElevenLabs": "",
|
||||
"Email": "البريد",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "نموذج التضمين",
|
||||
@@ -215,6 +224,7 @@
|
||||
"Enable Community Sharing": "تمكين مشاركة المجتمع",
|
||||
"Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة",
|
||||
"Enable Web Search": "تمكين بحث الويب",
|
||||
"Enabled": "",
|
||||
"Engine": "",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
|
||||
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
|
||||
@@ -236,6 +246,7 @@
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||
"Enter stop sequence": "أدخل تسلسل التوقف",
|
||||
"Enter system prompt": "",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Tika Server URL": "",
|
||||
"Enter Top K": "أدخل Top K",
|
||||
@@ -243,6 +254,7 @@
|
||||
"Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)",
|
||||
"Enter Your Email": "أدخل البريد الاكتروني",
|
||||
"Enter Your Full Name": "أدخل الاسم كامل",
|
||||
"Enter your message": "",
|
||||
"Enter Your Password": "ادخل كلمة المرور",
|
||||
"Enter Your Role": "أدخل الصلاحيات",
|
||||
"Error": "خطأ",
|
||||
@@ -266,6 +278,7 @@
|
||||
"File": "",
|
||||
"File Mode": "وضع الملف",
|
||||
"File not found.": "لم يتم العثور على الملف.",
|
||||
"Files": "",
|
||||
"Filter is now globally disabled": "",
|
||||
"Filter is now globally enabled": "",
|
||||
"Filters": "",
|
||||
@@ -278,14 +291,22 @@
|
||||
"Frequency Penalty": "عقوبة التردد",
|
||||
"Function created successfully": "",
|
||||
"Function deleted successfully": "",
|
||||
"Function Description (e.g. A filter to remove profanity from text)": "",
|
||||
"Function ID (e.g. my_filter)": "",
|
||||
"Function is now globally disabled": "",
|
||||
"Function is now globally enabled": "",
|
||||
"Function Name (e.g. My Filter)": "",
|
||||
"Function updated successfully": "",
|
||||
"Functions": "",
|
||||
"Functions allow arbitrary code execution": "",
|
||||
"Functions allow arbitrary code execution.": "",
|
||||
"Functions imported successfully": "",
|
||||
"General": "عام",
|
||||
"General Settings": "الاعدادات العامة",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "إنشاء استعلام بحث",
|
||||
"Generation Info": "معلومات الجيل",
|
||||
"Get up and running with": "",
|
||||
"Global": "",
|
||||
"Good Response": "استجابة جيدة",
|
||||
"Google PSE API Key": "مفتاح واجهة برمجة تطبيقات PSE من Google",
|
||||
@@ -298,6 +319,7 @@
|
||||
"Hide Model": "",
|
||||
"How can I help you today?": "كيف استطيع مساعدتك اليوم؟",
|
||||
"Hybrid Search": "البحث الهجين",
|
||||
"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.": "",
|
||||
"Image Generation (Experimental)": "توليد الصور (تجريبي)",
|
||||
"Image Generation Engine": "محرك توليد الصور",
|
||||
"Image Settings": "إعدادات الصورة",
|
||||
@@ -328,6 +350,7 @@
|
||||
"Keyboard shortcuts": "اختصارات لوحة المفاتيح",
|
||||
"Knowledge": "",
|
||||
"Language": "اللغة",
|
||||
"large language models, locally.": "",
|
||||
"Last Active": "آخر نشاط",
|
||||
"Last Modified": "",
|
||||
"Light": "فاتح",
|
||||
@@ -341,7 +364,6 @@
|
||||
"Manage Models": "إدارة النماذج",
|
||||
"Manage Ollama Models": "Ollama إدارة موديلات ",
|
||||
"Manage Pipelines": "إدارة خطوط الأنابيب",
|
||||
"Manage Valves": "",
|
||||
"March": "مارس",
|
||||
"Max Tokens (num_predict)": "ماكس توكنز (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
@@ -353,6 +375,7 @@
|
||||
"Memory deleted successfully": "",
|
||||
"Memory updated successfully": "",
|
||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة",
|
||||
"Min P": "",
|
||||
"Minimum Score": "الحد الأدنى من النقاط",
|
||||
"Mirostat": "Mirostat",
|
||||
"Mirostat Eta": "Mirostat Eta",
|
||||
@@ -438,6 +461,7 @@
|
||||
"Pipelines Valves": "صمامات خطوط الأنابيب",
|
||||
"Plain text (.txt)": "نص عادي (.txt)",
|
||||
"Playground": "مكان التجربة",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Positive attitude": "موقف ايجابي",
|
||||
"Previous 30 days": "أخر 30 يوم",
|
||||
"Previous 7 days": "أخر 7 أيام",
|
||||
@@ -475,14 +499,17 @@
|
||||
"Rosé Pine": "Rosé Pine",
|
||||
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
||||
"RTL": "من اليمين إلى اليسار",
|
||||
"Run Llama 2, Code Llama, and other models. Customize and create your own.": "",
|
||||
"Running": "",
|
||||
"Save": "حفظ",
|
||||
"Save & Create": "حفظ وإنشاء",
|
||||
"Save & Update": "حفظ وتحديث",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله",
|
||||
"Scan": "مسح",
|
||||
"Scan complete!": "تم المسح",
|
||||
"Scan for documents from {{path}}": "{{path}} مسح على الملفات من",
|
||||
"Scroll to bottom when switching between branches": "",
|
||||
"Search": "البحث",
|
||||
"Search a model": "البحث عن موديل",
|
||||
"Search Chats": "البحث في الدردشات",
|
||||
@@ -562,6 +589,8 @@
|
||||
"Success": "نجاح",
|
||||
"Successfully updated.": "تم التحديث بنجاح",
|
||||
"Suggested": "مقترحات",
|
||||
"Support": "",
|
||||
"Support this plugin:": "",
|
||||
"System": "النظام",
|
||||
"System Prompt": "محادثة النظام",
|
||||
"Tags": "الوسوم",
|
||||
@@ -574,6 +603,7 @@
|
||||
"Text-to-Speech Engine": "محرك تحويل النص إلى كلام",
|
||||
"Tfs Z": "Tfs Z",
|
||||
"Thanks for your feedback!": "شكرا لملاحظاتك!",
|
||||
"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "يجب أن تكون النتيجة قيمة تتراوح بين 0.0 (0%) و1.0 (100%).",
|
||||
"Theme": "الثيم",
|
||||
"Thinking...": "",
|
||||
@@ -597,6 +627,7 @@
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "الى كتابة المحادثه",
|
||||
"To select actions here, add them to the \"Functions\" workspace first.": "",
|
||||
"To select filters here, add them to the \"Functions\" workspace first.": "",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "اليوم",
|
||||
@@ -607,7 +638,13 @@
|
||||
"Tool deleted successfully": "",
|
||||
"Tool imported successfully": "",
|
||||
"Tool updated successfully": "",
|
||||
"Toolkit Description (e.g. A toolkit for performing various operations)": "",
|
||||
"Toolkit ID (e.g. my_toolkit)": "",
|
||||
"Toolkit Name (e.g. My ToolKit)": "",
|
||||
"Tools": "",
|
||||
"Tools are a function calling system with arbitrary code execution": "",
|
||||
"Tools have a function calling system that allows arbitrary code execution": "",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
|
||||
@@ -649,6 +686,7 @@
|
||||
"Version": "إصدار",
|
||||
"Voice": "",
|
||||
"Warning": "تحذير",
|
||||
"Warning:": "",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "تحذير: إذا قمت بتحديث أو تغيير نموذج التضمين الخاص بك، فستحتاج إلى إعادة استيراد كافة المستندات.",
|
||||
"Web": "Web",
|
||||
"Web API": "",
|
||||
@@ -675,6 +713,7 @@
|
||||
"You're a helpful assistant.": "مساعدك المفيد هنا",
|
||||
"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"Youtube": "Youtube",
|
||||
"Youtube Loader Settings": "Youtube تحميل اعدادات"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Account": "Акаунт",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "Точни информация",
|
||||
"Actions": "",
|
||||
"Active Users": "",
|
||||
"Add": "Добавяне",
|
||||
"Add a model id": "Добавяне на ИД на модел",
|
||||
@@ -27,6 +28,7 @@
|
||||
"Add Memory": "Добавяне на Памет",
|
||||
"Add message": "Добавяне на съобщение",
|
||||
"Add Model": "Добавяне на Модел",
|
||||
"Add Tag": "",
|
||||
"Add Tags": "добавяне на тагове",
|
||||
"Add User": "Добавяне на потребител",
|
||||
"Adjusting these settings will apply changes universally to all users.": "При промяна на тези настройки промените се прилагат за всички потребители.",
|
||||
@@ -168,6 +170,7 @@
|
||||
"Delete chat": "Изтриване на чат",
|
||||
"Delete Chat": "Изтриване на Чат",
|
||||
"Delete chat?": "",
|
||||
"Delete Doc": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "Изтриване на този линк",
|
||||
@@ -177,6 +180,7 @@
|
||||
"Deleted {{name}}": "Изтрито {{име}}",
|
||||
"Description": "Описание",
|
||||
"Didn't fully follow instructions": "Не следва инструкциите",
|
||||
"Disabled": "",
|
||||
"Discover a function": "",
|
||||
"Discover a model": "Открийте модел",
|
||||
"Discover a prompt": "Откриване на промпт",
|
||||
@@ -188,6 +192,8 @@
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
|
||||
"Do not install functions from sources you do not fully trust.": "",
|
||||
"Do not install tools from sources you do not fully trust.": "",
|
||||
"Document": "Документ",
|
||||
"Document Settings": "Документ Настройки",
|
||||
"Documentation": "",
|
||||
@@ -195,6 +201,8 @@
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "няма външни връзки, и вашите данни остават сигурни на локално назначен сървър.",
|
||||
"Don't Allow": "Не Позволявай",
|
||||
"Don't have an account?": "Нямате акаунт?",
|
||||
"don't install random functions from sources you don't trust.": "",
|
||||
"don't install random tools from sources you don't trust.": "",
|
||||
"Don't like the style": "Не харесваш стила?",
|
||||
"Done": "",
|
||||
"Download": "Изтегляне отменено",
|
||||
@@ -206,6 +214,7 @@
|
||||
"Edit Doc": "Редактиране на документ",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "Редактиране на потребител",
|
||||
"ElevenLabs": "",
|
||||
"Email": "Имейл",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "Модел за вграждане",
|
||||
@@ -215,6 +224,7 @@
|
||||
"Enable Community Sharing": "Разрешаване на споделяне в общност",
|
||||
"Enable New Sign Ups": "Вклюване на Нови Потребители",
|
||||
"Enable Web Search": "Разрешаване на търсене в уеб",
|
||||
"Enabled": "",
|
||||
"Engine": "",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Уверете се, че вашият CSV файл включва 4 колони в следния ред: Име, Имейл, Парола, Роля.",
|
||||
"Enter {{role}} message here": "Въведете съобщение за {{role}} тук",
|
||||
@@ -236,6 +246,7 @@
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "Въведете Serpstack API ключ",
|
||||
"Enter stop sequence": "Въведете стоп последователност",
|
||||
"Enter system prompt": "",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Tika Server URL": "",
|
||||
"Enter Top K": "Въведете Top K",
|
||||
@@ -243,6 +254,7 @@
|
||||
"Enter URL (e.g. http://localhost:11434)": "Въведете URL (напр. http://localhost:11434)",
|
||||
"Enter Your Email": "Въведете имейл",
|
||||
"Enter Your Full Name": "Въведете вашето пълно име",
|
||||
"Enter your message": "",
|
||||
"Enter Your Password": "Въведете вашата парола",
|
||||
"Enter Your Role": "Въведете вашата роля",
|
||||
"Error": "Грешка",
|
||||
@@ -266,6 +278,7 @@
|
||||
"File": "",
|
||||
"File Mode": "Файл Мод",
|
||||
"File not found.": "Файл не е намерен.",
|
||||
"Files": "",
|
||||
"Filter is now globally disabled": "",
|
||||
"Filter is now globally enabled": "",
|
||||
"Filters": "",
|
||||
@@ -278,14 +291,22 @@
|
||||
"Frequency Penalty": "Наказание за честота",
|
||||
"Function created successfully": "",
|
||||
"Function deleted successfully": "",
|
||||
"Function Description (e.g. A filter to remove profanity from text)": "",
|
||||
"Function ID (e.g. my_filter)": "",
|
||||
"Function is now globally disabled": "",
|
||||
"Function is now globally enabled": "",
|
||||
"Function Name (e.g. My Filter)": "",
|
||||
"Function updated successfully": "",
|
||||
"Functions": "",
|
||||
"Functions allow arbitrary code execution": "",
|
||||
"Functions allow arbitrary code execution.": "",
|
||||
"Functions imported successfully": "",
|
||||
"General": "Основни",
|
||||
"General Settings": "Основни Настройки",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "Генериране на заявка за търсене",
|
||||
"Generation Info": "Информация за Генерация",
|
||||
"Get up and running with": "",
|
||||
"Global": "",
|
||||
"Good Response": "Добра отговор",
|
||||
"Google PSE API Key": "Google PSE API ключ",
|
||||
@@ -298,6 +319,7 @@
|
||||
"Hide Model": "",
|
||||
"How can I help you today?": "Как мога да ви помогна днес?",
|
||||
"Hybrid Search": "Hybrid Search",
|
||||
"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.": "",
|
||||
"Image Generation (Experimental)": "Генерация на изображения (Експериментално)",
|
||||
"Image Generation Engine": "Двигател за генериране на изображения",
|
||||
"Image Settings": "Настройки на изображения",
|
||||
@@ -328,6 +350,7 @@
|
||||
"Keyboard shortcuts": "Клавиши за бърз достъп",
|
||||
"Knowledge": "",
|
||||
"Language": "Език",
|
||||
"large language models, locally.": "",
|
||||
"Last Active": "Последни активни",
|
||||
"Last Modified": "",
|
||||
"Light": "Светъл",
|
||||
@@ -341,7 +364,6 @@
|
||||
"Manage Models": "Управление на Моделите",
|
||||
"Manage Ollama Models": "Управление на Ollama Моделите",
|
||||
"Manage Pipelines": "Управление на тръбопроводи",
|
||||
"Manage Valves": "",
|
||||
"March": "Март",
|
||||
"Max Tokens (num_predict)": "Макс токени (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модели могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.",
|
||||
@@ -353,6 +375,7 @@
|
||||
"Memory deleted successfully": "",
|
||||
"Memory updated successfully": "",
|
||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Съобщенията, които изпращате след създаването на връзката, няма да бъдат споделяни. Потребителите с URL адреса ще могат да видят споделения чат.",
|
||||
"Min P": "",
|
||||
"Minimum Score": "Минимална оценка",
|
||||
"Mirostat": "Mirostat",
|
||||
"Mirostat Eta": "Mirostat Eta",
|
||||
@@ -438,6 +461,7 @@
|
||||
"Pipelines Valves": "Тръбопроводи Вентили",
|
||||
"Plain text (.txt)": "Plain text (.txt)",
|
||||
"Playground": "Плейграунд",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Positive attitude": "Позитивна ативност",
|
||||
"Previous 30 days": "Предыдущите 30 дни",
|
||||
"Previous 7 days": "Предыдущите 7 дни",
|
||||
@@ -475,14 +499,17 @@
|
||||
"Rosé Pine": "Rosé Pine",
|
||||
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
||||
"RTL": "RTL",
|
||||
"Run Llama 2, Code Llama, and other models. Customize and create your own.": "",
|
||||
"Running": "",
|
||||
"Save": "Запис",
|
||||
"Save & Create": "Запис & Създаване",
|
||||
"Save & Update": "Запис & Актуализиране",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
|
||||
"Scan": "Сканиране",
|
||||
"Scan complete!": "Сканиране завършено!",
|
||||
"Scan for documents from {{path}}": "Сканиране за документи в {{path}}",
|
||||
"Scroll to bottom when switching between branches": "",
|
||||
"Search": "Търси",
|
||||
"Search a model": "Търси модел",
|
||||
"Search Chats": "Търсене на чатове",
|
||||
@@ -558,6 +585,8 @@
|
||||
"Success": "Успех",
|
||||
"Successfully updated.": "Успешно обновено.",
|
||||
"Suggested": "Препоръчано",
|
||||
"Support": "",
|
||||
"Support this plugin:": "",
|
||||
"System": "Система",
|
||||
"System Prompt": "Системен Промпт",
|
||||
"Tags": "Тагове",
|
||||
@@ -570,6 +599,7 @@
|
||||
"Text-to-Speech Engine": "Text-to-Speech Engine",
|
||||
"Tfs Z": "Tfs Z",
|
||||
"Thanks for your feedback!": "Благодарим ви за вашия отзив!",
|
||||
"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "The score should be a value between 0.0 (0%) and 1.0 (100%).",
|
||||
"Theme": "Тема",
|
||||
"Thinking...": "",
|
||||
@@ -593,6 +623,7 @@
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "към чат входа.",
|
||||
"To select actions here, add them to the \"Functions\" workspace first.": "",
|
||||
"To select filters here, add them to the \"Functions\" workspace first.": "",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "днес",
|
||||
@@ -603,7 +634,13 @@
|
||||
"Tool deleted successfully": "",
|
||||
"Tool imported successfully": "",
|
||||
"Tool updated successfully": "",
|
||||
"Toolkit Description (e.g. A toolkit for performing various operations)": "",
|
||||
"Toolkit ID (e.g. my_toolkit)": "",
|
||||
"Toolkit Name (e.g. My ToolKit)": "",
|
||||
"Tools": "",
|
||||
"Tools are a function calling system with arbitrary code execution": "",
|
||||
"Tools have a function calling system that allows arbitrary code execution": "",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Проблеми с достъпът до Ollama?",
|
||||
@@ -645,6 +682,7 @@
|
||||
"Version": "Версия",
|
||||
"Voice": "",
|
||||
"Warning": "Предупреждение",
|
||||
"Warning:": "",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Предупреждение: Ако актуализирате или промените вашия модел за вграждане, трябва да повторите импортирането на всички документи.",
|
||||
"Web": "Уеб",
|
||||
"Web API": "",
|
||||
@@ -671,6 +709,7 @@
|
||||
"You're a helpful assistant.": "Вие сте полезен асистент.",
|
||||
"You're now logged in.": "Сега, вие влязохте в системата.",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"Youtube": "Youtube",
|
||||
"Youtube Loader Settings": "Youtube Loader Settings"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Account": "একাউন্ট",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "সঠিক তথ্য",
|
||||
"Actions": "",
|
||||
"Active Users": "",
|
||||
"Add": "যোগ করুন",
|
||||
"Add a model id": "একটি মডেল ID যোগ করুন",
|
||||
@@ -27,6 +28,7 @@
|
||||
"Add Memory": "মেমোরি যোগ করুন",
|
||||
"Add message": "মেসেজ যোগ করুন",
|
||||
"Add Model": "মডেল যোগ করুন",
|
||||
"Add Tag": "",
|
||||
"Add Tags": "ট্যাগ যোগ করুন",
|
||||
"Add User": "ইউজার যোগ করুন",
|
||||
"Adjusting these settings will apply changes universally to all users.": "এই সেটিংগুলো পরিবর্তন করলে তা সব ইউজারের উপরেই প্রয়োগ করা হবে",
|
||||
@@ -168,6 +170,7 @@
|
||||
"Delete chat": "চ্যাট মুছে ফেলুন",
|
||||
"Delete Chat": "চ্যাট মুছে ফেলুন",
|
||||
"Delete chat?": "",
|
||||
"Delete Doc": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "এই লিংক মুছে ফেলুন",
|
||||
@@ -177,6 +180,7 @@
|
||||
"Deleted {{name}}": "{{name}} মোছা হয়েছে",
|
||||
"Description": "বিবরণ",
|
||||
"Didn't fully follow instructions": "ইনস্ট্রাকশন সম্পূর্ণ অনুসরণ করা হয়নি",
|
||||
"Disabled": "",
|
||||
"Discover a function": "",
|
||||
"Discover a model": "একটি মডেল আবিষ্কার করুন",
|
||||
"Discover a prompt": "একটি প্রম্পট খুঁজে বের করুন",
|
||||
@@ -188,6 +192,8 @@
|
||||
"Dismissible": "",
|
||||
"Display Emoji in Call": "",
|
||||
"Display the username instead of You in the Chat": "চ্যাটে 'আপনি'-র পরবর্তে ইউজারনেম দেখান",
|
||||
"Do not install functions from sources you do not fully trust.": "",
|
||||
"Do not install tools from sources you do not fully trust.": "",
|
||||
"Document": "ডকুমেন্ট",
|
||||
"Document Settings": "ডকুমেন্ট সেটিংসমূহ",
|
||||
"Documentation": "",
|
||||
@@ -195,6 +201,8 @@
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "কোন এক্সটার্নাল কানেকশন তৈরি করে না, এবং আপনার ডেটা আর লোকালি হোস্টেড সার্ভারেই নিরাপদে থাকে।",
|
||||
"Don't Allow": "অনুমোদন দেবেন না",
|
||||
"Don't have an account?": "একাউন্ট নেই?",
|
||||
"don't install random functions from sources you don't trust.": "",
|
||||
"don't install random tools from sources you don't trust.": "",
|
||||
"Don't like the style": "স্টাইল পছন্দ করেন না",
|
||||
"Done": "",
|
||||
"Download": "ডাউনলোড",
|
||||
@@ -206,6 +214,7 @@
|
||||
"Edit Doc": "ডকুমেন্ট এডিট করুন",
|
||||
"Edit Memory": "",
|
||||
"Edit User": "ইউজার এডিট করুন",
|
||||
"ElevenLabs": "",
|
||||
"Email": "ইমেইল",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "ইমেজ ইমেবডিং মডেল",
|
||||
@@ -215,6 +224,7 @@
|
||||
"Enable Community Sharing": "সম্প্রদায় শেয়ারকরণ সক্ষম করুন",
|
||||
"Enable New Sign Ups": "নতুন সাইনআপ চালু করুন",
|
||||
"Enable Web Search": "ওয়েব অনুসন্ধান সক্ষম করুন",
|
||||
"Enabled": "",
|
||||
"Engine": "",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "আপনার সিএসভি ফাইলটিতে এই ক্রমে 4 টি কলাম অন্তর্ভুক্ত রয়েছে তা নিশ্চিত করুন: নাম, ইমেল, পাসওয়ার্ড, ভূমিকা।.",
|
||||
"Enter {{role}} message here": "{{role}} মেসেজ এখানে লিখুন",
|
||||
@@ -236,6 +246,7 @@
|
||||
"Enter Serply API Key": "",
|
||||
"Enter Serpstack API Key": "Serpstack API কী লিখুন",
|
||||
"Enter stop sequence": "স্টপ সিকোয়েন্স লিখুন",
|
||||
"Enter system prompt": "",
|
||||
"Enter Tavily API Key": "",
|
||||
"Enter Tika Server URL": "",
|
||||
"Enter Top K": "Top K লিখুন",
|
||||
@@ -243,6 +254,7 @@
|
||||
"Enter URL (e.g. http://localhost:11434)": "ইউআরএল দিন (যেমন http://localhost:11434)",
|
||||
"Enter Your Email": "আপনার ইমেইল লিখুন",
|
||||
"Enter Your Full Name": "আপনার পূর্ণ নাম লিখুন",
|
||||
"Enter your message": "",
|
||||
"Enter Your Password": "আপনার পাসওয়ার্ড লিখুন",
|
||||
"Enter Your Role": "আপনার রোল লিখুন",
|
||||
"Error": "ত্রুটি",
|
||||
@@ -266,6 +278,7 @@
|
||||
"File": "",
|
||||
"File Mode": "ফাইল মোড",
|
||||
"File not found.": "ফাইল পাওয়া যায়নি",
|
||||
"Files": "",
|
||||
"Filter is now globally disabled": "",
|
||||
"Filter is now globally enabled": "",
|
||||
"Filters": "",
|
||||
@@ -278,14 +291,22 @@
|
||||
"Frequency Penalty": "ফ্রিকোয়েন্সি পেনাল্টি",
|
||||
"Function created successfully": "",
|
||||
"Function deleted successfully": "",
|
||||
"Function Description (e.g. A filter to remove profanity from text)": "",
|
||||
"Function ID (e.g. my_filter)": "",
|
||||
"Function is now globally disabled": "",
|
||||
"Function is now globally enabled": "",
|
||||
"Function Name (e.g. My Filter)": "",
|
||||
"Function updated successfully": "",
|
||||
"Functions": "",
|
||||
"Functions allow arbitrary code execution": "",
|
||||
"Functions allow arbitrary code execution.": "",
|
||||
"Functions imported successfully": "",
|
||||
"General": "সাধারণ",
|
||||
"General Settings": "সাধারণ সেটিংসমূহ",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "অনুসন্ধান ক্যোয়ারী তৈরি করা হচ্ছে",
|
||||
"Generation Info": "জেনারেশন ইনফো",
|
||||
"Get up and running with": "",
|
||||
"Global": "",
|
||||
"Good Response": "ভালো সাড়া",
|
||||
"Google PSE API Key": "গুগল পিএসই এপিআই কী",
|
||||
@@ -298,6 +319,7 @@
|
||||
"Hide Model": "",
|
||||
"How can I help you today?": "আপনাকে আজ কিভাবে সাহায্য করতে পারি?",
|
||||
"Hybrid Search": "হাইব্রিড অনুসন্ধান",
|
||||
"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.": "",
|
||||
"Image Generation (Experimental)": "ইমেজ জেনারেশন (পরিক্ষামূলক)",
|
||||
"Image Generation Engine": "ইমেজ জেনারেশন ইঞ্জিন",
|
||||
"Image Settings": "ছবির সেটিংসমূহ",
|
||||
@@ -328,6 +350,7 @@
|
||||
"Keyboard shortcuts": "কিবোর্ড শর্টকাটসমূহ",
|
||||
"Knowledge": "",
|
||||
"Language": "ভাষা",
|
||||
"large language models, locally.": "",
|
||||
"Last Active": "সর্বশেষ সক্রিয়",
|
||||
"Last Modified": "",
|
||||
"Light": "লাইট",
|
||||
@@ -341,7 +364,6 @@
|
||||
"Manage Models": "মডেলসমূহ ব্যবস্থাপনা করুন",
|
||||
"Manage Ollama Models": "Ollama মডেলসূহ ব্যবস্থাপনা করুন",
|
||||
"Manage Pipelines": "পাইপলাইন পরিচালনা করুন",
|
||||
"Manage Valves": "",
|
||||
"March": "মার্চ",
|
||||
"Max Tokens (num_predict)": "সর্বোচ্চ টোকেন (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "একসঙ্গে সর্বোচ্চ তিনটি মডেল ডাউনলোড করা যায়। দয়া করে পরে আবার চেষ্টা করুন।",
|
||||
@@ -353,6 +375,7 @@
|
||||
"Memory deleted successfully": "",
|
||||
"Memory updated successfully": "",
|
||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "আপনার লিঙ্ক তৈরি করার পরে আপনার পাঠানো বার্তাগুলি শেয়ার করা হবে না। ইউআরএল ব্যবহারকারীরা শেয়ার করা চ্যাট দেখতে পারবেন।",
|
||||
"Min P": "",
|
||||
"Minimum Score": "Minimum Score",
|
||||
"Mirostat": "Mirostat",
|
||||
"Mirostat Eta": "Mirostat Eta",
|
||||
@@ -438,6 +461,7 @@
|
||||
"Pipelines Valves": "পাইপলাইন ভালভ",
|
||||
"Plain text (.txt)": "প্লায়েন টেক্সট (.txt)",
|
||||
"Playground": "খেলাঘর",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Positive attitude": "পজিটিভ আক্রমণ",
|
||||
"Previous 30 days": "পূর্ব ৩০ দিন",
|
||||
"Previous 7 days": "পূর্ব ৭ দিন",
|
||||
@@ -475,14 +499,17 @@
|
||||
"Rosé Pine": "রোজ পাইন",
|
||||
"Rosé Pine Dawn": "ভোরের রোজ পাইন",
|
||||
"RTL": "RTL",
|
||||
"Run Llama 2, Code Llama, and other models. Customize and create your own.": "",
|
||||
"Running": "",
|
||||
"Save": "সংরক্ষণ",
|
||||
"Save & Create": "সংরক্ষণ এবং তৈরি করুন",
|
||||
"Save & Update": "সংরক্ষণ এবং আপডেট করুন",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "মাধ্যমে",
|
||||
"Scan": "স্ক্যান",
|
||||
"Scan complete!": "স্ক্যান সম্পন্ন হয়েছে!",
|
||||
"Scan for documents from {{path}}": "ডকুমেন্টসমূহের জন্য {{path}} স্ক্যান করুন",
|
||||
"Scroll to bottom when switching between branches": "",
|
||||
"Search": "অনুসন্ধান",
|
||||
"Search a model": "মডেল অনুসন্ধান করুন",
|
||||
"Search Chats": "চ্যাট অনুসন্ধান করুন",
|
||||
@@ -558,6 +585,8 @@
|
||||
"Success": "সফল",
|
||||
"Successfully updated.": "সফলভাবে আপডেট হয়েছে",
|
||||
"Suggested": "প্রস্তাবিত",
|
||||
"Support": "",
|
||||
"Support this plugin:": "",
|
||||
"System": "সিস্টেম",
|
||||
"System Prompt": "সিস্টেম প্রম্পট",
|
||||
"Tags": "ট্যাগসমূহ",
|
||||
@@ -570,6 +599,7 @@
|
||||
"Text-to-Speech Engine": "টেক্সট-টু-স্পিচ ইঞ্জিন",
|
||||
"Tfs Z": "Tfs Z",
|
||||
"Thanks for your feedback!": "আপনার মতামত ধন্যবাদ!",
|
||||
"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "স্কোর একটি 0.0 (0%) এবং 1.0 (100%) এর মধ্যে একটি মান হওয়া উচিত।",
|
||||
"Theme": "থিম",
|
||||
"Thinking...": "",
|
||||
@@ -593,6 +623,7 @@
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"To add documents here, upload them to the \"Documents\" workspace first.": "",
|
||||
"to chat input.": "চ্যাট ইনপুটে",
|
||||
"To select actions here, add them to the \"Functions\" workspace first.": "",
|
||||
"To select filters here, add them to the \"Functions\" workspace first.": "",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
|
||||
"Today": "আজ",
|
||||
@@ -603,7 +634,13 @@
|
||||
"Tool deleted successfully": "",
|
||||
"Tool imported successfully": "",
|
||||
"Tool updated successfully": "",
|
||||
"Toolkit Description (e.g. A toolkit for performing various operations)": "",
|
||||
"Toolkit ID (e.g. my_toolkit)": "",
|
||||
"Toolkit Name (e.g. My ToolKit)": "",
|
||||
"Tools": "",
|
||||
"Tools are a function calling system with arbitrary code execution": "",
|
||||
"Tools have a function calling system that allows arbitrary code execution": "",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Ollama এক্সেস করতে সমস্যা হচ্ছে?",
|
||||
@@ -645,6 +682,7 @@
|
||||
"Version": "ভার্সন",
|
||||
"Voice": "",
|
||||
"Warning": "সতর্কীকরণ",
|
||||
"Warning:": "",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "সতর্কীকরণ: আপনি যদি আপনার এম্বেডিং মডেল আপডেট বা পরিবর্তন করেন, তাহলে আপনাকে সমস্ত নথি পুনরায় আমদানি করতে হবে।.",
|
||||
"Web": "ওয়েব",
|
||||
"Web API": "",
|
||||
@@ -671,6 +709,7 @@
|
||||
"You're a helpful assistant.": "আপনি একজন উপকারী এসিস্ট্যান্ট",
|
||||
"You're now logged in.": "আপনি এখন লগইন করা অবস্থায় আছেন",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"Youtube": "YouTube",
|
||||
"Youtube Loader Settings": "YouTube লোডার সেটিংস"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user