Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3933db2c91 | ||
|
|
dc8f0987e1 | ||
|
|
644f0fe6c3 | ||
|
|
87b04b0fc3 | ||
|
|
455403a4a4 | ||
|
|
547b990041 | ||
|
|
fcf8f2a704 | ||
|
|
496e798cc7 | ||
|
|
e01b3569d5 | ||
|
|
a75b68c19c | ||
|
|
34f04c53fc | ||
|
|
2b87ab32ff | ||
|
|
d20f6cb45b | ||
|
|
48c9fb0ddf | ||
|
|
c609cc23eb | ||
|
|
3566002b0b | ||
|
|
140d7ff721 | ||
|
|
7349e1d8c8 | ||
|
|
bf5a62298c | ||
|
|
2972774d87 | ||
|
|
75d455ac8f | ||
|
|
64b6a8c5db | ||
|
|
8947da57af | ||
|
|
16f09fff63 | ||
|
|
14eb667fc8 | ||
|
|
79bf8d6dd3 | ||
|
|
14d33f0fcc | ||
|
|
4dcec4855e | ||
|
|
96a004d4d8 | ||
|
|
1fa16d73f0 | ||
|
|
d2cd803a57 | ||
|
|
e685953df7 | ||
|
|
d6520e7fd8 | ||
|
|
c54e3d5bf4 | ||
|
|
efb4a710c8 | ||
|
|
a71b824728 | ||
|
|
bc1a7ab567 | ||
|
|
b565301a47 | ||
|
|
78272aed8d | ||
|
|
3f7913b36f | ||
|
|
e1edf9227d | ||
|
|
a8d709102f | ||
|
|
9032ab8857 | ||
|
|
c9137edf5a | ||
|
|
d11c373cf1 | ||
|
|
2755ef62d9 | ||
|
|
8debb71197 | ||
|
|
8b4867deb5 | ||
|
|
a93645aee8 | ||
|
|
28ca6fb678 | ||
|
|
da4586f27d | ||
|
|
591cd993c2 | ||
|
|
aa1bb4fb6d | ||
|
|
5e7237b9cb | ||
|
|
84defafc14 | ||
|
|
7b1404f490 | ||
|
|
9b38788aa2 | ||
|
|
c44fc82ecd | ||
|
|
8b6f422d45 | ||
|
|
2eb8c3456f | ||
|
|
fa9835a7ad | ||
|
|
3db9d19ae3 | ||
|
|
762db11bf4 | ||
|
|
8e9e429a91 | ||
|
|
f2b9a5f5bf | ||
|
|
277fc3feac | ||
|
|
9053bfdadf | ||
|
|
414ab53144 | ||
|
|
4a7d3a076c | ||
|
|
8198807fc9 | ||
|
|
e556c02fe9 | ||
|
|
f7de01cbc2 | ||
|
|
1bc96de620 | ||
|
|
23489105fc | ||
|
|
8c95a8be3a | ||
|
|
d00c8b00df | ||
|
|
4bae43096b | ||
|
|
d90e138773 | ||
|
|
bb15eb016e | ||
|
|
97b39115a1 | ||
|
|
cc53fd8379 | ||
|
|
5342063e24 | ||
|
|
e274a0dc3e | ||
|
|
9c3920a489 | ||
|
|
d1da3dde65 | ||
|
|
0ccd96c950 | ||
|
|
3499ec3f79 | ||
|
|
40ea18d54d | ||
|
|
ffbc480bb4 | ||
|
|
9b839e5c9f | ||
|
|
575f23cde9 | ||
|
|
711317555c | ||
|
|
063e1ee46c | ||
|
|
0d0f8aac15 | ||
|
|
5cbb79fa6e | ||
|
|
277e7aead7 | ||
|
|
d3558fdb33 | ||
|
|
1271d1dccc | ||
|
|
3be1cb98d3 | ||
|
|
c2a7e7f510 | ||
|
|
dbde628141 | ||
|
|
2412f31ed9 | ||
|
|
8df0c9e0dd | ||
|
|
6f9148ac4c | ||
|
|
d7d8896e43 | ||
|
|
d5a2a8a880 | ||
|
|
cff8534f33 | ||
|
|
f69bc57fed | ||
|
|
4ecc1c06d3 | ||
|
|
f78e6a5fba | ||
|
|
411e6c2fe4 | ||
|
|
369816ace0 | ||
|
|
06e4b87c9e | ||
|
|
c6b74a3cf9 | ||
|
|
27417dd771 | ||
|
|
16615010f7 | ||
|
|
e516374d54 | ||
|
|
55dc6c1b3b | ||
|
|
da47c2dfa3 | ||
|
|
694d1708a3 | ||
|
|
f9aa03bc6d | ||
|
|
bd3e5d7bfa | ||
|
|
5743df2280 | ||
|
|
e3efe2c565 | ||
|
|
32c4748749 | ||
|
|
f4a15f9590 | ||
|
|
f92ef3211b | ||
|
|
508e8eb4b1 | ||
|
|
dfc2fb65af | ||
|
|
d7ce408d25 | ||
|
|
374f647048 | ||
|
|
00018085ed | ||
|
|
d17cdc8068 | ||
|
|
e5ad76615c | ||
|
|
54420f5300 | ||
|
|
f076f81e0c | ||
|
|
ea80285e9c | ||
|
|
f43d255e8f | ||
|
|
fc3a31e3d5 | ||
|
|
4e640daf83 | ||
|
|
404bb3fd67 | ||
|
|
eacce66a82 | ||
|
|
4f19a92dc5 | ||
|
|
5196b65c2e | ||
|
|
340b716a90 | ||
|
|
ff4cf16742 | ||
|
|
7eaf83cb7d | ||
|
|
4a73a01c24 | ||
|
|
1a9dee50ce | ||
|
|
f97f6601f7 | ||
|
|
5ab5231c73 | ||
|
|
046dd02a6d | ||
|
|
72e1615fe1 | ||
|
|
b512501d9d | ||
|
|
36cc3c24e6 | ||
|
|
62bfcf35e5 | ||
|
|
9bec2cea12 | ||
|
|
f4a122a4bb | ||
|
|
34aea03c02 | ||
|
|
e7fb86190a | ||
|
|
194564e253 | ||
|
|
d4b10097d2 | ||
|
|
3069fa8074 | ||
|
|
c2bbb37fc1 | ||
|
|
e52568e7cb | ||
|
|
bc4aeeeec8 | ||
|
|
25ee5f6cb0 | ||
|
|
882ad219ea | ||
|
|
c108d863f6 | ||
|
|
a8d80f936a | ||
|
|
1bb7fc7c92 | ||
|
|
dbb83f9824 | ||
|
|
e1889b0c5e | ||
|
|
88758af621 | ||
|
|
43f83673e2 | ||
|
|
950073c1de | ||
|
|
651c85fcc4 | ||
|
|
ec7c5a3f07 | ||
|
|
d077b3dcdb | ||
|
|
1d6bbdf917 | ||
|
|
f248425931 | ||
|
|
bdf7ed7f02 | ||
|
|
66200cfb7d | ||
|
|
9174e01d6a | ||
|
|
f17df4f114 | ||
|
|
1948e78351 | ||
|
|
73a98ff938 | ||
|
|
92d056c982 | ||
|
|
2be9c25ba7 | ||
|
|
56fda46215 | ||
|
|
1d1b945483 | ||
|
|
9aac6e05a9 | ||
|
|
73e2912849 | ||
|
|
7cf80289c3 | ||
|
|
2fa594d5bd | ||
|
|
45f1a4157b | ||
|
|
b3e4ecd2f4 | ||
|
|
ab04e0dfd4 | ||
|
|
ee625a0519 | ||
|
|
fea563cf89 | ||
|
|
fcfdfa3dea | ||
|
|
844923d0a4 | ||
|
|
bbfa54a6b9 | ||
|
|
e2429314f3 | ||
|
|
4925a6530b | ||
|
|
85484392b2 | ||
|
|
8d9712d66e | ||
|
|
0495f01acb | ||
|
|
f28877f4db | ||
|
|
b673ae7ab2 | ||
|
|
8c3a6eb262 | ||
|
|
25336f85f3 | ||
|
|
74ed74f1a4 | ||
|
|
94ebf02719 | ||
|
|
11bb1a6f7d | ||
|
|
61867c1545 | ||
|
|
4068a421bf | ||
|
|
8a94d8a226 | ||
|
|
5a24ce06f0 | ||
|
|
e72170bac7 | ||
|
|
dd93b0dd24 | ||
|
|
0ac04c9518 | ||
|
|
d28925de05 | ||
|
|
2c4c62fb75 | ||
|
|
de162a1f32 | ||
|
|
aa7e5dbfce | ||
|
|
8d99bffbdc | ||
|
|
768941bded | ||
|
|
e0772c6807 | ||
|
|
cbd60168ea | ||
|
|
c6c5c0ddb2 | ||
|
|
7c35447666 | ||
|
|
58b5e0bf3e | ||
|
|
0586d76b5d | ||
|
|
eb3fcef6d7 | ||
|
|
59a730cb8b | ||
|
|
7fefbb316d | ||
|
|
14646e84ea | ||
|
|
ba56edab19 | ||
|
|
a233a8855b | ||
|
|
c11fdad82d | ||
|
|
82f7b6c315 | ||
|
|
92d9b38110 | ||
|
|
36a66fcfc4 | ||
|
|
ea27bc50f5 | ||
|
|
8a9c18ddd5 | ||
|
|
3729771f33 | ||
|
|
ab58947a19 | ||
|
|
f9351af6a3 | ||
|
|
8f03daeaf6 | ||
|
|
de03bf834e | ||
|
|
db3d48ee15 | ||
|
|
6022f6f5df | ||
|
|
2d1a890ed9 | ||
|
|
08443b3c55 | ||
|
|
7e761a69a7 | ||
|
|
be08dbf00d | ||
|
|
b4fca046a8 | ||
|
|
61287a16d5 | ||
|
|
16e72b2f67 | ||
|
|
80d173c1c3 | ||
|
|
4b459e372f | ||
|
|
fddf693dad | ||
|
|
a39ab4dd24 | ||
|
|
13db33e947 | ||
|
|
727a837e53 | ||
|
|
22d7ce2b7f | ||
|
|
1d4c358624 | ||
|
|
a3ecf62564 | ||
|
|
6199296205 | ||
|
|
5ea88f2e4f | ||
|
|
8114fa93cf | ||
|
|
0cb8163321 |
No files matched your search
@@ -11,7 +11,7 @@
|
|||||||
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
||||||
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
|
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
|
||||||
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
|
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
|
||||||
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
|
- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
|
||||||
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
||||||
- **build**: Changes that affect the build system or external dependencies
|
- **build**: Changes that affect the build system or external dependencies
|
||||||
- **ci**: Changes to our continuous integration processes or workflows
|
- **ci**: Changes to our continuous integration processes or workflows
|
||||||
|
|||||||
@@ -5,6 +5,100 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.3.2] - 2024-06-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
|
||||||
|
- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
|
||||||
|
- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
|
||||||
|
- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
|
||||||
|
- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
|
||||||
|
|
||||||
|
## [0.3.1] - 2024-06-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
|
||||||
|
|
||||||
|
## [0.3.0] - 2024-06-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
|
||||||
|
- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
|
||||||
|
- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
|
||||||
|
- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
|
||||||
|
- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
|
||||||
|
- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
|
||||||
|
- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
|
||||||
|
- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
|
||||||
|
- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
|
||||||
|
- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
|
||||||
|
- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
|
||||||
|
- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
|
||||||
|
- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
|
||||||
|
- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
|
||||||
|
- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
|
||||||
|
- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
|
||||||
|
- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
|
||||||
|
- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
|
||||||
|
- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
|
||||||
|
- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
|
||||||
|
|
||||||
|
## [0.2.5] - 2024-06-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
|
||||||
|
- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
|
||||||
|
- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
|
||||||
|
- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
|
||||||
|
|
||||||
|
## [0.2.4] - 2024-06-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed.
|
||||||
|
- **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings.
|
||||||
|
- **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs).
|
||||||
|
- **🌍 Enhanced Translation**: Improvements have been made to translations.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
|
||||||
|
|
||||||
|
## [0.2.3] - 2024-06-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier.
|
||||||
|
- **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
|
||||||
|
- **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
|
||||||
|
- **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
|
||||||
|
- **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input.
|
||||||
|
- **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
|
||||||
|
|
||||||
## [0.2.2] - 2024-06-02
|
## [0.2.2] - 2024-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
[](https://discord.gg/5rJgQTnV4s)
|
[](https://discord.gg/5rJgQTnV4s)
|
||||||
[](https://github.com/sponsors/tjbck)
|
[](https://github.com/sponsors/tjbck)
|
||||||
|
|
||||||
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
|||||||
|
|
||||||
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
|
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
|
||||||
|
|
||||||
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
||||||
|
|
||||||
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
|||||||
|
|
||||||
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
||||||
|
|
||||||
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
|
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, and `Serply` and inject the results directly into your chat experience.
|
||||||
|
|
||||||
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
||||||
|
|
||||||
@@ -146,10 +146,19 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
|
|||||||
|
|
||||||
In the last part of the command, replace `open-webui` with your container name if it is different.
|
In the last part of the command, replace `open-webui` with your container name if it is different.
|
||||||
|
|
||||||
### Moving from Ollama WebUI to Open WebUI
|
|
||||||
|
|
||||||
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
||||||
|
|
||||||
|
### Using the Dev Branch 🌙
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
|
||||||
|
|
||||||
|
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:dev
|
||||||
|
```
|
||||||
|
|
||||||
## What's Next? 🌟
|
## What's Next? 🌟
|
||||||
|
|
||||||
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
||||||
|
|||||||
+186
-38
@@ -17,13 +17,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from faster_whisper import WhisperModel
|
from faster_whisper import WhisperModel
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
decode_token,
|
decode_token,
|
||||||
@@ -41,10 +40,15 @@ from config import (
|
|||||||
WHISPER_MODEL_DIR,
|
WHISPER_MODEL_DIR,
|
||||||
WHISPER_MODEL_AUTO_UPDATE,
|
WHISPER_MODEL_AUTO_UPDATE,
|
||||||
DEVICE_TYPE,
|
DEVICE_TYPE,
|
||||||
AUDIO_OPENAI_API_BASE_URL,
|
AUDIO_STT_OPENAI_API_BASE_URL,
|
||||||
AUDIO_OPENAI_API_KEY,
|
AUDIO_STT_OPENAI_API_KEY,
|
||||||
AUDIO_OPENAI_API_MODEL,
|
AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||||
AUDIO_OPENAI_API_VOICE,
|
AUDIO_TTS_OPENAI_API_KEY,
|
||||||
|
AUDIO_STT_ENGINE,
|
||||||
|
AUDIO_STT_MODEL,
|
||||||
|
AUDIO_TTS_ENGINE,
|
||||||
|
AUDIO_TTS_MODEL,
|
||||||
|
AUDIO_TTS_VOICE,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,10 +65,17 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
|
||||||
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
|
||||||
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
|
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
|
||||||
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
|
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
|
||||||
|
app.state.config.STT_MODEL = AUDIO_STT_MODEL
|
||||||
|
|
||||||
|
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
|
||||||
|
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
||||||
|
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||||
|
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||||
|
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||||
|
|
||||||
# setting device type for whisper model
|
# setting device type for whisper model
|
||||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||||
@@ -74,41 +85,101 @@ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
|||||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class OpenAIConfigUpdateForm(BaseModel):
|
class TTSConfigForm(BaseModel):
|
||||||
url: str
|
OPENAI_API_BASE_URL: str
|
||||||
key: str
|
OPENAI_API_KEY: str
|
||||||
model: str
|
ENGINE: str
|
||||||
speaker: str
|
MODEL: str
|
||||||
|
VOICE: str
|
||||||
|
|
||||||
|
|
||||||
|
class STTConfigForm(BaseModel):
|
||||||
|
OPENAI_API_BASE_URL: str
|
||||||
|
OPENAI_API_KEY: str
|
||||||
|
ENGINE: str
|
||||||
|
MODEL: str
|
||||||
|
|
||||||
|
|
||||||
|
class AudioConfigUpdateForm(BaseModel):
|
||||||
|
tts: TTSConfigForm
|
||||||
|
stt: STTConfigForm
|
||||||
|
|
||||||
|
|
||||||
|
from pydub import AudioSegment
|
||||||
|
from pydub.utils import mediainfo
|
||||||
|
|
||||||
|
|
||||||
|
def is_mp4_audio(file_path):
|
||||||
|
"""Check if the given file is an MP4 audio file."""
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
print(f"File not found: {file_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
info = mediainfo(file_path)
|
||||||
|
if (
|
||||||
|
info.get("codec_name") == "aac"
|
||||||
|
and info.get("codec_type") == "audio"
|
||||||
|
and info.get("codec_tag_string") == "mp4a"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def convert_mp4_to_wav(file_path, output_path):
|
||||||
|
"""Convert MP4 audio file to WAV format."""
|
||||||
|
audio = AudioSegment.from_file(file_path, format="mp4")
|
||||||
|
audio.export(output_path, format="wav")
|
||||||
|
print(f"Converted {file_path} to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
async def get_openai_config(user=Depends(get_admin_user)):
|
async def get_audio_config(user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
"tts": {
|
||||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
"ENGINE": app.state.config.TTS_ENGINE,
|
||||||
|
"MODEL": app.state.config.TTS_MODEL,
|
||||||
|
"VOICE": app.state.config.TTS_VOICE,
|
||||||
|
},
|
||||||
|
"stt": {
|
||||||
|
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
||||||
|
"ENGINE": app.state.config.STT_ENGINE,
|
||||||
|
"MODEL": app.state.config.STT_MODEL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/config/update")
|
@app.post("/config/update")
|
||||||
async def update_openai_config(
|
async def update_audio_config(
|
||||||
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
if form_data.key == "":
|
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_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
|
||||||
|
|
||||||
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
|
||||||
app.state.config.OPENAI_API_KEY = form_data.key
|
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
|
||||||
app.state.config.OPENAI_API_MODEL = form_data.model
|
app.state.config.STT_ENGINE = form_data.stt.ENGINE
|
||||||
app.state.config.OPENAI_API_VOICE = form_data.speaker
|
app.state.config.STT_MODEL = form_data.stt.MODEL
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"tts": {
|
||||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
"ENGINE": app.state.config.TTS_ENGINE,
|
||||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
"MODEL": app.state.config.TTS_MODEL,
|
||||||
|
"VOICE": app.state.config.TTS_VOICE,
|
||||||
|
},
|
||||||
|
"stt": {
|
||||||
|
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
||||||
|
"ENGINE": app.state.config.STT_ENGINE,
|
||||||
|
"MODEL": app.state.config.STT_MODEL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -125,13 +196,21 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
||||||
headers["Content-Type"] = "application/json"
|
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
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
|
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||||
data=body,
|
data=body,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
stream=True,
|
stream=True,
|
||||||
@@ -181,13 +260,23 @@ def transcribe(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename = file.filename
|
ext = file.filename.split(".")[-1]
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
|
||||||
|
id = uuid.uuid4()
|
||||||
|
filename = f"{id}.{ext}"
|
||||||
|
|
||||||
|
file_dir = f"{CACHE_DIR}/audio/transcriptions"
|
||||||
|
os.makedirs(file_dir, exist_ok=True)
|
||||||
|
file_path = f"{file_dir}/{filename}"
|
||||||
|
|
||||||
|
print(filename)
|
||||||
|
|
||||||
contents = file.file.read()
|
contents = file.file.read()
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
if app.state.config.STT_ENGINE == "":
|
||||||
whisper_kwargs = {
|
whisper_kwargs = {
|
||||||
"model_size_or_path": WHISPER_MODEL,
|
"model_size_or_path": WHISPER_MODEL,
|
||||||
"device": whisper_device_type,
|
"device": whisper_device_type,
|
||||||
@@ -215,7 +304,66 @@ def transcribe(
|
|||||||
|
|
||||||
transcript = "".join([segment.text for segment in list(segments)])
|
transcript = "".join([segment.text for segment in list(segments)])
|
||||||
|
|
||||||
return {"text": transcript.strip()}
|
data = {"text": transcript.strip()}
|
||||||
|
|
||||||
|
# save the transcript to a json file
|
||||||
|
transcript_file = f"{file_dir}/{id}.json"
|
||||||
|
with open(transcript_file, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
elif app.state.config.STT_ENGINE == "openai":
|
||||||
|
if is_mp4_audio(file_path):
|
||||||
|
print("is_mp4_audio")
|
||||||
|
os.rename(file_path, file_path.replace(".wav", ".mp4"))
|
||||||
|
# Convert MP4 audio file to WAV format
|
||||||
|
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
|
||||||
|
|
||||||
|
files = {"file": (filename, open(file_path, "rb"))}
|
||||||
|
data = {"model": "whisper-1"}
|
||||||
|
|
||||||
|
print(files, data)
|
||||||
|
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||||
|
headers=headers,
|
||||||
|
files=files,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
# save the transcript to a json file
|
||||||
|
transcript_file = f"{file_dir}/{id}.json"
|
||||||
|
with open(transcript_file, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
return data
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|||||||
+15
-11
@@ -41,8 +41,6 @@ from utils.utils import (
|
|||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
from utils.models import get_model_id_from_custom_model_id
|
|
||||||
|
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
@@ -134,7 +132,7 @@ async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin
|
|||||||
async def fetch_url(url):
|
async def fetch_url(url):
|
||||||
timeout = aiohttp.ClientTimeout(total=5)
|
timeout = aiohttp.ClientTimeout(total=5)
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||||
async with session.get(url) as response:
|
async with session.get(url) as response:
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -156,7 +154,7 @@ async def cleanup_response(
|
|||||||
async def post_streaming_url(url: str, payload: str):
|
async def post_streaming_url(url: str, payload: str):
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
session = aiohttp.ClientSession()
|
session = aiohttp.ClientSession(trust_env=True)
|
||||||
r = await session.post(url, data=payload)
|
r = await session.post(url, data=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
@@ -274,12 +272,13 @@ async def get_ollama_tags(
|
|||||||
@app.get("/api/version")
|
@app.get("/api/version")
|
||||||
@app.get("/api/version/{url_idx}")
|
@app.get("/api/version/{url_idx}")
|
||||||
async def get_ollama_versions(url_idx: Optional[int] = None):
|
async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||||
|
if app.state.config.ENABLE_OLLAMA_API:
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
|
|
||||||
# returns lowest version
|
# returns lowest version
|
||||||
tasks = [
|
tasks = [
|
||||||
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
|
fetch_url(f"{url}/api/version")
|
||||||
|
for url in app.state.config.OLLAMA_BASE_URLS
|
||||||
]
|
]
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
responses = list(filter(lambda x: x is not None, responses))
|
responses = list(filter(lambda x: x is not None, responses))
|
||||||
@@ -322,6 +321,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
|||||||
status_code=r.status_code if r else 500,
|
status_code=r.status_code if r else 500,
|
||||||
detail=error_detail,
|
detail=error_detail,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return {"version": False}
|
||||||
|
|
||||||
|
|
||||||
class ModelNameForm(BaseModel):
|
class ModelNameForm(BaseModel):
|
||||||
@@ -725,7 +726,6 @@ async def generate_chat_completion(
|
|||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
if model_info:
|
if model_info:
|
||||||
print(model_info)
|
|
||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
||||||
@@ -761,7 +761,7 @@ async def generate_chat_completion(
|
|||||||
"frequency_penalty", None
|
"frequency_penalty", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_info.params.get("temperature", None):
|
if model_info.params.get("temperature", None) is not None:
|
||||||
payload["options"]["temperature"] = model_info.params.get(
|
payload["options"]["temperature"] = model_info.params.get(
|
||||||
"temperature", None
|
"temperature", None
|
||||||
)
|
)
|
||||||
@@ -846,9 +846,14 @@ async def generate_chat_completion(
|
|||||||
|
|
||||||
|
|
||||||
# TODO: we should update this part once Ollama supports other types
|
# TODO: we should update this part once Ollama supports other types
|
||||||
|
class OpenAIChatMessageContent(BaseModel):
|
||||||
|
type: str
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
class OpenAIChatMessage(BaseModel):
|
class OpenAIChatMessage(BaseModel):
|
||||||
role: str
|
role: str
|
||||||
content: str
|
content: Union[str, OpenAIChatMessageContent]
|
||||||
|
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
@@ -876,7 +881,6 @@ async def generate_openai_chat_completion(
|
|||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
if model_info:
|
if model_info:
|
||||||
print(model_info)
|
|
||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
||||||
@@ -1045,7 +1049,7 @@ async def download_file_stream(
|
|||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
||||||
|
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||||
async with session.get(file_url, headers=headers) as response:
|
async with session.get(file_url, headers=headers) as response:
|
||||||
total_size = int(response.headers.get("content-length", 0)) + current_size
|
total_size = int(response.headers.get("content-length", 0)) + current_size
|
||||||
|
|
||||||
|
|||||||
+81
-34
@@ -186,7 +186,7 @@ async def fetch_url(url, key):
|
|||||||
timeout = aiohttp.ClientTimeout(total=5)
|
timeout = aiohttp.ClientTimeout(total=5)
|
||||||
try:
|
try:
|
||||||
headers = {"Authorization": f"Bearer {key}"}
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||||
async with session.get(url, headers=headers) as response:
|
async with session.get(url, headers=headers) as response:
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -345,46 +345,34 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
@app.post("/chat/completions")
|
||||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
@app.post("/chat/completions/{url_idx}")
|
||||||
|
async def generate_chat_completion(
|
||||||
|
form_data: dict,
|
||||||
|
url_idx: Optional[int] = None,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
idx = 0
|
idx = 0
|
||||||
|
payload = {**form_data}
|
||||||
|
|
||||||
body = await request.body()
|
model_id = form_data.get("model")
|
||||||
# TODO: Remove below after gpt-4-vision fix from Open AI
|
|
||||||
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
|
||||||
|
|
||||||
payload = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
if "chat/completions" in path:
|
|
||||||
body = body.decode("utf-8")
|
|
||||||
body = json.loads(body)
|
|
||||||
|
|
||||||
payload = {**body}
|
|
||||||
|
|
||||||
model_id = body.get("model")
|
|
||||||
model_info = Models.get_model_by_id(model_id)
|
model_info = Models.get_model_by_id(model_id)
|
||||||
|
|
||||||
if model_info:
|
if model_info:
|
||||||
print(model_info)
|
|
||||||
if model_info.base_model_id:
|
if model_info.base_model_id:
|
||||||
payload["model"] = model_info.base_model_id
|
payload["model"] = model_info.base_model_id
|
||||||
|
|
||||||
model_info.params = model_info.params.model_dump()
|
model_info.params = model_info.params.model_dump()
|
||||||
|
|
||||||
if model_info.params:
|
if model_info.params:
|
||||||
if model_info.params.get("temperature", None):
|
if model_info.params.get("temperature", None) is not None:
|
||||||
payload["temperature"] = int(
|
payload["temperature"] = float(model_info.params.get("temperature"))
|
||||||
model_info.params.get("temperature")
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("top_p", None):
|
if model_info.params.get("top_p", None):
|
||||||
payload["top_p"] = int(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):
|
if model_info.params.get("max_tokens", None):
|
||||||
payload["max_tokens"] = int(
|
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||||
model_info.params.get("max_tokens", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
if model_info.params.get("frequency_penalty", None):
|
if model_info.params.get("frequency_penalty", None):
|
||||||
payload["frequency_penalty"] = int(
|
payload["frequency_penalty"] = int(
|
||||||
@@ -411,8 +399,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
for message in payload["messages"]:
|
for message in payload["messages"]:
|
||||||
if message.get("role") == "system":
|
if message.get("role") == "system":
|
||||||
message["content"] = (
|
message["content"] = (
|
||||||
model_info.params.get("system", None)
|
model_info.params.get("system", None) + message["content"]
|
||||||
+ message["content"]
|
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -423,11 +410,11 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
"content": model_info.params.get("system", None),
|
"content": model_info.params.get("system", None),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
model = app.state.MODELS[payload.get("model")]
|
model = app.state.MODELS[payload.get("model")]
|
||||||
|
|
||||||
idx = model["urlIdx"]
|
idx = model["urlIdx"]
|
||||||
|
|
||||||
if "pipeline" in model and model.get("pipeline"):
|
if "pipeline" in model and model.get("pipeline"):
|
||||||
@@ -443,15 +430,12 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
# Convert the modified body back to JSON
|
# Convert the modified body back to JSON
|
||||||
payload = json.dumps(payload)
|
payload = json.dumps(payload)
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
log.error("Error loading request body into a dictionary:", e)
|
|
||||||
|
|
||||||
print(payload)
|
print(payload)
|
||||||
|
|
||||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||||
|
|
||||||
target_url = f"{url}/{path}"
|
print(payload)
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
@@ -462,11 +446,74 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
streaming = False
|
streaming = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = aiohttp.ClientSession()
|
session = aiohttp.ClientSession(trust_env=True)
|
||||||
r = await session.request(
|
r = await session.request(
|
||||||
|
method="POST",
|
||||||
|
url=f"{url}/chat/completions",
|
||||||
|
data=payload,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# Check if response is SSE
|
||||||
|
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||||
|
streaming = True
|
||||||
|
return StreamingResponse(
|
||||||
|
r.content,
|
||||||
|
status_code=r.status,
|
||||||
|
headers=dict(r.headers),
|
||||||
|
background=BackgroundTask(
|
||||||
|
cleanup_response, response=r, session=session
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response_data = await r.json()
|
||||||
|
return response_data
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = await r.json()
|
||||||
|
print(res)
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||||
|
finally:
|
||||||
|
if not streaming and session:
|
||||||
|
if r:
|
||||||
|
r.close()
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||||
|
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
body = await request.body()
|
||||||
|
|
||||||
|
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
|
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||||
|
|
||||||
|
target_url = f"{url}/{path}"
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
r = None
|
||||||
|
session = None
|
||||||
|
streaming = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = aiohttp.ClientSession(trust_env=True)
|
||||||
|
r = await session.request(
|
||||||
method=request.method,
|
method=request.method,
|
||||||
url=target_url,
|
url=target_url,
|
||||||
data=payload if payload else body,
|
data=body,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os, shutil, logging, re
|
import os, shutil, logging, re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Union, Sequence
|
from typing import List, Union, Sequence
|
||||||
@@ -30,6 +31,7 @@ from langchain_community.document_loaders import (
|
|||||||
UnstructuredExcelLoader,
|
UnstructuredExcelLoader,
|
||||||
UnstructuredPowerPointLoader,
|
UnstructuredPowerPointLoader,
|
||||||
YoutubeLoader,
|
YoutubeLoader,
|
||||||
|
OutlookMessageLoader,
|
||||||
)
|
)
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ from apps.rag.search.main import SearchResult
|
|||||||
from apps.rag.search.searxng import search_searxng
|
from apps.rag.search.searxng import search_searxng
|
||||||
from apps.rag.search.serper import search_serper
|
from apps.rag.search.serper import search_serper
|
||||||
from apps.rag.search.serpstack import search_serpstack
|
from apps.rag.search.serpstack import search_serpstack
|
||||||
|
from apps.rag.search.serply import search_serply
|
||||||
|
|
||||||
from utils.misc import (
|
from utils.misc import (
|
||||||
calculate_sha256,
|
calculate_sha256,
|
||||||
@@ -78,6 +80,7 @@ from utils.misc import (
|
|||||||
from utils.utils import get_current_user, get_admin_user
|
from utils.utils import get_current_user, get_admin_user
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
|
AppConfig,
|
||||||
ENV,
|
ENV,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
@@ -112,9 +115,10 @@ from config import (
|
|||||||
SERPSTACK_API_KEY,
|
SERPSTACK_API_KEY,
|
||||||
SERPSTACK_HTTPS,
|
SERPSTACK_HTTPS,
|
||||||
SERPER_API_KEY,
|
SERPER_API_KEY,
|
||||||
|
SERPLY_API_KEY,
|
||||||
RAG_WEB_SEARCH_RESULT_COUNT,
|
RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
AppConfig,
|
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
@@ -139,6 +143,7 @@ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
|||||||
|
|
||||||
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||||
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||||
|
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
|
||||||
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||||
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
||||||
|
|
||||||
@@ -163,6 +168,7 @@ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
|
|||||||
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
||||||
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
||||||
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
||||||
|
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
||||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
||||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
||||||
|
|
||||||
@@ -212,6 +218,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
|||||||
app.state.sentence_transformer_ef,
|
app.state.sentence_transformer_ef,
|
||||||
app.state.config.OPENAI_API_KEY,
|
app.state.config.OPENAI_API_KEY,
|
||||||
app.state.config.OPENAI_API_BASE_URL,
|
app.state.config.OPENAI_API_BASE_URL,
|
||||||
|
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
@@ -248,6 +255,7 @@ async def get_status():
|
|||||||
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
||||||
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
||||||
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
||||||
|
"openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -260,6 +268,7 @@ async def get_embedding_config(user=Depends(get_admin_user)):
|
|||||||
"openai_config": {
|
"openai_config": {
|
||||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||||
"key": app.state.config.OPENAI_API_KEY,
|
"key": app.state.config.OPENAI_API_KEY,
|
||||||
|
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +284,7 @@ async def get_reraanking_config(user=Depends(get_admin_user)):
|
|||||||
class OpenAIConfigForm(BaseModel):
|
class OpenAIConfigForm(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
key: str
|
key: str
|
||||||
|
batch_size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModelUpdateForm(BaseModel):
|
class EmbeddingModelUpdateForm(BaseModel):
|
||||||
@@ -295,9 +305,14 @@ async def update_embedding_config(
|
|||||||
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||||
|
|
||||||
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||||
if form_data.openai_config != None:
|
if form_data.openai_config is not None:
|
||||||
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||||
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
||||||
|
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
|
||||||
|
form_data.openai_config.batch_size
|
||||||
|
if form_data.openai_config.batch_size
|
||||||
|
else 1
|
||||||
|
)
|
||||||
|
|
||||||
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
||||||
|
|
||||||
@@ -307,6 +322,7 @@ async def update_embedding_config(
|
|||||||
app.state.sentence_transformer_ef,
|
app.state.sentence_transformer_ef,
|
||||||
app.state.config.OPENAI_API_KEY,
|
app.state.config.OPENAI_API_KEY,
|
||||||
app.state.config.OPENAI_API_BASE_URL,
|
app.state.config.OPENAI_API_BASE_URL,
|
||||||
|
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -316,6 +332,7 @@ async def update_embedding_config(
|
|||||||
"openai_config": {
|
"openai_config": {
|
||||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||||
"key": app.state.config.OPENAI_API_KEY,
|
"key": app.state.config.OPENAI_API_KEY,
|
||||||
|
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -379,6 +396,7 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
|||||||
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
||||||
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
||||||
"serper_api_key": app.state.config.SERPER_API_KEY,
|
"serper_api_key": app.state.config.SERPER_API_KEY,
|
||||||
|
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
||||||
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
},
|
},
|
||||||
@@ -406,6 +424,7 @@ class WebSearchConfig(BaseModel):
|
|||||||
serpstack_api_key: Optional[str] = None
|
serpstack_api_key: Optional[str] = None
|
||||||
serpstack_https: Optional[bool] = None
|
serpstack_https: Optional[bool] = None
|
||||||
serper_api_key: Optional[str] = None
|
serper_api_key: Optional[str] = None
|
||||||
|
serply_api_key: Optional[str] = None
|
||||||
result_count: Optional[int] = None
|
result_count: Optional[int] = None
|
||||||
concurrent_requests: Optional[int] = None
|
concurrent_requests: Optional[int] = None
|
||||||
|
|
||||||
@@ -456,6 +475,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
|||||||
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
|
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
|
||||||
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
|
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
|
||||||
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
|
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
|
||||||
|
app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
|
||||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
|
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
|
||||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
||||||
form_data.web.search.concurrent_requests
|
form_data.web.search.concurrent_requests
|
||||||
@@ -484,6 +504,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
|||||||
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
||||||
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
||||||
"serper_api_key": app.state.config.SERPER_API_KEY,
|
"serper_api_key": app.state.config.SERPER_API_KEY,
|
||||||
|
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
||||||
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
},
|
},
|
||||||
@@ -731,6 +752,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
|
|||||||
- BRAVE_SEARCH_API_KEY
|
- BRAVE_SEARCH_API_KEY
|
||||||
- SERPSTACK_API_KEY
|
- SERPSTACK_API_KEY
|
||||||
- SERPER_API_KEY
|
- SERPER_API_KEY
|
||||||
|
- SERPLY_API_KEY
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query (str): The query to search for
|
query (str): The query to search for
|
||||||
@@ -789,6 +811,15 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("No SERPER_API_KEY found in environment variables")
|
raise Exception("No SERPER_API_KEY found in environment variables")
|
||||||
|
elif engine == "serply":
|
||||||
|
if app.state.config.SERPLY_API_KEY:
|
||||||
|
return search_serply(
|
||||||
|
app.state.config.SERPLY_API_KEY,
|
||||||
|
query,
|
||||||
|
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception("No SERPLY_API_KEY found in environment variables")
|
||||||
else:
|
else:
|
||||||
raise Exception("No search engine API key found in environment variables")
|
raise Exception("No search engine API key found in environment variables")
|
||||||
|
|
||||||
@@ -796,6 +827,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
|
|||||||
@app.post("/web/search")
|
@app.post("/web/search")
|
||||||
def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
|
def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
|
||||||
try:
|
try:
|
||||||
|
logging.info(f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}")
|
||||||
web_results = search_web(
|
web_results = search_web(
|
||||||
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
|
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
|
||||||
)
|
)
|
||||||
@@ -866,6 +898,13 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
|||||||
texts = [doc.page_content for doc in docs]
|
texts = [doc.page_content for doc in docs]
|
||||||
metadatas = [doc.metadata for doc in docs]
|
metadatas = [doc.metadata for doc in docs]
|
||||||
|
|
||||||
|
# ChromaDB does not like datetime formats
|
||||||
|
# for meta-data so convert them to string.
|
||||||
|
for metadata in metadatas:
|
||||||
|
for key, value in metadata.items():
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
metadata[key] = str(value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if overwrite:
|
if overwrite:
|
||||||
for collection in CHROMA_CLIENT.list_collections():
|
for collection in CHROMA_CLIENT.list_collections():
|
||||||
@@ -881,6 +920,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
|||||||
app.state.sentence_transformer_ef,
|
app.state.sentence_transformer_ef,
|
||||||
app.state.config.OPENAI_API_KEY,
|
app.state.config.OPENAI_API_KEY,
|
||||||
app.state.config.OPENAI_API_BASE_URL,
|
app.state.config.OPENAI_API_BASE_URL,
|
||||||
|
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
||||||
@@ -951,6 +991,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
"swift",
|
"swift",
|
||||||
"vue",
|
"vue",
|
||||||
"svelte",
|
"svelte",
|
||||||
|
"msg",
|
||||||
]
|
]
|
||||||
|
|
||||||
if file_ext == "pdf":
|
if file_ext == "pdf":
|
||||||
@@ -985,6 +1026,8 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
] or file_ext in ["ppt", "pptx"]:
|
] or file_ext in ["ppt", "pptx"]:
|
||||||
loader = UnstructuredPowerPointLoader(file_path)
|
loader = UnstructuredPowerPointLoader(file_path)
|
||||||
|
elif file_ext == "msg":
|
||||||
|
loader = OutlookMessageLoader(file_path)
|
||||||
elif file_ext in known_source_ext or (
|
elif file_ext in known_source_ext or (
|
||||||
file_content_type and file_content_type.find("text/") >= 0
|
file_content_type and file_content_type.find("text/") >= 0
|
||||||
):
|
):
|
||||||
@@ -1150,6 +1193,30 @@ def reset_vector_db(user=Depends(get_admin_user)):
|
|||||||
CHROMA_CLIENT.reset()
|
CHROMA_CLIENT.reset()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/reset/uploads")
|
||||||
|
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
|
||||||
|
folder = f"{UPLOAD_DIR}"
|
||||||
|
try:
|
||||||
|
# Check if the directory exists
|
||||||
|
if os.path.exists(folder):
|
||||||
|
# Iterate over all the files and directories in the specified directory
|
||||||
|
for filename in os.listdir(folder):
|
||||||
|
file_path = os.path.join(folder, filename)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(file_path) or os.path.islink(file_path):
|
||||||
|
os.unlink(file_path) # Remove the file or link
|
||||||
|
elif os.path.isdir(file_path):
|
||||||
|
shutil.rmtree(file_path) # Remove the directory
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to delete {file_path}. Reason: {e}")
|
||||||
|
else:
|
||||||
|
print(f"The directory {folder} does not exist")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to process the directory {folder}. Reason: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@app.get("/reset")
|
@app.get("/reset")
|
||||||
def reset(user=Depends(get_admin_user)) -> bool:
|
def reset(user=Depends(get_admin_user)) -> bool:
|
||||||
folder = f"{UPLOAD_DIR}"
|
folder = f"{UPLOAD_DIR}"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from apps.rag.search.main import SearchResult
|
from apps.rag.search.main import SearchResult
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
@@ -9,20 +10,56 @@ log = logging.getLogger(__name__)
|
|||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
def search_searxng(query_url: str, query: str, count: int) -> list[SearchResult]:
|
def search_searxng(
|
||||||
"""Search a SearXNG instance for a query and return the results as a list of SearchResult objects.
|
query_url: str, query: str, count: int, **kwargs
|
||||||
|
) -> List[SearchResult]:
|
||||||
|
"""
|
||||||
|
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
The function allows passing additional parameters such as language or time_range to tailor the search result.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_url (str): The URL of the SearXNG instance to search. Must contain "<query>" as a placeholder
|
query_url (str): The base URL of the SearXNG server.
|
||||||
query (str): The query to search for
|
query (str): The search term or question to find in the SearXNG database.
|
||||||
"""
|
count (int): The maximum number of results to retrieve from the search.
|
||||||
url = query_url.replace("<query>", query)
|
|
||||||
if "&format=json" not in url:
|
|
||||||
url += "&format=json"
|
|
||||||
log.debug(f"searching {url}")
|
|
||||||
|
|
||||||
r = requests.get(
|
Keyword Args:
|
||||||
url,
|
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
||||||
|
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
||||||
|
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
||||||
|
|
||||||
|
Raise:
|
||||||
|
requests.exceptions.RequestException: If a request error occurs during the search process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Default values for optional parameters are provided as empty strings or None when not specified.
|
||||||
|
language = kwargs.get("language", "en-US")
|
||||||
|
time_range = kwargs.get("time_range", "")
|
||||||
|
categories = "".join(kwargs.get("categories", []))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"q": query,
|
||||||
|
"format": "json",
|
||||||
|
"pageno": 1,
|
||||||
|
"language": language,
|
||||||
|
"time_range": time_range,
|
||||||
|
"categories": categories,
|
||||||
|
"theme": "simple",
|
||||||
|
"image_proxy": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Legacy query format
|
||||||
|
if "<query>" in query_url:
|
||||||
|
# Strip all query parameters from the URL
|
||||||
|
query_url = query_url.split("?")[0]
|
||||||
|
|
||||||
|
log.debug(f"searching {query_url}")
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
query_url,
|
||||||
headers={
|
headers={
|
||||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||||
"Accept": "text/html",
|
"Accept": "text/html",
|
||||||
@@ -30,10 +67,12 @@ def search_searxng(query_url: str, query: str, count: int) -> list[SearchResult]
|
|||||||
"Accept-Language": "en-US,en;q=0.5",
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
},
|
},
|
||||||
|
params=params,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
json_response = r.json()
|
response.raise_for_status() # Raise an exception for HTTP errors.
|
||||||
|
|
||||||
|
json_response = response.json()
|
||||||
results = json_response.get("results", [])
|
results = json_response.get("results", [])
|
||||||
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
|
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from apps.rag.search.main import SearchResult
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
|
def search_serply(
|
||||||
|
api_key: str,
|
||||||
|
query: str,
|
||||||
|
count: int,
|
||||||
|
hl: str = "us",
|
||||||
|
limit: int = 10,
|
||||||
|
device_type: str = "desktop",
|
||||||
|
proxy_location: str = "US"
|
||||||
|
) -> list[SearchResult]:
|
||||||
|
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key (str): A serply.io API key
|
||||||
|
query (str): The query to search for
|
||||||
|
hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
|
||||||
|
limit (int): The maximum number of results to return [10-100, defaults to 10]
|
||||||
|
"""
|
||||||
|
log.info("Searching with Serply")
|
||||||
|
|
||||||
|
url = "https://api.serply.io/v1/search/"
|
||||||
|
|
||||||
|
query_payload = {
|
||||||
|
"q": query,
|
||||||
|
"language": "en",
|
||||||
|
"num": limit,
|
||||||
|
"gl": proxy_location.upper(),
|
||||||
|
"hl": hl.lower()
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{url}{urlencode(query_payload)}"
|
||||||
|
headers = {
|
||||||
|
"X-API-KEY": api_key,
|
||||||
|
"X-User-Agent": device_type,
|
||||||
|
"User-Agent": "open-webui",
|
||||||
|
"X-Proxy-Location": proxy_location
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.request("GET", url, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
json_response = response.json()
|
||||||
|
log.info(f"results from serply search: {json_response}")
|
||||||
|
|
||||||
|
results = sorted(
|
||||||
|
json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
SearchResult(
|
||||||
|
link=result["link"],
|
||||||
|
title=result.get("title"),
|
||||||
|
snippet=result.get("description"),
|
||||||
|
)
|
||||||
|
for result in results[:count]
|
||||||
|
]
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
{
|
||||||
|
"ads": [],
|
||||||
|
"ads_count": 0,
|
||||||
|
"answers": [],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Apple",
|
||||||
|
"link": "https://www.apple.com/",
|
||||||
|
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||||
|
"additional_links": [
|
||||||
|
{
|
||||||
|
"text": "AppleApplehttps://www.apple.com",
|
||||||
|
"href": "https://www.apple.com/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cite": {},
|
||||||
|
"subdomains": [
|
||||||
|
{
|
||||||
|
"title": "Support",
|
||||||
|
"link": "https://support.apple.com/",
|
||||||
|
"description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Store",
|
||||||
|
"link": "https://www.apple.com/store",
|
||||||
|
"description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Mac",
|
||||||
|
"link": "https://www.apple.com/mac/",
|
||||||
|
"description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "iPad",
|
||||||
|
"link": "https://www.apple.com/ipad/",
|
||||||
|
"description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Watch",
|
||||||
|
"link": "https://www.apple.com/watch/",
|
||||||
|
"description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"realPosition": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple",
|
||||||
|
"link": "https://www.apple.com/",
|
||||||
|
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||||
|
"additional_links": [
|
||||||
|
{
|
||||||
|
"text": "AppleApplehttps://www.apple.com",
|
||||||
|
"href": "https://www.apple.com/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cite": {},
|
||||||
|
"realPosition": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple Inc.",
|
||||||
|
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
|
||||||
|
"description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...",
|
||||||
|
"additional_links": [
|
||||||
|
{
|
||||||
|
"text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc",
|
||||||
|
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "History",
|
||||||
|
"href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "List of Apple products",
|
||||||
|
"href": "https://en.wikipedia.org/wiki/List_of_Apple_products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Litigation involving Apple Inc.",
|
||||||
|
"href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Apple Park",
|
||||||
|
"href": "https://en.wikipedia.org/wiki/Apple_Park"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cite": {
|
||||||
|
"domain": "https://en.wikipedia.org › wiki › Apple_Inc",
|
||||||
|
"span": " › wiki › Apple_Inc"
|
||||||
|
},
|
||||||
|
"realPosition": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple Inc. (AAPL) Company Profile & Facts",
|
||||||
|
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
|
||||||
|
"description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...",
|
||||||
|
"additional_links": [
|
||||||
|
{
|
||||||
|
"text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile",
|
||||||
|
"href": "https://finance.yahoo.com/quote/AAPL/profile/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cite": {
|
||||||
|
"domain": "https://finance.yahoo.com › quote › AAPL › profile",
|
||||||
|
"span": " › quote › AAPL › profile"
|
||||||
|
},
|
||||||
|
"realPosition": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple Inc - Company Profile and News",
|
||||||
|
"link": "https://www.bloomberg.com/profile/company/AAPL:US",
|
||||||
|
"description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...",
|
||||||
|
"additional_links": [
|
||||||
|
{
|
||||||
|
"text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US",
|
||||||
|
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cite": {
|
||||||
|
"domain": "https://www.bloomberg.com › company › AAPL:US",
|
||||||
|
"span": " › company › AAPL:US"
|
||||||
|
},
|
||||||
|
"realPosition": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple Inc. | History, Products, Headquarters, & Facts",
|
||||||
|
"link": "https://www.britannica.com/money/Apple-Inc",
|
||||||
|
"description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...",
|
||||||
|
"additional_links": [
|
||||||
|
{
|
||||||
|
"text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc",
|
||||||
|
"href": "https://www.britannica.com/money/Apple-Inc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"href": "https://www.britannica.com/money/Apple-Inc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cite": {
|
||||||
|
"domain": "https://www.britannica.com › money › Apple-Inc",
|
||||||
|
"span": " › money › Apple-Inc"
|
||||||
|
},
|
||||||
|
"realPosition": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shopping_ads": [],
|
||||||
|
"places": [
|
||||||
|
{
|
||||||
|
"title": "Apple Inc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple Inc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apple Inc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"related_searches": {
|
||||||
|
"images": [],
|
||||||
|
"text": [
|
||||||
|
{
|
||||||
|
"title": "apple inc full form",
|
||||||
|
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "apple company history",
|
||||||
|
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "apple store",
|
||||||
|
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "apple id",
|
||||||
|
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "apple inc industry",
|
||||||
|
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "apple login",
|
||||||
|
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"image_results": [],
|
||||||
|
"carousel": [],
|
||||||
|
"total": 2450000000,
|
||||||
|
"knowledge_graph": "",
|
||||||
|
"related_questions": [
|
||||||
|
"What does the Apple Inc do?",
|
||||||
|
"Why did Apple change to Apple Inc?",
|
||||||
|
"Who owns Apple Inc.?",
|
||||||
|
"What is Apple Inc best known for?"
|
||||||
|
],
|
||||||
|
"carousel_count": 0,
|
||||||
|
"ts": 2.491065263748169,
|
||||||
|
"device_type": null
|
||||||
|
}
|
||||||
+29
-59
@@ -2,7 +2,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
from apps.ollama.main import (
|
from apps.ollama.main import (
|
||||||
generate_ollama_embeddings,
|
generate_ollama_embeddings,
|
||||||
@@ -20,18 +20,8 @@ from langchain.retrievers import (
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from utils.misc import get_last_user_message, add_or_update_system_message
|
||||||
from config import (
|
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||||
SRC_LOG_LEVELS,
|
|
||||||
CHROMA_CLIENT,
|
|
||||||
SEARXNG_QUERY_URL,
|
|
||||||
GOOGLE_PSE_API_KEY,
|
|
||||||
GOOGLE_PSE_ENGINE_ID,
|
|
||||||
BRAVE_SEARCH_API_KEY,
|
|
||||||
SERPSTACK_API_KEY,
|
|
||||||
SERPSTACK_HTTPS,
|
|
||||||
SERPER_API_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
@@ -209,6 +199,7 @@ def get_embedding_function(
|
|||||||
embedding_function,
|
embedding_function,
|
||||||
openai_key,
|
openai_key,
|
||||||
openai_url,
|
openai_url,
|
||||||
|
batch_size,
|
||||||
):
|
):
|
||||||
if embedding_engine == "":
|
if embedding_engine == "":
|
||||||
return lambda query: embedding_function.encode(query).tolist()
|
return lambda query: embedding_function.encode(query).tolist()
|
||||||
@@ -232,6 +223,12 @@ def get_embedding_function(
|
|||||||
|
|
||||||
def generate_multiple(query, f):
|
def generate_multiple(query, f):
|
||||||
if isinstance(query, list):
|
if isinstance(query, list):
|
||||||
|
if embedding_engine == "openai":
|
||||||
|
embeddings = []
|
||||||
|
for i in range(0, len(query), batch_size):
|
||||||
|
embeddings.extend(f(query[i : i + batch_size]))
|
||||||
|
return embeddings
|
||||||
|
else:
|
||||||
return [f(q) for q in query]
|
return [f(q) for q in query]
|
||||||
else:
|
else:
|
||||||
return f(query)
|
return f(query)
|
||||||
@@ -250,31 +247,7 @@ def rag_messages(
|
|||||||
hybrid_search,
|
hybrid_search,
|
||||||
):
|
):
|
||||||
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
|
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
|
||||||
|
query = get_last_user_message(messages)
|
||||||
last_user_message_idx = None
|
|
||||||
for i in range(len(messages) - 1, -1, -1):
|
|
||||||
if messages[i]["role"] == "user":
|
|
||||||
last_user_message_idx = i
|
|
||||||
break
|
|
||||||
|
|
||||||
user_message = messages[last_user_message_idx]
|
|
||||||
|
|
||||||
if isinstance(user_message["content"], list):
|
|
||||||
# Handle list content input
|
|
||||||
content_type = "list"
|
|
||||||
query = ""
|
|
||||||
for content_item in user_message["content"]:
|
|
||||||
if content_item["type"] == "text":
|
|
||||||
query = content_item["text"]
|
|
||||||
break
|
|
||||||
elif isinstance(user_message["content"], str):
|
|
||||||
# Handle text content input
|
|
||||||
content_type = "text"
|
|
||||||
query = user_message["content"]
|
|
||||||
else:
|
|
||||||
# Fallback in case the input does not match expected types
|
|
||||||
content_type = None
|
|
||||||
query = ""
|
|
||||||
|
|
||||||
extracted_collections = []
|
extracted_collections = []
|
||||||
relevant_contexts = []
|
relevant_contexts = []
|
||||||
@@ -352,24 +325,7 @@ def rag_messages(
|
|||||||
)
|
)
|
||||||
|
|
||||||
log.debug(f"ra_content: {ra_content}")
|
log.debug(f"ra_content: {ra_content}")
|
||||||
|
messages = add_or_update_system_message(ra_content, messages)
|
||||||
if content_type == "list":
|
|
||||||
new_content = []
|
|
||||||
for content_item in user_message["content"]:
|
|
||||||
if content_item["type"] == "text":
|
|
||||||
# Update the text item's content with ra_content
|
|
||||||
new_content.append({"type": "text", "text": ra_content})
|
|
||||||
else:
|
|
||||||
# Keep other types of content as they are
|
|
||||||
new_content.append(content_item)
|
|
||||||
new_user_message = {**user_message, "content": new_content}
|
|
||||||
else:
|
|
||||||
new_user_message = {
|
|
||||||
**user_message,
|
|
||||||
"content": ra_content,
|
|
||||||
}
|
|
||||||
|
|
||||||
messages[last_user_message_idx] = new_user_message
|
|
||||||
|
|
||||||
return messages, citations
|
return messages, citations
|
||||||
|
|
||||||
@@ -413,8 +369,22 @@ def get_model_path(model: str, update_model: bool = False):
|
|||||||
|
|
||||||
|
|
||||||
def generate_openai_embeddings(
|
def generate_openai_embeddings(
|
||||||
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
|
model: str,
|
||||||
|
text: Union[str, list[str]],
|
||||||
|
key: str,
|
||||||
|
url: str = "https://api.openai.com/v1",
|
||||||
):
|
):
|
||||||
|
if isinstance(text, list):
|
||||||
|
embeddings = generate_openai_batch_embeddings(model, text, key, url)
|
||||||
|
else:
|
||||||
|
embeddings = generate_openai_batch_embeddings(model, [text], key, url)
|
||||||
|
|
||||||
|
return embeddings[0] if isinstance(text, str) else embeddings
|
||||||
|
|
||||||
|
|
||||||
|
def generate_openai_batch_embeddings(
|
||||||
|
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
|
||||||
|
) -> Optional[list[list[float]]]:
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{url}/embeddings",
|
f"{url}/embeddings",
|
||||||
@@ -422,12 +392,12 @@ def generate_openai_embeddings(
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": f"Bearer {key}",
|
"Authorization": f"Bearer {key}",
|
||||||
},
|
},
|
||||||
json={"input": text, "model": model},
|
json={"input": texts, "model": model},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if "data" in data:
|
if "data" in data:
|
||||||
return data["data"][0]["embedding"]
|
return [elem["embedding"] for elem in data["data"]]
|
||||||
else:
|
else:
|
||||||
raise "Something went wrong :/"
|
raise "Something went wrong :/"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import socketio
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
from apps.webui.models.users import Users
|
||||||
|
from utils.utils import decode_token
|
||||||
|
|
||||||
|
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
|
||||||
|
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
|
||||||
|
|
||||||
|
# Dictionary to maintain the user pool
|
||||||
|
|
||||||
|
SESSION_POOL = {}
|
||||||
|
USER_POOL = {}
|
||||||
|
USAGE_POOL = {}
|
||||||
|
# Timeout duration in seconds
|
||||||
|
TIMEOUT_DURATION = 3
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def connect(sid, environ, auth):
|
||||||
|
print("connect ", sid)
|
||||||
|
|
||||||
|
user = None
|
||||||
|
if auth and "token" in auth:
|
||||||
|
data = decode_token(auth["token"])
|
||||||
|
|
||||||
|
if data is not None and "id" in data:
|
||||||
|
user = Users.get_user_by_id(data["id"])
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||||
|
|
||||||
|
print(len(set(USER_POOL)))
|
||||||
|
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||||
|
await sio.emit("usage", {"models": get_models_in_use()})
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("user-join")
|
||||||
|
async def user_join(sid, data):
|
||||||
|
print("user-join", sid, data)
|
||||||
|
|
||||||
|
auth = data["auth"] if "auth" in data else None
|
||||||
|
|
||||||
|
if auth and "token" in auth:
|
||||||
|
data = decode_token(auth["token"])
|
||||||
|
|
||||||
|
if data is not None and "id" in data:
|
||||||
|
user = Users.get_user_by_id(data["id"])
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||||
|
|
||||||
|
print(len(set(USER_POOL)))
|
||||||
|
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("user-count")
|
||||||
|
async def user_count(sid):
|
||||||
|
print("user-count", sid)
|
||||||
|
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||||
|
|
||||||
|
|
||||||
|
def get_models_in_use():
|
||||||
|
# Aggregate all models in use
|
||||||
|
models_in_use = []
|
||||||
|
for model_id, data in USAGE_POOL.items():
|
||||||
|
models_in_use.append(model_id)
|
||||||
|
print(f"Models in use: {models_in_use}")
|
||||||
|
|
||||||
|
return models_in_use
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("usage")
|
||||||
|
async def usage(sid, data):
|
||||||
|
print(f'Received "usage" event from {sid}: {data}')
|
||||||
|
|
||||||
|
model_id = data["model"]
|
||||||
|
|
||||||
|
# Cancel previous callback if there is one
|
||||||
|
if model_id in USAGE_POOL:
|
||||||
|
USAGE_POOL[model_id]["callback"].cancel()
|
||||||
|
|
||||||
|
# Store the new usage data and task
|
||||||
|
|
||||||
|
if model_id in USAGE_POOL:
|
||||||
|
USAGE_POOL[model_id]["sids"].append(sid)
|
||||||
|
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||||
|
|
||||||
|
else:
|
||||||
|
USAGE_POOL[model_id] = {"sids": [sid]}
|
||||||
|
|
||||||
|
# Schedule a task to remove the usage data after TIMEOUT_DURATION
|
||||||
|
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
|
||||||
|
remove_after_timeout(sid, model_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Broadcast the usage data to all clients
|
||||||
|
await sio.emit("usage", {"models": get_models_in_use()})
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_after_timeout(sid, model_id):
|
||||||
|
try:
|
||||||
|
print("remove_after_timeout", sid, model_id)
|
||||||
|
await asyncio.sleep(TIMEOUT_DURATION)
|
||||||
|
if model_id in USAGE_POOL:
|
||||||
|
print(USAGE_POOL[model_id]["sids"])
|
||||||
|
USAGE_POOL[model_id]["sids"].remove(sid)
|
||||||
|
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||||
|
|
||||||
|
if len(USAGE_POOL[model_id]["sids"]) == 0:
|
||||||
|
del USAGE_POOL[model_id]
|
||||||
|
|
||||||
|
print(f"Removed usage data for {model_id} due to timeout")
|
||||||
|
# Broadcast the usage data to all clients
|
||||||
|
await sio.emit("usage", {"models": get_models_in_use()})
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Task was cancelled due to new 'usage' event
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def disconnect(sid):
|
||||||
|
if sid in SESSION_POOL:
|
||||||
|
user_id = SESSION_POOL[sid]
|
||||||
|
del SESSION_POOL[sid]
|
||||||
|
|
||||||
|
USER_POOL[user_id].remove(sid)
|
||||||
|
|
||||||
|
if len(USER_POOL[user_id]) == 0:
|
||||||
|
del USER_POOL[user_id]
|
||||||
|
|
||||||
|
print(f"user {user_id} disconnected with session ID {sid}")
|
||||||
|
print(USER_POOL)
|
||||||
|
|
||||||
|
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||||
|
else:
|
||||||
|
print(f"Unknown session ID {sid} disconnected")
|
||||||
@@ -14,6 +14,8 @@ from apps.webui.routers import (
|
|||||||
)
|
)
|
||||||
from config import (
|
from config import (
|
||||||
WEBUI_BUILD_HASH,
|
WEBUI_BUILD_HASH,
|
||||||
|
SHOW_ADMIN_DETAILS,
|
||||||
|
ADMIN_EMAIL,
|
||||||
WEBUI_AUTH,
|
WEBUI_AUTH,
|
||||||
DEFAULT_MODELS,
|
DEFAULT_MODELS,
|
||||||
DEFAULT_PROMPT_SUGGESTIONS,
|
DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
@@ -37,6 +39,11 @@ app.state.config = AppConfig()
|
|||||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||||
|
|
||||||
|
|
||||||
|
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||||
|
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
||||||
|
|
||||||
|
|
||||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||||
|
|||||||
@@ -298,6 +298,15 @@ class ChatTable:
|
|||||||
# .limit(limit).offset(skip)
|
# .limit(limit).offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||||
|
return [
|
||||||
|
ChatModel(**model_to_dict(chat))
|
||||||
|
for chat in Chat.select()
|
||||||
|
.where(Chat.archived == True)
|
||||||
|
.where(Chat.user_id == user_id)
|
||||||
|
.order_by(Chat.updated_at.desc())
|
||||||
|
]
|
||||||
|
|
||||||
def delete_chat_by_id(self, id: str) -> bool:
|
def delete_chat_by_id(self, id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Chat.delete().where((Chat.id == id))
|
query = Chat.delete().where((Chat.id == id))
|
||||||
|
|||||||
@@ -269,73 +269,88 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
|||||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetAdminDetails
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/details")
|
||||||
|
async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
||||||
|
if request.app.state.config.SHOW_ADMIN_DETAILS:
|
||||||
|
admin_email = request.app.state.config.ADMIN_EMAIL
|
||||||
|
admin_name = None
|
||||||
|
|
||||||
|
print(admin_email, admin_name)
|
||||||
|
|
||||||
|
if admin_email:
|
||||||
|
admin = Users.get_user_by_email(admin_email)
|
||||||
|
if admin:
|
||||||
|
admin_name = admin.name
|
||||||
|
else:
|
||||||
|
admin = Users.get_first_user()
|
||||||
|
if admin:
|
||||||
|
admin_email = admin.email
|
||||||
|
admin_name = admin.name
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": admin_name,
|
||||||
|
"email": admin_email,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# ToggleSignUp
|
# ToggleSignUp
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/signup/enabled", response_model=bool)
|
@router.get("/admin/config")
|
||||||
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
|
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return request.app.state.config.ENABLE_SIGNUP
|
return {
|
||||||
|
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||||
|
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||||
|
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||||
|
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||||
|
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/signup/enabled/toggle", response_model=bool)
|
class AdminConfig(BaseModel):
|
||||||
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
|
SHOW_ADMIN_DETAILS: bool
|
||||||
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
|
ENABLE_SIGNUP: bool
|
||||||
return request.app.state.config.ENABLE_SIGNUP
|
DEFAULT_USER_ROLE: str
|
||||||
|
JWT_EXPIRES_IN: str
|
||||||
|
ENABLE_COMMUNITY_SHARING: bool
|
||||||
|
|
||||||
|
|
||||||
############################
|
@router.post("/admin/config")
|
||||||
# Default User Role
|
async def update_admin_config(
|
||||||
############################
|
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/signup/user/role")
|
|
||||||
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
|
|
||||||
return request.app.state.config.DEFAULT_USER_ROLE
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleForm(BaseModel):
|
|
||||||
role: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/signup/user/role")
|
|
||||||
async def update_default_user_role(
|
|
||||||
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
|
|
||||||
):
|
):
|
||||||
if form_data.role in ["pending", "user", "admin"]:
|
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
|
||||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
|
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
|
||||||
return request.app.state.config.DEFAULT_USER_ROLE
|
|
||||||
|
|
||||||
|
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
|
||||||
|
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
|
||||||
|
|
||||||
############################
|
|
||||||
# JWT Expiration
|
|
||||||
############################
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/token/expires")
|
|
||||||
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
|
|
||||||
return request.app.state.config.JWT_EXPIRES_IN
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateJWTExpiresDurationForm(BaseModel):
|
|
||||||
duration: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/token/expires/update")
|
|
||||||
async def update_token_expires_duration(
|
|
||||||
request: Request,
|
|
||||||
form_data: UpdateJWTExpiresDurationForm,
|
|
||||||
user=Depends(get_admin_user),
|
|
||||||
):
|
|
||||||
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
|
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
|
||||||
|
|
||||||
# Check if the input string matches the pattern
|
# Check if the input string matches the pattern
|
||||||
if re.match(pattern, form_data.duration):
|
if re.match(pattern, form_data.JWT_EXPIRES_IN):
|
||||||
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
|
request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN
|
||||||
return request.app.state.config.JWT_EXPIRES_IN
|
|
||||||
else:
|
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
|
||||||
return request.app.state.config.JWT_EXPIRES_IN
|
form_data.ENABLE_COMMUNITY_SHARING
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||||
|
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||||
|
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||||
|
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||||
|
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -113,6 +113,19 @@ async def get_user_chats(user=Depends(get_current_user)):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetArchivedChats
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/all/archived", response_model=List[ChatResponse])
|
||||||
|
async def get_user_chats(user=Depends(get_current_user)):
|
||||||
|
return [
|
||||||
|
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
|
for chat in Chats.get_archived_chats_by_user_id(user.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetAllChatsInDB
|
# GetAllChatsInDB
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ from apps.webui.models.users import (
|
|||||||
from apps.webui.models.auths import Auths
|
from apps.webui.models.auths import Auths
|
||||||
from apps.webui.models.chats import Chats
|
from apps.webui.models.chats import Chats
|
||||||
|
|
||||||
from utils.utils import get_verified_user, get_password_hash, get_admin_user
|
from utils.utils import (
|
||||||
|
get_verified_user,
|
||||||
|
get_password_hash,
|
||||||
|
get_current_user,
|
||||||
|
get_admin_user,
|
||||||
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|||||||
+140
-16
@@ -306,7 +306,11 @@ STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
|
|||||||
|
|
||||||
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
|
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
|
||||||
if frontend_favicon.exists():
|
if frontend_favicon.exists():
|
||||||
|
try:
|
||||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"An error occurred: {e}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||||
|
|
||||||
@@ -601,6 +605,80 @@ WEBUI_BANNERS = PersistentConfig(
|
|||||||
[BannerModel(**banner) for banner in json.loads("[]")],
|
[BannerModel(**banner) for banner in json.loads("[]")],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SHOW_ADMIN_DETAILS = PersistentConfig(
|
||||||
|
"SHOW_ADMIN_DETAILS",
|
||||||
|
"auth.admin.show",
|
||||||
|
os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
ADMIN_EMAIL = PersistentConfig(
|
||||||
|
"ADMIN_EMAIL",
|
||||||
|
"auth.admin.email",
|
||||||
|
os.environ.get("ADMIN_EMAIL", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# TASKS
|
||||||
|
####################################
|
||||||
|
|
||||||
|
|
||||||
|
TASK_MODEL = PersistentConfig(
|
||||||
|
"TASK_MODEL",
|
||||||
|
"task.model.default",
|
||||||
|
os.environ.get("TASK_MODEL", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
TASK_MODEL_EXTERNAL = PersistentConfig(
|
||||||
|
"TASK_MODEL_EXTERNAL",
|
||||||
|
"task.model.external",
|
||||||
|
os.environ.get("TASK_MODEL_EXTERNAL", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||||
|
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||||
|
"task.title.prompt_template",
|
||||||
|
os.environ.get(
|
||||||
|
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||||
|
"""Here is the query:
|
||||||
|
{{prompt:middletruncate:8000}}
|
||||||
|
|
||||||
|
Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||||
|
|
||||||
|
Examples of titles:
|
||||||
|
📉 Stock Market Trends
|
||||||
|
🍪 Perfect Chocolate Chip Recipe
|
||||||
|
Evolution of Music Streaming
|
||||||
|
Remote Work Productivity Tips
|
||||||
|
Artificial Intelligence in Healthcare
|
||||||
|
🎮 Video Game Development Insights""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||||
|
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||||
|
"task.search.prompt_template",
|
||||||
|
os.environ.get(
|
||||||
|
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||||
|
"""You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}.
|
||||||
|
|
||||||
|
Question:
|
||||||
|
{{prompt:end:4000}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||||
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||||
|
"task.search.prompt_length_threshold",
|
||||||
|
os.environ.get(
|
||||||
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_SECRET_KEY
|
# WEBUI_SECRET_KEY
|
||||||
####################################
|
####################################
|
||||||
@@ -683,6 +761,12 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
|||||||
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
|
||||||
|
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
|
||||||
|
"rag.embedding_openai_batch_size",
|
||||||
|
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
|
||||||
|
)
|
||||||
|
|
||||||
RAG_RERANKING_MODEL = PersistentConfig(
|
RAG_RERANKING_MODEL = PersistentConfig(
|
||||||
"RAG_RERANKING_MODEL",
|
"RAG_RERANKING_MODEL",
|
||||||
"rag.reranking_model",
|
"rag.reranking_model",
|
||||||
@@ -832,6 +916,12 @@ SERPER_API_KEY = PersistentConfig(
|
|||||||
os.getenv("SERPER_API_KEY", ""),
|
os.getenv("SERPER_API_KEY", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SERPLY_API_KEY = PersistentConfig(
|
||||||
|
"SERPLY_API_KEY",
|
||||||
|
"rag.web.search.serply_api_key",
|
||||||
|
os.getenv("SERPLY_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
||||||
"RAG_WEB_SEARCH_RESULT_COUNT",
|
"RAG_WEB_SEARCH_RESULT_COUNT",
|
||||||
@@ -913,25 +1003,59 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
|
|||||||
# Audio
|
# Audio
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
|
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
"AUDIO_OPENAI_API_BASE_URL",
|
"AUDIO_STT_OPENAI_API_BASE_URL",
|
||||||
"audio.openai.api_base_url",
|
"audio.stt.openai.api_base_url",
|
||||||
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||||
)
|
)
|
||||||
AUDIO_OPENAI_API_KEY = PersistentConfig(
|
|
||||||
"AUDIO_OPENAI_API_KEY",
|
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
|
||||||
"audio.openai.api_key",
|
"AUDIO_STT_OPENAI_API_KEY",
|
||||||
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
|
"audio.stt.openai.api_key",
|
||||||
|
os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||||
)
|
)
|
||||||
AUDIO_OPENAI_API_MODEL = PersistentConfig(
|
|
||||||
"AUDIO_OPENAI_API_MODEL",
|
AUDIO_STT_ENGINE = PersistentConfig(
|
||||||
"audio.openai.api_model",
|
"AUDIO_STT_ENGINE",
|
||||||
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
|
"audio.stt.engine",
|
||||||
|
os.getenv("AUDIO_STT_ENGINE", ""),
|
||||||
)
|
)
|
||||||
AUDIO_OPENAI_API_VOICE = PersistentConfig(
|
|
||||||
"AUDIO_OPENAI_API_VOICE",
|
AUDIO_STT_MODEL = PersistentConfig(
|
||||||
"audio.openai.api_voice",
|
"AUDIO_STT_MODEL",
|
||||||
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
|
"audio.stt.model",
|
||||||
|
os.getenv("AUDIO_STT_MODEL", "whisper-1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
|
"AUDIO_TTS_OPENAI_API_BASE_URL",
|
||||||
|
"audio.tts.openai.api_base_url",
|
||||||
|
os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||||
|
)
|
||||||
|
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
||||||
|
"AUDIO_TTS_OPENAI_API_KEY",
|
||||||
|
"audio.tts.openai.api_key",
|
||||||
|
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AUDIO_TTS_ENGINE = PersistentConfig(
|
||||||
|
"AUDIO_TTS_ENGINE",
|
||||||
|
"audio.tts.engine",
|
||||||
|
os.getenv("AUDIO_TTS_ENGINE", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AUDIO_TTS_MODEL = PersistentConfig(
|
||||||
|
"AUDIO_TTS_MODEL",
|
||||||
|
"audio.tts.model",
|
||||||
|
os.getenv("AUDIO_TTS_MODEL", "tts-1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
AUDIO_TTS_VOICE = PersistentConfig(
|
||||||
|
"AUDIO_TTS_VOICE",
|
||||||
|
"audio.tts.voice",
|
||||||
|
os.getenv("AUDIO_TTS_VOICE", "alloy"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,3 +84,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
WEB_SEARCH_ERROR = (
|
WEB_SEARCH_ERROR = (
|
||||||
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
|
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OLLAMA_API_DISABLED = (
|
||||||
|
"The Ollama API is disabled. Please enable it to use this feature."
|
||||||
|
)
|
||||||
+310
-58
@@ -9,8 +9,11 @@ import logging
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import requests
|
import requests
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status
|
from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -20,15 +23,26 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
|
|||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.responses import StreamingResponse, Response
|
from starlette.responses import StreamingResponse, Response
|
||||||
|
|
||||||
from apps.ollama.main import app as ollama_app, get_all_models as get_ollama_models
|
|
||||||
from apps.openai.main import app as openai_app, get_all_models as get_openai_models
|
from apps.socket.main import app as socket_app
|
||||||
|
from apps.ollama.main import (
|
||||||
|
app as ollama_app,
|
||||||
|
OpenAIChatCompletionForm,
|
||||||
|
get_all_models as get_ollama_models,
|
||||||
|
generate_openai_chat_completion as generate_ollama_chat_completion,
|
||||||
|
)
|
||||||
|
from apps.openai.main import (
|
||||||
|
app as openai_app,
|
||||||
|
get_all_models as get_openai_models,
|
||||||
|
generate_chat_completion as generate_openai_chat_completion,
|
||||||
|
)
|
||||||
|
|
||||||
from apps.audio.main import app as audio_app
|
from apps.audio.main import app as audio_app
|
||||||
from apps.images.main import app as images_app
|
from apps.images.main import app as images_app
|
||||||
from apps.rag.main import app as rag_app
|
from apps.rag.main import app as rag_app
|
||||||
from apps.webui.main import app as webui_app
|
from apps.webui.main import app as webui_app
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
@@ -39,6 +53,8 @@ from utils.utils import (
|
|||||||
get_current_user,
|
get_current_user,
|
||||||
get_http_authorization_cred,
|
get_http_authorization_cred,
|
||||||
)
|
)
|
||||||
|
from utils.task import title_generation_template, search_query_generation_template
|
||||||
|
|
||||||
from apps.rag.utils import rag_messages
|
from apps.rag.utils import rag_messages
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
@@ -60,8 +76,13 @@ from config import (
|
|||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
WEBHOOK_URL,
|
WEBHOOK_URL,
|
||||||
ENABLE_ADMIN_EXPORT,
|
ENABLE_ADMIN_EXPORT,
|
||||||
AppConfig,
|
|
||||||
WEBUI_BUILD_HASH,
|
WEBUI_BUILD_HASH,
|
||||||
|
TASK_MODEL,
|
||||||
|
TASK_MODEL_EXTERNAL,
|
||||||
|
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||||
|
AppConfig,
|
||||||
)
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
@@ -115,10 +136,19 @@ app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
|||||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
|
||||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
|
||||||
|
|
||||||
|
app.state.config.TASK_MODEL = TASK_MODEL
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
||||||
|
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
|
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||||
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||||
|
)
|
||||||
|
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||||
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||||
|
)
|
||||||
|
|
||||||
app.state.MODELS = {}
|
app.state.MODELS = {}
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
@@ -226,22 +256,9 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
|||||||
app.add_middleware(RAGMiddleware)
|
app.add_middleware(RAGMiddleware)
|
||||||
|
|
||||||
|
|
||||||
class PipelineMiddleware(BaseHTTPMiddleware):
|
def filter_pipeline(payload, user):
|
||||||
async def dispatch(self, request: Request, call_next):
|
user = {"id": user.id, "name": user.name, "role": user.role}
|
||||||
if request.method == "POST" and (
|
model_id = payload["model"]
|
||||||
"/ollama/api/chat" in request.url.path
|
|
||||||
or "/chat/completions" in request.url.path
|
|
||||||
):
|
|
||||||
log.debug(f"request.url.path: {request.url.path}")
|
|
||||||
|
|
||||||
# Read the original request body
|
|
||||||
body = await request.body()
|
|
||||||
# Decode body to string
|
|
||||||
body_str = body.decode("utf-8")
|
|
||||||
# Parse string to JSON
|
|
||||||
data = json.loads(body_str) if body_str else {}
|
|
||||||
|
|
||||||
model_id = data["model"]
|
|
||||||
filters = [
|
filters = [
|
||||||
model
|
model
|
||||||
for model in app.state.MODELS.values()
|
for model in app.state.MODELS.values()
|
||||||
@@ -258,18 +275,6 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
|||||||
]
|
]
|
||||||
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
|
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
|
||||||
|
|
||||||
user = None
|
|
||||||
if len(sorted_filters) > 0:
|
|
||||||
try:
|
|
||||||
user = get_current_user(
|
|
||||||
get_http_authorization_cred(
|
|
||||||
request.headers.get("Authorization")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
user = {"id": user.id, "name": user.name, "role": user.role}
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
model = app.state.MODELS[model_id]
|
model = app.state.MODELS[model_id]
|
||||||
|
|
||||||
if "pipeline" in model:
|
if "pipeline" in model:
|
||||||
@@ -290,12 +295,12 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
"user": user,
|
"user": user,
|
||||||
"body": data,
|
"body": payload,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
payload = r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
print(f"Connection error: {e}")
|
||||||
@@ -315,11 +320,33 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if "pipeline" not in app.state.MODELS[model_id]:
|
if "pipeline" not in app.state.MODELS[model_id]:
|
||||||
if "chat_id" in data:
|
if "chat_id" in payload:
|
||||||
del data["chat_id"]
|
del payload["chat_id"]
|
||||||
|
|
||||||
if "title" in data:
|
if "title" in payload:
|
||||||
del data["title"]
|
del payload["title"]
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
if request.method == "POST" and (
|
||||||
|
"/ollama/api/chat" in request.url.path
|
||||||
|
or "/chat/completions" in request.url.path
|
||||||
|
):
|
||||||
|
log.debug(f"request.url.path: {request.url.path}")
|
||||||
|
|
||||||
|
# Read the original request body
|
||||||
|
body = await request.body()
|
||||||
|
# Decode body to string
|
||||||
|
body_str = body.decode("utf-8")
|
||||||
|
# Parse string to JSON
|
||||||
|
data = json.loads(body_str) if body_str else {}
|
||||||
|
|
||||||
|
user = get_current_user(
|
||||||
|
get_http_authorization_cred(request.headers.get("Authorization"))
|
||||||
|
)
|
||||||
|
data = filter_pipeline(data, user)
|
||||||
|
|
||||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||||
# Replace the request body with the modified one
|
# Replace the request body with the modified one
|
||||||
@@ -376,6 +403,9 @@ async def update_embedding_function(request: Request, call_next):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
app.mount("/ws", socket_app)
|
||||||
|
|
||||||
|
|
||||||
app.mount("/ollama", ollama_app)
|
app.mount("/ollama", ollama_app)
|
||||||
app.mount("/openai", openai_app)
|
app.mount("/openai", openai_app)
|
||||||
|
|
||||||
@@ -477,6 +507,178 @@ async def get_models(user=Depends(get_verified_user)):
|
|||||||
return {"data": models}
|
return {"data": models}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/task/config")
|
||||||
|
async def get_task_config(user=Depends(get_verified_user)):
|
||||||
|
return {
|
||||||
|
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||||
|
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaskConfigForm(BaseModel):
|
||||||
|
TASK_MODEL: Optional[str]
|
||||||
|
TASK_MODEL_EXTERNAL: Optional[str]
|
||||||
|
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||||
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
||||||
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/task/config/update")
|
||||||
|
async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.config.TASK_MODEL = form_data.TASK_MODEL
|
||||||
|
app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
|
||||||
|
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
||||||
|
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
|
)
|
||||||
|
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||||
|
form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||||
|
)
|
||||||
|
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||||
|
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||||
|
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
|
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/task/title/completions")
|
||||||
|
async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
print("generate_title")
|
||||||
|
|
||||||
|
model_id = form_data["model"]
|
||||||
|
if model_id not in app.state.MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user has a custom task model
|
||||||
|
# If the user has a custom task model, use that model
|
||||||
|
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||||
|
if app.state.config.TASK_MODEL:
|
||||||
|
task_model_id = app.state.config.TASK_MODEL
|
||||||
|
if task_model_id in app.state.MODELS:
|
||||||
|
model_id = task_model_id
|
||||||
|
else:
|
||||||
|
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||||
|
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||||
|
if task_model_id in app.state.MODELS:
|
||||||
|
model_id = task_model_id
|
||||||
|
|
||||||
|
print(model_id)
|
||||||
|
model = app.state.MODELS[model_id]
|
||||||
|
|
||||||
|
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
|
|
||||||
|
content = title_generation_template(
|
||||||
|
template, form_data["prompt"], user.model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model_id,
|
||||||
|
"messages": [{"role": "user", "content": content}],
|
||||||
|
"stream": False,
|
||||||
|
"max_tokens": 50,
|
||||||
|
"chat_id": form_data.get("chat_id", None),
|
||||||
|
"title": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
print(payload)
|
||||||
|
payload = filter_pipeline(payload, user)
|
||||||
|
|
||||||
|
if model["owned_by"] == "ollama":
|
||||||
|
return await generate_ollama_chat_completion(
|
||||||
|
OpenAIChatCompletionForm(**payload), user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await generate_openai_chat_completion(payload, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/task/query/completions")
|
||||||
|
async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
print("generate_search_query")
|
||||||
|
|
||||||
|
if len(form_data["prompt"]) < app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Skip search query generation for short prompts (< {app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} characters)",
|
||||||
|
)
|
||||||
|
|
||||||
|
model_id = form_data["model"]
|
||||||
|
if model_id not in app.state.MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Model not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user has a custom task model
|
||||||
|
# If the user has a custom task model, use that model
|
||||||
|
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||||
|
if app.state.config.TASK_MODEL:
|
||||||
|
task_model_id = app.state.config.TASK_MODEL
|
||||||
|
if task_model_id in app.state.MODELS:
|
||||||
|
model_id = task_model_id
|
||||||
|
else:
|
||||||
|
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||||
|
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||||
|
if task_model_id in app.state.MODELS:
|
||||||
|
model_id = task_model_id
|
||||||
|
|
||||||
|
print(model_id)
|
||||||
|
model = app.state.MODELS[model_id]
|
||||||
|
|
||||||
|
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||||
|
|
||||||
|
content = search_query_generation_template(
|
||||||
|
template, form_data["prompt"], user.model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model_id,
|
||||||
|
"messages": [{"role": "user", "content": content}],
|
||||||
|
"stream": False,
|
||||||
|
"max_tokens": 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
print(payload)
|
||||||
|
payload = filter_pipeline(payload, user)
|
||||||
|
|
||||||
|
if model["owned_by"] == "ollama":
|
||||||
|
return await generate_ollama_chat_completion(
|
||||||
|
OpenAIChatCompletionForm(**payload), user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await generate_openai_chat_completion(payload, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/chat/completions")
|
||||||
|
async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
|
||||||
|
model_id = form_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]
|
||||||
|
print(model)
|
||||||
|
|
||||||
|
if model["owned_by"] == "ollama":
|
||||||
|
return await generate_ollama_chat_completion(
|
||||||
|
OpenAIChatCompletionForm(**form_data), user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await generate_openai_chat_completion(form_data, user=user)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/chat/completed")
|
@app.post("/api/chat/completed")
|
||||||
async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
||||||
data = form_data
|
data = form_data
|
||||||
@@ -569,6 +771,63 @@ async def get_pipelines_list(user=Depends(get_admin_user)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/pipelines/upload")
|
||||||
|
async def upload_pipeline(
|
||||||
|
urlIdx: int = Form(...), file: UploadFile = File(...), user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
print("upload_pipeline", urlIdx, file.filename)
|
||||||
|
# Check if the uploaded file is a python file
|
||||||
|
if not file.filename.endswith(".py"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Only Python (.py) files are allowed.",
|
||||||
|
)
|
||||||
|
|
||||||
|
upload_folder = f"{CACHE_DIR}/pipelines"
|
||||||
|
os.makedirs(upload_folder, exist_ok=True)
|
||||||
|
file_path = os.path.join(upload_folder, file.filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Save the uploaded file
|
||||||
|
with open(file_path, "wb") as buffer:
|
||||||
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
|
||||||
|
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||||
|
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
|
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
files = {"file": f}
|
||||||
|
r = requests.post(f"{url}/pipelines/upload", headers=headers, files=files)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
return {**data}
|
||||||
|
except Exception as e:
|
||||||
|
# Handle connection error here
|
||||||
|
print(f"Connection error: {e}")
|
||||||
|
|
||||||
|
detail = "Pipeline not found"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "detail" in res:
|
||||||
|
detail = res["detail"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Ensure the file is deleted after the upload is completed or on failure
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
|
||||||
class AddPipelineForm(BaseModel):
|
class AddPipelineForm(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
urlIdx: int
|
urlIdx: int
|
||||||
@@ -835,6 +1094,15 @@ async def get_app_config():
|
|||||||
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
|
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||||
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
||||||
},
|
},
|
||||||
|
"audio": {
|
||||||
|
"tts": {
|
||||||
|
"engine": audio_app.state.config.TTS_ENGINE,
|
||||||
|
"voice": audio_app.state.config.TTS_VOICE,
|
||||||
|
},
|
||||||
|
"stt": {
|
||||||
|
"engine": audio_app.state.config.STT_ENGINE,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -879,23 +1147,7 @@ class UrlForm(BaseModel):
|
|||||||
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||||
app.state.config.WEBHOOK_URL = form_data.url
|
app.state.config.WEBHOOK_URL = form_data.url
|
||||||
webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
|
webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
|
||||||
|
return {"url": app.state.config.WEBHOOK_URL}
|
||||||
return {
|
|
||||||
"url": app.state.config.WEBHOOK_URL,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/community_sharing", response_model=bool)
|
|
||||||
async def get_community_sharing_status(request: Request, user=Depends(get_admin_user)):
|
|
||||||
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/community_sharing/toggle", response_model=bool)
|
|
||||||
async def toggle_community_sharing(request: Request, user=Depends(get_admin_user)):
|
|
||||||
webui_app.state.config.ENABLE_COMMUNITY_SHARING = (
|
|
||||||
not webui_app.state.config.ENABLE_COMMUNITY_SHARING
|
|
||||||
)
|
|
||||||
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/version")
|
@app.get("/api/version")
|
||||||
@@ -913,7 +1165,7 @@ async def get_app_changelog():
|
|||||||
@app.get("/api/version/updates")
|
@app.get("/api/version/updates")
|
||||||
async def get_app_latest_release_version():
|
async def get_app_latest_release_version():
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
"https://api.github.com/repos/open-webui/open-webui/releases/latest"
|
"https://api.github.com/repos/open-webui/open-webui/releases/latest"
|
||||||
) as response:
|
) as response:
|
||||||
|
|||||||
@@ -57,3 +57,6 @@ black==24.4.2
|
|||||||
langfuse==2.33.0
|
langfuse==2.33.0
|
||||||
youtube-transcript-api==0.6.2
|
youtube-transcript-api==0.6.2
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
|
|
||||||
|
extract_msg
|
||||||
|
pydub
|
||||||
+2
-2
@@ -20,12 +20,12 @@ if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
|||||||
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
|
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
|
if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then
|
||||||
echo "USE_OLLAMA is set to true, starting ollama serve."
|
echo "USE_OLLAMA is set to true, starting ollama serve."
|
||||||
ollama serve &
|
ollama serve &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$USE_CUDA_DOCKER" = "true" ]; then
|
if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
|
||||||
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
||||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+71
-2
@@ -3,7 +3,48 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_user_message(messages: List[dict]) -> str:
|
||||||
|
for message in reversed(messages):
|
||||||
|
if message["role"] == "user":
|
||||||
|
if isinstance(message["content"], list):
|
||||||
|
for item in message["content"]:
|
||||||
|
if item["type"] == "text":
|
||||||
|
return item["text"]
|
||||||
|
return message["content"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_assistant_message(messages: List[dict]) -> 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 None
|
||||||
|
|
||||||
|
|
||||||
|
def add_or_update_system_message(content: str, messages: List[dict]):
|
||||||
|
"""
|
||||||
|
Adds a new system message at the beginning of the messages list
|
||||||
|
or updates the existing system message at the beginning.
|
||||||
|
|
||||||
|
:param msg: The message to be added or appended.
|
||||||
|
:param messages: The list of message dictionaries.
|
||||||
|
:return: The updated list of message dictionaries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if messages and messages[0].get("role") == "system":
|
||||||
|
messages[0]["content"] += f"{content}\n{messages[0]['content']}"
|
||||||
|
else:
|
||||||
|
# Insert at the beginning
|
||||||
|
messages.insert(0, {"role": "system", "content": content})
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
def get_gravatar_url(email):
|
def get_gravatar_url(email):
|
||||||
@@ -123,11 +164,25 @@ def parse_ollama_modelfile(model_text):
|
|||||||
"repeat_penalty": float,
|
"repeat_penalty": float,
|
||||||
"temperature": float,
|
"temperature": float,
|
||||||
"seed": int,
|
"seed": int,
|
||||||
"stop": str,
|
|
||||||
"tfs_z": float,
|
"tfs_z": float,
|
||||||
"num_predict": int,
|
"num_predict": int,
|
||||||
"top_k": int,
|
"top_k": int,
|
||||||
"top_p": float,
|
"top_p": float,
|
||||||
|
"num_keep": int,
|
||||||
|
"typical_p": float,
|
||||||
|
"presence_penalty": float,
|
||||||
|
"frequency_penalty": float,
|
||||||
|
"penalize_newline": bool,
|
||||||
|
"numa": bool,
|
||||||
|
"num_batch": int,
|
||||||
|
"num_gpu": int,
|
||||||
|
"main_gpu": int,
|
||||||
|
"low_vram": bool,
|
||||||
|
"f16_kv": bool,
|
||||||
|
"vocab_only": bool,
|
||||||
|
"use_mmap": bool,
|
||||||
|
"use_mlock": bool,
|
||||||
|
"num_thread": int,
|
||||||
}
|
}
|
||||||
|
|
||||||
data = {"base_model_id": None, "params": {}}
|
data = {"base_model_id": None, "params": {}}
|
||||||
@@ -156,10 +211,18 @@ def parse_ollama_modelfile(model_text):
|
|||||||
param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
|
param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
|
||||||
if param_match:
|
if param_match:
|
||||||
value = param_match.group(1)
|
value = param_match.group(1)
|
||||||
|
|
||||||
|
try:
|
||||||
if param_type == int:
|
if param_type == int:
|
||||||
value = int(value)
|
value = int(value)
|
||||||
elif param_type == float:
|
elif param_type == float:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
|
elif param_type == bool:
|
||||||
|
value = value.lower() == "true"
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
continue
|
||||||
|
|
||||||
data["params"][param] = value
|
data["params"][param] = value
|
||||||
|
|
||||||
# Parse adapter
|
# Parse adapter
|
||||||
@@ -171,8 +234,14 @@ def parse_ollama_modelfile(model_text):
|
|||||||
system_desc_match = re.search(
|
system_desc_match = re.search(
|
||||||
r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
|
r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
system_desc_match_single = re.search(
|
||||||
|
r"SYSTEM\s+([^\n]+)", model_text, re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
if system_desc_match:
|
if system_desc_match:
|
||||||
data["params"]["system"] = system_desc_match.group(1).strip()
|
data["params"]["system"] = system_desc_match.group(1).strip()
|
||||||
|
elif system_desc_match_single:
|
||||||
|
data["params"]["system"] = system_desc_match_single.group(1).strip()
|
||||||
|
|
||||||
# Parse messages
|
# Parse messages
|
||||||
messages = []
|
messages = []
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
|
|
||||||
|
|
||||||
|
|
||||||
def get_model_id_from_custom_model_id(id: str):
|
|
||||||
model = Models.get_model_by_id(id)
|
|
||||||
|
|
||||||
if model:
|
|
||||||
return model.id
|
|
||||||
else:
|
|
||||||
return id
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import re
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_template(
|
||||||
|
template: str, user_name: str = None, current_location: str = None
|
||||||
|
) -> str:
|
||||||
|
# Get the current date
|
||||||
|
current_date = datetime.now()
|
||||||
|
|
||||||
|
# Format the date to YYYY-MM-DD
|
||||||
|
formatted_date = current_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Replace {{CURRENT_DATE}} in the template with the formatted date
|
||||||
|
template = template.replace("{{CURRENT_DATE}}", formatted_date)
|
||||||
|
|
||||||
|
if user_name:
|
||||||
|
# Replace {{USER_NAME}} in the template with the user's name
|
||||||
|
template = template.replace("{{USER_NAME}}", user_name)
|
||||||
|
|
||||||
|
if current_location:
|
||||||
|
# Replace {{CURRENT_LOCATION}} in the template with the current location
|
||||||
|
template = template.replace("{{CURRENT_LOCATION}}", current_location)
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def title_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)
|
||||||
|
end_length = match.group(2)
|
||||||
|
middle_length = match.group(3)
|
||||||
|
|
||||||
|
if full_match == "{{prompt}}":
|
||||||
|
return prompt
|
||||||
|
elif start_length is not None:
|
||||||
|
return prompt[: int(start_length)]
|
||||||
|
elif end_length is not None:
|
||||||
|
return prompt[-int(end_length) :]
|
||||||
|
elif middle_length is not None:
|
||||||
|
middle_length = int(middle_length)
|
||||||
|
if len(prompt) <= middle_length:
|
||||||
|
return prompt
|
||||||
|
start = prompt[: math.ceil(middle_length / 2)]
|
||||||
|
end = prompt[-math.floor(middle_length / 2) :]
|
||||||
|
return f"{start}...{end}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
template = re.sub(
|
||||||
|
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||||
|
replacement_function,
|
||||||
|
template,
|
||||||
|
)
|
||||||
|
|
||||||
|
template = prompt_template(
|
||||||
|
template,
|
||||||
|
**(
|
||||||
|
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||||
|
if user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return 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)
|
||||||
|
end_length = match.group(2)
|
||||||
|
middle_length = match.group(3)
|
||||||
|
|
||||||
|
if full_match == "{{prompt}}":
|
||||||
|
return prompt
|
||||||
|
elif start_length is not None:
|
||||||
|
return prompt[: int(start_length)]
|
||||||
|
elif end_length is not None:
|
||||||
|
return prompt[-int(end_length) :]
|
||||||
|
elif middle_length is not None:
|
||||||
|
middle_length = int(middle_length)
|
||||||
|
if len(prompt) <= middle_length:
|
||||||
|
return prompt
|
||||||
|
start = prompt[: math.ceil(middle_length / 2)]
|
||||||
|
end = prompt[-math.floor(middle_length / 2) :]
|
||||||
|
return f"{start}...{end}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
template = re.sub(
|
||||||
|
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||||
|
replacement_function,
|
||||||
|
template,
|
||||||
|
)
|
||||||
|
|
||||||
|
template = prompt_template(
|
||||||
|
template,
|
||||||
|
**(
|
||||||
|
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||||
|
if user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return template
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 4.1 MiB |
@@ -41,10 +41,11 @@ Looking to contribute? Great! Here's how you can help:
|
|||||||
|
|
||||||
We welcome pull requests. Before submitting one, please:
|
We welcome pull requests. Before submitting one, please:
|
||||||
|
|
||||||
1. Discuss your idea or issue in the [issues section](https://github.com/open-webui/open-webui/issues).
|
1. Open a discussion regarding your ideas [here](https://github.com/open-webui/open-webui/discussions/new/choose).
|
||||||
2. Follow the project's coding standards and include tests for new features.
|
2. Follow the project's coding standards and include tests for new features.
|
||||||
3. Update documentation as necessary.
|
3. Update documentation as necessary.
|
||||||
4. Write clear, descriptive commit messages.
|
4. Write clear, descriptive commit messages.
|
||||||
|
5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward.
|
||||||
|
|
||||||
### 📚 Documentation & Tutorials
|
### 📚 Documentation & Tutorials
|
||||||
|
|
||||||
|
|||||||
Generated
+82
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.2.2",
|
"version": "0.3.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.2.2",
|
"version": "0.3.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pyscript/core": "^0.4.32",
|
"@pyscript/core": "^0.4.32",
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^10.9.1",
|
"mermaid": "^10.9.1",
|
||||||
"pyodide": "^0.26.0-alpha.4",
|
"pyodide": "^0.26.0-alpha.4",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
@@ -1214,6 +1215,11 @@
|
|||||||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||||
|
},
|
||||||
"node_modules/@sveltejs/adapter-auto": {
|
"node_modules/@sveltejs/adapter-auto": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz",
|
||||||
@@ -3800,6 +3806,46 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||||
|
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.11.0",
|
||||||
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/ws": {
|
||||||
|
"version": "8.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||||
|
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enquirer": {
|
"node_modules/enquirer": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
||||||
@@ -7949,6 +7995,32 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
|
||||||
|
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sorcery": {
|
"node_modules/sorcery": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
||||||
@@ -10142,6 +10214,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.2.2",
|
"version": "0.3.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^10.9.1",
|
"mermaid": "^10.9.1",
|
||||||
"pyodide": "^0.26.0-alpha.4",
|
"pyodide": "^0.26.0-alpha.4",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
|
|||||||
+18
-9
@@ -59,15 +59,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="splash-screen"
|
id="splash-screen"
|
||||||
style="
|
style="position: fixed; z-index: 100; top: 0; left: 0; width: 100%; height: 100%"
|
||||||
position: fixed;
|
|
||||||
z-index: 100;
|
|
||||||
background: #fff;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<style type="text/css" nonce="">
|
<style type="text/css" nonce="">
|
||||||
html {
|
html {
|
||||||
@@ -93,3 +85,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
<style type="text/css" nonce="">
|
||||||
|
html {
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-screen {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
html.dark #splash-screen {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark #splash-screen img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -98,7 +98,7 @@ export const synthesizeOpenAISpeech = async (
|
|||||||
token: string = '',
|
token: string = '',
|
||||||
speaker: string = 'alloy',
|
speaker: string = 'alloy',
|
||||||
text: string = '',
|
text: string = '',
|
||||||
model: string = 'tts-1'
|
model?: string
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -109,9 +109,9 @@ export const synthesizeOpenAISpeech = async (
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: model,
|
|
||||||
input: text,
|
input: text,
|
||||||
voice: speaker
|
voice: speaker,
|
||||||
|
...(model && { model })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|||||||
@@ -1,5 +1,87 @@
|
|||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export const getAdminDetails = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/details`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAdminConfig = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateAdminConfig = async (token: string, body: object) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
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);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getSessionUser = async (token: string) => {
|
export const getSessionUser = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,37 @@ export const getAllChats = async (token: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAllArchivedChats = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/archived`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAllUserChats = async (token: string) => {
|
export const getAllUserChats = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,147 @@ export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTaskConfig = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/task/config`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTaskConfig = async (token: string, config: object) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/task/config/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
})
|
||||||
|
.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 generateTitle = async (
|
||||||
|
token: string = '',
|
||||||
|
model: string,
|
||||||
|
prompt: string,
|
||||||
|
chat_id?: string
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/task/title/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
prompt: prompt,
|
||||||
|
...(chat_id && { chat_id: chat_id })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateSearchQuery = async (
|
||||||
|
token: string = '',
|
||||||
|
model: string,
|
||||||
|
messages: object[],
|
||||||
|
prompt: string
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/task/query/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: messages,
|
||||||
|
prompt: prompt
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? prompt;
|
||||||
|
};
|
||||||
|
|
||||||
export const getPipelinesList = async (token: string = '') => {
|
export const getPipelinesList = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -133,6 +274,43 @@ export const getPipelinesList = async (token: string = '') => {
|
|||||||
return pipelines;
|
return pipelines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadPipeline = async (token: string, file: File, urlIdx: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
// Create a new FormData object to handle the file upload
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('urlIdx', urlIdx);
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
// 'Content-Type': 'multipart/form-data' is not needed as Fetch API will set it automatically
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.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 downloadPipeline = async (token: string, url: string, urlIdx: string) => {
|
export const downloadPipeline = async (token: string, url: string, urlIdx: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -369,10 +369,17 @@ export const generateChatCompletion = async (token: string = '', body: object) =
|
|||||||
return [res, controller];
|
return [res, controller];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createModel = async (token: string, tagName: string, content: string) => {
|
export const createModel = async (
|
||||||
|
token: string,
|
||||||
|
tagName: string,
|
||||||
|
content: string,
|
||||||
|
urlIdx: string | null = null
|
||||||
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, {
|
const res = await fetch(
|
||||||
|
`${OLLAMA_API_BASE_URL}/api/create${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||||
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -383,7 +390,8 @@ export const createModel = async (token: string, tagName: string, content: strin
|
|||||||
name: tagName,
|
name: tagName,
|
||||||
modelfile: content
|
modelfile: content
|
||||||
})
|
})
|
||||||
}).catch((err) => {
|
}
|
||||||
|
).catch((err) => {
|
||||||
error = err;
|
error = err;
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -359,6 +359,32 @@ export const scanDocs = async (token: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const resetUploadDir = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/reset/uploads`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const resetVectorDB = async (token: string) => {
|
export const resetVectorDB = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -415,6 +441,7 @@ export const getEmbeddingConfig = async (token: string) => {
|
|||||||
type OpenAIConfigForm = {
|
type OpenAIConfigForm = {
|
||||||
key: string;
|
key: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
batch_size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EmbeddingModelUpdateForm = {
|
type EmbeddingModelUpdateForm = {
|
||||||
|
|||||||
@@ -0,0 +1,390 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext, tick } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import Database from './Settings/Database.svelte';
|
||||||
|
|
||||||
|
import General from './Settings/General.svelte';
|
||||||
|
import Users from './Settings/Users.svelte';
|
||||||
|
|
||||||
|
import Pipelines from './Settings/Pipelines.svelte';
|
||||||
|
import Audio from './Settings/Audio.svelte';
|
||||||
|
import Images from './Settings/Images.svelte';
|
||||||
|
import Interface from './Settings/Interface.svelte';
|
||||||
|
import Models from './Settings/Models.svelte';
|
||||||
|
import Connections from './Settings/Connections.svelte';
|
||||||
|
import Documents from './Settings/Documents.svelte';
|
||||||
|
import WebSearch from './Settings/WebSearch.svelte';
|
||||||
|
import { config } from '$lib/stores';
|
||||||
|
import { getBackendConfig } from '$lib/apis';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let selectedTab = 'general';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
|
||||||
|
<div
|
||||||
|
class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
||||||
|
'general'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'general';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('General')}</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 ===
|
||||||
|
'users'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'users';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Users')}</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 ===
|
||||||
|
'connections'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'connections';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Connections')}</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 ===
|
||||||
|
'models'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'models';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Models')}</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 ===
|
||||||
|
'documents'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'documents';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M11.625 16.5a1.875 1.875 0 1 0 0-3.75 1.875 1.875 0 0 0 0 3.75Z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm6 16.5c.66 0 1.277-.19 1.797-.518l1.048 1.048a.75.75 0 0 0 1.06-1.06l-1.047-1.048A3.375 3.375 0 1 0 11.625 18Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.25 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 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Documents')}</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 ===
|
||||||
|
'web'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'web';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Web Search')}</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 ===
|
||||||
|
'interface'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'interface';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
|
'audio'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'audio';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<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 ===
|
||||||
|
'images'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'images';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Images')}</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 ===
|
||||||
|
'pipelines'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'pipelines';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Pipelines')}</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 ===
|
||||||
|
'db'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'db';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
|
||||||
|
<path
|
||||||
|
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Database')}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
|
||||||
|
{#if selectedTab === 'general'}
|
||||||
|
<General
|
||||||
|
saveHandler={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'users'}
|
||||||
|
<Users
|
||||||
|
saveHandler={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'connections'}
|
||||||
|
<Connections
|
||||||
|
on:save={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'models'}
|
||||||
|
<Models />
|
||||||
|
{:else if selectedTab === 'documents'}
|
||||||
|
<Documents
|
||||||
|
saveHandler={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'web'}
|
||||||
|
<WebSearch
|
||||||
|
saveHandler={async () => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
await config.set(await getBackendConfig());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'interface'}
|
||||||
|
<Interface
|
||||||
|
on:save={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'audio'}
|
||||||
|
<Audio
|
||||||
|
saveHandler={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'images'}
|
||||||
|
<Images
|
||||||
|
on:save={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'db'}
|
||||||
|
<Database
|
||||||
|
saveHandler={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if selectedTab === 'pipelines'}
|
||||||
|
<Pipelines
|
||||||
|
saveHandler={() => {
|
||||||
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
<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';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
|
||||||
|
let TTS_OPENAI_API_BASE_URL = '';
|
||||||
|
let TTS_OPENAI_API_KEY = '';
|
||||||
|
let TTS_ENGINE = '';
|
||||||
|
let TTS_MODEL = '';
|
||||||
|
let TTS_VOICE = '';
|
||||||
|
|
||||||
|
let STT_OPENAI_API_BASE_URL = '';
|
||||||
|
let STT_OPENAI_API_KEY = '';
|
||||||
|
let STT_ENGINE = '';
|
||||||
|
let STT_MODEL = '';
|
||||||
|
|
||||||
|
let voices = [];
|
||||||
|
let models = [];
|
||||||
|
let nonLocalVoices = false;
|
||||||
|
|
||||||
|
const getOpenAIVoices = () => {
|
||||||
|
voices = [
|
||||||
|
{ name: 'alloy' },
|
||||||
|
{ name: 'echo' },
|
||||||
|
{ name: 'fable' },
|
||||||
|
{ name: 'onyx' },
|
||||||
|
{ name: 'nova' },
|
||||||
|
{ name: 'shimmer' }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfigHandler = async () => {
|
||||||
|
const res = await updateAudioConfig(localStorage.token, {
|
||||||
|
tts: {
|
||||||
|
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
|
||||||
|
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
|
||||||
|
ENGINE: TTS_ENGINE,
|
||||||
|
MODEL: TTS_MODEL,
|
||||||
|
VOICE: TTS_VOICE
|
||||||
|
},
|
||||||
|
stt: {
|
||||||
|
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
|
||||||
|
OPENAI_API_KEY: STT_OPENAI_API_KEY,
|
||||||
|
ENGINE: STT_ENGINE,
|
||||||
|
MODEL: STT_MODEL
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Audio settings updated successfully');
|
||||||
|
|
||||||
|
config.set(await getBackendConfig());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const res = await getAudioConfig(localStorage.token);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
console.log(res);
|
||||||
|
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
|
||||||
|
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
|
||||||
|
|
||||||
|
TTS_ENGINE = res.tts.ENGINE;
|
||||||
|
TTS_MODEL = res.tts.MODEL;
|
||||||
|
TTS_VOICE = res.tts.VOICE;
|
||||||
|
|
||||||
|
STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
|
||||||
|
STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;
|
||||||
|
|
||||||
|
STT_ENGINE = res.stt.ENGINE;
|
||||||
|
STT_MODEL = res.stt.MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TTS_ENGINE === 'openai') {
|
||||||
|
getOpenAIVoices();
|
||||||
|
getOpenAIModels();
|
||||||
|
} else {
|
||||||
|
getWebAPIVoices();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
on:submit|preventDefault={async () => {
|
||||||
|
await updateConfigHandler();
|
||||||
|
dispatch('save');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
|
bind:value={STT_ENGINE}
|
||||||
|
placeholder="Select an engine"
|
||||||
|
>
|
||||||
|
<option value="">{$i18n.t('Whisper (Local)')}</option>
|
||||||
|
<option value="openai">OpenAI</option>
|
||||||
|
<option value="web">{$i18n.t('Web API')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if STT_ENGINE === 'openai'}
|
||||||
|
<div>
|
||||||
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={STT_OPENAI_API_BASE_URL}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
|
bind:value={STT_OPENAI_API_KEY}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
list="model-list"
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={STT_MODEL}
|
||||||
|
placeholder="Select a model"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<datalist id="model-list">
|
||||||
|
<option value="whisper-1" />
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-800" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
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) => {
|
||||||
|
if (e.target.value === 'openai') {
|
||||||
|
getOpenAIVoices();
|
||||||
|
TTS_VOICE = 'alloy';
|
||||||
|
TTS_MODEL = 'tts-1';
|
||||||
|
} else {
|
||||||
|
getWebAPIVoices();
|
||||||
|
TTS_VOICE = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">{$i18n.t('Web API')}</option>
|
||||||
|
<option value="openai">{$i18n.t('Open AI')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if TTS_ENGINE === 'openai'}
|
||||||
|
<div>
|
||||||
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
|
bind:value={TTS_OPENAI_API_KEY}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
{#if TTS_ENGINE === ''}
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<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={TTS_VOICE}
|
||||||
|
>
|
||||||
|
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
||||||
|
{#each voices as voice}
|
||||||
|
<option
|
||||||
|
value={voice.voiceURI}
|
||||||
|
class="bg-gray-100 dark:bg-gray-700"
|
||||||
|
selected={TTS_VOICE === voice.voiceURI}>{voice.name}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if TTS_ENGINE === 'openai'}
|
||||||
|
<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 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.name} />
|
||||||
|
{/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="model-list"
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={TTS_MODEL}
|
||||||
|
placeholder="Select a model"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<datalist id="model-list">
|
||||||
|
{#each models as model}
|
||||||
|
<option value={model.name} />
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { getContext, onMount } from 'svelte';
|
|
||||||
import { banners as _banners } from '$lib/stores';
|
|
||||||
import type { Banner } from '$lib/types';
|
|
||||||
|
|
||||||
import { getBanners, setBanners } from '$lib/apis/configs';
|
|
||||||
|
|
||||||
import type { Writable } from 'svelte/store';
|
|
||||||
import type { i18n as i18nType } from 'i18next';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
|
||||||
const i18n: Writable<i18nType> = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
|
||||||
|
|
||||||
let banners: Banner[] = [];
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
banners = await getBanners(localStorage.token);
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateBanners = async () => {
|
|
||||||
_banners.set(await setBanners(localStorage.token, banners));
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
|
||||||
on:submit|preventDefault={async () => {
|
|
||||||
updateBanners();
|
|
||||||
saveHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80 h-full">
|
|
||||||
<div class=" space-y-3 pr-1.5">
|
|
||||||
<div class="flex w-full justify-between mb-2">
|
|
||||||
<div class=" self-center text-sm font-semibold">
|
|
||||||
{$i18n.t('Banners')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
|
||||||
banners = [
|
|
||||||
...banners,
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
type: '',
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
dismissible: true,
|
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-1">
|
|
||||||
{#each banners as banner, bannerIdx}
|
|
||||||
<div class=" flex justify-between">
|
|
||||||
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
|
||||||
<select
|
|
||||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
|
||||||
bind:value={banner.type}
|
|
||||||
>
|
|
||||||
{#if banner.type == ''}
|
|
||||||
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
|
||||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
|
||||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
|
||||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
|
||||||
placeholder={$i18n.t('Content')}
|
|
||||||
bind:value={banner.content}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="relative top-1.5 -left-2">
|
|
||||||
<Tooltip content="Dismissible" className="flex h-fit items-center">
|
|
||||||
<Switch bind:state={banner.dismissible} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-2"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
banners.splice(bannerIdx, 1);
|
|
||||||
banners = banners;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end pt-3 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"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
+7
-3
@@ -23,10 +23,14 @@
|
|||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import { getModels as _getModels } from '$lib/apis';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let getModels: Function;
|
const getModels = async () => {
|
||||||
|
const models = await _getModels(localStorage.token);
|
||||||
|
return models;
|
||||||
|
};
|
||||||
|
|
||||||
// External
|
// External
|
||||||
let OLLAMA_BASE_URLS = [''];
|
let OLLAMA_BASE_URLS = [''];
|
||||||
@@ -158,7 +162,7 @@
|
|||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="space-y-3 pr-1.5 overflow-y-scroll h-[24rem] max-h-[25rem]">
|
<div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
|
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
|
||||||
<div class=" space-y-3">
|
<div class=" space-y-3">
|
||||||
<div class="mt-2 space-y-2 pr-1.5">
|
<div class="mt-2 space-y-2 pr-1.5">
|
||||||
@@ -300,7 +304,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="pr-1.5 space-y-2">
|
<div class="pr-1.5 space-y-2">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
saveHandler();
|
saveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
||||||
|
|
||||||
|
|||||||
+282
-12
@@ -8,7 +8,10 @@
|
|||||||
getEmbeddingConfig,
|
getEmbeddingConfig,
|
||||||
updateEmbeddingConfig,
|
updateEmbeddingConfig,
|
||||||
getRerankingConfig,
|
getRerankingConfig,
|
||||||
updateRerankingConfig
|
updateRerankingConfig,
|
||||||
|
resetUploadDir,
|
||||||
|
getRAGConfig,
|
||||||
|
updateRAGConfig
|
||||||
} from '$lib/apis/rag';
|
} from '$lib/apis/rag';
|
||||||
|
|
||||||
import { documents, models } from '$lib/stores';
|
import { documents, models } from '$lib/stores';
|
||||||
@@ -24,13 +27,19 @@
|
|||||||
let updateRerankingModelLoading = false;
|
let updateRerankingModelLoading = false;
|
||||||
|
|
||||||
let showResetConfirm = false;
|
let showResetConfirm = false;
|
||||||
|
let showResetUploadDirConfirm = false;
|
||||||
|
|
||||||
let embeddingEngine = '';
|
let embeddingEngine = '';
|
||||||
let embeddingModel = '';
|
let embeddingModel = '';
|
||||||
let rerankingModel = '';
|
let rerankingModel = '';
|
||||||
|
|
||||||
|
let chunkSize = 0;
|
||||||
|
let chunkOverlap = 0;
|
||||||
|
let pdfExtractImages = true;
|
||||||
|
|
||||||
let OpenAIKey = '';
|
let OpenAIKey = '';
|
||||||
let OpenAIUrl = '';
|
let OpenAIUrl = '';
|
||||||
|
let OpenAIBatchSize = 1;
|
||||||
|
|
||||||
let querySettings = {
|
let querySettings = {
|
||||||
template: '',
|
template: '',
|
||||||
@@ -92,7 +101,8 @@
|
|||||||
? {
|
? {
|
||||||
openai_config: {
|
openai_config: {
|
||||||
key: OpenAIKey,
|
key: OpenAIKey,
|
||||||
url: OpenAIUrl
|
url: OpenAIUrl,
|
||||||
|
batch_size: OpenAIBatchSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
@@ -148,6 +158,14 @@
|
|||||||
if (querySettings.hybrid) {
|
if (querySettings.hybrid) {
|
||||||
rerankingModelUpdateHandler();
|
rerankingModelUpdateHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const res = await updateRAGConfig(localStorage.token, {
|
||||||
|
pdf_extract_images: pdfExtractImages,
|
||||||
|
chunk: {
|
||||||
|
chunk_overlap: chunkOverlap,
|
||||||
|
chunk_size: chunkSize
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setEmbeddingConfig = async () => {
|
const setEmbeddingConfig = async () => {
|
||||||
@@ -159,6 +177,7 @@
|
|||||||
|
|
||||||
OpenAIKey = embeddingConfig.openai_config.key;
|
OpenAIKey = embeddingConfig.openai_config.key;
|
||||||
OpenAIUrl = embeddingConfig.openai_config.url;
|
OpenAIUrl = embeddingConfig.openai_config.url;
|
||||||
|
OpenAIBatchSize = embeddingConfig.openai_config.batch_size ?? 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,6 +199,15 @@
|
|||||||
await setRerankingConfig();
|
await setRerankingConfig();
|
||||||
|
|
||||||
querySettings = await getQuerySettings(localStorage.token);
|
querySettings = await getQuerySettings(localStorage.token);
|
||||||
|
|
||||||
|
const res = await getRAGConfig(localStorage.token);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
pdfExtractImages = res.pdf_extract_images;
|
||||||
|
|
||||||
|
chunkSize = res.chunk.chunk_size;
|
||||||
|
chunkOverlap = res.chunk.chunk_overlap;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -190,7 +218,7 @@
|
|||||||
saveHandler();
|
saveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[28rem]">
|
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||||
|
|
||||||
@@ -282,6 +310,30 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex mt-0.5 space-x-2">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
|
||||||
|
<div class=" flex-1">
|
||||||
|
<input
|
||||||
|
id="steps-range"
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="2048"
|
||||||
|
step="1"
|
||||||
|
bind:value={OpenAIBatchSize}
|
||||||
|
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<input
|
||||||
|
bind:value={OpenAIBatchSize}
|
||||||
|
type="number"
|
||||||
|
class=" bg-transparent text-center w-14"
|
||||||
|
min="-2"
|
||||||
|
max="16000"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
@@ -303,7 +355,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-1" />
|
<hr class=" dark:border-gray-850 my-1" />
|
||||||
|
|
||||||
<div class="space-y-2" />
|
<div class="space-y-2" />
|
||||||
<div>
|
<div>
|
||||||
@@ -321,10 +373,8 @@
|
|||||||
{#if !embeddingModel}
|
{#if !embeddingModel}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $models.filter((m) => m.id && !m.external) as model}
|
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
|
||||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||||
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
|
|
||||||
>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,7 +519,224 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
|
<div class=" ">
|
||||||
|
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
||||||
|
|
||||||
|
<div class=" flex">
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
|
||||||
|
|
||||||
|
<div class="self-center p-3">
|
||||||
|
<input
|
||||||
|
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="number"
|
||||||
|
placeholder={$i18n.t('Enter Top K')}
|
||||||
|
bind:value={querySettings.k}
|
||||||
|
autocomplete="off"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if querySettings.hybrid === true}
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit">
|
||||||
|
{$i18n.t('Minimum Score')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="self-center p-3">
|
||||||
|
<input
|
||||||
|
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder={$i18n.t('Enter Score')}
|
||||||
|
bind:value={querySettings.r}
|
||||||
|
autocomplete="off"
|
||||||
|
min="0.0"
|
||||||
|
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if querySettings.hybrid === true}
|
||||||
|
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-3" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
|
||||||
|
<textarea
|
||||||
|
bind:value={querySettings.template}
|
||||||
|
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
|
rows="4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
|
<div class=" ">
|
||||||
|
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
||||||
|
|
||||||
|
<div class=" my-2 flex gap-1.5">
|
||||||
|
<div class=" w-full justify-between">
|
||||||
|
<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
|
||||||
|
<div class="self-center">
|
||||||
|
<input
|
||||||
|
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="number"
|
||||||
|
placeholder={$i18n.t('Enter Chunk Size')}
|
||||||
|
bind:value={chunkSize}
|
||||||
|
autocomplete="off"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Chunk Overlap')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="self-center">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type="number"
|
||||||
|
placeholder={$i18n.t('Enter Chunk Overlap')}
|
||||||
|
bind:value={chunkOverlap}
|
||||||
|
autocomplete="off"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" text-xs font-medium text-gray-500"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
pdfExtractImages = !pdfExtractImages;
|
||||||
|
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if showResetUploadDirConfirm}
|
||||||
|
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.25 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 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{$i18n.t('Are you sure?')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-1.5 items-center">
|
||||||
|
<button
|
||||||
|
class="hover:text-white transition"
|
||||||
|
on:click={() => {
|
||||||
|
const res = resetUploadDir(localStorage.token).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
showResetUploadDirConfirm = false;
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="hover:text-white transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showResetUploadDirConfirm = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
<button
|
||||||
|
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
|
on:click={() => {
|
||||||
|
showResetUploadDirConfirm = true;
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.25 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 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showResetConfirm}
|
{#if showResetConfirm}
|
||||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||||
@@ -480,10 +747,9 @@
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
|
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -505,6 +771,7 @@
|
|||||||
|
|
||||||
showResetConfirm = false;
|
showResetConfirm = false;
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -524,6 +791,7 @@
|
|||||||
on:click={() => {
|
on:click={() => {
|
||||||
showResetConfirm = false;
|
showResetConfirm = false;
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -540,10 +808,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showResetConfirm = true;
|
showResetConfirm = true;
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3">
|
||||||
<svg
|
<svg
|
||||||
@@ -563,6 +832,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
@@ -6,61 +6,44 @@
|
|||||||
updateWebhookUrl
|
updateWebhookUrl
|
||||||
} from '$lib/apis';
|
} from '$lib/apis';
|
||||||
import {
|
import {
|
||||||
|
getAdminConfig,
|
||||||
getDefaultUserRole,
|
getDefaultUserRole,
|
||||||
getJWTExpiresDuration,
|
getJWTExpiresDuration,
|
||||||
getSignUpEnabledStatus,
|
getSignUpEnabledStatus,
|
||||||
toggleSignUpEnabledStatus,
|
toggleSignUpEnabledStatus,
|
||||||
|
updateAdminConfig,
|
||||||
updateDefaultUserRole,
|
updateDefaultUserRole,
|
||||||
updateJWTExpiresDuration
|
updateJWTExpiresDuration
|
||||||
} from '$lib/apis/auths';
|
} from '$lib/apis/auths';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
let signUpEnabled = true;
|
|
||||||
let defaultUserRole = 'pending';
|
|
||||||
let JWTExpiresIn = '';
|
|
||||||
|
|
||||||
|
let adminConfig = null;
|
||||||
let webhookUrl = '';
|
let webhookUrl = '';
|
||||||
let communitySharingEnabled = true;
|
|
||||||
|
|
||||||
const toggleSignUpEnabled = async () => {
|
const updateHandler = async () => {
|
||||||
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDefaultUserRoleHandler = async (role) => {
|
|
||||||
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateJWTExpiresDurationHandler = async (duration) => {
|
|
||||||
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateWebhookUrlHandler = async () => {
|
|
||||||
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
|
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
|
||||||
};
|
const res = await updateAdminConfig(localStorage.token, adminConfig);
|
||||||
|
|
||||||
const toggleCommunitySharingEnabled = async () => {
|
if (res) {
|
||||||
communitySharingEnabled = await toggleCommunitySharingEnabledStatus(localStorage.token);
|
toast.success(i18n.t('Settings updated successfully'));
|
||||||
|
} else {
|
||||||
|
toast.error(i18n.t('Failed to update settings'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
adminConfig = await getAdminConfig(localStorage.token);
|
||||||
})(),
|
|
||||||
(async () => {
|
|
||||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
|
||||||
})(),
|
|
||||||
(async () => {
|
|
||||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
|
||||||
})(),
|
})(),
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
webhookUrl = await getWebhookUrl(localStorage.token);
|
webhookUrl = await getWebhookUrl(localStorage.token);
|
||||||
})(),
|
|
||||||
(async () => {
|
|
||||||
communitySharingEnabled = await getCommunitySharingEnabledStatus(localStorage.token);
|
|
||||||
})()
|
})()
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -69,66 +52,28 @@
|
|||||||
<form
|
<form
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
updateHandler();
|
||||||
updateWebhookUrlHandler();
|
|
||||||
saveHandler();
|
saveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
|
{#if adminConfig !== null}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
<div class=" mb-3 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between pr-2">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||||
|
|
||||||
<button
|
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
|
||||||
on:click={() => {
|
|
||||||
toggleSignUpEnabled();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{#if signUpEnabled}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" my-3 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={defaultUserRole}
|
bind:value={adminConfig.DEFAULT_USER_ROLE}
|
||||||
placeholder="Select a theme"
|
placeholder="Select a role"
|
||||||
on:change={(e) => {
|
|
||||||
updateDefaultUserRoleHandler(e.target.value);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="pending">{$i18n.t('pending')}</option>
|
<option value="pending">{$i18n.t('pending')}</option>
|
||||||
<option value="user">{$i18n.t('user')}</option>
|
<option value="user">{$i18n.t('user')}</option>
|
||||||
@@ -137,48 +82,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
|
||||||
|
|
||||||
<button
|
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
<div class=" self-center text-xs font-medium">
|
||||||
on:click={() => {
|
{$i18n.t('Show Admin Details in Account Pending Overlay')}
|
||||||
toggleCommunitySharingEnabled();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{#if communitySharingEnabled}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class=" w-full justify-between">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2 space-x-2">
|
||||||
|
<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={`e.g.) "30m","1h", "10d". `}
|
||||||
|
bind:value={adminConfig.JWT_EXPIRES_IN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t('Valid time units:')}
|
||||||
|
<span class=" text-gray-300 font-medium"
|
||||||
|
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
<div class=" w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
@@ -194,31 +138,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
|
||||||
<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={`e.g.) "30m","1h", "10d". `}
|
|
||||||
bind:value={JWTExpiresIn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
{$i18n.t('Valid time units:')}
|
|
||||||
<span class=" text-gray-300 font-medium"
|
|
||||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
|||||||
+3
-5
@@ -23,8 +23,6 @@
|
|||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveSettings: Function;
|
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
let imageGenerationEngine = '';
|
let imageGenerationEngine = '';
|
||||||
@@ -171,7 +169,7 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]">
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||||
|
|
||||||
@@ -228,7 +226,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
{#if imageGenerationEngine === ''}
|
{#if imageGenerationEngine === ''}
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
||||||
@@ -326,7 +324,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if enableImageGeneration}
|
{#if enableImageGeneration}
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { getBackendConfig, getTaskConfig, updateTaskConfig } from '$lib/apis';
|
||||||
|
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||||
|
import { config, models, settings, user } from '$lib/stores';
|
||||||
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { banners as _banners } from '$lib/stores';
|
||||||
|
import type { Banner } from '$lib/types';
|
||||||
|
|
||||||
|
import { getBanners, setBanners } from '$lib/apis/configs';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let taskConfig = {
|
||||||
|
TASK_MODEL: '',
|
||||||
|
TASK_MODEL_EXTERNAL: '',
|
||||||
|
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
|
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
|
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let promptSuggestions = [];
|
||||||
|
let banners: Banner[] = [];
|
||||||
|
|
||||||
|
const updateInterfaceHandler = async () => {
|
||||||
|
taskConfig = await updateTaskConfig(localStorage.token, taskConfig);
|
||||||
|
|
||||||
|
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||||
|
await updateBanners();
|
||||||
|
|
||||||
|
await config.set(await getBackendConfig());
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
taskConfig = await getTaskConfig(localStorage.token);
|
||||||
|
|
||||||
|
promptSuggestions = $config?.default_prompt_suggestions;
|
||||||
|
|
||||||
|
banners = await getBanners(localStorage.token);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateBanners = async () => {
|
||||||
|
_banners.set(await setBanners(localStorage.token, banners));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
updateInterfaceHandler();
|
||||||
|
dispatch('save');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2.5 text-sm font-medium flex">
|
||||||
|
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={taskConfig.TASK_MODEL}
|
||||||
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
>
|
||||||
|
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||||
|
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
|
||||||
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
||||||
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
>
|
||||||
|
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||||
|
{#each $models as model}
|
||||||
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||||
|
<textarea
|
||||||
|
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||||
|
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
|
rows="6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
|
||||||
|
<textarea
|
||||||
|
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
||||||
|
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
|
rows="6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">
|
||||||
|
{$i18n.t('Search Query Generation Prompt Length Threshold')}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-3" />
|
||||||
|
|
||||||
|
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-sm font-semibold">
|
||||||
|
{$i18n.t('Banners')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||||
|
banners = [
|
||||||
|
...banners,
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
dismissible: true,
|
||||||
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-1">
|
||||||
|
{#each banners as banner, bannerIdx}
|
||||||
|
<div class=" flex justify-between">
|
||||||
|
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
||||||
|
<select
|
||||||
|
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
||||||
|
bind:value={banner.type}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{#if banner.type == ''}
|
||||||
|
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||||
|
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||||
|
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||||
|
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
||||||
|
placeholder={$i18n.t('Content')}
|
||||||
|
bind:value={banner.content}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="relative top-1.5 -left-2">
|
||||||
|
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
||||||
|
<Switch bind:state={banner.dismissible} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
banners.splice(bannerIdx, 1);
|
||||||
|
banners = banners;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $user.role === 'admin'}
|
||||||
|
<div class=" space-y-3">
|
||||||
|
<div class="flex w-full justify-between mb-2">
|
||||||
|
<div class=" self-center text-sm font-semibold">
|
||||||
|
{$i18n.t('Default Prompt Suggestions')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||||
|
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
||||||
|
{#each promptSuggestions as prompt, promptIdx}
|
||||||
|
<div class=" flex dark:bg-gray-850 rounded-xl py-1.5">
|
||||||
|
<div class="flex flex-col flex-1 pl-1">
|
||||||
|
<div class="flex border-b dark:border-gray-800 w-full">
|
||||||
|
<input
|
||||||
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
||||||
|
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||||
|
bind:value={prompt.title[0]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
||||||
|
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||||
|
bind:value={prompt.title[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
||||||
|
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||||
|
bind:value={prompt.content}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-3"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
promptSuggestions.splice(promptIdx, 1);
|
||||||
|
promptSuggestions = promptSuggestions;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if promptSuggestions.length > 0}
|
||||||
|
<div class="text-xs text-left w-full mt-2">
|
||||||
|
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||||
|
</div>
|
||||||
|
{/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>
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -14,7 +14,8 @@
|
|||||||
getModels,
|
getModels,
|
||||||
getPipelinesList,
|
getPipelinesList,
|
||||||
downloadPipeline,
|
downloadPipeline,
|
||||||
deletePipeline
|
deletePipeline,
|
||||||
|
uploadPipeline
|
||||||
} from '$lib/apis';
|
} from '$lib/apis';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
@@ -24,6 +25,9 @@
|
|||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
let downloading = false;
|
let downloading = false;
|
||||||
|
let uploading = false;
|
||||||
|
|
||||||
|
let pipelineFiles;
|
||||||
|
|
||||||
let PIPELINES_LIST = null;
|
let PIPELINES_LIST = null;
|
||||||
let selectedPipelinesUrlIdx = '';
|
let selectedPipelinesUrlIdx = '';
|
||||||
@@ -126,6 +130,41 @@
|
|||||||
downloading = false;
|
downloading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadPipelineHandler = async () => {
|
||||||
|
uploading = true;
|
||||||
|
|
||||||
|
if (pipelineFiles && pipelineFiles.length !== 0) {
|
||||||
|
const file = pipelineFiles[0];
|
||||||
|
|
||||||
|
console.log(file);
|
||||||
|
|
||||||
|
const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch(
|
||||||
|
(error) => {
|
||||||
|
console.log(error);
|
||||||
|
toast.error('Something went wrong :/');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Pipeline downloaded successfully');
|
||||||
|
setPipelines();
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('No file selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
pipelineFiles = null;
|
||||||
|
const pipelineUploadInputElement = document.getElementById('pipeline-upload-input');
|
||||||
|
|
||||||
|
if (pipelineUploadInputElement) {
|
||||||
|
pipelineUploadInputElement.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading = false;
|
||||||
|
};
|
||||||
|
|
||||||
const deletePipelineHandler = async () => {
|
const deletePipelineHandler = async () => {
|
||||||
const res = await deletePipeline(
|
const res = await deletePipeline(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
@@ -161,7 +200,7 @@
|
|||||||
updateHandler();
|
updateHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" pr-1.5 overflow-y-scroll max-h-80 h-full">
|
<div class="overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if PIPELINES_LIST !== null}
|
{#if PIPELINES_LIST !== null}
|
||||||
<div class="flex w-full justify-between mb-2">
|
<div class="flex w-full justify-between mb-2">
|
||||||
<div class=" self-center text-sm font-semibold">
|
<div class=" self-center text-sm font-semibold">
|
||||||
@@ -196,6 +235,91 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" my-2">
|
||||||
|
<div class=" mb-2 text-sm font-medium">
|
||||||
|
{$i18n.t('Upload Pipeline')}
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
id="pipelines-upload-input"
|
||||||
|
bind:files={pipelineFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".py"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
document.getElementById('pipelines-upload-input')?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if pipelineFiles}
|
||||||
|
{pipelineFiles.length > 0 ? `${pipelineFiles.length}` : ''} pipeline(s) selected.
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Click here to select a py file.')}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
on:click={() => {
|
||||||
|
uploadPipelineHandler();
|
||||||
|
}}
|
||||||
|
disabled={uploading}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if uploading}
|
||||||
|
<div class="self-center">
|
||||||
|
<svg
|
||||||
|
class=" w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<style>
|
||||||
|
.spinner_ajPY {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner_AtaB 0.75s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner_AtaB {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path
|
||||||
|
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||||
|
opacity=".25"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||||
|
class="spinner_ajPY"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" my-2">
|
<div class=" my-2">
|
||||||
<div class=" mb-2 text-sm font-medium">
|
<div class=" mb-2 text-sm font-medium">
|
||||||
{$i18n.t('Install from Github URL')}
|
{$i18n.t('Install from Github URL')}
|
||||||
@@ -384,6 +508,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div>Pipelines Not Detected</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex justify-center h-full">
|
<div class="flex justify-center h-full">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
await config.set(await getBackendConfig());
|
await config.set(await getBackendConfig());
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 overflow-y-scroll max-h-full">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-2" />
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div class="mt-2 space-y-3">
|
<div class="mt-2 space-y-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
|
||||||
|
import { documents, models } from '$lib/stores';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
let webConfig = null;
|
||||||
|
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper', 'serply'];
|
||||||
|
|
||||||
|
let youtubeLanguage = 'en';
|
||||||
|
let youtubeTranslation = null;
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
const res = await updateRAGConfig(localStorage.token, {
|
||||||
|
web: webConfig,
|
||||||
|
youtube: {
|
||||||
|
language: youtubeLanguage.split(',').map((lang) => lang.trim()),
|
||||||
|
translation: youtubeTranslation
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const res = await getRAGConfig(localStorage.token);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
webConfig = res.web;
|
||||||
|
|
||||||
|
youtubeLanguage = res.youtube.language.join(',');
|
||||||
|
youtubeTranslation = res.youtube.translation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
on:submit|preventDefault={async () => {
|
||||||
|
await submitHandler();
|
||||||
|
saveHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
|
{#if webConfig}
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1 text-sm font-medium">
|
||||||
|
{$i18n.t('Web Search')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Enable Web Search')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={webConfig.search.enabled} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
|
bind:value={webConfig.search.engine}
|
||||||
|
placeholder={$i18n.t('Select a engine')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||||
|
{#each webSearchEngines as engine}
|
||||||
|
<option value={engine}>{engine}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if webConfig.search.engine !== ''}
|
||||||
|
<div class="mt-1.5">
|
||||||
|
{#if webConfig.search.engine === 'searxng'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Searxng Query URL')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Searxng Query URL')}
|
||||||
|
bind:value={webConfig.search.searxng_query_url}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'google_pse'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Google PSE API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Google PSE API Key')}
|
||||||
|
bind:value={webConfig.search.google_pse_api_key}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1.5">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Google PSE Engine Id')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Google PSE Engine Id')}
|
||||||
|
bind:value={webConfig.search.google_pse_engine_id}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'brave'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Brave Search API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Brave Search API Key')}
|
||||||
|
bind:value={webConfig.search.brave_search_api_key}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'serpstack'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Serpstack API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Serpstack API Key')}
|
||||||
|
bind:value={webConfig.search.serpstack_api_key}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'serper'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Serper API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Serper API Key')}
|
||||||
|
bind:value={webConfig.search.serper_api_key}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'serply'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Serply API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter Serply API Key')}
|
||||||
|
bind:value={webConfig.search.serply_api_key}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if webConfig.search.enabled}
|
||||||
|
<div class="mt-2 flex gap-2 mb-1">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Search Result Count')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Search Result Count')}
|
||||||
|
bind:value={webConfig.search.result_count}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Concurrent Requests')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Concurrent Requests')}
|
||||||
|
bind:value={webConfig.search.concurrent_requests}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1 text-sm font-medium">
|
||||||
|
{$i18n.t('Web Loader Settings')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Bypass SSL verification for Websites')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
on:click={() => {
|
||||||
|
webConfig.ssl_verification = !webConfig.ssl_verification;
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if webConfig.ssl_verification === 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 class=" mt-2 mb-1 text-sm font-medium">
|
||||||
|
{$i18n.t('Youtube Loader Settings')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
||||||
|
<div class=" flex-1 self-center">
|
||||||
|
<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={$i18n.t('Enter language codes')}
|
||||||
|
bind:value={youtubeLanguage}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end pt-3 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>
|
||||||
@@ -39,181 +39,5 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
|
||||||
<div
|
|
||||||
class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
|
||||||
'general'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'general';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('General')}</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 ===
|
|
||||||
'users'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'users';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Users')}</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 ===
|
|
||||||
'db'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'db';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
|
|
||||||
<path
|
|
||||||
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Database')}</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 ===
|
|
||||||
'banners'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'banners';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 9.72 0 0 0 19.266 2.5Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 2.25A6.75 6.75 0 0 0 5.25 9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 .298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 0Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Banners')}</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 ===
|
|
||||||
'pipelines'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'pipelines';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Pipelines')}</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 md:min-h-[380px]">
|
|
||||||
{#if selectedTab === 'general'}
|
|
||||||
<General
|
|
||||||
saveHandler={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if selectedTab === 'users'}
|
|
||||||
<Users
|
|
||||||
saveHandler={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if selectedTab === 'db'}
|
|
||||||
<Database
|
|
||||||
saveHandler={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if selectedTab === 'banners'}
|
|
||||||
<Banners
|
|
||||||
saveHandler={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if selectedTab === 'pipelines'}
|
|
||||||
<Pipelines
|
|
||||||
saveHandler={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
+199
-173
@@ -7,6 +7,10 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { i18n as i18nType } from 'i18next';
|
||||||
|
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatId,
|
chatId,
|
||||||
chats,
|
chats,
|
||||||
@@ -18,7 +22,9 @@
|
|||||||
tags as _tags,
|
tags as _tags,
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
banners,
|
banners,
|
||||||
user
|
user,
|
||||||
|
socket,
|
||||||
|
showCallOverlay
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
@@ -38,24 +44,19 @@
|
|||||||
getTagsById,
|
getTagsById,
|
||||||
updateChatById
|
updateChatById
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import {
|
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||||
generateOpenAIChatCompletion,
|
import { runWebSearch } from '$lib/apis/rag';
|
||||||
generateSearchQuery,
|
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||||
generateTitle
|
import { queryMemory } from '$lib/apis/memories';
|
||||||
} from '$lib/apis/openai';
|
import { getUserSettings } from '$lib/apis/users';
|
||||||
|
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
|
||||||
|
|
||||||
|
import Banner from '../common/Banner.svelte';
|
||||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||||
import Messages from '$lib/components/chat/Messages.svelte';
|
import Messages from '$lib/components/chat/Messages.svelte';
|
||||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||||
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
||||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
import { error } from '@sveltejs/kit';
|
||||||
import { queryMemory } from '$lib/apis/memories';
|
|
||||||
import type { Writable } from 'svelte/store';
|
|
||||||
import type { i18n as i18nType } from 'i18next';
|
|
||||||
import { runWebSearch } from '$lib/apis/rag';
|
|
||||||
import Banner from '../common/Banner.svelte';
|
|
||||||
import { getUserSettings } from '$lib/apis/users';
|
|
||||||
import { chatCompleted } from '$lib/apis';
|
|
||||||
|
|
||||||
const i18n: Writable<i18nType> = getContext('i18n');
|
const i18n: Writable<i18nType> = getContext('i18n');
|
||||||
|
|
||||||
@@ -247,13 +248,13 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatCompletedHandler = async (model, messages) => {
|
const chatCompletedHandler = async (modelId, messages) => {
|
||||||
await mermaid.run({
|
await mermaid.run({
|
||||||
querySelector: '.mermaid'
|
querySelector: '.mermaid'
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await chatCompleted(localStorage.token, {
|
const res = await chatCompleted(localStorage.token, {
|
||||||
model: model.id,
|
model: modelId,
|
||||||
messages: messages.map((m) => ({
|
messages: messages.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
role: m.role,
|
role: m.role,
|
||||||
@@ -280,11 +281,22 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
|
||||||
|
return setInterval(() => {
|
||||||
|
$socket?.emit('usage', {
|
||||||
|
action: 'chat',
|
||||||
|
model: modelId,
|
||||||
|
chat_id: chatId
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// Ollama functions
|
// Chat functions
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
||||||
const submitPrompt = async (userPrompt, _user = null) => {
|
const submitPrompt = async (userPrompt, _user = null) => {
|
||||||
|
let _responses = [];
|
||||||
console.log('submitPrompt', $chatId);
|
console.log('submitPrompt', $chatId);
|
||||||
|
|
||||||
selectedModels = selectedModels.map((modelId) =>
|
selectedModels = selectedModels.map((modelId) =>
|
||||||
@@ -307,8 +319,18 @@
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Reset chat message textarea height
|
// Reset chat input textarea
|
||||||
document.getElementById('chat-textarea').style.height = '';
|
const chatTextAreaElement = document.getElementById('chat-textarea');
|
||||||
|
|
||||||
|
if (chatTextAreaElement) {
|
||||||
|
chatTextAreaElement.value = '';
|
||||||
|
chatTextAreaElement.style.height = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const _files = JSON.parse(JSON.stringify(files));
|
||||||
|
files = [];
|
||||||
|
|
||||||
|
prompt = '';
|
||||||
|
|
||||||
// Create user message
|
// Create user message
|
||||||
let userMessageId = uuidv4();
|
let userMessageId = uuidv4();
|
||||||
@@ -319,7 +341,7 @@
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
user: _user ?? undefined,
|
user: _user ?? undefined,
|
||||||
content: userPrompt,
|
content: userPrompt,
|
||||||
files: files.length > 0 ? files : undefined,
|
files: _files.length > 0 ? _files : undefined,
|
||||||
timestamp: Math.floor(Date.now() / 1000), // Unix epoch
|
timestamp: Math.floor(Date.now() / 1000), // Unix epoch
|
||||||
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
||||||
};
|
};
|
||||||
@@ -336,68 +358,29 @@
|
|||||||
// Wait until history/message have been updated
|
// Wait until history/message have been updated
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
// Create new chat if only one message in messages
|
|
||||||
if (messages.length == 1) {
|
|
||||||
if ($settings.saveChatHistory ?? true) {
|
|
||||||
chat = await createNewChat(localStorage.token, {
|
|
||||||
id: $chatId,
|
|
||||||
title: $i18n.t('New Chat'),
|
|
||||||
models: selectedModels,
|
|
||||||
system: $settings.system ?? undefined,
|
|
||||||
options: {
|
|
||||||
...($settings.params ?? {})
|
|
||||||
},
|
|
||||||
messages: messages,
|
|
||||||
history: history,
|
|
||||||
tags: [],
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
await chats.set(await getChatList(localStorage.token));
|
|
||||||
await chatId.set(chat.id);
|
|
||||||
} else {
|
|
||||||
await chatId.set('local');
|
|
||||||
}
|
|
||||||
await tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset chat input textarea
|
|
||||||
prompt = '';
|
|
||||||
document.getElementById('chat-textarea').style.height = '';
|
|
||||||
files = [];
|
|
||||||
|
|
||||||
// Send prompt
|
// Send prompt
|
||||||
await sendPrompt(userPrompt, userMessageId);
|
_responses = await sendPrompt(userPrompt, userMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _responses;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPrompt = async (prompt, parentId, modelId = null) => {
|
const sendPrompt = async (prompt, parentId, modelId = null) => {
|
||||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
let _responses = [];
|
||||||
|
|
||||||
await Promise.all(
|
// If modelId is provided, use it, else use selected model
|
||||||
(modelId
|
let selectedModelIds = modelId
|
||||||
? [modelId]
|
? [modelId]
|
||||||
: atSelectedModel !== undefined
|
: atSelectedModel !== undefined
|
||||||
? [atSelectedModel.id]
|
? [atSelectedModel.id]
|
||||||
: selectedModels
|
: selectedModels;
|
||||||
).map(async (modelId) => {
|
|
||||||
console.log('modelId', modelId);
|
// Create response messages for each selected model
|
||||||
|
const responseMessageIds = {};
|
||||||
|
for (const modelId of selectedModelIds) {
|
||||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
// If there are image files, check if model is vision capable
|
|
||||||
const hasImages = messages.some((message) =>
|
|
||||||
message.files?.some((file) => file.type === 'image')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
|
|
||||||
toast.error(
|
|
||||||
$i18n.t('Model {{modelName}} is not vision capable', {
|
|
||||||
modelName: model.name ?? model.id
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create response message
|
|
||||||
let responseMessageId = uuidv4();
|
let responseMessageId = uuidv4();
|
||||||
let responseMessage = {
|
let responseMessage = {
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
@@ -423,8 +406,59 @@
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responseMessageIds[modelId] = responseMessageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
|
// Create new chat if only one message in messages
|
||||||
|
if (messages.length == 2) {
|
||||||
|
if ($settings.saveChatHistory ?? true) {
|
||||||
|
chat = await createNewChat(localStorage.token, {
|
||||||
|
id: $chatId,
|
||||||
|
title: $i18n.t('New Chat'),
|
||||||
|
models: selectedModels,
|
||||||
|
system: $settings.system ?? undefined,
|
||||||
|
options: {
|
||||||
|
...($settings.params ?? {})
|
||||||
|
},
|
||||||
|
messages: messages,
|
||||||
|
history: history,
|
||||||
|
tags: [],
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
await chats.set(await getChatList(localStorage.token));
|
||||||
|
await chatId.set(chat.id);
|
||||||
|
} else {
|
||||||
|
await chatId.set('local');
|
||||||
|
}
|
||||||
|
await tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
selectedModelIds.map(async (modelId) => {
|
||||||
|
console.log('modelId', modelId);
|
||||||
|
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||||
|
|
||||||
|
if (model) {
|
||||||
|
// If there are image files, check if model is vision capable
|
||||||
|
const hasImages = messages.some((message) =>
|
||||||
|
message.files?.some((file) => file.type === 'image')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t('Model {{modelName}} is not vision capable', {
|
||||||
|
modelName: model.name ?? model.id
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseMessageId = responseMessageIds[modelId];
|
||||||
|
let responseMessage = history.messages[responseMessageId];
|
||||||
|
|
||||||
let userContext = null;
|
let userContext = null;
|
||||||
if ($settings?.memory ?? false) {
|
if ($settings?.memory ?? false) {
|
||||||
if (userContext === null) {
|
if (userContext === null) {
|
||||||
@@ -432,7 +466,6 @@
|
|||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
if (res.documents[0].length > 0) {
|
if (res.documents[0].length > 0) {
|
||||||
userContext = res.documents.reduce((acc, doc, index) => {
|
userContext = res.documents.reduce((acc, doc, index) => {
|
||||||
@@ -451,15 +484,23 @@
|
|||||||
}
|
}
|
||||||
responseMessage.userContext = userContext;
|
responseMessage.userContext = userContext;
|
||||||
|
|
||||||
|
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
|
||||||
|
|
||||||
if (webSearchEnabled) {
|
if (webSearchEnabled) {
|
||||||
await getWebSearchResults(model.id, parentId, responseMessageId);
|
await getWebSearchResults(model.id, parentId, responseMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _response = null;
|
||||||
if (model?.owned_by === 'openai') {
|
if (model?.owned_by === 'openai') {
|
||||||
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
_response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||||
} else if (model) {
|
} else if (model) {
|
||||||
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
_response = await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
||||||
}
|
}
|
||||||
|
_responses.push(_response);
|
||||||
|
|
||||||
|
console.log('chatEventEmitter', chatEventEmitter);
|
||||||
|
|
||||||
|
if (chatEventEmitter) clearInterval(chatEventEmitter);
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
||||||
}
|
}
|
||||||
@@ -467,39 +508,47 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
|
|
||||||
|
return _responses;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
|
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
|
||||||
const responseMessage = history.messages[responseId];
|
const responseMessage = history.messages[responseId];
|
||||||
|
|
||||||
responseMessage.status = {
|
responseMessage.statusHistory = [
|
||||||
|
{
|
||||||
done: false,
|
done: false,
|
||||||
action: 'web_search',
|
action: 'web_search',
|
||||||
description: $i18n.t('Generating search query')
|
description: $i18n.t('Generating search query')
|
||||||
};
|
}
|
||||||
|
];
|
||||||
messages = messages;
|
messages = messages;
|
||||||
|
|
||||||
const prompt = history.messages[parentId].content;
|
const prompt = history.messages[parentId].content;
|
||||||
let searchQuery = prompt;
|
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
|
||||||
if (prompt.length > 100) {
|
(error) => {
|
||||||
searchQuery = await generateChatSearchQuery(model, prompt);
|
console.log(error);
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
toast.warning($i18n.t('No search query generated'));
|
toast.warning($i18n.t('No search query generated'));
|
||||||
responseMessage.status = {
|
responseMessage.statusHistory.push({
|
||||||
...responseMessage.status,
|
|
||||||
done: true,
|
done: true,
|
||||||
error: true,
|
error: true,
|
||||||
|
action: 'web_search',
|
||||||
description: 'No search query generated'
|
description: 'No search query generated'
|
||||||
};
|
});
|
||||||
|
|
||||||
messages = messages;
|
messages = messages;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responseMessage.status = {
|
responseMessage.statusHistory.push({
|
||||||
...responseMessage.status,
|
done: false,
|
||||||
description: $i18n.t("Searching the web for '{{searchQuery}}'", { searchQuery })
|
action: 'web_search',
|
||||||
};
|
description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery })
|
||||||
|
});
|
||||||
messages = messages;
|
messages = messages;
|
||||||
|
|
||||||
const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
|
const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
|
||||||
@@ -510,12 +559,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
responseMessage.status = {
|
responseMessage.statusHistory.push({
|
||||||
...responseMessage.status,
|
|
||||||
done: true,
|
done: true,
|
||||||
|
action: 'web_search',
|
||||||
description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
|
description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
|
||||||
|
query: searchQuery,
|
||||||
urls: results.filenames
|
urls: results.filenames
|
||||||
};
|
});
|
||||||
|
|
||||||
if (responseMessage?.files ?? undefined === undefined) {
|
if (responseMessage?.files ?? undefined === undefined) {
|
||||||
responseMessage.files = [];
|
responseMessage.files = [];
|
||||||
@@ -530,18 +580,19 @@
|
|||||||
|
|
||||||
messages = messages;
|
messages = messages;
|
||||||
} else {
|
} else {
|
||||||
responseMessage.status = {
|
responseMessage.statusHistory.push({
|
||||||
...responseMessage.status,
|
|
||||||
done: true,
|
done: true,
|
||||||
error: true,
|
error: true,
|
||||||
|
action: 'web_search',
|
||||||
description: 'No search results found'
|
description: 'No search results found'
|
||||||
};
|
});
|
||||||
messages = messages;
|
messages = messages;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
|
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||||
model = model.id;
|
let _response = null;
|
||||||
|
|
||||||
const responseMessage = history.messages[responseMessageId];
|
const responseMessage = history.messages[responseMessageId];
|
||||||
|
|
||||||
// Wait until history/message have been updated
|
// Wait until history/message have been updated
|
||||||
@@ -599,17 +650,29 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = messages
|
let docs = [];
|
||||||
|
|
||||||
|
if (model?.info?.meta?.knowledge ?? false) {
|
||||||
|
docs = model.info.meta.knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = [
|
||||||
|
...docs,
|
||||||
|
...messages
|
||||||
.filter((message) => message?.files ?? null)
|
.filter((message) => message?.files ?? null)
|
||||||
.map((message) =>
|
.map((message) =>
|
||||||
message.files.filter((item) =>
|
message.files.filter((item) =>
|
||||||
['doc', 'collection', 'web_search_results'].includes(item.type)
|
['doc', 'collection', 'web_search_results'].includes(item.type)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.flat(1);
|
.flat(1)
|
||||||
|
].filter(
|
||||||
|
(item, index, array) =>
|
||||||
|
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||||
|
);
|
||||||
|
|
||||||
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
||||||
model: model,
|
model: model.id,
|
||||||
messages: messagesBody,
|
messages: messagesBody,
|
||||||
options: {
|
options: {
|
||||||
...($settings.params ?? {}),
|
...($settings.params ?? {}),
|
||||||
@@ -647,9 +710,10 @@
|
|||||||
controller.abort('User: Stop Response');
|
controller.abort('User: Stop Response');
|
||||||
} else {
|
} else {
|
||||||
const messages = createMessagesList(responseMessageId);
|
const messages = createMessagesList(responseMessageId);
|
||||||
await chatCompletedHandler(model, messages);
|
await chatCompletedHandler(model.id, messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_response = responseMessage.content;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,7 +771,7 @@
|
|||||||
selectedModelfile.title.charAt(0).toUpperCase() +
|
selectedModelfile.title.charAt(0).toUpperCase() +
|
||||||
selectedModelfile.title.slice(1)
|
selectedModelfile.title.slice(1)
|
||||||
}`
|
}`
|
||||||
: `${model}`,
|
: `${model.id}`,
|
||||||
{
|
{
|
||||||
body: responseMessage.content,
|
body: responseMessage.content,
|
||||||
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||||
@@ -719,7 +783,7 @@
|
|||||||
copyToClipboard(responseMessage.content);
|
copyToClipboard(responseMessage.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($settings.responseAutoPlayback) {
|
if ($settings.responseAutoPlayback && !$showCallOverlay) {
|
||||||
await tick();
|
await tick();
|
||||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||||
}
|
}
|
||||||
@@ -786,21 +850,34 @@
|
|||||||
const _title = await generateChatTitle(userPrompt);
|
const _title = await generateChatTitle(userPrompt);
|
||||||
await setChatTitle(_chatId, _title);
|
await setChatTitle(_chatId, _title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
|
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||||
|
let _response = null;
|
||||||
const responseMessage = history.messages[responseMessageId];
|
const responseMessage = history.messages[responseMessageId];
|
||||||
|
|
||||||
const docs = messages
|
let docs = [];
|
||||||
|
|
||||||
|
if (model?.info?.meta?.knowledge ?? false) {
|
||||||
|
docs = model.info.meta.knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
docs = [
|
||||||
|
...docs,
|
||||||
|
...messages
|
||||||
.filter((message) => message?.files ?? null)
|
.filter((message) => message?.files ?? null)
|
||||||
.map((message) =>
|
.map((message) =>
|
||||||
message.files.filter((item) =>
|
message.files.filter((item) =>
|
||||||
['doc', 'collection', 'web_search_results'].includes(item.type)
|
['doc', 'collection', 'web_search_results'].includes(item.type)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.flat(1);
|
.flat(1)
|
||||||
|
].filter(
|
||||||
console.log(docs);
|
(item, index, array) =>
|
||||||
|
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||||
|
);
|
||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
@@ -902,9 +979,11 @@
|
|||||||
} else {
|
} else {
|
||||||
const messages = createMessagesList(responseMessageId);
|
const messages = createMessagesList(responseMessageId);
|
||||||
|
|
||||||
await chatCompletedHandler(model, messages);
|
await chatCompletedHandler(model.id, messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_response = responseMessage.content;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,7 +1009,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($settings.notificationEnabled && !document.hasFocus()) {
|
if ($settings.notificationEnabled && !document.hasFocus()) {
|
||||||
const notification = new Notification(`OpenAI ${model}`, {
|
const notification = new Notification(`${model.id}`, {
|
||||||
body: responseMessage.content,
|
body: responseMessage.content,
|
||||||
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
||||||
});
|
});
|
||||||
@@ -940,8 +1019,9 @@
|
|||||||
copyToClipboard(responseMessage.content);
|
copyToClipboard(responseMessage.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($settings.responseAutoPlayback) {
|
if ($settings.responseAutoPlayback && !$showCallOverlay) {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,6 +1060,8 @@
|
|||||||
const _title = await generateChatTitle(userPrompt);
|
const _title = await generateChatTitle(userPrompt);
|
||||||
await setChatTitle(_chatId, _title);
|
await setChatTitle(_chatId, _title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenAIError = async (error, res: Response | null, model, responseMessage) => {
|
const handleOpenAIError = async (error, res: Response | null, model, responseMessage) => {
|
||||||
@@ -1075,28 +1157,15 @@
|
|||||||
|
|
||||||
const generateChatTitle = async (userPrompt) => {
|
const generateChatTitle = async (userPrompt) => {
|
||||||
if ($settings?.title?.auto ?? true) {
|
if ($settings?.title?.auto ?? true) {
|
||||||
const model = $models.find((model) => model.id === selectedModels[0]);
|
|
||||||
|
|
||||||
const titleModelId =
|
|
||||||
model?.owned_by === 'openai' ?? false
|
|
||||||
? $settings?.title?.modelExternal ?? selectedModels[0]
|
|
||||||
: $settings?.title?.model ?? selectedModels[0];
|
|
||||||
const titleModel = $models.find((model) => model.id === titleModelId);
|
|
||||||
|
|
||||||
console.log(titleModel);
|
|
||||||
const title = await generateTitle(
|
const title = await generateTitle(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$settings?.title?.prompt ??
|
selectedModels[0],
|
||||||
$i18n.t(
|
|
||||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
|
||||||
) + ' {{prompt}}',
|
|
||||||
titleModelId,
|
|
||||||
userPrompt,
|
userPrompt,
|
||||||
$chatId,
|
$chatId
|
||||||
titleModel?.owned_by === 'openai' ?? false
|
).catch((error) => {
|
||||||
? `${OPENAI_API_BASE_URL}`
|
console.error(error);
|
||||||
: `${OLLAMA_API_BASE_URL}/v1`
|
return 'New Chat';
|
||||||
);
|
});
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
} else {
|
} else {
|
||||||
@@ -1104,29 +1173,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateChatSearchQuery = async (modelId: string, prompt: string) => {
|
|
||||||
const model = $models.find((model) => model.id === modelId);
|
|
||||||
const taskModelId =
|
|
||||||
model?.owned_by === 'openai' ?? false
|
|
||||||
? $settings?.title?.modelExternal ?? modelId
|
|
||||||
: $settings?.title?.model ?? modelId;
|
|
||||||
const taskModel = $models.find((model) => model.id === taskModelId);
|
|
||||||
|
|
||||||
const previousMessages = messages
|
|
||||||
.filter((message) => message.role === 'user')
|
|
||||||
.map((message) => message.content);
|
|
||||||
|
|
||||||
return await generateSearchQuery(
|
|
||||||
localStorage.token,
|
|
||||||
taskModelId,
|
|
||||||
previousMessages,
|
|
||||||
prompt,
|
|
||||||
taskModel?.owned_by === 'openai' ?? false
|
|
||||||
? `${OPENAI_API_BASE_URL}`
|
|
||||||
: `${OLLAMA_API_BASE_URL}/v1`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setChatTitle = async (_chatId, _title) => {
|
const setChatTitle = async (_chatId, _title) => {
|
||||||
if (_chatId === $chatId) {
|
if (_chatId === $chatId) {
|
||||||
title = _title;
|
title = _title;
|
||||||
@@ -1143,28 +1189,6 @@
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTag = async (tagName) => {
|
|
||||||
const res = await addTagById(localStorage.token, $chatId, tagName);
|
|
||||||
tags = await getTags();
|
|
||||||
|
|
||||||
chat = await updateChatById(localStorage.token, $chatId, {
|
|
||||||
tags: tags
|
|
||||||
});
|
|
||||||
|
|
||||||
_tags.set(await getAllChatTags(localStorage.token));
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTag = async (tagName) => {
|
|
||||||
const res = await deleteTagById(localStorage.token, $chatId, tagName);
|
|
||||||
tags = await getTags();
|
|
||||||
|
|
||||||
chat = await updateChatById(localStorage.token, $chatId, {
|
|
||||||
tags: tags
|
|
||||||
});
|
|
||||||
|
|
||||||
_tags.set(await getAllChatTags(localStorage.token));
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -1175,9 +1199,11 @@
|
|||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<CallOverlay {submitPrompt} bind:files />
|
||||||
|
|
||||||
{#if !chatIdProp || (loaded && chatIdProp)}
|
{#if !chatIdProp || (loaded && chatIdProp)}
|
||||||
<div
|
<div
|
||||||
class="min-h-screen max-h-screen {$showSidebar
|
class="h-screen max-h-[100dvh] {$showSidebar
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
? 'md:max-w-[calc(100%-260px)]'
|
||||||
: ''} w-full max-w-full flex flex-col"
|
: ''} w-full max-w-full flex flex-col"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { type Model, mobile, settings, showSidebar, models, config } from '$lib/stores';
|
import {
|
||||||
|
type Model,
|
||||||
|
mobile,
|
||||||
|
settings,
|
||||||
|
showSidebar,
|
||||||
|
models,
|
||||||
|
config,
|
||||||
|
showCallOverlay
|
||||||
|
} from '$lib/stores';
|
||||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,8 +19,6 @@
|
|||||||
} from '$lib/apis/rag';
|
} from '$lib/apis/rag';
|
||||||
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
|
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import { transcribeAudio } from '$lib/apis/audio';
|
|
||||||
|
|
||||||
import Prompts from './MessageInput/PromptCommands.svelte';
|
import Prompts from './MessageInput/PromptCommands.svelte';
|
||||||
import Suggestions from './MessageInput/Suggestions.svelte';
|
import Suggestions from './MessageInput/Suggestions.svelte';
|
||||||
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
||||||
@@ -21,7 +27,9 @@
|
|||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||||
import { t } from 'i18next';
|
import Headphone from '../icons/Headphone.svelte';
|
||||||
|
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||||
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -33,6 +41,8 @@
|
|||||||
export let atSelectedModel: Model | undefined;
|
export let atSelectedModel: Model | undefined;
|
||||||
export let selectedModels: [''];
|
export let selectedModels: [''];
|
||||||
|
|
||||||
|
let recording = false;
|
||||||
|
|
||||||
let chatTextAreaElement: HTMLTextAreaElement;
|
let chatTextAreaElement: HTMLTextAreaElement;
|
||||||
let filesInputElement;
|
let filesInputElement;
|
||||||
|
|
||||||
@@ -48,14 +58,11 @@
|
|||||||
|
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
export let speechRecognitionEnabled = true;
|
|
||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let messages = [];
|
export let messages = [];
|
||||||
|
|
||||||
let speechRecognition;
|
|
||||||
|
|
||||||
let visionCapableModels = [];
|
let visionCapableModels = [];
|
||||||
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
|
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
|
||||||
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
|
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
|
||||||
@@ -68,176 +75,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mediaRecorder;
|
|
||||||
let audioChunks = [];
|
|
||||||
let isRecording = false;
|
|
||||||
const MIN_DECIBELS = -45;
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
const element = document.getElementById('messages-container');
|
const element = document.getElementById('messages-container');
|
||||||
element.scrollTop = element.scrollHeight;
|
element.scrollTop = element.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const startRecording = async () => {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
mediaRecorder = new MediaRecorder(stream);
|
|
||||||
mediaRecorder.onstart = () => {
|
|
||||||
isRecording = true;
|
|
||||||
console.log('Recording started');
|
|
||||||
};
|
|
||||||
mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
|
|
||||||
mediaRecorder.onstop = async () => {
|
|
||||||
isRecording = false;
|
|
||||||
console.log('Recording stopped');
|
|
||||||
|
|
||||||
// Create a blob from the audio chunks
|
|
||||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
|
||||||
|
|
||||||
const file = blobToFile(audioBlob, 'recording.wav');
|
|
||||||
|
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
|
||||||
toast.error(error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
prompt = res.text;
|
|
||||||
await tick();
|
|
||||||
chatTextAreaElement?.focus();
|
|
||||||
|
|
||||||
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
|
||||||
submitPrompt(prompt, user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveRecording(audioBlob);
|
|
||||||
audioChunks = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start recording
|
|
||||||
mediaRecorder.start();
|
|
||||||
|
|
||||||
// Monitor silence
|
|
||||||
monitorSilence(stream);
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitorSilence = (stream) => {
|
|
||||||
const audioContext = new AudioContext();
|
|
||||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
|
||||||
const analyser = audioContext.createAnalyser();
|
|
||||||
analyser.minDecibels = MIN_DECIBELS;
|
|
||||||
audioStreamSource.connect(analyser);
|
|
||||||
|
|
||||||
const bufferLength = analyser.frequencyBinCount;
|
|
||||||
const domainData = new Uint8Array(bufferLength);
|
|
||||||
|
|
||||||
let lastSoundTime = Date.now();
|
|
||||||
|
|
||||||
const detectSound = () => {
|
|
||||||
analyser.getByteFrequencyData(domainData);
|
|
||||||
|
|
||||||
if (domainData.some((value) => value > 0)) {
|
|
||||||
lastSoundTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRecording && Date.now() - lastSoundTime > 3000) {
|
|
||||||
mediaRecorder.stop();
|
|
||||||
audioContext.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.requestAnimationFrame(detectSound);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.requestAnimationFrame(detectSound);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveRecording = (blob) => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.style = 'display: none';
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'recording.wav';
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const speechRecognitionHandler = () => {
|
|
||||||
// Check if SpeechRecognition is supported
|
|
||||||
|
|
||||||
if (isRecording) {
|
|
||||||
if (speechRecognition) {
|
|
||||||
speechRecognition.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaRecorder) {
|
|
||||||
mediaRecorder.stop();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isRecording = true;
|
|
||||||
|
|
||||||
if ($settings?.audio?.STTEngine ?? '' !== '') {
|
|
||||||
startRecording();
|
|
||||||
} else {
|
|
||||||
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
|
|
||||||
// Create a SpeechRecognition object
|
|
||||||
speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
|
||||||
|
|
||||||
// Set continuous to true for continuous recognition
|
|
||||||
speechRecognition.continuous = true;
|
|
||||||
|
|
||||||
// Set the timeout for turning off the recognition after inactivity (in milliseconds)
|
|
||||||
const inactivityTimeout = 3000; // 3 seconds
|
|
||||||
|
|
||||||
let timeoutId;
|
|
||||||
// Start recognition
|
|
||||||
speechRecognition.start();
|
|
||||||
|
|
||||||
// Event triggered when speech is recognized
|
|
||||||
speechRecognition.onresult = async (event) => {
|
|
||||||
// Clear the inactivity timeout
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// Handle recognized speech
|
|
||||||
console.log(event);
|
|
||||||
const transcript = event.results[Object.keys(event.results).length - 1][0].transcript;
|
|
||||||
|
|
||||||
prompt = `${prompt}${transcript}`;
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
chatTextAreaElement?.focus();
|
|
||||||
|
|
||||||
// Restart the inactivity timeout
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
console.log('Speech recognition turned off due to inactivity.');
|
|
||||||
speechRecognition.stop();
|
|
||||||
}, inactivityTimeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Event triggered when recognition is ended
|
|
||||||
speechRecognition.onend = function () {
|
|
||||||
// Restart recognition after it ends
|
|
||||||
console.log('recognition ended');
|
|
||||||
isRecording = false;
|
|
||||||
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
|
||||||
submitPrompt(prompt, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Event triggered when an error occurs
|
|
||||||
speechRecognition.onerror = function (event) {
|
|
||||||
console.log(event);
|
|
||||||
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
|
|
||||||
isRecording = false;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadDoc = async (file) => {
|
const uploadDoc = async (file) => {
|
||||||
console.log(file);
|
console.log(file);
|
||||||
|
|
||||||
@@ -472,7 +314,7 @@
|
|||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
{#if prompt.charAt(0) === '/'}
|
{#if prompt.charAt(0) === '/'}
|
||||||
<Prompts bind:this={promptsElement} bind:prompt />
|
<Prompts bind:this={promptsElement} bind:prompt bind:files />
|
||||||
{:else if prompt.charAt(0) === '#'}
|
{:else if prompt.charAt(0) === '#'}
|
||||||
<Documents
|
<Documents
|
||||||
bind:this={documentsElement}
|
bind:this={documentsElement}
|
||||||
@@ -601,13 +443,41 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if recording}
|
||||||
|
<VoiceRecording
|
||||||
|
bind:recording
|
||||||
|
on:cancel={async () => {
|
||||||
|
recording = false;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
document.getElementById('chat-textarea')?.focus();
|
||||||
|
}}
|
||||||
|
on:confirm={async (e) => {
|
||||||
|
const response = e.detail;
|
||||||
|
prompt = `${prompt}${response} `;
|
||||||
|
|
||||||
|
recording = false;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
document.getElementById('chat-textarea')?.focus();
|
||||||
|
|
||||||
|
if ($settings?.speechAutoSend ?? false) {
|
||||||
|
submitPrompt(prompt, user);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<form
|
<form
|
||||||
dir={$settings?.chatDirection ?? 'LTR'}
|
class="w-full flex gap-1.5"
|
||||||
class=" flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
|
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
// check if selectedModels support image input
|
// check if selectedModels support image input
|
||||||
submitPrompt(prompt, user);
|
submitPrompt(prompt, user);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex-1 flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
|
||||||
|
dir={$settings?.chatDirection ?? 'LTR'}
|
||||||
>
|
>
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
|
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
|
||||||
@@ -615,7 +485,11 @@
|
|||||||
<div class=" relative group">
|
<div class=" relative group">
|
||||||
{#if file.type === 'image'}
|
{#if file.type === 'image'}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" />
|
<img
|
||||||
|
src={file.url}
|
||||||
|
alt="input"
|
||||||
|
class=" h-16 w-16 rounded-xl object-cover"
|
||||||
|
/>
|
||||||
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className=" absolute top-1 left-1"
|
className=" absolute top-1 left-1"
|
||||||
@@ -691,7 +565,12 @@
|
|||||||
transform: translate(0);
|
transform: translate(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
</style><circle
|
||||||
|
class="spinner_qM83"
|
||||||
|
cx="4"
|
||||||
|
cy="12"
|
||||||
|
r="2.5"
|
||||||
|
/><circle
|
||||||
class="spinner_qM83 spinner_oXPr"
|
class="spinner_qM83 spinner_oXPr"
|
||||||
cx="12"
|
cx="12"
|
||||||
cy="12"
|
cy="12"
|
||||||
@@ -771,7 +650,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class=" flex">
|
<div class=" flex">
|
||||||
<div class=" ml-1 self-end mb-2 flex space-x-1">
|
<div class=" ml-0.5 self-end mb-1.5 flex space-x-1">
|
||||||
<InputMenu
|
<InputMenu
|
||||||
bind:webSearchEnabled
|
bind:webSearchEnabled
|
||||||
uploadFilesHandler={() => {
|
uploadFilesHandler={() => {
|
||||||
@@ -783,7 +662,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
|
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -803,17 +682,12 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="chat-textarea"
|
id="chat-textarea"
|
||||||
bind:this={chatTextAreaElement}
|
bind:this={chatTextAreaElement}
|
||||||
class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-3 rounded-xl resize-none h-[48px]"
|
class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
|
||||||
placeholder={chatInputPlaceholder !== ''
|
placeholder={chatInputPlaceholder !== ''
|
||||||
? chatInputPlaceholder
|
? chatInputPlaceholder
|
||||||
: isRecording
|
|
||||||
? $i18n.t('Listening...')
|
|
||||||
: $i18n.t('Send a Message')}
|
: $i18n.t('Send a Message')}
|
||||||
bind:value={prompt}
|
bind:value={prompt}
|
||||||
on:keypress={(e) => {}}
|
on:keypress={(e) => {
|
||||||
on:keydown={async (e) => {
|
|
||||||
// Check if the device is not a mobile device or if it is a mobile device, check if it is not a touch device
|
|
||||||
// This is to prevent the Enter key from submitting the prompt on mobile devices
|
|
||||||
if (
|
if (
|
||||||
!$mobile ||
|
!$mobile ||
|
||||||
!(
|
!(
|
||||||
@@ -822,22 +696,18 @@
|
|||||||
navigator.msMaxTouchPoints > 0
|
navigator.msMaxTouchPoints > 0
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Check if Enter is pressed
|
// Prevent Enter key from creating a new line
|
||||||
// Check if Shift key is not pressed
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey && prompt !== '') {
|
// Submit the prompt when Enter key is pressed
|
||||||
|
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||||
submitPrompt(prompt, user);
|
submitPrompt(prompt, user);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Enter' && e.shiftKey && prompt !== '') {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
|
on:keydown={async (e) => {
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||||
|
|
||||||
// Check if Ctrl + R is pressed
|
// Check if Ctrl + R is pressed
|
||||||
@@ -898,7 +768,9 @@
|
|||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
]?.at(-1);
|
]?.at(-1);
|
||||||
|
|
||||||
if (commandOptionButton) {
|
if (e.shiftKey) {
|
||||||
|
prompt = `${prompt}\n`;
|
||||||
|
} else if (commandOptionButton) {
|
||||||
commandOptionButton?.click();
|
commandOptionButton?.click();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('send-message-button')?.click();
|
document.getElementById('send-message-button')?.click();
|
||||||
@@ -979,58 +851,34 @@
|
|||||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||||
<Tooltip content={$i18n.t('Record voice')}>
|
<Tooltip content={$i18n.t('Record voice')}>
|
||||||
{#if speechRecognitionEnabled}
|
|
||||||
<button
|
<button
|
||||||
id="voice-input-button"
|
id="voice-input-button"
|
||||||
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
|
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={async () => {
|
||||||
speechRecognitionHandler();
|
try {
|
||||||
|
const res = await navigator.mediaDevices
|
||||||
|
.getUserMedia({ audio: true })
|
||||||
|
.catch(function (err) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(
|
||||||
|
`Permission denied when accessing microphone: {{error}}`,
|
||||||
|
{
|
||||||
|
error: err
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
recording = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error($i18n.t('Permission denied when accessing microphone'));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if isRecording}
|
|
||||||
<svg
|
|
||||||
class=" w-5 h-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
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -1042,17 +890,58 @@
|
|||||||
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end w-10">
|
||||||
|
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||||
|
{#if prompt === ''}
|
||||||
|
<div class=" flex items-center mb-1">
|
||||||
|
<Tooltip content={$i18n.t('Call')}>
|
||||||
|
<button
|
||||||
|
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-2 self-center"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
if (selectedModels.length > 1) {
|
||||||
|
toast.error($i18n.t('Select only one model to call'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config.audio.stt.engine === 'web') {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t('Call feature is not supported when using Web STT engine')
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check if user has access to getUserMedia
|
||||||
|
try {
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||||
|
// If the user grants the permission, proceed to show the call overlay
|
||||||
|
|
||||||
|
showCallOverlay.set(true);
|
||||||
|
} catch (err) {
|
||||||
|
// If the user denies the permission or an error occurs, show an error message
|
||||||
|
toast.error($i18n.t('Permission denied when accessing media devices'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Headphone className="size-6" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class=" flex items-center mb-1">
|
||||||
<Tooltip content={$i18n.t('Send message')}>
|
<Tooltip content={$i18n.t('Send message')}>
|
||||||
<button
|
<button
|
||||||
id="send-message-button"
|
id="send-message-button"
|
||||||
class="{prompt !== ''
|
class="{prompt !== ''
|
||||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||||
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
|
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 m-0.5 self-center"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={prompt === ''}
|
disabled={prompt === ''}
|
||||||
>
|
>
|
||||||
@@ -1060,7 +949,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-5 h-5"
|
class="size-6"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
@@ -1070,16 +959,21 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class=" flex items-center mb-1.5">
|
||||||
<button
|
<button
|
||||||
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||||
on:click={stopResponse}
|
on:click={() => {
|
||||||
|
stopResponse();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-5 h-5"
|
class="size-6"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
@@ -1088,12 +982,13 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="mt-1.5 text-xs text-gray-500 text-center">
|
<div class="mt-1.5 text-xs text-gray-500 text-center line-clamp-1">
|
||||||
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,693 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { config, settings, showCallOverlay } from '$lib/stores';
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { blobToFile, calculateSHA256, extractSentences, findWordIndices } from '$lib/utils';
|
||||||
|
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let submitPrompt: Function;
|
||||||
|
export let files;
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
let confirmed = false;
|
||||||
|
|
||||||
|
let camera = false;
|
||||||
|
let cameraStream = null;
|
||||||
|
|
||||||
|
let assistantSpeaking = false;
|
||||||
|
let assistantAudio = {};
|
||||||
|
let assistantAudioIdx = null;
|
||||||
|
|
||||||
|
let rmsLevel = 0;
|
||||||
|
let hasStartedSpeaking = false;
|
||||||
|
|
||||||
|
let currentUtterance = null;
|
||||||
|
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioChunks = [];
|
||||||
|
|
||||||
|
const MIN_DECIBELS = -45;
|
||||||
|
const VISUALIZER_BUFFER_LENGTH = 300;
|
||||||
|
|
||||||
|
// Function to calculate the RMS level from time domain data
|
||||||
|
const calculateRMS = (data: Uint8Array) => {
|
||||||
|
let sumSquares = 0;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
|
||||||
|
sumSquares += normalizedValue * normalizedValue;
|
||||||
|
}
|
||||||
|
return Math.sqrt(sumSquares / data.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRMS = (rms) => {
|
||||||
|
rms = rms * 10;
|
||||||
|
const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more
|
||||||
|
const scaledRMS = Math.pow(rms, exp);
|
||||||
|
|
||||||
|
// Scale between 0.01 (1%) and 1.0 (100%)
|
||||||
|
return Math.min(1.0, Math.max(0.01, scaledRMS));
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyseAudio = (stream) => {
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||||
|
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
analyser.minDecibels = MIN_DECIBELS;
|
||||||
|
audioStreamSource.connect(analyser);
|
||||||
|
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
|
||||||
|
const domainData = new Uint8Array(bufferLength);
|
||||||
|
const timeDomainData = new Uint8Array(analyser.fftSize);
|
||||||
|
|
||||||
|
let lastSoundTime = Date.now();
|
||||||
|
hasStartedSpeaking = false;
|
||||||
|
|
||||||
|
const detectSound = () => {
|
||||||
|
const processFrame = () => {
|
||||||
|
if (!mediaRecorder || !$showCallOverlay) {
|
||||||
|
if (mediaRecorder) {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
analyser.getByteTimeDomainData(timeDomainData);
|
||||||
|
analyser.getByteFrequencyData(domainData);
|
||||||
|
|
||||||
|
// Calculate RMS level from time domain data
|
||||||
|
rmsLevel = calculateRMS(timeDomainData);
|
||||||
|
|
||||||
|
// Check if initial speech/noise has started
|
||||||
|
const hasSound = domainData.some((value) => value > 0);
|
||||||
|
if (hasSound) {
|
||||||
|
stopAllAudio();
|
||||||
|
hasStartedSpeaking = true;
|
||||||
|
lastSoundTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start silence detection only after initial speech/noise has been detected
|
||||||
|
if (hasStartedSpeaking) {
|
||||||
|
if (Date.now() - lastSoundTime > 2000) {
|
||||||
|
confirmed = true;
|
||||||
|
|
||||||
|
if (mediaRecorder) {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(processFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.requestAnimationFrame(processFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
detectSound();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopAllAudio = () => {
|
||||||
|
if (currentUtterance) {
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
currentUtterance = null;
|
||||||
|
}
|
||||||
|
if (assistantAudio[assistantAudioIdx]) {
|
||||||
|
assistantAudio[assistantAudioIdx].pause();
|
||||||
|
assistantAudio[assistantAudioIdx].currentTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioElement = document.getElementById('audioElement');
|
||||||
|
audioElement.pause();
|
||||||
|
audioElement.currentTime = 0;
|
||||||
|
|
||||||
|
assistantSpeaking = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const playAudio = (idx) => {
|
||||||
|
if ($showCallOverlay) {
|
||||||
|
return new Promise((res) => {
|
||||||
|
assistantAudioIdx = idx;
|
||||||
|
const audioElement = document.getElementById('audioElement');
|
||||||
|
const audio = assistantAudio[idx];
|
||||||
|
|
||||||
|
audioElement.src = audio.src; // Assume `assistantAudio` has objects with a `src` property
|
||||||
|
|
||||||
|
audioElement.muted = true;
|
||||||
|
|
||||||
|
audioElement
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
audioElement.muted = false;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.onended = async (e) => {
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
|
||||||
|
if (Object.keys(assistantAudio).length - 1 === idx) {
|
||||||
|
assistantSpeaking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
res(e);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOpenAISpeech = async (text) => {
|
||||||
|
const res = await synthesizeOpenAISpeech(
|
||||||
|
localStorage.token,
|
||||||
|
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||||
|
text
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
assistantSpeaking = false;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const blob = await res.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const audio = new Audio(blobUrl);
|
||||||
|
assistantAudio = audio;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const transcribeHandler = async (audioBlob) => {
|
||||||
|
// Create a blob from the audio chunks
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const file = blobToFile(audioBlob, 'recording.wav');
|
||||||
|
|
||||||
|
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
console.log(res.text);
|
||||||
|
|
||||||
|
if (res.text !== '') {
|
||||||
|
const _responses = await submitPrompt(res.text);
|
||||||
|
console.log(_responses);
|
||||||
|
|
||||||
|
if (_responses.at(0)) {
|
||||||
|
const content = _responses[0];
|
||||||
|
if ((content ?? '').trim() !== '') {
|
||||||
|
assistantSpeakingHandler(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assistantSpeakingHandler = async (content) => {
|
||||||
|
assistantSpeaking = true;
|
||||||
|
|
||||||
|
if (($config.audio.tts.engine ?? '') == '') {
|
||||||
|
let voices = [];
|
||||||
|
const getVoicesLoop = setInterval(async () => {
|
||||||
|
voices = await speechSynthesis.getVoices();
|
||||||
|
if (voices.length > 0) {
|
||||||
|
clearInterval(getVoicesLoop);
|
||||||
|
|
||||||
|
const voice =
|
||||||
|
voices
|
||||||
|
?.filter(
|
||||||
|
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||||
|
)
|
||||||
|
?.at(0) ?? undefined;
|
||||||
|
|
||||||
|
currentUtterance = new SpeechSynthesisUtterance(content);
|
||||||
|
|
||||||
|
if (voice) {
|
||||||
|
currentUtterance.voice = voice;
|
||||||
|
}
|
||||||
|
|
||||||
|
speechSynthesis.speak(currentUtterance);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else if ($config.audio.tts.engine === 'openai') {
|
||||||
|
console.log('openai');
|
||||||
|
|
||||||
|
const sentences = extractSentences(content).reduce((mergedTexts, currentText) => {
|
||||||
|
const lastIndex = mergedTexts.length - 1;
|
||||||
|
if (lastIndex >= 0) {
|
||||||
|
const previousText = mergedTexts[lastIndex];
|
||||||
|
const wordCount = previousText.split(/\s+/).length;
|
||||||
|
if (wordCount < 2) {
|
||||||
|
mergedTexts[lastIndex] = previousText + ' ' + currentText;
|
||||||
|
} else {
|
||||||
|
mergedTexts.push(currentText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mergedTexts.push(currentText);
|
||||||
|
}
|
||||||
|
return mergedTexts;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
console.log(sentences);
|
||||||
|
|
||||||
|
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||||
|
|
||||||
|
for (const [idx, sentence] of sentences.entries()) {
|
||||||
|
const res = await synthesizeOpenAISpeech(
|
||||||
|
localStorage.token,
|
||||||
|
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||||
|
sentence
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
|
||||||
|
assistantSpeaking = false;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const blob = await res.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const audio = new Audio(blobUrl);
|
||||||
|
assistantAudio[idx] = audio;
|
||||||
|
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecordingCallback = async () => {
|
||||||
|
if ($showCallOverlay) {
|
||||||
|
if (confirmed) {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
if (cameraStream) {
|
||||||
|
const imageUrl = takeScreenshot();
|
||||||
|
|
||||||
|
files = [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
url: imageUrl
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||||
|
await transcribeHandler(audioBlob);
|
||||||
|
|
||||||
|
confirmed = false;
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
audioChunks = [];
|
||||||
|
mediaRecorder = false;
|
||||||
|
|
||||||
|
startRecording();
|
||||||
|
} else {
|
||||||
|
audioChunks = [];
|
||||||
|
mediaRecorder = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
mediaRecorder = new MediaRecorder(stream);
|
||||||
|
mediaRecorder.onstart = () => {
|
||||||
|
console.log('Recording started');
|
||||||
|
audioChunks = [];
|
||||||
|
analyseAudio(stream);
|
||||||
|
};
|
||||||
|
mediaRecorder.ondataavailable = (event) => {
|
||||||
|
if (hasStartedSpeaking) {
|
||||||
|
audioChunks.push(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mediaRecorder.onstop = async () => {
|
||||||
|
console.log('Recording stopped');
|
||||||
|
|
||||||
|
await stopRecordingCallback();
|
||||||
|
};
|
||||||
|
mediaRecorder.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
let videoInputDevices = [];
|
||||||
|
let selectedVideoInputDeviceId = null;
|
||||||
|
|
||||||
|
const getVideoInputDevices = async () => {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
videoInputDevices = devices.filter((device) => device.kind === 'videoinput');
|
||||||
|
|
||||||
|
if (!!navigator.mediaDevices.getDisplayMedia) {
|
||||||
|
videoInputDevices = [
|
||||||
|
...videoInputDevices,
|
||||||
|
{
|
||||||
|
deviceId: 'screen',
|
||||||
|
label: 'Screen Share'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(videoInputDevices);
|
||||||
|
if (selectedVideoInputDeviceId === null && videoInputDevices.length > 0) {
|
||||||
|
selectedVideoInputDeviceId = videoInputDevices[0].deviceId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startCamera = async () => {
|
||||||
|
await getVideoInputDevices();
|
||||||
|
|
||||||
|
if (cameraStream === null) {
|
||||||
|
camera = true;
|
||||||
|
await tick();
|
||||||
|
try {
|
||||||
|
await startVideoStream();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error accessing webcam: ', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startVideoStream = async () => {
|
||||||
|
const video = document.getElementById('camera-feed');
|
||||||
|
if (video) {
|
||||||
|
if (selectedVideoInputDeviceId === 'screen') {
|
||||||
|
cameraStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: {
|
||||||
|
cursor: 'always'
|
||||||
|
},
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
deviceId: selectedVideoInputDeviceId ? { exact: selectedVideoInputDeviceId } : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraStream) {
|
||||||
|
await getVideoInputDevices();
|
||||||
|
video.srcObject = cameraStream;
|
||||||
|
await video.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopVideoStream = async () => {
|
||||||
|
if (cameraStream) {
|
||||||
|
const tracks = cameraStream.getTracks();
|
||||||
|
tracks.forEach((track) => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraStream = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const takeScreenshot = () => {
|
||||||
|
const video = document.getElementById('camera-feed');
|
||||||
|
const canvas = document.getElementById('camera-canvas');
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Make the canvas match the video dimensions
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
|
||||||
|
// Draw the image from the video onto the canvas
|
||||||
|
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||||
|
|
||||||
|
// Convert the canvas to a data base64 URL and console log it
|
||||||
|
const dataURL = canvas.toDataURL('image/png');
|
||||||
|
console.log(dataURL);
|
||||||
|
|
||||||
|
return dataURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopCamera = async () => {
|
||||||
|
await stopVideoStream();
|
||||||
|
camera = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if ($showCallOverlay) {
|
||||||
|
startRecording();
|
||||||
|
} else {
|
||||||
|
stopCamera();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $showCallOverlay}
|
||||||
|
<audio id="audioElement" src="" style="display: none;" />
|
||||||
|
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
||||||
|
>
|
||||||
|
<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
|
||||||
|
{#if camera}
|
||||||
|
<div class="flex justify-center items-center w-full min-h-20">
|
||||||
|
{#if loading}
|
||||||
|
<svg
|
||||||
|
class="size-12 text-gray-900 dark:text-gray-400"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
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="3" /><circle
|
||||||
|
class="spinner_qM83 spinner_oXPr"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class=" {rmsLevel * 100 > 4
|
||||||
|
? ' size-[4.5rem]'
|
||||||
|
: rmsLevel * 100 > 2
|
||||||
|
? ' size-16'
|
||||||
|
: rmsLevel * 100 > 1
|
||||||
|
? 'size-14'
|
||||||
|
: 'size-12'} transition-all bg-black dark:bg-white rounded-full"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<!-- navbar -->
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
|
||||||
|
{#if !camera}
|
||||||
|
{#if loading}
|
||||||
|
<svg
|
||||||
|
class="size-44 text-gray-900 dark:text-gray-400"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
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="3" /><circle
|
||||||
|
class="spinner_qM83 spinner_oXPr"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class=" {rmsLevel * 100 > 4
|
||||||
|
? ' size-52'
|
||||||
|
: rmsLevel * 100 > 2
|
||||||
|
? 'size-48'
|
||||||
|
: rmsLevel * 100 > 1
|
||||||
|
? 'size-[11.5rem]'
|
||||||
|
: 'size-44'} transition-all bg-black dark:bg-white rounded-full"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
id="camera-feed"
|
||||||
|
autoplay
|
||||||
|
class="rounded-2xl h-full min-w-full object-cover object-center"
|
||||||
|
playsinline
|
||||||
|
/>
|
||||||
|
|
||||||
|
<canvas id="camera-canvas" style="display:none;" />
|
||||||
|
|
||||||
|
<div class=" absolute top-4 md:top-8 left-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full"
|
||||||
|
on:click={() => {
|
||||||
|
stopCamera();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center pb-2 w-full">
|
||||||
|
<div>
|
||||||
|
{#if camera}
|
||||||
|
<VideoInputMenu
|
||||||
|
devices={videoInputDevices}
|
||||||
|
on:change={async (e) => {
|
||||||
|
console.log(e.detail);
|
||||||
|
selectedVideoInputDeviceId = e.detail;
|
||||||
|
await stopVideoStream();
|
||||||
|
await startVideoStream();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</VideoInputMenu>
|
||||||
|
{:else}
|
||||||
|
<Tooltip content={$i18n.t('Camera')}>
|
||||||
|
<button
|
||||||
|
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
startCamera();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="button">
|
||||||
|
<div class=" line-clamp-1 text-sm font-medium">
|
||||||
|
{#if loading}
|
||||||
|
{$i18n.t('Thinking...')}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Listening...')}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
|
||||||
|
on:click={async () => {
|
||||||
|
showCallOverlay.set(false);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
import { getContext, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
|
|
||||||
|
export let onClose: Function = () => {};
|
||||||
|
export let devices: any;
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
bind:show
|
||||||
|
on:change={(e) => {
|
||||||
|
if (e.detail === false) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow-sm"
|
||||||
|
sideOffset={6}
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
{#each devices as device}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('change', device.deviceId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class=" line-clamp-1">
|
||||||
|
{device?.label ?? 'Camera'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class="flex w-full px-2">
|
<div class="flex w-full px-2">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">#</div>
|
<div class=" text-lg font-semibold mt-2">#</div>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
{#if prompt.charAt(0) === '@'}
|
{#if prompt.charAt(0) === '@'}
|
||||||
{#if filteredModels.length > 0}
|
{#if filteredModels.length > 0}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class="flex w-full px-2">
|
<div class="flex w-full px-2">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">@</div>
|
<div class=" text-lg font-semibold mt-2">@</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let files;
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
let selectedCommandIdx = 0;
|
let selectedCommandIdx = 0;
|
||||||
let filteredPromptCommands = [];
|
let filteredPromptCommands = [];
|
||||||
@@ -35,6 +36,32 @@
|
|||||||
return '{{CLIPBOARD}}';
|
return '{{CLIPBOARD}}';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(clipboardText);
|
||||||
|
|
||||||
|
const clipboardItems = await navigator.clipboard.read();
|
||||||
|
|
||||||
|
let imageUrl = null;
|
||||||
|
for (const item of clipboardItems) {
|
||||||
|
// Check for known image types
|
||||||
|
for (const type of item.types) {
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
const blob = await item.getType(type);
|
||||||
|
imageUrl = URL.createObjectURL(blob);
|
||||||
|
console.log(`Image URL (${type}): ${imageUrl}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
files = [
|
||||||
|
...files,
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
url: imageUrl
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
|
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +88,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredPromptCommands.length > 0}
|
{#if filteredPromptCommands.length > 0}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class="flex w-full px-2">
|
<div class="flex w-full px-2">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">/</div>
|
<div class=" text-lg font-semibold mt-2">/</div>
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { createEventDispatcher, tick, getContext } from 'svelte';
|
||||||
|
import { config, settings } from '$lib/stores';
|
||||||
|
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let recording = false;
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
let confirmed = false;
|
||||||
|
|
||||||
|
let durationSeconds = 0;
|
||||||
|
let durationCounter = null;
|
||||||
|
|
||||||
|
let transcription = '';
|
||||||
|
|
||||||
|
const startDurationCounter = () => {
|
||||||
|
durationCounter = setInterval(() => {
|
||||||
|
durationSeconds++;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDurationCounter = () => {
|
||||||
|
clearInterval(durationCounter);
|
||||||
|
durationSeconds = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (recording) {
|
||||||
|
startRecording();
|
||||||
|
} else {
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatSeconds = (seconds) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds;
|
||||||
|
return `${minutes}:${formattedSeconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let speechRecognition;
|
||||||
|
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioChunks = [];
|
||||||
|
|
||||||
|
const MIN_DECIBELS = -45;
|
||||||
|
const VISUALIZER_BUFFER_LENGTH = 300;
|
||||||
|
|
||||||
|
let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
|
||||||
|
|
||||||
|
// Function to calculate the RMS level from time domain data
|
||||||
|
const calculateRMS = (data: Uint8Array) => {
|
||||||
|
let sumSquares = 0;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
|
||||||
|
sumSquares += normalizedValue * normalizedValue;
|
||||||
|
}
|
||||||
|
return Math.sqrt(sumSquares / data.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRMS = (rms) => {
|
||||||
|
rms = rms * 10;
|
||||||
|
const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more
|
||||||
|
const scaledRMS = Math.pow(rms, exp);
|
||||||
|
|
||||||
|
// Scale between 0.01 (1%) and 1.0 (100%)
|
||||||
|
return Math.min(1.0, Math.max(0.01, scaledRMS));
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyseAudio = (stream) => {
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||||
|
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
analyser.minDecibels = MIN_DECIBELS;
|
||||||
|
audioStreamSource.connect(analyser);
|
||||||
|
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
|
||||||
|
const domainData = new Uint8Array(bufferLength);
|
||||||
|
const timeDomainData = new Uint8Array(analyser.fftSize);
|
||||||
|
|
||||||
|
let lastSoundTime = Date.now();
|
||||||
|
|
||||||
|
const detectSound = () => {
|
||||||
|
const processFrame = () => {
|
||||||
|
if (!recording || loading) return;
|
||||||
|
|
||||||
|
if (recording && !loading) {
|
||||||
|
analyser.getByteTimeDomainData(timeDomainData);
|
||||||
|
analyser.getByteFrequencyData(domainData);
|
||||||
|
|
||||||
|
// Calculate RMS level from time domain data
|
||||||
|
const rmsLevel = calculateRMS(timeDomainData);
|
||||||
|
// Push the calculated decibel level to visualizerData
|
||||||
|
visualizerData.push(normalizeRMS(rmsLevel));
|
||||||
|
|
||||||
|
// Ensure visualizerData array stays within the buffer length
|
||||||
|
if (visualizerData.length >= VISUALIZER_BUFFER_LENGTH) {
|
||||||
|
visualizerData.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
visualizerData = visualizerData;
|
||||||
|
|
||||||
|
// if (domainData.some((value) => value > 0)) {
|
||||||
|
// lastSoundTime = Date.now();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (recording && Date.now() - lastSoundTime > 3000) {
|
||||||
|
// if ($settings?.speechAutoSend ?? false) {
|
||||||
|
// confirmRecording();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(processFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.requestAnimationFrame(processFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
detectSound();
|
||||||
|
};
|
||||||
|
|
||||||
|
const transcribeHandler = async (audioBlob) => {
|
||||||
|
// Create a blob from the audio chunks
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const file = blobToFile(audioBlob, 'recording.wav');
|
||||||
|
|
||||||
|
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
console.log(res.text);
|
||||||
|
dispatch('confirm', res.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveRecording = (blob) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.style = 'display: none';
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'recording.wav';
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
startDurationCounter();
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
mediaRecorder = new MediaRecorder(stream);
|
||||||
|
mediaRecorder.onstart = () => {
|
||||||
|
console.log('Recording started');
|
||||||
|
audioChunks = [];
|
||||||
|
analyseAudio(stream);
|
||||||
|
};
|
||||||
|
mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
|
||||||
|
mediaRecorder.onstop = async () => {
|
||||||
|
console.log('Recording stopped');
|
||||||
|
if (($settings?.audio?.stt?.engine ?? '') === 'web') {
|
||||||
|
audioChunks = [];
|
||||||
|
} else {
|
||||||
|
if (confirmed) {
|
||||||
|
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||||
|
|
||||||
|
await transcribeHandler(audioBlob);
|
||||||
|
|
||||||
|
confirmed = false;
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
audioChunks = [];
|
||||||
|
recording = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mediaRecorder.start();
|
||||||
|
if ($config.audio.stt.engine === 'web' || ($settings?.audio?.stt?.engine ?? '') === 'web') {
|
||||||
|
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
|
||||||
|
// Create a SpeechRecognition object
|
||||||
|
speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
||||||
|
|
||||||
|
// Set continuous to true for continuous recognition
|
||||||
|
speechRecognition.continuous = true;
|
||||||
|
|
||||||
|
// Set the timeout for turning off the recognition after inactivity (in milliseconds)
|
||||||
|
const inactivityTimeout = 2000; // 3 seconds
|
||||||
|
|
||||||
|
let timeoutId;
|
||||||
|
// Start recognition
|
||||||
|
speechRecognition.start();
|
||||||
|
|
||||||
|
// Event triggered when speech is recognized
|
||||||
|
speechRecognition.onresult = async (event) => {
|
||||||
|
// Clear the inactivity timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Handle recognized speech
|
||||||
|
console.log(event);
|
||||||
|
const transcript = event.results[Object.keys(event.results).length - 1][0].transcript;
|
||||||
|
|
||||||
|
transcription = `${transcription}${transcript}`;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
document.getElementById('chat-textarea')?.focus();
|
||||||
|
|
||||||
|
// Restart the inactivity timeout
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
console.log('Speech recognition turned off due to inactivity.');
|
||||||
|
speechRecognition.stop();
|
||||||
|
}, inactivityTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event triggered when recognition is ended
|
||||||
|
speechRecognition.onend = function () {
|
||||||
|
// Restart recognition after it ends
|
||||||
|
console.log('recognition ended');
|
||||||
|
|
||||||
|
confirmRecording();
|
||||||
|
dispatch('confirm', transcription);
|
||||||
|
|
||||||
|
confirmed = false;
|
||||||
|
loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event triggered when an error occurs
|
||||||
|
speechRecognition.onerror = function (event) {
|
||||||
|
console.log(event);
|
||||||
|
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
|
||||||
|
dispatch('cancel');
|
||||||
|
|
||||||
|
stopRecording();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = async () => {
|
||||||
|
if (recording && mediaRecorder) {
|
||||||
|
await mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
stopDurationCounter();
|
||||||
|
audioChunks = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRecording = async () => {
|
||||||
|
loading = true;
|
||||||
|
confirmed = true;
|
||||||
|
|
||||||
|
if (recording && mediaRecorder) {
|
||||||
|
await mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
clearInterval(durationCounter);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="{loading
|
||||||
|
? ' bg-gray-100/50 dark:bg-gray-850/50'
|
||||||
|
: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex p-2.5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mr-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5
|
||||||
|
|
||||||
|
{loading
|
||||||
|
? ' bg-gray-200 dark:bg-gray-700/50'
|
||||||
|
: 'bg-indigo-400/20 text-indigo-600 dark:text-indigo-300 '}
|
||||||
|
|
||||||
|
|
||||||
|
rounded-full"
|
||||||
|
on:click={async () => {
|
||||||
|
dispatch('cancel');
|
||||||
|
stopRecording();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div class="flex-1 flex items-center gap-0.5 h-6">
|
||||||
|
{#each visualizerData.slice().reverse() as rms}
|
||||||
|
<div
|
||||||
|
class="w-[2px]
|
||||||
|
|
||||||
|
{loading
|
||||||
|
? ' bg-gray-500 dark:bg-gray-400 '
|
||||||
|
: 'bg-indigo-500 dark:bg-indigo-400 '}
|
||||||
|
|
||||||
|
inline-block h-full"
|
||||||
|
style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" mx-1.5 pr-1 flex justify-center items-center">
|
||||||
|
<div
|
||||||
|
class="text-sm
|
||||||
|
|
||||||
|
|
||||||
|
{loading ? ' text-gray-500 dark:text-gray-400 ' : ' text-indigo-400 '}
|
||||||
|
font-medium flex-1 mx-auto text-center"
|
||||||
|
>
|
||||||
|
{formatSeconds(durationSeconds)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mr-1">
|
||||||
|
{#if loading}
|
||||||
|
<div class=" text-gray-500 rounded-full cursor-not-allowed">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
><style>
|
||||||
|
.spinner_OSmW {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner_T6mA 0.75s step-end infinite;
|
||||||
|
}
|
||||||
|
@keyframes spinner_T6mA {
|
||||||
|
8.3% {
|
||||||
|
transform: rotate(30deg);
|
||||||
|
}
|
||||||
|
16.6% {
|
||||||
|
transform: rotate(60deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
33.3% {
|
||||||
|
transform: rotate(120deg);
|
||||||
|
}
|
||||||
|
41.6% {
|
||||||
|
transform: rotate(150deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
58.3% {
|
||||||
|
transform: rotate(210deg);
|
||||||
|
}
|
||||||
|
66.6% {
|
||||||
|
transform: rotate(240deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
83.3% {
|
||||||
|
transform: rotate(300deg);
|
||||||
|
}
|
||||||
|
91.6% {
|
||||||
|
transform: rotate(330deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style><g class="spinner_OSmW"
|
||||||
|
><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
|
||||||
|
x="11"
|
||||||
|
y="1"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(30 12 12)"
|
||||||
|
opacity=".29"
|
||||||
|
/><rect
|
||||||
|
x="11"
|
||||||
|
y="1"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(60 12 12)"
|
||||||
|
opacity=".43"
|
||||||
|
/><rect
|
||||||
|
x="11"
|
||||||
|
y="1"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(90 12 12)"
|
||||||
|
opacity=".57"
|
||||||
|
/><rect
|
||||||
|
x="11"
|
||||||
|
y="1"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(120 12 12)"
|
||||||
|
opacity=".71"
|
||||||
|
/><rect
|
||||||
|
x="11"
|
||||||
|
y="1"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(150 12 12)"
|
||||||
|
opacity=".86"
|
||||||
|
/><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
|
||||||
|
></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
|
||||||
|
on:click={async () => {
|
||||||
|
await confirmRecording();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.visualizer {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualizer-bar {
|
||||||
|
width: 2px;
|
||||||
|
background-color: #4a5aba; /* or whatever color you need */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
{#each messages as message, messageIdx}
|
{#each messages as message, messageIdx}
|
||||||
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
|
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
|
||||||
? 'max-w-full'
|
? 'max-w-full'
|
||||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
||||||
currentMessageId
|
currentMessageId
|
||||||
].model === model
|
].model === model
|
||||||
? 'border-gray-100 dark:border-gray-700 border-[1.5px]'
|
? 'border-gray-100 dark:border-gray-850 border-[1.5px]'
|
||||||
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||||
|
|||||||
@@ -211,9 +211,10 @@
|
|||||||
speaking = null;
|
speaking = null;
|
||||||
speakingIdx = null;
|
speakingIdx = null;
|
||||||
} else {
|
} else {
|
||||||
|
if ((message?.content ?? '').trim() !== '') {
|
||||||
speaking = true;
|
speaking = true;
|
||||||
|
|
||||||
if ($settings?.audio?.TTSEngine === 'openai') {
|
if ($config.audio.tts.engine === 'openai') {
|
||||||
loadingSpeech = true;
|
loadingSpeech = true;
|
||||||
|
|
||||||
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
|
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
|
||||||
@@ -244,9 +245,8 @@
|
|||||||
for (const [idx, sentence] of sentences.entries()) {
|
for (const [idx, sentence] of sentences.entries()) {
|
||||||
const res = await synthesizeOpenAISpeech(
|
const res = await synthesizeOpenAISpeech(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$settings?.audio?.speaker,
|
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||||
sentence,
|
sentence
|
||||||
$settings?.audio?.model
|
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
|
|
||||||
@@ -273,21 +273,37 @@
|
|||||||
clearInterval(getVoicesLoop);
|
clearInterval(getVoicesLoop);
|
||||||
|
|
||||||
const voice =
|
const voice =
|
||||||
voices?.filter((v) => v.name === $settings?.audio?.speaker)?.at(0) ?? undefined;
|
voices
|
||||||
|
?.filter(
|
||||||
|
(v) =>
|
||||||
|
v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||||
|
)
|
||||||
|
?.at(0) ?? undefined;
|
||||||
|
|
||||||
|
console.log(voice);
|
||||||
|
|
||||||
const speak = new SpeechSynthesisUtterance(message.content);
|
const speak = new SpeechSynthesisUtterance(message.content);
|
||||||
|
|
||||||
|
console.log(speak);
|
||||||
|
|
||||||
speak.onend = () => {
|
speak.onend = () => {
|
||||||
speaking = null;
|
speaking = null;
|
||||||
if ($settings.conversationMode) {
|
if ($settings.conversationMode) {
|
||||||
document.getElementById('voice-input-button')?.click();
|
document.getElementById('voice-input-button')?.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (voice) {
|
||||||
speak.voice = voice;
|
speak.voice = voice;
|
||||||
|
}
|
||||||
|
|
||||||
speechSynthesis.speak(speak);
|
speechSynthesis.speak(speak);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('No content to speak');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -341,6 +357,17 @@
|
|||||||
generatingImage = false;
|
generatingImage = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$: if (!edit) {
|
||||||
|
(async () => {
|
||||||
|
await tick();
|
||||||
|
renderStyling();
|
||||||
|
|
||||||
|
await mermaid.run({
|
||||||
|
querySelector: '.mermaid'
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await tick();
|
await tick();
|
||||||
renderStyling();
|
renderStyling();
|
||||||
@@ -393,26 +420,29 @@
|
|||||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{#if message?.status}
|
{#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
|
||||||
|
{@const status = (
|
||||||
|
message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
|
||||||
|
).at(-1)}
|
||||||
<div class="flex items-center gap-2 pt-1 pb-1">
|
<div class="flex items-center gap-2 pt-1 pb-1">
|
||||||
{#if message?.status?.done === false}
|
{#if status.done === false}
|
||||||
<div class="">
|
<div class="">
|
||||||
<Spinner className="size-4" />
|
<Spinner className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message?.status?.action === 'web_search' && message?.status?.urls}
|
{#if status?.action === 'web_search' && status?.urls}
|
||||||
<WebSearchResults urls={message?.status?.urls}>
|
<WebSearchResults {status}>
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
<div class="text-base line-clamp-1 text-wrap">
|
<div class="text-base line-clamp-1 text-wrap">
|
||||||
{message.status.description}
|
{status?.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</WebSearchResults>
|
</WebSearchResults>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
<div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
|
<div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
|
||||||
{message.status.description}
|
{status?.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -746,7 +776,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if $config?.features.enable_image_generation && !readOnly}
|
{#if $config?.features.enable_image_generation && !readOnly}
|
||||||
<Tooltip content="Generate Image" placement="bottom">
|
<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
|
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||||
import { Collapsible } from 'bits-ui';
|
import { Collapsible } from 'bits-ui';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
export let urls = [];
|
export let status = { urls: [], query: '' };
|
||||||
let state = false;
|
let state = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -27,11 +28,45 @@
|
|||||||
class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
|
class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
|
||||||
transition={slide}
|
transition={slide}
|
||||||
>
|
>
|
||||||
{#each urls as url, urlIdx}
|
{#if status?.query}
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/search?q={status.query}"
|
||||||
|
target="_blank"
|
||||||
|
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<MagnifyingGlass />
|
||||||
|
|
||||||
|
<div class=" line-clamp-1">
|
||||||
|
{status.query}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition"
|
||||||
|
>
|
||||||
|
<!-- -->
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each status.urls as url, urlIdx}
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex w-full items-center p-3 px-4 {urlIdx === urls.length - 1
|
class="flex w-full items-center p-3 px-4 {urlIdx === status.urls.length - 1
|
||||||
? ''
|
? ''
|
||||||
: 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300"
|
: 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -219,7 +219,7 @@
|
|||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class=" z-40 {$mobile
|
class=" z-40 {$mobile
|
||||||
? `w-full`
|
? `w-full`
|
||||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none "
|
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none "
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center min-w-fit">
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ollamaVersion}
|
{#if ollamaVersion}
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="flex space-x-1">
|
<div class="flex space-x-1">
|
||||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
<UpdatePassword />
|
<UpdatePassword />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-4" />
|
<hr class=" dark:border-gray-850 my-4" />
|
||||||
|
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('API keys')}</div>
|
<div class=" font-medium">{$i18n.t('API keys')}</div>
|
||||||
|
|||||||
@@ -5,21 +5,23 @@
|
|||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let admin = false;
|
||||||
|
|
||||||
export let params = {
|
export let params = {
|
||||||
// Advanced
|
// Advanced
|
||||||
seed: 0,
|
seed: null,
|
||||||
stop: null,
|
stop: null,
|
||||||
temperature: '',
|
temperature: null,
|
||||||
frequency_penalty: '',
|
frequency_penalty: null,
|
||||||
repeat_last_n: '',
|
repeat_last_n: null,
|
||||||
mirostat: '',
|
mirostat: null,
|
||||||
mirostat_eta: '',
|
mirostat_eta: null,
|
||||||
mirostat_tau: '',
|
mirostat_tau: null,
|
||||||
top_k: '',
|
top_k: null,
|
||||||
top_p: '',
|
top_p: null,
|
||||||
tfs_z: '',
|
tfs_z: null,
|
||||||
num_ctx: '',
|
num_ctx: null,
|
||||||
max_tokens: '',
|
max_tokens: null,
|
||||||
use_mmap: null,
|
use_mmap: null,
|
||||||
use_mlock: null,
|
use_mlock: null,
|
||||||
num_thread: null,
|
num_thread: null,
|
||||||
@@ -112,10 +114,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.temperature = (params?.temperature ?? '') === '' ? 0.8 : '';
|
params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.temperature ?? '') === ''}
|
{#if (params?.temperature ?? null) === null}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||||
@@ -123,7 +125,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.temperature ?? '') !== ''}
|
{#if (params?.temperature ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -143,7 +145,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.05"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,10 +160,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.mirostat = (params?.mirostat ?? '') === '' ? 0 : '';
|
params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.mirostat ?? '') === ''}
|
{#if (params?.mirostat ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -169,7 +171,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.mirostat ?? '') !== ''}
|
{#if (params?.mirostat ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -204,10 +206,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.mirostat_eta = (params?.mirostat_eta ?? '') === '' ? 0.1 : '';
|
params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.mirostat_eta ?? '') === ''}
|
{#if (params?.mirostat_eta ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -215,7 +217,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.mirostat_eta ?? '') !== ''}
|
{#if (params?.mirostat_eta ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -235,7 +237,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.05"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,10 +252,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.mirostat_tau = (params?.mirostat_tau ?? '') === '' ? 5.0 : '';
|
params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.mirostat_tau ?? '') === ''}
|
{#if (params?.mirostat_tau ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -261,7 +263,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.mirostat_tau ?? '') !== ''}
|
{#if (params?.mirostat_tau ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -281,7 +283,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="10"
|
max="10"
|
||||||
step="0.5"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,10 +298,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.top_k = (params?.top_k ?? '') === '' ? 40 : '';
|
params.top_k = (params?.top_k ?? null) === null ? 40 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.top_k ?? '') === ''}
|
{#if (params?.top_k ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -307,7 +309,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.top_k ?? '') !== ''}
|
{#if (params?.top_k ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -327,7 +329,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.5"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,10 +344,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.top_p = (params?.top_p ?? '') === '' ? 0.9 : '';
|
params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.top_p ?? '') === ''}
|
{#if (params?.top_p ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -353,7 +355,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.top_p ?? '') !== ''}
|
{#if (params?.top_p ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -373,7 +375,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.05"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,10 +390,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.frequency_penalty = (params?.frequency_penalty ?? '') === '' ? 1.1 : '';
|
params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.frequency_penalty ?? '') === ''}
|
{#if (params?.frequency_penalty ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -399,7 +401,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.frequency_penalty ?? '') !== ''}
|
{#if (params?.frequency_penalty ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -419,7 +421,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="2"
|
max="2"
|
||||||
step="0.05"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,10 +436,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.repeat_last_n = (params?.repeat_last_n ?? '') === '' ? 64 : '';
|
params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.repeat_last_n ?? '') === ''}
|
{#if (params?.repeat_last_n ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -445,7 +447,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.repeat_last_n ?? '') !== ''}
|
{#if (params?.repeat_last_n ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -480,10 +482,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.tfs_z = (params?.tfs_z ?? '') === '' ? 1 : '';
|
params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.tfs_z ?? '') === ''}
|
{#if (params?.tfs_z ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -491,7 +493,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.tfs_z ?? '') !== ''}
|
{#if (params?.tfs_z ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -511,7 +513,7 @@
|
|||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="0"
|
min="0"
|
||||||
max="2"
|
max="2"
|
||||||
step="0.05"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -526,10 +528,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.num_ctx = (params?.num_ctx ?? '') === '' ? 2048 : '';
|
params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.num_ctx ?? '') === ''}
|
{#if (params?.num_ctx ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -537,7 +539,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.num_ctx ?? '') !== ''}
|
{#if (params?.num_ctx ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -556,7 +558,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
class=" bg-transparent text-center w-14"
|
class=" bg-transparent text-center w-14"
|
||||||
min="-1"
|
min="-1"
|
||||||
step="10"
|
step="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -571,10 +573,10 @@
|
|||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
params.max_tokens = (params?.max_tokens ?? '') === '' ? 128 : '';
|
params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if (params?.max_tokens ?? '') === ''}
|
{#if (params?.max_tokens ?? null) === null}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
@@ -582,7 +584,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (params?.max_tokens ?? '') !== ''}
|
{#if (params?.max_tokens ?? null) !== null}
|
||||||
<div class="flex mt-0.5 space-x-2">
|
<div class="flex mt-0.5 space-x-2">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -609,6 +611,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if admin}
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div>
|
||||||
@@ -695,7 +698,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<!-- <div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
|
||||||
|
|
||||||
@@ -726,5 +729,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div> -->
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
import { user, settings, config } from '$lib/stores';
|
||||||
import { user, settings } from '$lib/stores';
|
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
@@ -10,24 +10,15 @@
|
|||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
|
|
||||||
let OpenAIUrl = '';
|
|
||||||
let OpenAIKey = '';
|
|
||||||
|
|
||||||
let STTEngines = ['', 'openai'];
|
|
||||||
let STTEngine = '';
|
|
||||||
|
|
||||||
let conversationMode = false;
|
let conversationMode = false;
|
||||||
let speechAutoSend = false;
|
let speechAutoSend = false;
|
||||||
let responseAutoPlayback = false;
|
let responseAutoPlayback = false;
|
||||||
|
let nonLocalVoices = false;
|
||||||
|
|
||||||
let TTSEngines = ['', 'openai'];
|
let STTEngine = '';
|
||||||
let TTSEngine = '';
|
|
||||||
|
|
||||||
let voices = [];
|
let voices = [];
|
||||||
let speaker = '';
|
let voice = '';
|
||||||
let models = [];
|
|
||||||
let model = '';
|
|
||||||
|
|
||||||
const getOpenAIVoices = () => {
|
const getOpenAIVoices = () => {
|
||||||
voices = [
|
voices = [
|
||||||
@@ -40,10 +31,6 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOpenAIVoicesModel = () => {
|
|
||||||
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWebAPIVoices = () => {
|
const getWebAPIVoices = () => {
|
||||||
const getVoicesLoop = setInterval(async () => {
|
const getVoicesLoop = setInterval(async () => {
|
||||||
voices = await speechSynthesis.getVoices();
|
voices = await speechSynthesis.getVoices();
|
||||||
@@ -55,21 +42,6 @@
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleConversationMode = async () => {
|
|
||||||
conversationMode = !conversationMode;
|
|
||||||
|
|
||||||
if (conversationMode) {
|
|
||||||
responseAutoPlayback = true;
|
|
||||||
speechAutoSend = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings({
|
|
||||||
conversationMode: conversationMode,
|
|
||||||
responseAutoPlayback: responseAutoPlayback,
|
|
||||||
speechAutoSend: speechAutoSend
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleResponseAutoPlayback = async () => {
|
const toggleResponseAutoPlayback = async () => {
|
||||||
responseAutoPlayback = !responseAutoPlayback;
|
responseAutoPlayback = !responseAutoPlayback;
|
||||||
saveSettings({ responseAutoPlayback: responseAutoPlayback });
|
saveSettings({ responseAutoPlayback: responseAutoPlayback });
|
||||||
@@ -80,66 +52,35 @@
|
|||||||
saveSettings({ speechAutoSend: speechAutoSend });
|
saveSettings({ speechAutoSend: speechAutoSend });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfigHandler = async () => {
|
|
||||||
if (TTSEngine === 'openai') {
|
|
||||||
const res = await updateAudioConfig(localStorage.token, {
|
|
||||||
url: OpenAIUrl,
|
|
||||||
key: OpenAIKey,
|
|
||||||
model: model,
|
|
||||||
speaker: speaker
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
|
||||||
OpenAIKey = res.OPENAI_API_KEY;
|
|
||||||
model = res.OPENAI_API_MODEL;
|
|
||||||
speaker = res.OPENAI_API_VOICE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
conversationMode = $settings.conversationMode ?? false;
|
conversationMode = $settings.conversationMode ?? false;
|
||||||
speechAutoSend = $settings.speechAutoSend ?? false;
|
speechAutoSend = $settings.speechAutoSend ?? false;
|
||||||
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
|
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
|
||||||
|
|
||||||
STTEngine = $settings?.audio?.STTEngine ?? '';
|
STTEngine = $settings?.audio?.stt?.engine ?? '';
|
||||||
TTSEngine = $settings?.audio?.TTSEngine ?? '';
|
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
|
||||||
speaker = $settings?.audio?.speaker ?? '';
|
nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
|
||||||
model = $settings?.audio?.model ?? '';
|
|
||||||
|
|
||||||
if (TTSEngine === 'openai') {
|
if ($config.audio.tts.engine === 'openai') {
|
||||||
getOpenAIVoices();
|
getOpenAIVoices();
|
||||||
getOpenAIVoicesModel();
|
|
||||||
} else {
|
} else {
|
||||||
getWebAPIVoices();
|
getWebAPIVoices();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user.role === 'admin') {
|
|
||||||
const res = await getAudioConfig(localStorage.token);
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
|
||||||
OpenAIKey = res.OPENAI_API_KEY;
|
|
||||||
model = res.OPENAI_API_MODEL;
|
|
||||||
speaker = res.OPENAI_API_VOICE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
on:submit|preventDefault={async () => {
|
on:submit|preventDefault={async () => {
|
||||||
if ($user.role === 'admin') {
|
|
||||||
await updateConfigHandler();
|
|
||||||
}
|
|
||||||
saveSettings({
|
saveSettings({
|
||||||
audio: {
|
audio: {
|
||||||
STTEngine: STTEngine !== '' ? STTEngine : undefined,
|
stt: {
|
||||||
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
|
engine: STTEngine !== '' ? STTEngine : undefined
|
||||||
speaker: speaker !== '' ? speaker : undefined,
|
},
|
||||||
model: model !== '' ? model : undefined
|
tts: {
|
||||||
|
voice: voice !== '' ? voice : undefined,
|
||||||
|
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
@@ -149,53 +90,25 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||||
|
|
||||||
|
{#if $config.audio.stt.engine !== 'web'}
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={STTEngine}
|
bind:value={STTEngine}
|
||||||
placeholder="Select a mode"
|
placeholder="Select an engine"
|
||||||
on:change={(e) => {
|
|
||||||
if (e.target.value !== '') {
|
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
|
||||||
toast.error(
|
|
||||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
|
||||||
error: err
|
|
||||||
})
|
|
||||||
);
|
|
||||||
STTEngine = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
<option value="">{$i18n.t('Default')}</option>
|
||||||
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
|
<option value="web">{$i18n.t('Web API')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
|
||||||
on:click={() => {
|
|
||||||
toggleConversationMode();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{#if conversationMode === true}
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
|
||||||
{:else}
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Auto-send input after 3 sec.')}
|
{$i18n.t('Instant Auto-Send After Voice Transcription')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -217,50 +130,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<select
|
|
||||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
|
||||||
bind:value={TTSEngine}
|
|
||||||
placeholder="Select a mode"
|
|
||||||
on:change={(e) => {
|
|
||||||
if (e.target.value === 'openai') {
|
|
||||||
getOpenAIVoices();
|
|
||||||
speaker = 'alloy';
|
|
||||||
model = 'tts-1';
|
|
||||||
} else {
|
|
||||||
getWebAPIVoices();
|
|
||||||
speaker = '';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
|
||||||
<option value="openai">{$i18n.t('Open AI')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $user.role === 'admin'}
|
|
||||||
{#if TTSEngine === 'openai'}
|
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
placeholder={$i18n.t('API Base URL')}
|
|
||||||
bind:value={OpenAIUrl}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
placeholder={$i18n.t('API Key')}
|
|
||||||
bind:value={OpenAIKey}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
||||||
|
|
||||||
@@ -280,28 +149,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
{#if TTSEngine === ''}
|
{#if $config.audio.tts.engine === ''}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={speaker}
|
bind:value={voice}
|
||||||
placeholder="Select a voice"
|
|
||||||
>
|
>
|
||||||
<option value="" selected>{$i18n.t('Default')}</option>
|
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
|
||||||
{#each voices.filter((v) => v.localService === true) as voice}
|
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
|
||||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
<option
|
||||||
|
value={_voice.name}
|
||||||
|
class="bg-gray-100 dark:bg-gray-700"
|
||||||
|
selected={voice === _voice.name}>{_voice.name}</option
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between my-1.5">
|
||||||
|
<div class="text-xs">
|
||||||
|
{$i18n.t('Allow non-local voices')}
|
||||||
</div>
|
</div>
|
||||||
{:else if TTSEngine === 'openai'}
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<Switch bind:state={nonLocalVoices} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if $config.audio.tts.engine === 'openai'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
@@ -309,7 +189,7 @@
|
|||||||
<input
|
<input
|
||||||
list="voice-list"
|
list="voice-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={speaker}
|
bind:value={voice}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -321,25 +201,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Model')}</div>
|
|
||||||
<div class="flex w-full">
|
|
||||||
<div class="flex-1">
|
|
||||||
<input
|
|
||||||
list="model-list"
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
bind:value={model}
|
|
||||||
placeholder="Select a model"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist id="model-list">
|
|
||||||
{#each models as model}
|
|
||||||
<option value={model.name} />
|
|
||||||
{/each}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<input
|
<input
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if showArchiveConfirm}
|
{#if showArchiveConfirm}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { getLanguages } from '$lib/i18n';
|
import { getLanguages } from '$lib/i18n';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { models, settings, theme } from '$lib/stores';
|
import { models, settings, theme, user } from '$lib/stores';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -43,19 +43,19 @@
|
|||||||
|
|
||||||
let params = {
|
let params = {
|
||||||
// Advanced
|
// Advanced
|
||||||
seed: 0,
|
seed: null,
|
||||||
temperature: '',
|
temperature: null,
|
||||||
frequency_penalty: '',
|
frequency_penalty: null,
|
||||||
repeat_last_n: '',
|
repeat_last_n: null,
|
||||||
mirostat: '',
|
mirostat: null,
|
||||||
mirostat_eta: '',
|
mirostat_eta: null,
|
||||||
mirostat_tau: '',
|
mirostat_tau: null,
|
||||||
top_k: '',
|
top_k: null,
|
||||||
top_p: '',
|
top_p: null,
|
||||||
stop: null,
|
stop: null,
|
||||||
tfs_z: '',
|
tfs_z: null,
|
||||||
num_ctx: '',
|
num_ctx: null,
|
||||||
max_tokens: ''
|
max_tokens: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRequestFormat = async () => {
|
const toggleRequestFormat = async () => {
|
||||||
@@ -79,12 +79,6 @@
|
|||||||
requestFormat = $settings.requestFormat ?? '';
|
requestFormat = $settings.requestFormat ?? '';
|
||||||
keepAlive = $settings.keepAlive ?? null;
|
keepAlive = $settings.keepAlive ?? null;
|
||||||
|
|
||||||
params.seed = $settings.seed ?? 0;
|
|
||||||
params.temperature = $settings.temperature ?? '';
|
|
||||||
params.frequency_penalty = $settings.frequency_penalty ?? '';
|
|
||||||
params.top_k = $settings.top_k ?? '';
|
|
||||||
params.top_p = $settings.top_p ?? '';
|
|
||||||
params.num_ctx = $settings.num_ctx ?? '';
|
|
||||||
params = { ...params, ...$settings.params };
|
params = { ...params, ...$settings.params };
|
||||||
params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
|
params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
|
||||||
});
|
});
|
||||||
@@ -203,7 +197,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-850 my-3" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||||
@@ -227,8 +221,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showAdvanced}
|
{#if showAdvanced}
|
||||||
<AdvancedParams bind:params />
|
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div class=" py-1 w-full justify-between">
|
<div class=" py-1 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
@@ -300,20 +294,23 @@
|
|||||||
saveSettings({
|
saveSettings({
|
||||||
system: system !== '' ? system : undefined,
|
system: system !== '' ? system : undefined,
|
||||||
params: {
|
params: {
|
||||||
seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined,
|
seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
|
||||||
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
|
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
|
||||||
temperature: params.temperature !== '' ? params.temperature : undefined,
|
temperature: params.temperature !== null ? params.temperature : undefined,
|
||||||
frequency_penalty:
|
frequency_penalty:
|
||||||
params.frequency_penalty !== '' ? params.frequency_penalty : undefined,
|
params.frequency_penalty !== null ? params.frequency_penalty : undefined,
|
||||||
repeat_last_n: params.repeat_last_n !== '' ? params.repeat_last_n : undefined,
|
repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined,
|
||||||
mirostat: params.mirostat !== '' ? params.mirostat : undefined,
|
mirostat: params.mirostat !== null ? params.mirostat : undefined,
|
||||||
mirostat_eta: params.mirostat_eta !== '' ? params.mirostat_eta : undefined,
|
mirostat_eta: params.mirostat_eta !== null ? params.mirostat_eta : undefined,
|
||||||
mirostat_tau: params.mirostat_tau !== '' ? params.mirostat_tau : undefined,
|
mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined,
|
||||||
top_k: params.top_k !== '' ? params.top_k : undefined,
|
top_k: params.top_k !== null ? params.top_k : undefined,
|
||||||
top_p: params.top_p !== '' ? params.top_p : undefined,
|
top_p: params.top_p !== null ? params.top_p : undefined,
|
||||||
tfs_z: params.tfs_z !== '' ? params.tfs_z : undefined,
|
tfs_z: params.tfs_z !== null ? params.tfs_z : undefined,
|
||||||
num_ctx: params.num_ctx !== '' ? params.num_ctx : undefined,
|
num_ctx: params.num_ctx !== null ? params.num_ctx : undefined,
|
||||||
max_tokens: params.max_tokens !== '' ? params.max_tokens : undefined
|
max_tokens: params.max_tokens !== null ? params.max_tokens : undefined,
|
||||||
|
use_mmap: params.use_mmap !== null ? params.use_mmap : undefined,
|
||||||
|
use_mlock: params.use_mlock !== null ? params.use_mlock : undefined,
|
||||||
|
num_thread: params.num_thread !== null ? params.num_thread : undefined
|
||||||
},
|
},
|
||||||
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
|
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,14 +14,11 @@
|
|||||||
// Addons
|
// Addons
|
||||||
let titleAutoGenerate = true;
|
let titleAutoGenerate = true;
|
||||||
let responseAutoCopy = false;
|
let responseAutoCopy = false;
|
||||||
let titleAutoGenerateModel = '';
|
let widescreenMode = false;
|
||||||
let titleAutoGenerateModelExternal = '';
|
|
||||||
let fullScreenMode = false;
|
|
||||||
let titleGenerationPrompt = '';
|
|
||||||
let splitLargeChunks = false;
|
let splitLargeChunks = false;
|
||||||
|
|
||||||
// Interface
|
// Interface
|
||||||
let promptSuggestions = [];
|
let defaultModelId = '';
|
||||||
let showUsername = false;
|
let showUsername = false;
|
||||||
let chatBubble = true;
|
let chatBubble = true;
|
||||||
let chatDirection: 'LTR' | 'RTL' = 'LTR';
|
let chatDirection: 'LTR' | 'RTL' = 'LTR';
|
||||||
@@ -31,9 +28,9 @@
|
|||||||
saveSettings({ splitLargeChunks: splitLargeChunks });
|
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFullScreenMode = async () => {
|
const togglewidescreenMode = async () => {
|
||||||
fullScreenMode = !fullScreenMode;
|
widescreenMode = !widescreenMode;
|
||||||
saveSettings({ fullScreenMode: fullScreenMode });
|
saveSettings({ widescreenMode: widescreenMode });
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleChatBubble = async () => {
|
const toggleChatBubble = async () => {
|
||||||
@@ -84,39 +81,21 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateInterfaceHandler = async () => {
|
const updateInterfaceHandler = async () => {
|
||||||
if ($user.role === 'admin') {
|
|
||||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
|
||||||
await config.set(await getBackendConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings({
|
saveSettings({
|
||||||
title: {
|
models: [defaultModelId]
|
||||||
...$settings.title,
|
|
||||||
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
|
||||||
modelExternal:
|
|
||||||
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
|
|
||||||
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user.role === 'admin') {
|
|
||||||
promptSuggestions = $config?.default_prompt_suggestions;
|
|
||||||
}
|
|
||||||
|
|
||||||
titleAutoGenerate = $settings?.title?.auto ?? true;
|
titleAutoGenerate = $settings?.title?.auto ?? true;
|
||||||
titleAutoGenerateModel = $settings?.title?.model ?? '';
|
|
||||||
titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
|
|
||||||
titleGenerationPrompt =
|
|
||||||
$settings?.title?.prompt ??
|
|
||||||
`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
|
|
||||||
responseAutoCopy = $settings.responseAutoCopy ?? false;
|
responseAutoCopy = $settings.responseAutoCopy ?? false;
|
||||||
showUsername = $settings.showUsername ?? false;
|
showUsername = $settings.showUsername ?? false;
|
||||||
chatBubble = $settings.chatBubble ?? true;
|
chatBubble = $settings.chatBubble ?? true;
|
||||||
fullScreenMode = $settings.fullScreenMode ?? false;
|
widescreenMode = $settings.widescreenMode ?? false;
|
||||||
splitLargeChunks = $settings.splitLargeChunks ?? false;
|
splitLargeChunks = $settings.splitLargeChunks ?? false;
|
||||||
chatDirection = $settings.chatDirection ?? 'LTR';
|
chatDirection = $settings.chatDirection ?? 'LTR';
|
||||||
|
|
||||||
|
defaultModelId = ($settings?.models ?? ['']).at(0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -195,16 +174,16 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
toggleFullScreenMode();
|
togglewidescreenMode();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if fullScreenMode === true}
|
{#if widescreenMode === true}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
@@ -278,161 +257,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div class=" space-y-1 mb-3">
|
||||||
<div class=" mb-2.5 text-sm font-medium flex">
|
<div class="mb-2">
|
||||||
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
<div class="flex justify-between items-center text-xs">
|
||||||
<Tooltip
|
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
|
||||||
content={$i18n.t(
|
|
||||||
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full gap-2 pr-2">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class=" text-xs mb-1">Local Models</div>
|
|
||||||
<select
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
bind:value={titleAutoGenerateModel}
|
|
||||||
placeholder={$i18n.t('Select a model')}
|
|
||||||
>
|
|
||||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
|
||||||
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
|
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
|
||||||
{model.name}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1 mr-2">
|
||||||
<div class=" text-xs mb-1">External Models</div>
|
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={titleAutoGenerateModelExternal}
|
bind:value={defaultModelId}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{#each $models as model}
|
{#each $models.filter((model) => model.id) as model}
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||||
{model.name}
|
|
||||||
</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 mr-2">
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
|
||||||
<textarea
|
|
||||||
bind:value={titleGenerationPrompt}
|
|
||||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $user.role === 'admin'}
|
|
||||||
<hr class=" dark:border-gray-700" />
|
|
||||||
|
|
||||||
<div class=" space-y-3 pr-1.5">
|
|
||||||
<div class="flex w-full justify-between mb-2">
|
|
||||||
<div class=" self-center text-sm font-semibold">
|
|
||||||
{$i18n.t('Default Prompt Suggestions')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
|
||||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-1">
|
|
||||||
{#each promptSuggestions as prompt, promptIdx}
|
|
||||||
<div class=" flex border dark:border-gray-600 rounded-lg">
|
|
||||||
<div class="flex flex-col flex-1">
|
|
||||||
<div class="flex border-b dark:border-gray-600 w-full">
|
|
||||||
<input
|
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
|
||||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
|
||||||
bind:value={prompt.title[0]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
|
||||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
|
||||||
bind:value={prompt.title[1]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
|
||||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
|
||||||
bind:value={prompt.content}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-2"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
promptSuggestions.splice(promptIdx, 1);
|
|
||||||
promptSuggestions = promptSuggestions;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if promptSuggestions.length > 0}
|
|
||||||
<div class="text-xs text-left w-full mt-2">
|
|
||||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end text-sm font-medium">
|
<div class="flex justify-end text-sm font-medium">
|
||||||
|
|||||||
@@ -43,6 +43,13 @@
|
|||||||
|
|
||||||
let modelTransferring = false;
|
let modelTransferring = false;
|
||||||
let modelTag = '';
|
let modelTag = '';
|
||||||
|
|
||||||
|
let createModelLoading = false;
|
||||||
|
let createModelTag = '';
|
||||||
|
let createModelContent = '';
|
||||||
|
let createModelDigest = '';
|
||||||
|
let createModelPullProgress = null;
|
||||||
|
|
||||||
let digest = '';
|
let digest = '';
|
||||||
let pullProgress = null;
|
let pullProgress = null;
|
||||||
|
|
||||||
@@ -434,6 +441,83 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createModelHandler = async () => {
|
||||||
|
createModelLoading = true;
|
||||||
|
const res = await createModel(
|
||||||
|
localStorage.token,
|
||||||
|
createModelTag,
|
||||||
|
createModelContent,
|
||||||
|
selectedOllamaUrlIdx
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
const reader = res.body
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(splitStream('\n'))
|
||||||
|
.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let lines = value.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line !== '') {
|
||||||
|
console.log(line);
|
||||||
|
let data = JSON.parse(line);
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
throw data.error;
|
||||||
|
}
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status) {
|
||||||
|
if (
|
||||||
|
!data.digest &&
|
||||||
|
!data.status.includes('writing') &&
|
||||||
|
!data.status.includes('sha256')
|
||||||
|
) {
|
||||||
|
toast.success(data.status);
|
||||||
|
} else {
|
||||||
|
if (data.digest) {
|
||||||
|
createModelDigest = data.digest;
|
||||||
|
|
||||||
|
if (data.completed) {
|
||||||
|
createModelPullProgress =
|
||||||
|
Math.round((data.completed / data.total) * 1000) / 10;
|
||||||
|
} else {
|
||||||
|
createModelPullProgress = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
models.set(await getModels());
|
||||||
|
|
||||||
|
createModelLoading = false;
|
||||||
|
|
||||||
|
createModelTag = '';
|
||||||
|
createModelContent = '';
|
||||||
|
createModelDigest = '';
|
||||||
|
createModelPullProgress = null;
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||||
|
|
||||||
@@ -457,13 +541,13 @@
|
|||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
ollamaEnabled = false;
|
ollamaEnabled = false;
|
||||||
toast.error('Ollama API is disabled');
|
toast.error($i18n.t('Ollama API is disabled'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[27rem]">
|
||||||
{#if ollamaEnabled}
|
{#if ollamaEnabled}
|
||||||
{#if ollamaVersion !== null}
|
{#if ollamaVersion !== null}
|
||||||
<div class="space-y-2 pr-1.5">
|
<div class="space-y-2 pr-1.5">
|
||||||
@@ -695,6 +779,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2 flex flex-col gap-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||||
|
modelTag: 'my-modelfile'
|
||||||
|
})}
|
||||||
|
bind:value={createModelTag}
|
||||||
|
disabled={createModelLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:value={createModelContent}
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
|
||||||
|
rows="6"
|
||||||
|
placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
|
||||||
|
disabled={createModelLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex self-start">
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
|
||||||
|
on:click={() => {
|
||||||
|
createModelHandler();
|
||||||
|
}}
|
||||||
|
disabled={createModelLoading}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if createModelDigest !== ''}
|
||||||
|
<div class="flex flex-col mt-1">
|
||||||
|
<div class="font-medium mb-1">{createModelTag}</div>
|
||||||
|
<div class="">
|
||||||
|
<div class="flex flex-row justify-between space-x-4 pr-2">
|
||||||
|
<div class=" flex-1">
|
||||||
|
<div
|
||||||
|
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||||
|
style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
|
||||||
|
>
|
||||||
|
{createModelPullProgress ?? 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if createModelDigest}
|
||||||
|
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||||
|
{createModelDigest}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
|
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
|
||||||
@@ -908,7 +1063,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if ollamaEnabled === false}
|
{:else if ollamaEnabled === false}
|
||||||
<div>Ollama API is disabled</div>
|
<div>{$i18n.t('Ollama API is disabled')}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-full justify-center">
|
<div class="flex h-full justify-center">
|
||||||
<div class="my-auto">
|
<div class="my-auto">
|
||||||
|
|||||||
@@ -35,7 +35,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="This is an experimental feature, it may not function as expected and is subject to change at any time."
|
content={$i18n.t(
|
||||||
|
'This is an experimental feature, it may not function as expected and is subject to change at any time.'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div class="text-sm font-medium">
|
<div class="text-sm font-medium">
|
||||||
{$i18n.t('Memory')}
|
{$i18n.t('Memory')}
|
||||||
@@ -57,8 +59,9 @@
|
|||||||
|
|
||||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||||
<div>
|
<div>
|
||||||
You can personalize your interactions with LLMs by adding memories through the 'Manage'
|
{$i18n.t(
|
||||||
button below, making them more helpful and tailored to you.
|
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you."
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="mt-3">
|
<!-- <div class="mt-3">
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
showManageModal = true;
|
showManageModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Manage
|
{$i18n.t('Manage')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
ⓘ Refer to yourself as "User" (e.g., "User is learning Spanish")
|
ⓘ {$i18n.t('Refer to yourself as "User" (e.g., "User is learning Spanish")')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showAddMemoryModal = true;
|
showAddMemoryModal = true;
|
||||||
}}>Add memory</button
|
}}>{$i18n.t('Add Memory')}</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
|
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
toast.success('Memory cleared successfully');
|
toast.success('Memory cleared successfully');
|
||||||
memories = [];
|
memories = [];
|
||||||
}
|
}
|
||||||
}}>Clear memory</button
|
}}>{$i18n.t('Clear memory')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,16 +8,14 @@
|
|||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Account from './Settings/Account.svelte';
|
import Account from './Settings/Account.svelte';
|
||||||
import About from './Settings/About.svelte';
|
import About from './Settings/About.svelte';
|
||||||
import Models from './Settings/Models.svelte';
|
|
||||||
import General from './Settings/General.svelte';
|
import General from './Settings/General.svelte';
|
||||||
import Interface from './Settings/Interface.svelte';
|
import Interface from './Settings/Interface.svelte';
|
||||||
import Audio from './Settings/Audio.svelte';
|
import Audio from './Settings/Audio.svelte';
|
||||||
import Chats from './Settings/Chats.svelte';
|
import Chats from './Settings/Chats.svelte';
|
||||||
import Connections from './Settings/Connections.svelte';
|
|
||||||
import Images from './Settings/Images.svelte';
|
|
||||||
import User from '../icons/User.svelte';
|
import User from '../icons/User.svelte';
|
||||||
import Personalization from './Settings/Personalization.svelte';
|
import Personalization from './Settings/Personalization.svelte';
|
||||||
import { updateUserSettings } from '$lib/apis/users';
|
import { updateUserSettings } from '$lib/apis/users';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -90,55 +88,32 @@
|
|||||||
<div class=" self-center">{$i18n.t('General')}</div>
|
<div class=" self-center">{$i18n.t('General')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user.role === 'admin'}
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'connections'
|
'admin'
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
? 'bg-gray-200 dark:bg-gray-700'
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||||
on:click={() => {
|
on:click={async () => {
|
||||||
selectedTab = 'connections';
|
await goto('/admin/settings');
|
||||||
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="size-4"
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Connections')}</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 ===
|
|
||||||
'models'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'models';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
|
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Models')}</div>
|
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -210,34 +185,6 @@
|
|||||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $user.role === 'admin'}
|
|
||||||
<button
|
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
|
||||||
'images'
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'images';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Images')}</div>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'chats'
|
'chats'
|
||||||
@@ -325,15 +272,6 @@
|
|||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'models'}
|
|
||||||
<Models {getModels} />
|
|
||||||
{:else if selectedTab === 'connections'}
|
|
||||||
<Connections
|
|
||||||
{getModels}
|
|
||||||
on:save={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if selectedTab === 'interface'}
|
{:else if selectedTab === 'interface'}
|
||||||
<Interface
|
<Interface
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
@@ -355,13 +293,6 @@
|
|||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'images'}
|
|
||||||
<Images
|
|
||||||
{saveSettings}
|
|
||||||
on:save={() => {
|
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if selectedTab === 'chats'}
|
{:else if selectedTab === 'chats'}
|
||||||
<Chats {saveSettings} />
|
<Chats {saveSettings} />
|
||||||
{:else if selectedTab === 'account'}
|
{:else if selectedTab === 'account'}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
{#if !dismissed}
|
{#if !dismissed}
|
||||||
{#if mounted}
|
{#if mounted}
|
||||||
<div
|
<div
|
||||||
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-40"
|
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-30"
|
||||||
transition:fade={{ delay: 100, duration: 300 }}
|
transition:fade={{ delay: 100, duration: 300 }}
|
||||||
>
|
>
|
||||||
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
|
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content
|
<Select.Content
|
||||||
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none"
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
touch: touch
|
touch: touch
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (tooltipInstance && content === '') {
|
||||||
|
if (tooltipInstance) {
|
||||||
|
tooltipInstance.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-850 my-3" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
let webConfig = null;
|
let webConfig = null;
|
||||||
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper'];
|
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper', 'serply'];
|
||||||
|
|
||||||
let youtubeLanguage = 'en';
|
let youtubeLanguage = 'en';
|
||||||
let youtubeTranslation = null;
|
let youtubeTranslation = null;
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={webConfig.search.engine}
|
bind:value={webConfig.search.engine}
|
||||||
placeholder="Select a engine"
|
placeholder={$i18n.t('Select a engine')}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option disabled selected value="">Select a engine</option>
|
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||||
{#each webSearchEngines as engine}
|
{#each webSearchEngines as engine}
|
||||||
<option value={engine}>{engine}</option>
|
<option value={engine}>{engine}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 5a7 7 0 0 0-7 7v1.17c.313-.11.65-.17 1-.17h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H6a3 3 0 0 1-3-3v-6a9 9 0 0 1 18 0v6a3 3 0 0 1-3 3h-2a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h2c.35 0 .687.06 1 .17V12a7 7 0 0 0-7-7Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
export let strokeWidth = '2';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7Zm5.01 1H5v2.01h2.01V8Zm3 0H8v2.01h2.01V8Zm3 0H11v2.01h2.01V8Zm3 0H14v2.01h2.01V8Zm3 0H17v2.01h2.01V8Zm-12 3H5v2.01h2.01V11Zm3 0H8v2.01h2.01V11Zm3 0H11v2.01h2.01V11Zm3 0H14v2.01h2.01V11Zm3 0H17v2.01h2.01V11Zm-12 3H5v2.01h2.01V14ZM8 14l-.001 2 8.011.01V14H8Zm11.01 0H17v2.01h2.01V14Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '2';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.712 4.33a9.027 9.027 0 0 1 1.652 1.306c.51.51.944 1.064 1.306 1.652M16.712 4.33l-3.448 4.138m3.448-4.138a9.014 9.014 0 0 0-9.424 0M19.67 7.288l-4.138 3.448m4.138-3.448a9.014 9.014 0 0 1 0 9.424m-4.138-5.976a3.736 3.736 0 0 0-.88-1.388 3.737 3.737 0 0 0-1.388-.88m2.268 2.268a3.765 3.765 0 0 1 0 2.528m-2.268-4.796a3.765 3.765 0 0 0-2.528 0m4.796 4.796c-.181.506-.475.982-.88 1.388a3.736 3.736 0 0 1-1.388.88m2.268-2.268 4.138 3.448m0 0a9.027 9.027 0 0 1-1.306 1.652c-.51.51-1.064.944-1.652 1.306m0 0-3.448-4.138m3.448 4.138a9.014 9.014 0 0 1-9.424 0m5.976-4.138a3.765 3.765 0 0 1-2.528 0m0 0a3.736 3.736 0 0 1-1.388-.88 3.737 3.737 0 0 1-.88-1.388m2.268 2.268L7.288 19.67m0 0a9.024 9.024 0 0 1-1.652-1.306 9.027 9.027 0 0 1-1.306-1.652m0 0 4.138-3.448M4.33 16.712a9.014 9.014 0 0 1 0-9.424m4.138 5.976a3.765 3.765 0 0 1 0-2.528m0 0c.181-.506.475-.982.88-1.388a3.736 3.736 0 0 1 1.388-.88m-2.268 2.268L4.33 7.288m6.406 1.18L7.288 4.33m0 0a9.024 9.024 0 0 0-1.652 1.306A9.025 9.025 0 0 0 4.33 7.288"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
export let strokeWidth = '2';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'w-4 h-4';
|
||||||
|
export let strokeWidth = '2';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import ShortcutsModal from '../chat/ShortcutsModal.svelte';
|
||||||
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
import HelpMenu from './Help/HelpMenu.svelte';
|
||||||
|
|
||||||
|
let showShortcuts = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-10">
|
||||||
|
<button
|
||||||
|
id="show-shortcuts-button"
|
||||||
|
class="hidden"
|
||||||
|
on:click={() => {
|
||||||
|
showShortcuts = !showShortcuts;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HelpMenu
|
||||||
|
showDocsHandler={() => {
|
||||||
|
showShortcuts = !showShortcuts;
|
||||||
|
}}
|
||||||
|
showShortcutsHandler={() => {
|
||||||
|
showShortcuts = !showShortcuts;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip content={$i18n.t('Help')} placement="left">
|
||||||
|
<button
|
||||||
|
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-5 flex items-center justify-center text-[0.7rem] rounded-full"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</HelpMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ShortcutsModal bind:show={showShortcuts} />
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { showSettings } from '$lib/stores';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
|
import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte';
|
||||||
|
import Lifebuoy from '$lib/components/icons/Lifebuoy.svelte';
|
||||||
|
import Keyboard from '$lib/components/icons/Keyboard.svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let showDocsHandler: Function;
|
||||||
|
export let showShortcutsHandler: Function;
|
||||||
|
|
||||||
|
export let onClose: Function = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
on:change={(e) => {
|
||||||
|
if (e.detail === false) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||||
|
sideOffset={4}
|
||||||
|
side="top"
|
||||||
|
align="end"
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
id="chat-share-button"
|
||||||
|
on:click={() => {
|
||||||
|
window.open('https://docs.openwebui.com', '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<QuestionMarkCircle className="size-5" />
|
||||||
|
<div class="flex items-center">{$i18n.t('Documentation')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
id="chat-share-button"
|
||||||
|
on:click={() => {
|
||||||
|
showShortcutsHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Keyboard className="size-5" />
|
||||||
|
<div class="flex items-center">{$i18n.t('Keyboard shortcuts')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
|
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
|
||||||
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
|
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
|
||||||
|
|
||||||
{#if shareEnabled}
|
{#if shareEnabled && chat && chat.id}
|
||||||
<Menu
|
<Menu
|
||||||
{chat}
|
{chat}
|
||||||
{shareEnabled}
|
{shareEnabled}
|
||||||
|
|||||||
@@ -63,6 +63,13 @@
|
|||||||
// Revoke the URL to release memory
|
// Revoke the URL to release memory
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadJSONExport = async () => {
|
||||||
|
let blob = new Blob([JSON.stringify([chat])], {
|
||||||
|
type: 'application/json'
|
||||||
|
});
|
||||||
|
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -131,7 +138,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<!-- <DropdownMenu.Item
|
<!-- <DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -164,6 +170,14 @@
|
|||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
on:click={() => {
|
||||||
|
downloadJSONExport();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getAdminDetails } from '$lib/apis/auths';
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let adminDetails = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
adminDetails = await getAdminDetails(localStorage.token).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="fixed w-full h-full flex z-[999]">
|
||||||
|
<div
|
||||||
|
class="absolute w-full h-full backdrop-blur-lg bg-white/10 dark:bg-gray-900/50 flex justify-center"
|
||||||
|
>
|
||||||
|
<div class="m-auto pb-10 flex flex-col justify-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<div class="text-center dark:text-white text-2xl font-medium z-50">
|
||||||
|
{$i18n.t('Account Activation Pending')}<br />
|
||||||
|
{$i18n.t('Contact Admin for WebUI Access')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
|
||||||
|
{$i18n.t('Your account status is currently pending activation.')}<br />
|
||||||
|
{$i18n.t(
|
||||||
|
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if adminDetails}
|
||||||
|
<div class="mt-4 text-sm font-medium text-center">
|
||||||
|
<div>{$i18n.t('Admin')}: {adminDetails.name} ({adminDetails.email})</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class=" mt-6 mx-auto relative group w-fit">
|
||||||
|
<button
|
||||||
|
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 text-gray-700 transition font-medium text-sm"
|
||||||
|
on:click={async () => {
|
||||||
|
location.href = '/';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Check Again')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-xs text-center w-full mt-2 text-gray-400 underline"
|
||||||
|
on:click={async () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
location.href = '/auth';
|
||||||
|
}}>{$i18n.t('Sign Out')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -205,6 +205,10 @@
|
|||||||
await archiveChatById(localStorage.token, id);
|
await archiveChatById(localStorage.token, id);
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const focusEdit = async (node: HTMLInputElement) => {
|
||||||
|
node.focus();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
|
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
|
||||||
@@ -321,6 +325,10 @@
|
|||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedChatId = null;
|
selectedChatId = null;
|
||||||
chatId.set('');
|
chatId.set('');
|
||||||
|
|
||||||
|
if ($mobile) {
|
||||||
|
showSidebar.set(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
>
|
>
|
||||||
@@ -489,7 +497,11 @@
|
|||||||
? 'bg-gray-100 dark:bg-gray-950'
|
? 'bg-gray-100 dark:bg-gray-950'
|
||||||
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
|
<input
|
||||||
|
use:focusEdit
|
||||||
|
bind:value={chatTitle}
|
||||||
|
class=" bg-transparent w-full outline-none mr-10"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
@@ -507,6 +519,10 @@
|
|||||||
showSidebar.set(false);
|
showSidebar.set(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
on:dblclick={() => {
|
||||||
|
chatTitle = chat.title;
|
||||||
|
chatTitleEditId = chat.id;
|
||||||
|
}}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
>
|
>
|
||||||
<div class=" flex self-center flex-1 w-full">
|
<div class=" flex self-center flex-1 w-full">
|
||||||
|
|||||||
@@ -8,7 +8,12 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import { archiveChatById, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
import {
|
||||||
|
archiveChatById,
|
||||||
|
deleteChatById,
|
||||||
|
getAllArchivedChats,
|
||||||
|
getArchivedChatList
|
||||||
|
} from '$lib/apis/chats';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
@@ -38,6 +43,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const exportChatsHandler = async () => {
|
const exportChatsHandler = async () => {
|
||||||
|
const chats = await getAllArchivedChats(localStorage.token);
|
||||||
let blob = new Blob([JSON.stringify(chats)], {
|
let blob = new Blob([JSON.stringify(chats)], {
|
||||||
type: 'application/json'
|
type: 'application/json'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu } from 'bits-ui';
|
import { DropdownMenu } from 'bits-ui';
|
||||||
import { createEventDispatcher, getContext } from 'svelte';
|
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
||||||
|
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||||
import { showSettings } from '$lib/stores';
|
import { showSettings, activeUserCount, USAGE_POOL } from '$lib/stores';
|
||||||
import { fade, slide } from 'svelte/transition';
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800 my-2 p-0" />
|
<hr class=" dark:border-gray-800 my-1.5 p-0" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||||
@@ -139,6 +140,36 @@
|
|||||||
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
|
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if $activeUserCount}
|
||||||
|
<hr class=" dark:border-gray-800 my-1.5 p-0" />
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={$USAGE_POOL && $USAGE_POOL.length > 0
|
||||||
|
? `${$i18n.t('Running')}: ${$USAGE_POOL.join(', ')} ✨`
|
||||||
|
: ''}
|
||||||
|
>
|
||||||
|
<div class="flex rounded-md py-1.5 px-3 text-xs gap-2.5 items-center">
|
||||||
|
<div class=" flex items-center">
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
/>
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" ">
|
||||||
|
<span class=" font-medium">
|
||||||
|
{$i18n.t('Active Users')}:
|
||||||
|
</span>
|
||||||
|
<span class=" font-semibold">
|
||||||
|
{$activeUserCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||||
<div class="flex items-center">Profile</div>
|
<div class="flex items-center">Profile</div>
|
||||||
</DropdownMenu.Item> -->
|
</DropdownMenu.Item> -->
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { WEBUI_NAME, documents } from '$lib/stores';
|
import { WEBUI_NAME, documents, showSidebar } from '$lib/stores';
|
||||||
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
|
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
|
||||||
|
|
||||||
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
|
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
|
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
|
||||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||||
import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
|
|
||||||
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
|
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
@@ -158,12 +157,14 @@
|
|||||||
|
|
||||||
{#if dragged}
|
{#if dragged}
|
||||||
<div
|
<div
|
||||||
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
|
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"
|
id="dropzone"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Drag and Drop Container"
|
aria-label="Drag and Drop Container"
|
||||||
>
|
>
|
||||||
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
<div class="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="m-auto pt-64 flex flex-col justify-center">
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
<AddFilesPlaceholder>
|
<AddFilesPlaceholder>
|
||||||
@@ -183,39 +184,9 @@
|
|||||||
|
|
||||||
<AddDocModal bind:show={showAddDocModal} />
|
<AddDocModal bind:show={showAddDocModal} />
|
||||||
|
|
||||||
<SettingsModal bind:show={showSettingsModal} />
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
|
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
showSettingsModal = !showSettingsModal;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div class=" text-xs">{$i18n.t('Document Settings')}</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=" text-gray-500 text-xs mt-1">
|
|
||||||
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -520,6 +491,10 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-gray-500 text-xs mt-1">
|
||||||
|
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" flex justify-end w-full mb-2">
|
<div class=" flex justify-end w-full mb-2">
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -71,16 +71,19 @@
|
|||||||
const url = 'https://openwebui.com';
|
const url = 'https://openwebui.com';
|
||||||
|
|
||||||
const tab = await window.open(`${url}/models/create`, '_blank');
|
const tab = await window.open(`${url}/models/create`, '_blank');
|
||||||
window.addEventListener(
|
|
||||||
'message',
|
// Define the event handler function
|
||||||
(event) => {
|
const messageHandler = (event) => {
|
||||||
if (event.origin !== url) return;
|
if (event.origin !== url) return;
|
||||||
if (event.data === 'loaded') {
|
if (event.data === 'loaded') {
|
||||||
tab.postMessage(JSON.stringify(model), '*');
|
tab.postMessage(JSON.stringify(model), '*');
|
||||||
|
|
||||||
|
// Remove the event listener after handling the message
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
false
|
|
||||||
);
|
window.addEventListener('message', messageHandler, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideModelHandler = async (model) => {
|
const hideModelHandler = async (model) => {
|
||||||
@@ -384,6 +387,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await models.set(await getModels(localStorage.token));
|
await models.set(await getModels(localStorage.token));
|
||||||
|
_models = $models;
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsText(importFiles[0]);
|
reader.readAsText(importFiles[0]);
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import Selector from './Knowledge/Selector.svelte';
|
||||||
|
|
||||||
|
export let knowledge = [];
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex w-full justify-between mb-1">
|
||||||
|
<div class=" self-center text-sm font-semibold">{$i18n.t('Knowledge')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-xs dark:text-gray-500">
|
||||||
|
{$i18n.t('To add documents here, upload them to the "Documents" workspace first.')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#if knowledge.length > 0}
|
||||||
|
<div class=" flex items-center gap-2 mt-2">
|
||||||
|
{#each knowledge as file, fileIdx}
|
||||||
|
<div class=" relative group">
|
||||||
|
<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?.type ?? 'doc') === 'doc'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-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 if file.type === 'collection'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-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>
|
||||||
|
{/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?.title ?? `#${file.name}`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-gray-500 text-sm">{$i18n.t(file?.type ?? 'Document')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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={() => {
|
||||||
|
knowledge.splice(fileIdx, 1);
|
||||||
|
knowledge = knowledge;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2">
|
||||||
|
<Selector bind:knowledge>
|
||||||
|
<button
|
||||||
|
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
||||||
|
type="button">{$i18n.t('Select Documents')}</button
|
||||||
|
>
|
||||||
|
</Selector>
|
||||||
|
</div>
|
||||||
|
<!-- {knowledge} -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
|
||||||
|
import { documents } from '$lib/stores';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let onClose: Function = () => {};
|
||||||
|
|
||||||
|
export let knowledge = [];
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let collections = [
|
||||||
|
...($documents.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'All Documents',
|
||||||
|
type: 'collection',
|
||||||
|
title: $i18n.t('All Documents'),
|
||||||
|
collection_names: $documents.map((doc) => doc.collection_name)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...$documents
|
||||||
|
.reduce((a, e, i, arr) => {
|
||||||
|
return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
|
||||||
|
}, [])
|
||||||
|
.map((tag) => ({
|
||||||
|
name: tag,
|
||||||
|
type: 'collection',
|
||||||
|
collection_names: $documents
|
||||||
|
.filter((doc) => (doc?.content?.tags ?? []).map((tag) => tag.name).includes(tag))
|
||||||
|
.map((doc) => doc.collection_name)
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
items = [...collections, ...$documents];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
on:change={(e) => {
|
||||||
|
if (e.detail === false) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[300px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||||
|
sideOffset={8}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
<div class="max-h-[10rem] overflow-y-scroll">
|
||||||
|
{#if items.length === 0}
|
||||||
|
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{$i18n.t('No documents found')}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each items as item}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2.5 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
on:click={() => {
|
||||||
|
if (!knowledge.find((k) => k.name === item.name)) {
|
||||||
|
knowledge = [
|
||||||
|
...knowledge,
|
||||||
|
{
|
||||||
|
...item,
|
||||||
|
type: item?.type ?? 'doc'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex self-start">
|
||||||
|
{#if (item?.type ?? 'doc') === 'doc'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4"
|
||||||
|
>
|
||||||
|
<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 if item.type === 'collection'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div
|
||||||
|
class=" w-fit text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
{item?.type ?? 'Document'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="line-clamp-1 font-medium pr-0.5">
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
@@ -80,8 +80,6 @@
|
|||||||
if (stopResponseFlag) {
|
if (stopResponseFlag) {
|
||||||
controller.abort('User: Stop Response');
|
controller.abort('User: Stop Response');
|
||||||
}
|
}
|
||||||
|
|
||||||
currentRequestId = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,14 +95,10 @@
|
|||||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if ('request_id' in data) {
|
|
||||||
currentRequestId = data.request_id;
|
|
||||||
} else {
|
|
||||||
text += data.choices[0].delta.content ?? '';
|
text += data.choices[0].delta.content ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@@ -178,9 +172,6 @@
|
|||||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if ('request_id' in data) {
|
|
||||||
currentRequestId = data.request_id;
|
|
||||||
} else {
|
|
||||||
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
@@ -196,7 +187,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { browser, dev } from '$app/environment';
|
|||||||
// import { version } from '../../package.json';
|
// import { version } from '../../package.json';
|
||||||
|
|
||||||
export const APP_NAME = 'Open WebUI';
|
export const APP_NAME = 'Open WebUI';
|
||||||
export const WEBUI_BASE_URL = browser ? (dev ? `http://${location.hostname}:8080` : ``) : ``;
|
|
||||||
|
|
||||||
|
export const WEBUI_HOSTNAME = browser ? (dev ? `${location.hostname}:8080` : ``) : '';
|
||||||
|
export const WEBUI_BASE_URL = browser ? (dev ? `http://${WEBUI_HOSTNAME}` : ``) : ``;
|
||||||
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
||||||
|
|
||||||
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`;
|
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`;
|
||||||
@@ -88,7 +89,8 @@ export const SUPPORTED_FILE_EXTENSIONS = [
|
|||||||
'xls',
|
'xls',
|
||||||
'xlsx',
|
'xlsx',
|
||||||
'pptx',
|
'pptx',
|
||||||
'ppt'
|
'ppt',
|
||||||
|
'msg'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Source: https://kit.svelte.dev/docs/modules#$env-static-public
|
// Source: https://kit.svelte.dev/docs/modules#$env-static-public
|
||||||
|
|||||||
@@ -3,19 +3,21 @@
|
|||||||
"(Beta)": "(تجريبي)",
|
"(Beta)": "(تجريبي)",
|
||||||
"(e.g. `sh webui.sh --api`)": "( `sh webui.sh --api`مثال)",
|
"(e.g. `sh webui.sh --api`)": "( `sh webui.sh --api`مثال)",
|
||||||
"(latest)": "(الأخير)",
|
"(latest)": "(الأخير)",
|
||||||
"{{ models }}": "",
|
"{{ models }}": "{{ نماذج }}",
|
||||||
"{{ owner }}: You cannot delete a base model": "",
|
"{{ owner }}: You cannot delete a base model": "{{ المالك }}: لا يمكنك حذف نموذج أساسي",
|
||||||
"{{modelName}} is thinking...": "{{modelName}} ...يفكر",
|
"{{modelName}} is thinking...": "{{modelName}} ...يفكر",
|
||||||
"{{user}}'s Chats": "دردشات {{user}}",
|
"{{user}}'s Chats": "دردشات {{user}}",
|
||||||
"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
|
"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
|
||||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
"A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب",
|
||||||
"a user": "مستخدم",
|
"a user": "مستخدم",
|
||||||
"About": "عن",
|
"About": "عن",
|
||||||
"Account": "الحساب",
|
"Account": "الحساب",
|
||||||
|
"Account Activation Pending": "",
|
||||||
"Accurate information": "معلومات دقيقة",
|
"Accurate information": "معلومات دقيقة",
|
||||||
|
"Active Users": "",
|
||||||
"Add": "أضف",
|
"Add": "أضف",
|
||||||
"Add a model id": "",
|
"Add a model id": "إضافة معرف نموذج",
|
||||||
"Add a short description about what this model does": "",
|
"Add a short description about what this model does": "أضف وصفا موجزا حول ما يفعله هذا النموذج",
|
||||||
"Add a short title for this prompt": "أضف عنوانًا قصيرًا لبداء المحادثة",
|
"Add a short title for this prompt": "أضف عنوانًا قصيرًا لبداء المحادثة",
|
||||||
"Add a tag": "أضافة تاق",
|
"Add a tag": "أضافة تاق",
|
||||||
"Add custom prompt": "أضافة مطالبة مخصصه",
|
"Add custom prompt": "أضافة مطالبة مخصصه",
|
||||||
@@ -28,15 +30,17 @@
|
|||||||
"Add User": "اضافة مستخدم",
|
"Add User": "اضافة مستخدم",
|
||||||
"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين",
|
"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين",
|
||||||
"admin": "المشرف",
|
"admin": "المشرف",
|
||||||
|
"Admin": "",
|
||||||
"Admin Panel": "لوحة التحكم",
|
"Admin Panel": "لوحة التحكم",
|
||||||
"Admin Settings": "اعدادات المشرف",
|
"Admin Settings": "اعدادات المشرف",
|
||||||
"Advanced Parameters": "التعليمات المتقدمة",
|
"Advanced Parameters": "التعليمات المتقدمة",
|
||||||
"Advanced Params": "",
|
"Advanced Params": "المعلمات المتقدمة",
|
||||||
"all": "الكل",
|
"all": "الكل",
|
||||||
"All Documents": "جميع الملفات",
|
"All Documents": "جميع الملفات",
|
||||||
"All Users": "جميع المستخدمين",
|
"All Users": "جميع المستخدمين",
|
||||||
"Allow": "يسمح",
|
"Allow": "يسمح",
|
||||||
"Allow Chat Deletion": "يستطيع حذف المحادثات",
|
"Allow Chat Deletion": "يستطيع حذف المحادثات",
|
||||||
|
"Allow non-local voices": "",
|
||||||
"alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات",
|
"alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات",
|
||||||
"Already have an account?": "هل تملك حساب ؟",
|
"Already have an account?": "هل تملك حساب ؟",
|
||||||
"an assistant": "مساعد",
|
"an assistant": "مساعد",
|
||||||
@@ -48,7 +52,7 @@
|
|||||||
"API keys": "مفاتيح واجهة برمجة التطبيقات",
|
"API keys": "مفاتيح واجهة برمجة التطبيقات",
|
||||||
"April": "أبريل",
|
"April": "أبريل",
|
||||||
"Archive": "الأرشيف",
|
"Archive": "الأرشيف",
|
||||||
"Archive All Chats": "",
|
"Archive All Chats": "أرشفة جميع الدردشات",
|
||||||
"Archived Chats": "الأرشيف المحادثات",
|
"Archived Chats": "الأرشيف المحادثات",
|
||||||
"are allowed - Activate this command by typing": "مسموح - قم بتنشيط هذا الأمر عن طريق الكتابة",
|
"are allowed - Activate this command by typing": "مسموح - قم بتنشيط هذا الأمر عن طريق الكتابة",
|
||||||
"Are you sure?": "هل أنت متأكد ؟",
|
"Are you sure?": "هل أنت متأكد ؟",
|
||||||
@@ -57,20 +61,22 @@
|
|||||||
"Audio": "صوتي",
|
"Audio": "صوتي",
|
||||||
"August": "أغسطس",
|
"August": "أغسطس",
|
||||||
"Auto-playback response": "استجابة التشغيل التلقائي",
|
"Auto-playback response": "استجابة التشغيل التلقائي",
|
||||||
"Auto-send input after 3 sec.": "إرسال تلقائي للإدخال بعد 3 ثواني.",
|
|
||||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي",
|
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي",
|
||||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب",
|
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب",
|
||||||
"available!": "متاح",
|
"available!": "متاح",
|
||||||
"Back": "خلف",
|
"Back": "خلف",
|
||||||
"Bad Response": "استجابة خطاء",
|
"Bad Response": "استجابة خطاء",
|
||||||
"Banners": "",
|
"Banners": "لافتات",
|
||||||
"Base Model (From)": "",
|
"Base Model (From)": "النموذج الأساسي (من)",
|
||||||
"before": "قبل",
|
"before": "قبل",
|
||||||
"Being lazy": "كون كسول",
|
"Being lazy": "كون كسول",
|
||||||
"Brave Search API Key": "",
|
"Brave Search API Key": "مفتاح واجهة برمجة تطبيقات البحث الشجاع",
|
||||||
"Bypass SSL verification for Websites": "تجاوز التحقق من SSL للموقع",
|
"Bypass SSL verification for Websites": "تجاوز التحقق من SSL للموقع",
|
||||||
|
"Call": "",
|
||||||
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
|
"Camera": "",
|
||||||
"Cancel": "اللغاء",
|
"Cancel": "اللغاء",
|
||||||
"Capabilities": "",
|
"Capabilities": "قدرات",
|
||||||
"Change Password": "تغير الباسورد",
|
"Change Password": "تغير الباسورد",
|
||||||
"Chat": "المحادثة",
|
"Chat": "المحادثة",
|
||||||
"Chat Bubble UI": "UI الدردشة",
|
"Chat Bubble UI": "UI الدردشة",
|
||||||
@@ -86,35 +92,36 @@
|
|||||||
"Chunk Params": "Chunk المتغيرات",
|
"Chunk Params": "Chunk المتغيرات",
|
||||||
"Chunk Size": "Chunk حجم",
|
"Chunk Size": "Chunk حجم",
|
||||||
"Citation": "اقتباس",
|
"Citation": "اقتباس",
|
||||||
|
"Clear memory": "",
|
||||||
"Click here for help.": "أضغط هنا للمساعدة",
|
"Click here for help.": "أضغط هنا للمساعدة",
|
||||||
"Click here to": "أضغط هنا الانتقال",
|
"Click here to": "أضغط هنا الانتقال",
|
||||||
"Click here to select": "أضغط هنا للاختيار",
|
"Click here to select": "أضغط هنا للاختيار",
|
||||||
"Click here to select a csv file.": "أضغط هنا للاختيار ملف csv",
|
"Click here to select a csv file.": "أضغط هنا للاختيار ملف csv",
|
||||||
|
"Click here to select a py file.": "",
|
||||||
"Click here to select documents.": "انقر هنا لاختيار المستندات",
|
"Click here to select documents.": "انقر هنا لاختيار المستندات",
|
||||||
"click here.": "أضغط هنا",
|
"click here.": "أضغط هنا",
|
||||||
"Click on the user role button to change a user's role.": "أضغط على أسم الصلاحيات لتغيرها للمستخدم",
|
"Click on the user role button to change a user's role.": "أضغط على أسم الصلاحيات لتغيرها للمستخدم",
|
||||||
"Clone": "",
|
"Clone": "استنساخ",
|
||||||
"Close": "أغلق",
|
"Close": "أغلق",
|
||||||
"Collection": "مجموعة",
|
"Collection": "مجموعة",
|
||||||
"ComfyUI": "ComfyUI",
|
"ComfyUI": "ComfyUI",
|
||||||
"ComfyUI Base URL": "ComfyUI الرابط الافتراضي",
|
"ComfyUI Base URL": "ComfyUI الرابط الافتراضي",
|
||||||
"ComfyUI Base URL is required.": "ComfyUI الرابط مطلوب",
|
"ComfyUI Base URL is required.": "ComfyUI الرابط مطلوب",
|
||||||
"Command": "الأوامر",
|
"Command": "الأوامر",
|
||||||
"Concurrent Requests": "",
|
"Concurrent Requests": "الطلبات المتزامنة",
|
||||||
"Confirm Password": "تأكيد كلمة المرور",
|
"Confirm Password": "تأكيد كلمة المرور",
|
||||||
"Connections": "اتصالات",
|
"Connections": "اتصالات",
|
||||||
|
"Contact Admin for WebUI Access": "",
|
||||||
"Content": "الاتصال",
|
"Content": "الاتصال",
|
||||||
"Context Length": "طول السياق",
|
"Context Length": "طول السياق",
|
||||||
"Continue Response": "متابعة الرد",
|
"Continue Response": "متابعة الرد",
|
||||||
"Conversation Mode": "وضع المحادثة",
|
|
||||||
"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
|
"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
|
||||||
"Copy": "نسخ",
|
"Copy": "نسخ",
|
||||||
"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
|
"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
|
||||||
"Copy last response": "انسخ الرد الأخير",
|
"Copy last response": "انسخ الرد الأخير",
|
||||||
"Copy Link": "أنسخ الرابط",
|
"Copy Link": "أنسخ الرابط",
|
||||||
"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح",
|
"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح",
|
||||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "قم بإنشاء عبارة موجزة مكونة من 3-5 كلمات كرأس للاستعلام التالي، مع الالتزام الصارم بالحد الأقصى لعدد الكلمات الذي يتراوح بين 3-5 كلمات وتجنب استخدام الكلمة 'عنوان':",
|
"Create a model": "إنشاء نموذج",
|
||||||
"Create a model": "",
|
|
||||||
"Create Account": "إنشاء حساب",
|
"Create Account": "إنشاء حساب",
|
||||||
"Create new key": "عمل مفتاح جديد",
|
"Create new key": "عمل مفتاح جديد",
|
||||||
"Create new secret key": "عمل سر جديد",
|
"Create new secret key": "عمل سر جديد",
|
||||||
@@ -123,38 +130,39 @@
|
|||||||
"Current Model": "الموديل المختار",
|
"Current Model": "الموديل المختار",
|
||||||
"Current Password": "كلمة السر الحالية",
|
"Current Password": "كلمة السر الحالية",
|
||||||
"Custom": "مخصص",
|
"Custom": "مخصص",
|
||||||
"Customize models for a specific purpose": "",
|
"Customize models for a specific purpose": "تخصيص النماذج لغرض معين",
|
||||||
"Dark": "مظلم",
|
"Dark": "مظلم",
|
||||||
|
"Dashboard": "",
|
||||||
"Database": "قاعدة البيانات",
|
"Database": "قاعدة البيانات",
|
||||||
"December": "ديسمبر",
|
"December": "ديسمبر",
|
||||||
"Default": "الإفتراضي",
|
"Default": "الإفتراضي",
|
||||||
"Default (Automatic1111)": "(Automatic1111) الإفتراضي",
|
"Default (Automatic1111)": "(Automatic1111) الإفتراضي",
|
||||||
"Default (SentenceTransformers)": "(SentenceTransformers) الإفتراضي",
|
"Default (SentenceTransformers)": "(SentenceTransformers) الإفتراضي",
|
||||||
"Default (Web API)": "(Web API) الإفتراضي",
|
"Default Model": "النموذج الافتراضي",
|
||||||
"Default Model": "",
|
|
||||||
"Default model updated": "الإفتراضي تحديث الموديل",
|
"Default model updated": "الإفتراضي تحديث الموديل",
|
||||||
"Default Prompt Suggestions": "الإفتراضي Prompt الاقتراحات",
|
"Default Prompt Suggestions": "الإفتراضي Prompt الاقتراحات",
|
||||||
"Default User Role": "الإفتراضي صلاحيات المستخدم",
|
"Default User Role": "الإفتراضي صلاحيات المستخدم",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
"Delete": "حذف",
|
"Delete": "حذف",
|
||||||
"Delete a model": "حذف الموديل",
|
"Delete a model": "حذف الموديل",
|
||||||
"Delete All Chats": "",
|
"Delete All Chats": "حذف جميع الدردشات",
|
||||||
"Delete chat": "حذف المحادثه",
|
"Delete chat": "حذف المحادثه",
|
||||||
"Delete Chat": "حذف المحادثه.",
|
"Delete Chat": "حذف المحادثه.",
|
||||||
"delete this link": "أحذف هذا الرابط",
|
"delete this link": "أحذف هذا الرابط",
|
||||||
"Delete User": "حذف المستخدم",
|
"Delete User": "حذف المستخدم",
|
||||||
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
|
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
|
||||||
"Deleted {{name}}": "",
|
"Deleted {{name}}": "حذف {{name}}",
|
||||||
"Description": "وصف",
|
"Description": "وصف",
|
||||||
"Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل",
|
"Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل",
|
||||||
"Disabled": "تعطيل",
|
"Discover a model": "اكتشف نموذجا",
|
||||||
"Discover a model": "",
|
|
||||||
"Discover a prompt": "اكتشاف موجه",
|
"Discover a prompt": "اكتشاف موجه",
|
||||||
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
|
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
|
||||||
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
||||||
|
"Dismissible": "",
|
||||||
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
||||||
"Document": "المستند",
|
"Document": "المستند",
|
||||||
"Document Settings": "أعدادات المستند",
|
"Document Settings": "أعدادات المستند",
|
||||||
|
"Documentation": "",
|
||||||
"Documents": "مستندات",
|
"Documents": "مستندات",
|
||||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.",
|
"does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.",
|
||||||
"Don't Allow": "لا تسمح بذلك",
|
"Don't Allow": "لا تسمح بذلك",
|
||||||
@@ -169,31 +177,32 @@
|
|||||||
"Edit Doc": "تعديل الملف",
|
"Edit Doc": "تعديل الملف",
|
||||||
"Edit User": "تعديل المستخدم",
|
"Edit User": "تعديل المستخدم",
|
||||||
"Email": "البريد",
|
"Email": "البريد",
|
||||||
|
"Embedding Batch Size": "",
|
||||||
"Embedding Model": "نموذج التضمين",
|
"Embedding Model": "نموذج التضمين",
|
||||||
"Embedding Model Engine": "تضمين محرك النموذج",
|
"Embedding Model Engine": "تضمين محرك النموذج",
|
||||||
"Embedding model set to \"{{embedding_model}}\"": "تم تعيين نموذج التضمين على \"{{embedding_model}}\"",
|
"Embedding model set to \"{{embedding_model}}\"": "تم تعيين نموذج التضمين على \"{{embedding_model}}\"",
|
||||||
"Enable Chat History": "تمكين سجل الدردشة",
|
"Enable Chat History": "تمكين سجل الدردشة",
|
||||||
"Enable Community Sharing": "",
|
"Enable Community Sharing": "تمكين مشاركة المجتمع",
|
||||||
"Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة",
|
"Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة",
|
||||||
"Enable Web Search": "",
|
"Enable Web Search": "تمكين بحث الويب",
|
||||||
"Enabled": "تفعيل",
|
|
||||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
|
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
|
||||||
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
|
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
|
||||||
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
|
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
|
||||||
"Enter Brave Search API Key": "",
|
"Enter Brave Search API Key": "أدخل مفتاح واجهة برمجة تطبيقات البحث الشجاع",
|
||||||
"Enter Chunk Overlap": "أدخل الChunk Overlap",
|
"Enter Chunk Overlap": "أدخل الChunk Overlap",
|
||||||
"Enter Chunk Size": "أدخل Chunk الحجم",
|
"Enter Chunk Size": "أدخل Chunk الحجم",
|
||||||
"Enter Github Raw URL": "",
|
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
|
||||||
"Enter Google PSE API Key": "",
|
"Enter Google PSE API Key": "أدخل مفتاح واجهة برمجة تطبيقات PSE من Google",
|
||||||
"Enter Google PSE Engine Id": "",
|
"Enter Google PSE Engine Id": "أدخل معرف محرك PSE من Google",
|
||||||
"Enter Image Size (e.g. 512x512)": "(e.g. 512x512) أدخل حجم الصورة ",
|
"Enter Image Size (e.g. 512x512)": "(e.g. 512x512) أدخل حجم الصورة ",
|
||||||
"Enter language codes": "أدخل كود اللغة",
|
"Enter language codes": "أدخل كود اللغة",
|
||||||
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
|
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
|
||||||
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
|
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
|
||||||
"Enter Score": "أدخل النتيجة",
|
"Enter Score": "أدخل النتيجة",
|
||||||
"Enter Searxng Query URL": "",
|
"Enter Searxng Query URL": "أدخل عنوان URL لاستعلام Searxng",
|
||||||
"Enter Serper API Key": "",
|
"Enter Serper API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serper",
|
||||||
"Enter Serpstack API Key": "",
|
"Enter Serply API Key": "",
|
||||||
|
"Enter Serpstack API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||||
"Enter stop sequence": "أدخل تسلسل التوقف",
|
"Enter stop sequence": "أدخل تسلسل التوقف",
|
||||||
"Enter Top K": "أدخل Top K",
|
"Enter Top K": "أدخل Top K",
|
||||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
|
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
|
||||||
@@ -202,16 +211,19 @@
|
|||||||
"Enter Your Full Name": "أدخل الاسم كامل",
|
"Enter Your Full Name": "أدخل الاسم كامل",
|
||||||
"Enter Your Password": "ادخل كلمة المرور",
|
"Enter Your Password": "ادخل كلمة المرور",
|
||||||
"Enter Your Role": "أدخل الصلاحيات",
|
"Enter Your Role": "أدخل الصلاحيات",
|
||||||
"Error": "",
|
"Error": "خطأ",
|
||||||
"Experimental": "تجريبي",
|
"Experimental": "تجريبي",
|
||||||
"Export": "",
|
"Export": "تصدير",
|
||||||
"Export All Chats (All Users)": "تصدير جميع الدردشات (جميع المستخدمين)",
|
"Export All Chats (All Users)": "تصدير جميع الدردشات (جميع المستخدمين)",
|
||||||
|
"Export chat (.json)": "",
|
||||||
"Export Chats": "تصدير جميع الدردشات",
|
"Export Chats": "تصدير جميع الدردشات",
|
||||||
"Export Documents Mapping": "تصدير وثائق الخرائط",
|
"Export Documents Mapping": "تصدير وثائق الخرائط",
|
||||||
"Export Models": "",
|
"Export Models": "نماذج التصدير",
|
||||||
"Export Prompts": "مطالبات التصدير",
|
"Export Prompts": "مطالبات التصدير",
|
||||||
|
"External Models": "",
|
||||||
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
|
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
|
||||||
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
|
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
|
||||||
|
"Failed to update settings": "",
|
||||||
"February": "فبراير",
|
"February": "فبراير",
|
||||||
"Feel free to add specific details": "لا تتردد في إضافة تفاصيل محددة",
|
"Feel free to add specific details": "لا تتردد في إضافة تفاصيل محددة",
|
||||||
"File Mode": "وضع الملف",
|
"File Mode": "وضع الملف",
|
||||||
@@ -221,15 +233,15 @@
|
|||||||
"Focus chat input": "التركيز على إدخال الدردشة",
|
"Focus chat input": "التركيز على إدخال الدردشة",
|
||||||
"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
|
"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
|
||||||
"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
|
"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
|
||||||
"Frequency Penalty": "",
|
"Frequency Penalty": "عقوبة التردد",
|
||||||
"Full Screen Mode": "وضع ملء الشاشة",
|
|
||||||
"General": "عام",
|
"General": "عام",
|
||||||
"General Settings": "الاعدادات العامة",
|
"General Settings": "الاعدادات العامة",
|
||||||
"Generating search query": "",
|
"Generate Image": "",
|
||||||
|
"Generating search query": "إنشاء استعلام بحث",
|
||||||
"Generation Info": "معلومات الجيل",
|
"Generation Info": "معلومات الجيل",
|
||||||
"Good Response": "استجابة جيدة",
|
"Good Response": "استجابة جيدة",
|
||||||
"Google PSE API Key": "",
|
"Google PSE API Key": "مفتاح واجهة برمجة تطبيقات PSE من Google",
|
||||||
"Google PSE Engine Id": "",
|
"Google PSE Engine Id": "معرف محرك PSE من Google",
|
||||||
"h:mm a": "الساعة:الدقائق صباحا/مساء",
|
"h:mm a": "الساعة:الدقائق صباحا/مساء",
|
||||||
"has no conversations.": "ليس لديه محادثات.",
|
"has no conversations.": "ليس لديه محادثات.",
|
||||||
"Hello, {{name}}": " {{name}} مرحبا",
|
"Hello, {{name}}": " {{name}} مرحبا",
|
||||||
@@ -243,40 +255,44 @@
|
|||||||
"Images": "الصور",
|
"Images": "الصور",
|
||||||
"Import Chats": "استيراد الدردشات",
|
"Import Chats": "استيراد الدردشات",
|
||||||
"Import Documents Mapping": "استيراد خرائط المستندات",
|
"Import Documents Mapping": "استيراد خرائط المستندات",
|
||||||
"Import Models": "",
|
"Import Models": "استيراد النماذج",
|
||||||
"Import Prompts": "مطالبات الاستيراد",
|
"Import Prompts": "مطالبات الاستيراد",
|
||||||
"Include `--api` flag when running stable-diffusion-webui": "قم بتضمين علامة `-api` عند تشغيل Stable-diffusion-webui",
|
"Include `--api` flag when running stable-diffusion-webui": "قم بتضمين علامة `-api` عند تشغيل Stable-diffusion-webui",
|
||||||
"Info": "",
|
"Info": "معلومات",
|
||||||
"Input commands": "إدخال الأوامر",
|
"Input commands": "إدخال الأوامر",
|
||||||
"Install from Github URL": "",
|
"Install from Github URL": "التثبيت من عنوان URL لجيثب",
|
||||||
|
"Instant Auto-Send After Voice Transcription": "",
|
||||||
"Interface": "واجهه المستخدم",
|
"Interface": "واجهه المستخدم",
|
||||||
"Invalid Tag": "تاق غير صالحة",
|
"Invalid Tag": "تاق غير صالحة",
|
||||||
"January": "يناير",
|
"January": "يناير",
|
||||||
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
||||||
"JSON": "JSON",
|
"JSON": "JSON",
|
||||||
"JSON Preview": "",
|
"JSON Preview": "معاينة JSON",
|
||||||
"July": "يوليو",
|
"July": "يوليو",
|
||||||
"June": "يونيو",
|
"June": "يونيو",
|
||||||
"JWT Expiration": "JWT تجريبي",
|
"JWT Expiration": "JWT تجريبي",
|
||||||
"JWT Token": "JWT Token",
|
"JWT Token": "JWT Token",
|
||||||
"Keep Alive": "Keep Alive",
|
"Keep Alive": "Keep Alive",
|
||||||
"Keyboard shortcuts": "اختصارات لوحة المفاتيح",
|
"Keyboard shortcuts": "اختصارات لوحة المفاتيح",
|
||||||
|
"Knowledge": "",
|
||||||
"Language": "اللغة",
|
"Language": "اللغة",
|
||||||
"Last Active": "آخر نشاط",
|
"Last Active": "آخر نشاط",
|
||||||
"Light": "فاتح",
|
"Light": "فاتح",
|
||||||
"Listening...": "جاري الاستماع",
|
"Listening...": "",
|
||||||
"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
|
"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
|
||||||
|
"Local Models": "",
|
||||||
"LTR": "من جهة اليسار إلى اليمين",
|
"LTR": "من جهة اليسار إلى اليمين",
|
||||||
"Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
"Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
||||||
"Make sure to enclose them with": "تأكد من إرفاقها",
|
"Make sure to enclose them with": "تأكد من إرفاقها",
|
||||||
|
"Manage": "",
|
||||||
"Manage Models": "إدارة النماذج",
|
"Manage Models": "إدارة النماذج",
|
||||||
"Manage Ollama Models": "Ollama إدارة موديلات ",
|
"Manage Ollama Models": "Ollama إدارة موديلات ",
|
||||||
"Manage Pipelines": "",
|
"Manage Pipelines": "إدارة خطوط الأنابيب",
|
||||||
"March": "مارس",
|
"March": "مارس",
|
||||||
"Max Tokens (num_predict)": "",
|
"Max Tokens (num_predict)": "ماكس توكنز (num_predict)",
|
||||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
|
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||||
"May": "مايو",
|
"May": "مايو",
|
||||||
"Memories accessible by LLMs will be shown here.": "",
|
"Memories accessible by LLMs will be shown here.": "سيتم عرض الذكريات التي يمكن الوصول إليها بواسطة LLMs هنا.",
|
||||||
"Memory": "الذاكرة",
|
"Memory": "الذاكرة",
|
||||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة",
|
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة",
|
||||||
"Minimum Score": "الحد الأدنى من النقاط",
|
"Minimum Score": "الحد الأدنى من النقاط",
|
||||||
@@ -288,12 +304,12 @@
|
|||||||
"Model '{{modelName}}' has been successfully downloaded.": "تم تحميل النموذج '{{modelName}}' بنجاح",
|
"Model '{{modelName}}' has been successfully downloaded.": "تم تحميل النموذج '{{modelName}}' بنجاح",
|
||||||
"Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل",
|
"Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل",
|
||||||
"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
|
"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
|
||||||
"Model {{modelName}} is not vision capable": "",
|
"Model {{modelName}} is not vision capable": "نموذج {{modelName}} غير قادر على الرؤية",
|
||||||
"Model {{name}} is now {{status}}": "",
|
"Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}",
|
||||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
|
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
|
||||||
"Model ID": "",
|
"Model ID": "رقم الموديل",
|
||||||
"Model not selected": "لم تختار موديل",
|
"Model not selected": "لم تختار موديل",
|
||||||
"Model Params": "",
|
"Model Params": "معلمات النموذج",
|
||||||
"Model Whitelisting": "القائمة البيضاء للموديل",
|
"Model Whitelisting": "القائمة البيضاء للموديل",
|
||||||
"Model(s) Whitelisted": "القائمة البيضاء الموديل",
|
"Model(s) Whitelisted": "القائمة البيضاء الموديل",
|
||||||
"Modelfile Content": "محتوى الملف النموذجي",
|
"Modelfile Content": "محتوى الملف النموذجي",
|
||||||
@@ -301,25 +317,27 @@
|
|||||||
"More": "المزيد",
|
"More": "المزيد",
|
||||||
"Name": "الأسم",
|
"Name": "الأسم",
|
||||||
"Name Tag": "أسم التاق",
|
"Name Tag": "أسم التاق",
|
||||||
"Name your model": "",
|
"Name your model": "قم بتسمية النموذج الخاص بك",
|
||||||
"New Chat": "دردشة جديدة",
|
"New Chat": "دردشة جديدة",
|
||||||
"New Password": "كلمة المرور الجديدة",
|
"New Password": "كلمة المرور الجديدة",
|
||||||
|
"No documents found": "",
|
||||||
"No results found": "لا توجد نتايج",
|
"No results found": "لا توجد نتايج",
|
||||||
"No search query generated": "",
|
"No search query generated": "لم يتم إنشاء استعلام بحث",
|
||||||
"No source available": "لا يوجد مصدر متاح",
|
"No source available": "لا يوجد مصدر متاح",
|
||||||
"None": "",
|
"None": "اي",
|
||||||
"Not factually correct": "ليس صحيحا من حيث الواقع",
|
"Not factually correct": "ليس صحيحا من حيث الواقع",
|
||||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.",
|
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.",
|
||||||
"Notifications": "إشعارات",
|
"Notifications": "إشعارات",
|
||||||
"November": "نوفمبر",
|
"November": "نوفمبر",
|
||||||
"num_thread (Ollama)": "",
|
"num_thread (Ollama)": "num_thread (أولاما)",
|
||||||
"October": "اكتوبر",
|
"October": "اكتوبر",
|
||||||
"Off": "أغلاق",
|
"Off": "أغلاق",
|
||||||
"Okay, Let's Go!": "حسنا دعنا نذهب!",
|
"Okay, Let's Go!": "حسنا دعنا نذهب!",
|
||||||
"OLED Dark": "OLED داكن",
|
"OLED Dark": "OLED داكن",
|
||||||
"Ollama": "Ollama",
|
"Ollama": "Ollama",
|
||||||
"Ollama API": "",
|
"Ollama API": "أولاما API",
|
||||||
"Ollama API disabled": "",
|
"Ollama API disabled": "أولاما API معطلة",
|
||||||
|
"Ollama API is disabled": "",
|
||||||
"Ollama Version": "Ollama الاصدار",
|
"Ollama Version": "Ollama الاصدار",
|
||||||
"On": "تشغيل",
|
"On": "تشغيل",
|
||||||
"Only": "فقط",
|
"Only": "فقط",
|
||||||
@@ -342,10 +360,12 @@
|
|||||||
"PDF document (.pdf)": "PDF ملف (.pdf)",
|
"PDF document (.pdf)": "PDF ملف (.pdf)",
|
||||||
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
|
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
|
||||||
"pending": "قيد الانتظار",
|
"pending": "قيد الانتظار",
|
||||||
|
"Permission denied when accessing media devices": "",
|
||||||
|
"Permission denied when accessing microphone": "",
|
||||||
"Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ",
|
"Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ",
|
||||||
"Personalization": "التخصيص",
|
"Personalization": "التخصيص",
|
||||||
"Pipelines": "",
|
"Pipelines": "خطوط الانابيب",
|
||||||
"Pipelines Valves": "",
|
"Pipelines Valves": "صمامات خطوط الأنابيب",
|
||||||
"Plain text (.txt)": "نص عادي (.txt)",
|
"Plain text (.txt)": "نص عادي (.txt)",
|
||||||
"Playground": "مكان التجربة",
|
"Playground": "مكان التجربة",
|
||||||
"Positive attitude": "موقف ايجابي",
|
"Positive attitude": "موقف ايجابي",
|
||||||
@@ -364,6 +384,7 @@
|
|||||||
"Read Aloud": "أقراء لي",
|
"Read Aloud": "أقراء لي",
|
||||||
"Record voice": "سجل صوت",
|
"Record voice": "سجل صوت",
|
||||||
"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||||
|
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||||
"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
|
"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
|
||||||
"Regenerate": "تجديد",
|
"Regenerate": "تجديد",
|
||||||
"Release Notes": "ملاحظات الإصدار",
|
"Release Notes": "ملاحظات الإصدار",
|
||||||
@@ -375,12 +396,14 @@
|
|||||||
"Reranking Model": "إعادة تقييم النموذج",
|
"Reranking Model": "إعادة تقييم النموذج",
|
||||||
"Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب",
|
"Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب",
|
||||||
"Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"",
|
"Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"",
|
||||||
|
"Reset Upload Directory": "",
|
||||||
"Reset Vector Storage": "إعادة تعيين تخزين المتجهات",
|
"Reset Vector Storage": "إعادة تعيين تخزين المتجهات",
|
||||||
"Response AutoCopy to Clipboard": "النسخ التلقائي للاستجابة إلى الحافظة",
|
"Response AutoCopy to Clipboard": "النسخ التلقائي للاستجابة إلى الحافظة",
|
||||||
"Role": "منصب",
|
"Role": "منصب",
|
||||||
"Rosé Pine": "Rosé Pine",
|
"Rosé Pine": "Rosé Pine",
|
||||||
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
||||||
"RTL": "من اليمين إلى اليسار",
|
"RTL": "من اليمين إلى اليسار",
|
||||||
|
"Running": "",
|
||||||
"Save": "حفظ",
|
"Save": "حفظ",
|
||||||
"Save & Create": "حفظ وإنشاء",
|
"Save & Create": "حفظ وإنشاء",
|
||||||
"Save & Update": "حفظ وتحديث",
|
"Save & Update": "حفظ وتحديث",
|
||||||
@@ -390,53 +413,60 @@
|
|||||||
"Scan for documents from {{path}}": "{{path}} مسح على الملفات من",
|
"Scan for documents from {{path}}": "{{path}} مسح على الملفات من",
|
||||||
"Search": "البحث",
|
"Search": "البحث",
|
||||||
"Search a model": "البحث عن موديل",
|
"Search a model": "البحث عن موديل",
|
||||||
"Search Chats": "",
|
"Search Chats": "البحث في الدردشات",
|
||||||
"Search Documents": "البحث المستندات",
|
"Search Documents": "البحث المستندات",
|
||||||
"Search Models": "",
|
"Search Models": "نماذج البحث",
|
||||||
"Search Prompts": "أبحث حث",
|
"Search Prompts": "أبحث حث",
|
||||||
"Search Result Count": "",
|
"Search Query Generation Prompt": "",
|
||||||
"Searched {{count}} sites_zero": "",
|
"Search Query Generation Prompt Length Threshold": "",
|
||||||
"Searched {{count}} sites_one": "",
|
"Search Result Count": "عدد نتائج البحث",
|
||||||
"Searched {{count}} sites_two": "",
|
"Searched {{count}} sites_zero": "تم البحث في {{count}} sites_zero",
|
||||||
"Searched {{count}} sites_few": "",
|
"Searched {{count}} sites_one": "تم البحث في {{count}} sites_one",
|
||||||
"Searched {{count}} sites_many": "",
|
"Searched {{count}} sites_two": "تم البحث في {{count}} sites_two",
|
||||||
"Searched {{count}} sites_other": "",
|
"Searched {{count}} sites_few": "تم البحث في {{count}} sites_few",
|
||||||
"Searching the web for '{{searchQuery}}'": "",
|
"Searched {{count}} sites_many": "تم البحث في {{count}} sites_many",
|
||||||
"Searxng Query URL": "",
|
"Searched {{count}} sites_other": "تم البحث في {{count}} sites_other",
|
||||||
|
"Searching \"{{searchQuery}}\"": "",
|
||||||
|
"Searxng Query URL": "عنوان URL لاستعلام Searxng",
|
||||||
"See readme.md for instructions": "readme.md للحصول على التعليمات",
|
"See readme.md for instructions": "readme.md للحصول على التعليمات",
|
||||||
"See what's new": "ما الجديد",
|
"See what's new": "ما الجديد",
|
||||||
"Seed": "Seed",
|
"Seed": "Seed",
|
||||||
"Select a base model": "",
|
"Select a base model": "حدد نموذجا أساسيا",
|
||||||
|
"Select a engine": "",
|
||||||
"Select a mode": "أختار موديل",
|
"Select a mode": "أختار موديل",
|
||||||
"Select a model": "أختار الموديل",
|
"Select a model": "أختار الموديل",
|
||||||
"Select a pipeline": "",
|
"Select a pipeline": "حدد مسارا",
|
||||||
"Select a pipeline url": "",
|
"Select a pipeline url": "حدد عنوان URL لخط الأنابيب",
|
||||||
"Select an Ollama instance": "أختار سيرفر ",
|
"Select an Ollama instance": "أختار سيرفر ",
|
||||||
|
"Select Documents": "",
|
||||||
"Select model": " أختار موديل",
|
"Select model": " أختار موديل",
|
||||||
"Selected model(s) do not support image inputs": "",
|
"Select only one model to call": "",
|
||||||
|
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
|
||||||
"Send": "تم",
|
"Send": "تم",
|
||||||
"Send a Message": "يُرجى إدخال طلبك هنا",
|
"Send a Message": "يُرجى إدخال طلبك هنا",
|
||||||
"Send message": "يُرجى إدخال طلبك هنا.",
|
"Send message": "يُرجى إدخال طلبك هنا.",
|
||||||
"September": "سبتمبر",
|
"September": "سبتمبر",
|
||||||
"Serper API Key": "",
|
"Serper API Key": "مفتاح واجهة برمجة تطبيقات سيربر",
|
||||||
"Serpstack API Key": "",
|
"Serply API Key": "",
|
||||||
|
"Serpstack API Key": "مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||||
"Server connection verified": "تم التحقق من اتصال الخادم",
|
"Server connection verified": "تم التحقق من اتصال الخادم",
|
||||||
"Set as default": "الافتراضي",
|
"Set as default": "الافتراضي",
|
||||||
"Set Default Model": "تفعيد الموديل الافتراضي",
|
"Set Default Model": "تفعيد الموديل الافتراضي",
|
||||||
"Set embedding model (e.g. {{model}})": "ضبط نموذج المتجهات (على سبيل المثال: {{model}})",
|
"Set embedding model (e.g. {{model}})": "ضبط نموذج المتجهات (على سبيل المثال: {{model}})",
|
||||||
"Set Image Size": "حجم الصورة",
|
"Set Image Size": "حجم الصورة",
|
||||||
"Set Model": "ضبط النموذج",
|
|
||||||
"Set reranking model (e.g. {{model}})": "ضبط نموذج إعادة الترتيب (على سبيل المثال: {{model}})",
|
"Set reranking model (e.g. {{model}})": "ضبط نموذج إعادة الترتيب (على سبيل المثال: {{model}})",
|
||||||
"Set Steps": "ضبط الخطوات",
|
"Set Steps": "ضبط الخطوات",
|
||||||
"Set Task Model": "",
|
"Set Task Model": "تعيين نموذج المهمة",
|
||||||
"Set Voice": "ضبط الصوت",
|
"Set Voice": "ضبط الصوت",
|
||||||
"Settings": "الاعدادات",
|
"Settings": "الاعدادات",
|
||||||
"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
|
"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
|
||||||
|
"Settings updated successfully": "",
|
||||||
"Share": "كشاركة",
|
"Share": "كشاركة",
|
||||||
"Share Chat": "مشاركة الدردشة",
|
"Share Chat": "مشاركة الدردشة",
|
||||||
"Share to OpenWebUI Community": "OpenWebUI شارك في مجتمع",
|
"Share to OpenWebUI Community": "OpenWebUI شارك في مجتمع",
|
||||||
"short-summary": "ملخص قصير",
|
"short-summary": "ملخص قصير",
|
||||||
"Show": "عرض",
|
"Show": "عرض",
|
||||||
|
"Show Admin Details in Account Pending Overlay": "",
|
||||||
"Show shortcuts": "إظهار الاختصارات",
|
"Show shortcuts": "إظهار الاختصارات",
|
||||||
"Showcased creativity": "أظهر الإبداع",
|
"Showcased creativity": "أظهر الإبداع",
|
||||||
"sidebar": "الشريط الجانبي",
|
"sidebar": "الشريط الجانبي",
|
||||||
@@ -447,8 +477,8 @@
|
|||||||
"Source": "المصدر",
|
"Source": "المصدر",
|
||||||
"Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام",
|
"Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام",
|
||||||
"Speech-to-Text Engine": "محرك تحويل الكلام إلى نص",
|
"Speech-to-Text Engine": "محرك تحويل الكلام إلى نص",
|
||||||
"SpeechRecognition API is not supported in this browser.": "API SpeechRecognition غير مدعومة في هذا المتصفح.",
|
|
||||||
"Stop Sequence": "وقف التسلسل",
|
"Stop Sequence": "وقف التسلسل",
|
||||||
|
"STT Model": "",
|
||||||
"STT Settings": "STT اعدادات",
|
"STT Settings": "STT اعدادات",
|
||||||
"Submit": "إرسال",
|
"Submit": "إرسال",
|
||||||
"Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة",
|
"Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة",
|
||||||
@@ -467,7 +497,9 @@
|
|||||||
"Thanks for your feedback!": "شكرا لملاحظاتك!",
|
"Thanks for your feedback!": "شكرا لملاحظاتك!",
|
||||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "يجب أن تكون النتيجة قيمة تتراوح بين 0.0 (0%) و1.0 (100%).",
|
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "يجب أن تكون النتيجة قيمة تتراوح بين 0.0 (0%) و1.0 (100%).",
|
||||||
"Theme": "الثيم",
|
"Theme": "الثيم",
|
||||||
|
"Thinking...": "",
|
||||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "وهذا يضمن حفظ محادثاتك القيمة بشكل آمن في قاعدة بياناتك الخلفية. شكرًا لك!",
|
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "وهذا يضمن حفظ محادثاتك القيمة بشكل آمن في قاعدة بياناتك الخلفية. شكرًا لك!",
|
||||||
|
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||||
"This setting does not sync across browsers or devices.": "لا تتم مزامنة هذا الإعداد عبر المتصفحات أو الأجهزة.",
|
"This setting does not sync across browsers or devices.": "لا تتم مزامنة هذا الإعداد عبر المتصفحات أو الأجهزة.",
|
||||||
"Thorough explanation": "شرح شامل",
|
"Thorough explanation": "شرح شامل",
|
||||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
|
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
|
||||||
@@ -479,6 +511,8 @@
|
|||||||
"to": "الى",
|
"to": "الى",
|
||||||
"To access the available model names for downloading,": "للوصول إلى أسماء الموديلات المتاحة للتنزيل،",
|
"To access the available model names for downloading,": "للوصول إلى أسماء الموديلات المتاحة للتنزيل،",
|
||||||
"To access the GGUF models available for downloading,": "للوصول إلى الموديلات GGUF المتاحة للتنزيل،",
|
"To access the GGUF models available for downloading,": "للوصول إلى الموديلات GGUF المتاحة للتنزيل،",
|
||||||
|
"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 chat input.": "الى كتابة المحادثه",
|
||||||
"Today": "اليوم",
|
"Today": "اليوم",
|
||||||
"Toggle settings": "فتح وأغلاق الاعدادات",
|
"Toggle settings": "فتح وأغلاق الاعدادات",
|
||||||
@@ -486,22 +520,25 @@
|
|||||||
"Top K": "Top K",
|
"Top K": "Top K",
|
||||||
"Top P": "Top P",
|
"Top P": "Top P",
|
||||||
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
|
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
|
||||||
|
"TTS Model": "",
|
||||||
"TTS Settings": "TTS اعدادات",
|
"TTS Settings": "TTS اعدادات",
|
||||||
"Type": "",
|
"TTS Voice": "",
|
||||||
|
"Type": "نوع",
|
||||||
"Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).",
|
"Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).",
|
||||||
"Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ",
|
"Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ",
|
||||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع ملف غير معروف '{{file_type}}', ولكن القبول والتعامل كنص عادي ",
|
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع ملف غير معروف '{{file_type}}', ولكن القبول والتعامل كنص عادي ",
|
||||||
"Update and Copy Link": "تحديث ونسخ الرابط",
|
"Update and Copy Link": "تحديث ونسخ الرابط",
|
||||||
"Update password": "تحديث كلمة المرور",
|
"Update password": "تحديث كلمة المرور",
|
||||||
"Upload a GGUF model": "GGUF رفع موديل نوع",
|
"Upload a GGUF model": "GGUF رفع موديل نوع",
|
||||||
"Upload Files": "",
|
"Upload Files": "تحميل الملفات",
|
||||||
|
"Upload Pipeline": "",
|
||||||
"Upload Progress": "جاري التحميل",
|
"Upload Progress": "جاري التحميل",
|
||||||
"URL Mode": "رابط الموديل",
|
"URL Mode": "رابط الموديل",
|
||||||
"Use '#' in the prompt input to load and select your documents.": "أستخدم '#' في المحادثة لربطهامن المستندات",
|
"Use '#' in the prompt input to load and select your documents.": "أستخدم '#' في المحادثة لربطهامن المستندات",
|
||||||
"Use Gravatar": "Gravatar أستخدم",
|
"Use Gravatar": "Gravatar أستخدم",
|
||||||
"Use Initials": "Initials أستخدم",
|
"Use Initials": "Initials أستخدم",
|
||||||
"use_mlock (Ollama)": "",
|
"use_mlock (Ollama)": "use_mlock (أولاما)",
|
||||||
"use_mmap (Ollama)": "",
|
"use_mmap (Ollama)": "use_mmap (أولاما)",
|
||||||
"user": "مستخدم",
|
"user": "مستخدم",
|
||||||
"User Permissions": "صلاحيات المستخدم",
|
"User Permissions": "صلاحيات المستخدم",
|
||||||
"Users": "المستخدمين",
|
"Users": "المستخدمين",
|
||||||
@@ -510,30 +547,34 @@
|
|||||||
"variable": "المتغير",
|
"variable": "المتغير",
|
||||||
"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
|
"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
|
||||||
"Version": "إصدار",
|
"Version": "إصدار",
|
||||||
"Warning": "",
|
"Warning": "تحذير",
|
||||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "تحذير: إذا قمت بتحديث أو تغيير نموذج التضمين الخاص بك، فستحتاج إلى إعادة استيراد كافة المستندات.",
|
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "تحذير: إذا قمت بتحديث أو تغيير نموذج التضمين الخاص بك، فستحتاج إلى إعادة استيراد كافة المستندات.",
|
||||||
"Web": "Web",
|
"Web": "Web",
|
||||||
|
"Web API": "",
|
||||||
"Web Loader Settings": "Web تحميل اعدادات",
|
"Web Loader Settings": "Web تحميل اعدادات",
|
||||||
"Web Params": "Web تحميل اعدادات",
|
"Web Params": "Web تحميل اعدادات",
|
||||||
"Web Search": "",
|
"Web Search": "بحث الويب",
|
||||||
"Web Search Engine": "",
|
"Web Search Engine": "محرك بحث الويب",
|
||||||
"Webhook URL": "Webhook الرابط",
|
"Webhook URL": "Webhook الرابط",
|
||||||
"WebUI Add-ons": "WebUI الأضافات",
|
"WebUI Add-ons": "WebUI الأضافات",
|
||||||
"WebUI Settings": "WebUI اعدادات",
|
"WebUI Settings": "WebUI اعدادات",
|
||||||
"WebUI will make requests to": "سوف يقوم WebUI بتقديم طلبات ل",
|
"WebUI will make requests to": "سوف يقوم WebUI بتقديم طلبات ل",
|
||||||
"What’s New in": "ما هو الجديد",
|
"What’s New in": "ما هو الجديد",
|
||||||
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "عند إيقاف تشغيل السجل، لن تظهر الدردشات الجديدة على هذا المتصفح في سجلك على أي من أجهزتك.",
|
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "عند إيقاف تشغيل السجل، لن تظهر الدردشات الجديدة على هذا المتصفح في سجلك على أي من أجهزتك.",
|
||||||
"Whisper (Local)": "Whisper (Local)",
|
"Whisper (Local)": "",
|
||||||
|
"Widescreen Mode": "",
|
||||||
"Workspace": "مساحة العمل",
|
"Workspace": "مساحة العمل",
|
||||||
"Write a prompt suggestion (e.g. Who are you?)": "اكتب اقتراحًا سريعًا (على سبيل المثال، من أنت؟)",
|
"Write a prompt suggestion (e.g. Who are you?)": "اكتب اقتراحًا سريعًا (على سبيل المثال، من أنت؟)",
|
||||||
"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
|
"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
|
||||||
"Yesterday": "أمس",
|
"Yesterday": "أمس",
|
||||||
"You": "انت",
|
"You": "انت",
|
||||||
"You cannot clone a base model": "",
|
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
|
||||||
|
"You cannot clone a base model": "لا يمكنك استنساخ نموذج أساسي",
|
||||||
"You have no archived conversations.": "لا تملك محادثات محفوظه",
|
"You have no archived conversations.": "لا تملك محادثات محفوظه",
|
||||||
"You have shared this chat": "تم مشاركة هذه المحادثة",
|
"You have shared this chat": "تم مشاركة هذه المحادثة",
|
||||||
"You're a helpful assistant.": "مساعدك المفيد هنا",
|
"You're a helpful assistant.": "مساعدك المفيد هنا",
|
||||||
"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
|
"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
|
||||||
|
"Your account status is currently pending activation.": "",
|
||||||
"Youtube": "Youtube",
|
"Youtube": "Youtube",
|
||||||
"Youtube Loader Settings": "Youtube تحميل اعدادات"
|
"Youtube Loader Settings": "Youtube تحميل اعدادات"
|
||||||
}
|
}
|
||||||
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user