Spaces:
Running
Running
Upload 647 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- CHANGELOG.md +45 -0
- CODE_OF_CONDUCT.md +60 -38
- backend/open_webui/apps/audio/main.py +5 -5
- backend/open_webui/apps/ollama/main.py +40 -15
- backend/open_webui/apps/openai/main.py +2 -1
- backend/open_webui/apps/retrieval/loaders/youtube.py +21 -2
- backend/open_webui/apps/retrieval/main.py +13 -5
- backend/open_webui/apps/retrieval/utils.py +1 -1
- backend/open_webui/apps/webui/main.py +2 -0
- backend/open_webui/apps/webui/models/tools.py +7 -3
- backend/open_webui/apps/webui/routers/configs.py +32 -19
- backend/open_webui/config.py +81 -3
- backend/open_webui/constants.py +1 -0
- backend/open_webui/env.py +5 -2
- backend/open_webui/main.py +132 -4
- backend/open_webui/utils/security_headers.py +7 -0
- backend/open_webui/utils/task.py +37 -8
- backend/requirements.txt +5 -5
- package-lock.json +30 -23
- package.json +1 -1
- pyproject.toml +5 -5
- src/app.css +12 -5
- src/app.html +6 -3
- src/lib/apis/configs/index.ts +31 -4
- src/lib/apis/index.ts +77 -28
- src/lib/apis/retrieval/index.ts +1 -0
- src/lib/components/admin/Settings/Evaluations.svelte +2 -1
- src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte +2 -2
- src/lib/components/admin/Settings/Interface.svelte +291 -251
- src/lib/components/admin/Settings/Models.svelte +20 -24
- src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte +268 -0
- src/lib/components/admin/Settings/Models/ModelList.svelte +58 -0
- src/lib/components/admin/Settings/WebSearch.svelte +19 -1
- src/lib/components/admin/Users/UserList/UserChatsModal.svelte +31 -27
- src/lib/components/chat/Chat.svelte +3 -3
- src/lib/components/chat/MessageInput.svelte +198 -157
- src/lib/components/chat/MessageInput/Commands.svelte +1 -1
- src/lib/components/chat/MessageInput/Commands/Knowledge.svelte +1 -1
- src/lib/components/chat/MessageInput/Commands/Models.svelte +1 -1
- src/lib/components/chat/MessageInput/Commands/Prompts.svelte +1 -1
- src/lib/components/chat/Messages/Error.svelte +2 -2
- src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +14 -14
- src/lib/components/chat/Messages/ResponseMessage.svelte +1 -1
- src/lib/components/chat/ModelSelector.svelte +0 -2
- src/lib/components/chat/Settings/General.svelte +2 -0
- src/lib/components/common/FileItem.svelte +62 -40
- src/lib/components/common/Modal.svelte +5 -3
- src/lib/components/common/RichTextInput.svelte +87 -34
- src/lib/components/common/RichTextInput/AutoCompletion.js +278 -0
- src/lib/components/common/Textarea.svelte +47 -20
CHANGELOG.md
CHANGED
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
|
|
5 |
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6 |
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
## [0.4.4] - 2024-11-22
|
9 |
|
10 |
### Added
|
|
|
5 |
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6 |
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7 |
|
8 |
+
## [0.4.7] - 2024-12-01
|
9 |
+
|
10 |
+
### Added
|
11 |
+
|
12 |
+
- **✨ Prompt Input Auto-Completion**: Type a prompt and let AI intelligently suggest and complete your inputs. Simply press 'Tab' or swipe right on mobile to confirm. Available only with Rich Text Input (default setting). Disable via Admin Settings for full control.
|
13 |
+
- **🌍 Improved Translations**: Enhanced localization for multiple languages, ensuring a more polished and accessible experience for international users.
|
14 |
+
|
15 |
+
### Fixed
|
16 |
+
|
17 |
+
- **🛠️ Tools Export Issue**: Resolved a critical issue where exporting tools wasn’t functioning, restoring seamless export capabilities.
|
18 |
+
- **🔗 Model ID Registration**: Fixed an issue where model IDs weren’t registering correctly in the model editor, ensuring reliable model setup and tracking.
|
19 |
+
- **🖋️ Textarea Auto-Expansion**: Corrected a bug where textareas didn’t expand automatically on certain browsers, improving usability for multi-line inputs.
|
20 |
+
- **🔧 Ollama Embed Endpoint**: Addressed the /ollama/embed endpoint malfunction, ensuring consistent performance and functionality.
|
21 |
+
|
22 |
+
### Changed
|
23 |
+
|
24 |
+
- **🎨 Knowledge Base Styling**: Refined knowledge base visuals for a cleaner, more modern look, laying the groundwork for further enhancements in upcoming releases.
|
25 |
+
|
26 |
+
## [0.4.6] - 2024-11-26
|
27 |
+
|
28 |
+
### Added
|
29 |
+
|
30 |
+
- **🌍 Enhanced Translations**: Various language translations improved to make the WebUI more accessible and user-friendly worldwide.
|
31 |
+
|
32 |
+
### Fixed
|
33 |
+
|
34 |
+
- **✏️ Textarea Shifting Bug**: Resolved the issue where the textarea shifted unexpectedly, ensuring a smoother typing experience.
|
35 |
+
- **⚙️ Model Configuration Modal**: Fixed the issue where the models configuration modal introduced in 0.4.5 wasn’t working for some users.
|
36 |
+
- **🔍 Legacy Query Support**: Restored functionality for custom query generation in RAG when using legacy prompts, ensuring both default and custom templates now work seamlessly.
|
37 |
+
- **⚡ Improved General Reliability**: Various minor fixes improve platform stability and ensure a smoother overall experience across workflows.
|
38 |
+
|
39 |
+
## [0.4.5] - 2024-11-26
|
40 |
+
|
41 |
+
### Added
|
42 |
+
|
43 |
+
- **🎨 Model Order/Defaults Reintroduced**: Brought back the ability to set model order and default models, now configurable via Admin Settings > Models > Configure (Gear Icon).
|
44 |
+
|
45 |
+
### Fixed
|
46 |
+
|
47 |
+
- **🔍 Query Generation Issue**: Resolved an error in web search query generation, enhancing search accuracy and ensuring smoother search workflows.
|
48 |
+
- **📏 Textarea Auto Height Bug**: Fixed a layout issue where textarea input height was shifting unpredictably, particularly when editing system prompts.
|
49 |
+
- **🔑 Ollama Authentication**: Corrected an issue with Ollama’s authorization headers, guaranteeing reliable authentication across all endpoints.
|
50 |
+
- **⚙️ Missing Min_P Save**: Resolved an issue where the 'min_p' parameter was not being saved in configurations.
|
51 |
+
- **🛠️ Tools Description**: Fixed a key issue that omitted tool descriptions in tools payload.
|
52 |
+
|
53 |
## [0.4.4] - 2024-11-22
|
54 |
|
55 |
### Added
|
CODE_OF_CONDUCT.md
CHANGED
@@ -2,76 +2,98 @@
|
|
2 |
|
3 |
## Our Pledge
|
4 |
|
5 |
-
|
6 |
-
community a harassment-free experience for everyone, regardless of age, body
|
7 |
-
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8 |
-
identity and expression, level of experience, education, socio-economic status,
|
9 |
-
nationality, personal appearance, race, religion, or sexual identity
|
10 |
-
and orientation.
|
11 |
|
12 |
-
We
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
## Our Standards
|
15 |
|
16 |
-
Examples of behavior that contribute to a positive
|
17 |
|
18 |
-
-
|
19 |
-
-
|
20 |
-
-
|
21 |
-
-
|
22 |
-
- Focusing on what is best not just for us as individuals, but for the overall community
|
23 |
|
24 |
Examples of unacceptable behavior include:
|
25 |
|
26 |
-
- The use of
|
27 |
-
-
|
28 |
-
-
|
29 |
-
- Publishing others' private information
|
30 |
-
- **
|
31 |
-
-
|
32 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
## Enforcement Responsibilities
|
35 |
|
36 |
-
Community leaders are responsible for
|
37 |
|
38 |
## Scope
|
39 |
|
40 |
-
This Code of Conduct applies
|
|
|
|
|
41 |
|
42 |
-
##
|
43 |
|
44 |
-
Instances of
|
45 |
|
46 |
-
All community leaders are
|
47 |
|
48 |
## Enforcement Guidelines
|
49 |
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
-
|
53 |
|
54 |
-
|
55 |
|
56 |
-
|
57 |
|
58 |
-
|
59 |
|
60 |
-
|
61 |
|
62 |
-
|
63 |
|
64 |
## Attribution
|
65 |
|
66 |
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
67 |
-
version 2.0, available at
|
68 |
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
69 |
|
70 |
-
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
71 |
-
enforcement ladder](https://github.com/mozilla/diversity).
|
72 |
|
73 |
[homepage]: https://www.contributor-covenant.org
|
74 |
|
75 |
-
For answers to common questions about this code of conduct, see the FAQ at
|
76 |
-
https://www.contributor-covenant.org/faq. Translations are available at
|
77 |
https://www.contributor-covenant.org/translations.
|
|
|
2 |
|
3 |
## Our Pledge
|
4 |
|
5 |
+
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
+
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
|
8 |
+
|
9 |
+
## Why These Standards Are Important
|
10 |
+
|
11 |
+
Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
|
12 |
+
|
13 |
+
Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.**
|
14 |
+
|
15 |
+
This is a community where **respect and professionalism are mandatory.** Violations of these standards will result in **zero tolerance** and immediate enforcement to prevent disruption and ensure the well-being of all participants.
|
16 |
|
17 |
## Our Standards
|
18 |
|
19 |
+
Examples of behavior that contribute to a positive and professional community include:
|
20 |
|
21 |
+
- **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences.
|
22 |
+
- **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism.
|
23 |
+
- **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience.
|
24 |
+
- **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas.
|
|
|
25 |
|
26 |
Examples of unacceptable behavior include:
|
27 |
|
28 |
+
- The use of discriminatory, demeaning, or sexualized language or behavior.
|
29 |
+
- Personal attacks, derogatory comments, trolling, or inflammatory political or ideological arguments.
|
30 |
+
- Harassment, intimidation, or any behavior intended to create a hostile, uncomfortable, or unsafe environment.
|
31 |
+
- Publishing others' private information (e.g., physical or email addresses) without explicit permission.
|
32 |
+
- **Entitlement, demand, or aggression toward contributors.** Volunteers are under no obligation to provide immediate or personalized support. Rude or dismissive behavior will not be tolerated.
|
33 |
+
- **Unproductive or destructive behavior.** This includes venting frustration as hostility ("tantrums"), hypercriticism, attention-seeking negativity, or anything that distracts from the project's goals.
|
34 |
+
- **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion.
|
35 |
+
|
36 |
+
### Feedback and Community Engagement
|
37 |
+
|
38 |
+
- **Constructive feedback is encouraged, but hostile or entitled behavior will result in immediate action.** If you disagree with elements of the project, we encourage you to offer meaningful improvements or fork the project if necessary. Healthy discussions and technical disagreements are welcome only when handled with professionalism.
|
39 |
+
- **Respect contributors' time and efforts.** No one is entitled to personalized or on-demand assistance. This is a community built on collaboration and shared effort; demanding or demeaning behavior undermines that trust and will not be allowed.
|
40 |
+
|
41 |
+
### Zero Tolerance: No Warnings, Immediate Action
|
42 |
+
|
43 |
+
This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.**
|
44 |
+
|
45 |
+
We employ this approach to ensure that unproductive or disruptive behavior does not escalate further or cause unnecessary harm to other contributors. The standards are clear, and violations of any kind—whether mild or severe—will be addressed decisively to protect the community.
|
46 |
|
47 |
## Enforcement Responsibilities
|
48 |
|
49 |
+
Community leaders are responsible for upholding and enforcing these standards. They are empowered to take **immediate and appropriate action** to address any behaviors they deem unacceptable under this Code of Conduct. These actions are taken with the goal of protecting the community and preserving its safe, positive, and productive environment.
|
50 |
|
51 |
## Scope
|
52 |
|
53 |
+
This Code of Conduct applies to all community spaces, including forums, repositories, social media accounts, and in-person events. It also applies when an individual represents the community in public settings, such as conferences or official communications.
|
54 |
+
|
55 |
+
Additionally, any behavior outside of these defined spaces that negatively impacts the community or its members may fall within the scope of this Code of Conduct.
|
56 |
|
57 |
+
## Reporting Violations
|
58 |
|
59 |
+
Instances of unacceptable behavior can be reported to the leadership team at **[email protected]**. Reports will be handled promptly, confidentially, and with consideration for the safety and well-being of the reporter.
|
60 |
|
61 |
+
All community leaders are required to uphold confidentiality and impartiality when addressing reports of violations.
|
62 |
|
63 |
## Enforcement Guidelines
|
64 |
|
65 |
+
### Ban
|
66 |
+
|
67 |
+
**Community Impact**: Community leaders will issue a ban to any participant whose behavior is deemed unacceptable according to this Code of Conduct. Bans are enforced immediately and without prior notice.
|
68 |
+
|
69 |
+
A ban may be temporary or permanent, depending on the severity of the violation. This includes—but is not limited to—behavior such as:
|
70 |
+
|
71 |
+
- Harassment or abusive behavior toward contributors.
|
72 |
+
- Persistent negativity or hostility that disrupts the collaborative environment.
|
73 |
+
- Disrespectful, demanding, or aggressive interactions with others.
|
74 |
+
- Attempts to cause harm or sabotage the community.
|
75 |
|
76 |
+
**Consequence**: A banned individual is immediately removed from access to all community spaces, communication channels, and events. Community leaders reserve the right to enforce either a time-limited suspension or a permanent ban based on the specific circumstances of the violation.
|
77 |
|
78 |
+
This approach ensures that disruptive behaviors are addressed swiftly and decisively in order to maintain the integrity and productivity of the community.
|
79 |
|
80 |
+
## Why Zero Tolerance Is Necessary
|
81 |
|
82 |
+
Open-source projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
|
83 |
|
84 |
+
By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive.
|
85 |
|
86 |
+
Our expectations are clear, and our enforcement reflects our commitment to this project's long-term success.
|
87 |
|
88 |
## Attribution
|
89 |
|
90 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at
|
|
|
91 |
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
92 |
|
93 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
|
94 |
|
95 |
[homepage]: https://www.contributor-covenant.org
|
96 |
|
97 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
98 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
99 |
https://www.contributor-covenant.org/translations.
|
backend/open_webui/apps/audio/main.py
CHANGED
@@ -299,12 +299,12 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|
299 |
async with session.post(
|
300 |
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
301 |
data=body,
|
302 |
-
headers=headers
|
303 |
) as r:
|
304 |
r.raise_for_status()
|
305 |
async with aiofiles.open(file_path, "wb") as f:
|
306 |
await f.write(await r.read())
|
307 |
-
|
308 |
async with aiofiles.open(file_body_path, "w") as f:
|
309 |
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
|
310 |
|
@@ -322,7 +322,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|
322 |
error_detail = f"External: {e}"
|
323 |
|
324 |
raise HTTPException(
|
325 |
-
status_code=getattr(r,
|
326 |
detail=error_detail,
|
327 |
)
|
328 |
|
@@ -358,7 +358,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|
358 |
r.raise_for_status()
|
359 |
async with aiofiles.open(file_path, "wb") as f:
|
360 |
await f.write(await r.read())
|
361 |
-
|
362 |
async with aiofiles.open(file_body_path, "w") as f:
|
363 |
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
|
364 |
|
@@ -376,7 +376,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|
376 |
error_detail = f"External: {e}"
|
377 |
|
378 |
raise HTTPException(
|
379 |
-
status_code=getattr(r,
|
380 |
detail=error_detail,
|
381 |
)
|
382 |
|
|
|
299 |
async with session.post(
|
300 |
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
301 |
data=body,
|
302 |
+
headers=headers,
|
303 |
) as r:
|
304 |
r.raise_for_status()
|
305 |
async with aiofiles.open(file_path, "wb") as f:
|
306 |
await f.write(await r.read())
|
307 |
+
|
308 |
async with aiofiles.open(file_body_path, "w") as f:
|
309 |
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
|
310 |
|
|
|
322 |
error_detail = f"External: {e}"
|
323 |
|
324 |
raise HTTPException(
|
325 |
+
status_code=getattr(r, "status", 500),
|
326 |
detail=error_detail,
|
327 |
)
|
328 |
|
|
|
358 |
r.raise_for_status()
|
359 |
async with aiofiles.open(file_path, "wb") as f:
|
360 |
await f.write(await r.read())
|
361 |
+
|
362 |
async with aiofiles.open(file_body_path, "w") as f:
|
363 |
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
|
364 |
|
|
|
376 |
error_detail = f"External: {e}"
|
377 |
|
378 |
raise HTTPException(
|
379 |
+
status_code=getattr(r, "status", 500),
|
380 |
detail=error_detail,
|
381 |
)
|
382 |
|
backend/open_webui/apps/ollama/main.py
CHANGED
@@ -24,6 +24,7 @@ from open_webui.config import (
|
|
24 |
from open_webui.env import (
|
25 |
AIOHTTP_CLIENT_TIMEOUT,
|
26 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
|
|
27 |
)
|
28 |
|
29 |
|
@@ -195,7 +196,10 @@ async def post_streaming_url(
|
|
195 |
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
196 |
)
|
197 |
|
198 |
-
|
|
|
|
|
|
|
199 |
key = api_config.get("key", None)
|
200 |
|
201 |
headers = {"Content-Type": "application/json"}
|
@@ -210,13 +214,13 @@ async def post_streaming_url(
|
|
210 |
r.raise_for_status()
|
211 |
|
212 |
if stream:
|
213 |
-
|
214 |
if content_type:
|
215 |
-
|
216 |
return StreamingResponse(
|
217 |
r.content,
|
218 |
status_code=r.status,
|
219 |
-
headers=
|
220 |
background=BackgroundTask(
|
221 |
cleanup_response, response=r, session=session
|
222 |
),
|
@@ -324,7 +328,10 @@ async def get_ollama_tags(
|
|
324 |
else:
|
325 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
326 |
|
327 |
-
|
|
|
|
|
|
|
328 |
key = api_config.get("key", None)
|
329 |
|
330 |
headers = {}
|
@@ -353,7 +360,7 @@ async def get_ollama_tags(
|
|
353 |
detail=error_detail,
|
354 |
)
|
355 |
|
356 |
-
if user.role == "user":
|
357 |
# Filter models based on user access control
|
358 |
filtered_models = []
|
359 |
for model in models.get("models", []):
|
@@ -525,7 +532,10 @@ async def copy_model(
|
|
525 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
526 |
log.info(f"url: {url}")
|
527 |
|
528 |
-
|
|
|
|
|
|
|
529 |
key = api_config.get("key", None)
|
530 |
|
531 |
headers = {"Content-Type": "application/json"}
|
@@ -584,7 +594,10 @@ async def delete_model(
|
|
584 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
585 |
log.info(f"url: {url}")
|
586 |
|
587 |
-
|
|
|
|
|
|
|
588 |
key = api_config.get("key", None)
|
589 |
|
590 |
headers = {"Content-Type": "application/json"}
|
@@ -635,7 +648,10 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
|
|
635 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
636 |
log.info(f"url: {url}")
|
637 |
|
638 |
-
|
|
|
|
|
|
|
639 |
key = api_config.get("key", None)
|
640 |
|
641 |
headers = {"Content-Type": "application/json"}
|
@@ -691,7 +707,7 @@ async def generate_embeddings(
|
|
691 |
url_idx: Optional[int] = None,
|
692 |
user=Depends(get_verified_user),
|
693 |
):
|
694 |
-
return generate_ollama_batch_embeddings(form_data, url_idx)
|
695 |
|
696 |
|
697 |
@app.post("/api/embeddings")
|
@@ -730,7 +746,10 @@ async def generate_ollama_embeddings(
|
|
730 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
731 |
log.info(f"url: {url}")
|
732 |
|
733 |
-
|
|
|
|
|
|
|
734 |
key = api_config.get("key", None)
|
735 |
|
736 |
headers = {"Content-Type": "application/json"}
|
@@ -797,7 +816,10 @@ async def generate_ollama_batch_embeddings(
|
|
797 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
798 |
log.info(f"url: {url}")
|
799 |
|
800 |
-
|
|
|
|
|
|
|
801 |
key = api_config.get("key", None)
|
802 |
|
803 |
headers = {"Content-Type": "application/json"}
|
@@ -974,7 +996,10 @@ async def generate_chat_completion(
|
|
974 |
log.info(f"url: {url}")
|
975 |
log.debug(f"generate_chat_completion() - 2.payload = {payload}")
|
976 |
|
977 |
-
|
|
|
|
|
|
|
978 |
prefix_id = api_config.get("prefix_id", None)
|
979 |
if prefix_id:
|
980 |
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
|
@@ -1043,7 +1068,7 @@ async def generate_openai_chat_completion(
|
|
1043 |
payload = apply_model_system_prompt_to_body(params, payload, user)
|
1044 |
|
1045 |
# Check if user has access to the model
|
1046 |
-
if user.role == "user":
|
1047 |
if not (
|
1048 |
user.id == model_info.user_id
|
1049 |
or has_access(
|
@@ -1132,7 +1157,7 @@ async def get_openai_models(
|
|
1132 |
detail=error_detail,
|
1133 |
)
|
1134 |
|
1135 |
-
if user.role == "user":
|
1136 |
# Filter models based on user access control
|
1137 |
filtered_models = []
|
1138 |
for model in models:
|
|
|
24 |
from open_webui.env import (
|
25 |
AIOHTTP_CLIENT_TIMEOUT,
|
26 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
27 |
+
BYPASS_MODEL_ACCESS_CONTROL,
|
28 |
)
|
29 |
|
30 |
|
|
|
196 |
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
197 |
)
|
198 |
|
199 |
+
parsed_url = urlparse(url)
|
200 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
201 |
+
|
202 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
203 |
key = api_config.get("key", None)
|
204 |
|
205 |
headers = {"Content-Type": "application/json"}
|
|
|
214 |
r.raise_for_status()
|
215 |
|
216 |
if stream:
|
217 |
+
response_headers = dict(r.headers)
|
218 |
if content_type:
|
219 |
+
response_headers["Content-Type"] = content_type
|
220 |
return StreamingResponse(
|
221 |
r.content,
|
222 |
status_code=r.status,
|
223 |
+
headers=response_headers,
|
224 |
background=BackgroundTask(
|
225 |
cleanup_response, response=r, session=session
|
226 |
),
|
|
|
328 |
else:
|
329 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
330 |
|
331 |
+
parsed_url = urlparse(url)
|
332 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
333 |
+
|
334 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
335 |
key = api_config.get("key", None)
|
336 |
|
337 |
headers = {}
|
|
|
360 |
detail=error_detail,
|
361 |
)
|
362 |
|
363 |
+
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
364 |
# Filter models based on user access control
|
365 |
filtered_models = []
|
366 |
for model in models.get("models", []):
|
|
|
532 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
533 |
log.info(f"url: {url}")
|
534 |
|
535 |
+
parsed_url = urlparse(url)
|
536 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
537 |
+
|
538 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
539 |
key = api_config.get("key", None)
|
540 |
|
541 |
headers = {"Content-Type": "application/json"}
|
|
|
594 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
595 |
log.info(f"url: {url}")
|
596 |
|
597 |
+
parsed_url = urlparse(url)
|
598 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
599 |
+
|
600 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
601 |
key = api_config.get("key", None)
|
602 |
|
603 |
headers = {"Content-Type": "application/json"}
|
|
|
648 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
649 |
log.info(f"url: {url}")
|
650 |
|
651 |
+
parsed_url = urlparse(url)
|
652 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
653 |
+
|
654 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
655 |
key = api_config.get("key", None)
|
656 |
|
657 |
headers = {"Content-Type": "application/json"}
|
|
|
707 |
url_idx: Optional[int] = None,
|
708 |
user=Depends(get_verified_user),
|
709 |
):
|
710 |
+
return await generate_ollama_batch_embeddings(form_data, url_idx)
|
711 |
|
712 |
|
713 |
@app.post("/api/embeddings")
|
|
|
746 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
747 |
log.info(f"url: {url}")
|
748 |
|
749 |
+
parsed_url = urlparse(url)
|
750 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
751 |
+
|
752 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
753 |
key = api_config.get("key", None)
|
754 |
|
755 |
headers = {"Content-Type": "application/json"}
|
|
|
816 |
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
817 |
log.info(f"url: {url}")
|
818 |
|
819 |
+
parsed_url = urlparse(url)
|
820 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
821 |
+
|
822 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
823 |
key = api_config.get("key", None)
|
824 |
|
825 |
headers = {"Content-Type": "application/json"}
|
|
|
996 |
log.info(f"url: {url}")
|
997 |
log.debug(f"generate_chat_completion() - 2.payload = {payload}")
|
998 |
|
999 |
+
parsed_url = urlparse(url)
|
1000 |
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
1001 |
+
|
1002 |
+
api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {})
|
1003 |
prefix_id = api_config.get("prefix_id", None)
|
1004 |
if prefix_id:
|
1005 |
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
|
|
|
1068 |
payload = apply_model_system_prompt_to_body(params, payload, user)
|
1069 |
|
1070 |
# Check if user has access to the model
|
1071 |
+
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
1072 |
if not (
|
1073 |
user.id == model_info.user_id
|
1074 |
or has_access(
|
|
|
1157 |
detail=error_detail,
|
1158 |
)
|
1159 |
|
1160 |
+
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
1161 |
# Filter models based on user access control
|
1162 |
filtered_models = []
|
1163 |
for model in models:
|
backend/open_webui/apps/openai/main.py
CHANGED
@@ -24,6 +24,7 @@ from open_webui.env import (
|
|
24 |
AIOHTTP_CLIENT_TIMEOUT,
|
25 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
26 |
ENABLE_FORWARD_USER_INFO_HEADERS,
|
|
|
27 |
)
|
28 |
|
29 |
from open_webui.constants import ERROR_MESSAGES
|
@@ -422,7 +423,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
|
422 |
error_detail = f"Unexpected error: {str(e)}"
|
423 |
raise HTTPException(status_code=500, detail=error_detail)
|
424 |
|
425 |
-
if user.role == "user":
|
426 |
# Filter models based on user access control
|
427 |
filtered_models = []
|
428 |
for model in models.get("data", []):
|
|
|
24 |
AIOHTTP_CLIENT_TIMEOUT,
|
25 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
26 |
ENABLE_FORWARD_USER_INFO_HEADERS,
|
27 |
+
BYPASS_MODEL_ACCESS_CONTROL,
|
28 |
)
|
29 |
|
30 |
from open_webui.constants import ERROR_MESSAGES
|
|
|
423 |
error_detail = f"Unexpected error: {str(e)}"
|
424 |
raise HTTPException(status_code=500, detail=error_detail)
|
425 |
|
426 |
+
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
427 |
# Filter models based on user access control
|
428 |
filtered_models = []
|
429 |
for model in models.get("data", []):
|
backend/open_webui/apps/retrieval/loaders/youtube.py
CHANGED
@@ -1,7 +1,12 @@
|
|
|
|
|
|
1 |
from typing import Any, Dict, Generator, List, Optional, Sequence, Union
|
2 |
from urllib.parse import parse_qs, urlparse
|
3 |
from langchain_core.documents import Document
|
|
|
4 |
|
|
|
|
|
5 |
|
6 |
ALLOWED_SCHEMES = {"http", "https"}
|
7 |
ALLOWED_NETLOCS = {
|
@@ -51,12 +56,14 @@ class YoutubeLoader:
|
|
51 |
self,
|
52 |
video_id: str,
|
53 |
language: Union[str, Sequence[str]] = "en",
|
|
|
54 |
):
|
55 |
"""Initialize with YouTube video ID."""
|
56 |
_video_id = _parse_video_id(video_id)
|
57 |
self.video_id = _video_id if _video_id is not None else video_id
|
58 |
self._metadata = {"source": video_id}
|
59 |
self.language = language
|
|
|
60 |
if isinstance(language, str):
|
61 |
self.language = [language]
|
62 |
else:
|
@@ -76,10 +83,22 @@ class YoutubeLoader:
|
|
76 |
"Please install it with `pip install youtube-transcript-api`."
|
77 |
)
|
78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
try:
|
80 |
-
transcript_list = YouTubeTranscriptApi.list_transcripts(
|
|
|
|
|
81 |
except Exception as e:
|
82 |
-
|
83 |
return []
|
84 |
|
85 |
try:
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
from typing import Any, Dict, Generator, List, Optional, Sequence, Union
|
4 |
from urllib.parse import parse_qs, urlparse
|
5 |
from langchain_core.documents import Document
|
6 |
+
from open_webui.env import SRC_LOG_LEVELS
|
7 |
|
8 |
+
log = logging.getLogger(__name__)
|
9 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
10 |
|
11 |
ALLOWED_SCHEMES = {"http", "https"}
|
12 |
ALLOWED_NETLOCS = {
|
|
|
56 |
self,
|
57 |
video_id: str,
|
58 |
language: Union[str, Sequence[str]] = "en",
|
59 |
+
proxy_url: Optional[str] = None,
|
60 |
):
|
61 |
"""Initialize with YouTube video ID."""
|
62 |
_video_id = _parse_video_id(video_id)
|
63 |
self.video_id = _video_id if _video_id is not None else video_id
|
64 |
self._metadata = {"source": video_id}
|
65 |
self.language = language
|
66 |
+
self.proxy_url = proxy_url
|
67 |
if isinstance(language, str):
|
68 |
self.language = [language]
|
69 |
else:
|
|
|
83 |
"Please install it with `pip install youtube-transcript-api`."
|
84 |
)
|
85 |
|
86 |
+
if self.proxy_url:
|
87 |
+
youtube_proxies = {
|
88 |
+
"http": self.proxy_url,
|
89 |
+
"https": self.proxy_url,
|
90 |
+
}
|
91 |
+
# Don't log complete URL because it might contain secrets
|
92 |
+
log.debug(f"Using proxy URL: {self.proxy_url[:14]}...")
|
93 |
+
else:
|
94 |
+
youtube_proxies = None
|
95 |
+
|
96 |
try:
|
97 |
+
transcript_list = YouTubeTranscriptApi.list_transcripts(
|
98 |
+
self.video_id, proxies=youtube_proxies
|
99 |
+
)
|
100 |
except Exception as e:
|
101 |
+
log.exception("Loading YouTube transcript failed")
|
102 |
return []
|
103 |
|
104 |
try:
|
backend/open_webui/apps/retrieval/main.py
CHANGED
@@ -105,6 +105,7 @@ from open_webui.config import (
|
|
105 |
TIKA_SERVER_URL,
|
106 |
UPLOAD_DIR,
|
107 |
YOUTUBE_LOADER_LANGUAGE,
|
|
|
108 |
DEFAULT_LOCALE,
|
109 |
AppConfig,
|
110 |
)
|
@@ -171,6 +172,7 @@ app.state.config.OLLAMA_API_KEY = RAG_OLLAMA_API_KEY
|
|
171 |
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
172 |
|
173 |
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
|
|
174 |
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
175 |
|
176 |
|
@@ -471,6 +473,7 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
|
471 |
"youtube": {
|
472 |
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
473 |
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
|
|
474 |
},
|
475 |
"web": {
|
476 |
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
@@ -518,6 +521,7 @@ class ChunkParamUpdateForm(BaseModel):
|
|
518 |
class YoutubeLoaderConfig(BaseModel):
|
519 |
language: list[str]
|
520 |
translation: Optional[str] = None
|
|
|
521 |
|
522 |
|
523 |
class WebSearchConfig(BaseModel):
|
@@ -580,6 +584,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
|
580 |
|
581 |
if form_data.youtube is not None:
|
582 |
app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
|
|
|
583 |
app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
|
584 |
|
585 |
if form_data.web is not None:
|
@@ -640,6 +645,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
|
640 |
},
|
641 |
"youtube": {
|
642 |
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
|
|
643 |
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
644 |
},
|
645 |
"web": {
|
@@ -867,7 +873,7 @@ def save_docs_to_vector_db(
|
|
867 |
return True
|
868 |
except Exception as e:
|
869 |
log.exception(e)
|
870 |
-
|
871 |
|
872 |
|
873 |
class ProcessFileForm(BaseModel):
|
@@ -897,7 +903,7 @@ def process_file(
|
|
897 |
|
898 |
docs = [
|
899 |
Document(
|
900 |
-
page_content=form_data.content,
|
901 |
metadata={
|
902 |
**file.meta,
|
903 |
"name": file.filename,
|
@@ -1081,7 +1087,9 @@ def process_youtube_video(form_data: ProcessUrlForm, user=Depends(get_verified_u
|
|
1081 |
collection_name = calculate_sha256_string(form_data.url)[:63]
|
1082 |
|
1083 |
loader = YoutubeLoader(
|
1084 |
-
form_data.url,
|
|
|
|
|
1085 |
)
|
1086 |
|
1087 |
docs = loader.load()
|
@@ -1391,7 +1399,7 @@ def query_collection_handler(
|
|
1391 |
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
1392 |
return query_collection_with_hybrid_search(
|
1393 |
collection_names=form_data.collection_names,
|
1394 |
-
|
1395 |
embedding_function=app.state.EMBEDDING_FUNCTION,
|
1396 |
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
1397 |
reranking_function=app.state.sentence_transformer_rf,
|
@@ -1402,7 +1410,7 @@ def query_collection_handler(
|
|
1402 |
else:
|
1403 |
return query_collection(
|
1404 |
collection_names=form_data.collection_names,
|
1405 |
-
|
1406 |
embedding_function=app.state.EMBEDDING_FUNCTION,
|
1407 |
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
1408 |
)
|
|
|
105 |
TIKA_SERVER_URL,
|
106 |
UPLOAD_DIR,
|
107 |
YOUTUBE_LOADER_LANGUAGE,
|
108 |
+
YOUTUBE_LOADER_PROXY_URL,
|
109 |
DEFAULT_LOCALE,
|
110 |
AppConfig,
|
111 |
)
|
|
|
172 |
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
173 |
|
174 |
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
175 |
+
app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
|
176 |
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
177 |
|
178 |
|
|
|
473 |
"youtube": {
|
474 |
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
475 |
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
476 |
+
"proxy_url": app.state.config.YOUTUBE_LOADER_PROXY_URL,
|
477 |
},
|
478 |
"web": {
|
479 |
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
|
|
521 |
class YoutubeLoaderConfig(BaseModel):
|
522 |
language: list[str]
|
523 |
translation: Optional[str] = None
|
524 |
+
proxy_url: str = ""
|
525 |
|
526 |
|
527 |
class WebSearchConfig(BaseModel):
|
|
|
584 |
|
585 |
if form_data.youtube is not None:
|
586 |
app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
|
587 |
+
app.state.config.YOUTUBE_LOADER_PROXY_URL = form_data.youtube.proxy_url
|
588 |
app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
|
589 |
|
590 |
if form_data.web is not None:
|
|
|
645 |
},
|
646 |
"youtube": {
|
647 |
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
648 |
+
"proxy_url": app.state.config.YOUTUBE_LOADER_PROXY_URL,
|
649 |
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
650 |
},
|
651 |
"web": {
|
|
|
873 |
return True
|
874 |
except Exception as e:
|
875 |
log.exception(e)
|
876 |
+
raise e
|
877 |
|
878 |
|
879 |
class ProcessFileForm(BaseModel):
|
|
|
903 |
|
904 |
docs = [
|
905 |
Document(
|
906 |
+
page_content=form_data.content.replace("<br/>", "\n"),
|
907 |
metadata={
|
908 |
**file.meta,
|
909 |
"name": file.filename,
|
|
|
1087 |
collection_name = calculate_sha256_string(form_data.url)[:63]
|
1088 |
|
1089 |
loader = YoutubeLoader(
|
1090 |
+
form_data.url,
|
1091 |
+
language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
1092 |
+
proxy_url=app.state.config.YOUTUBE_LOADER_PROXY_URL,
|
1093 |
)
|
1094 |
|
1095 |
docs = loader.load()
|
|
|
1399 |
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
1400 |
return query_collection_with_hybrid_search(
|
1401 |
collection_names=form_data.collection_names,
|
1402 |
+
queries=[form_data.query],
|
1403 |
embedding_function=app.state.EMBEDDING_FUNCTION,
|
1404 |
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
1405 |
reranking_function=app.state.sentence_transformer_rf,
|
|
|
1410 |
else:
|
1411 |
return query_collection(
|
1412 |
collection_names=form_data.collection_names,
|
1413 |
+
queries=[form_data.query],
|
1414 |
embedding_function=app.state.EMBEDDING_FUNCTION,
|
1415 |
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
1416 |
)
|
backend/open_webui/apps/retrieval/utils.py
CHANGED
@@ -429,7 +429,7 @@ def generate_openai_batch_embeddings(
|
|
429 |
|
430 |
|
431 |
def generate_ollama_batch_embeddings(
|
432 |
-
model: str, texts: list[str], url: str, key: str
|
433 |
) -> Optional[list[list[float]]]:
|
434 |
try:
|
435 |
r = requests.post(
|
|
|
429 |
|
430 |
|
431 |
def generate_ollama_batch_embeddings(
|
432 |
+
model: str, texts: list[str], url: str, key: str = ""
|
433 |
) -> Optional[list[list[float]]]:
|
434 |
try:
|
435 |
r = requests.post(
|
backend/open_webui/apps/webui/main.py
CHANGED
@@ -31,6 +31,7 @@ from open_webui.config import (
|
|
31 |
DEFAULT_MODELS,
|
32 |
DEFAULT_PROMPT_SUGGESTIONS,
|
33 |
DEFAULT_USER_ROLE,
|
|
|
34 |
ENABLE_COMMUNITY_SHARING,
|
35 |
ENABLE_LOGIN_FORM,
|
36 |
ENABLE_MESSAGE_RATING,
|
@@ -120,6 +121,7 @@ app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
|
120 |
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
121 |
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
122 |
app.state.config.BANNERS = WEBUI_BANNERS
|
|
|
123 |
|
124 |
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
125 |
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
|
|
31 |
DEFAULT_MODELS,
|
32 |
DEFAULT_PROMPT_SUGGESTIONS,
|
33 |
DEFAULT_USER_ROLE,
|
34 |
+
MODEL_ORDER_LIST,
|
35 |
ENABLE_COMMUNITY_SHARING,
|
36 |
ENABLE_LOGIN_FORM,
|
37 |
ENABLE_MESSAGE_RATING,
|
|
|
121 |
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
122 |
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
123 |
app.state.config.BANNERS = WEBUI_BANNERS
|
124 |
+
app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
|
125 |
|
126 |
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
127 |
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
backend/open_webui/apps/webui/models/tools.py
CHANGED
@@ -76,6 +76,10 @@ class ToolModel(BaseModel):
|
|
76 |
####################
|
77 |
|
78 |
|
|
|
|
|
|
|
|
|
79 |
class ToolResponse(BaseModel):
|
80 |
id: str
|
81 |
user_id: str
|
@@ -138,13 +142,13 @@ class ToolsTable:
|
|
138 |
except Exception:
|
139 |
return None
|
140 |
|
141 |
-
def get_tools(self) -> list[
|
142 |
with get_db() as db:
|
143 |
tools = []
|
144 |
for tool in db.query(Tool).order_by(Tool.updated_at.desc()).all():
|
145 |
user = Users.get_user_by_id(tool.user_id)
|
146 |
tools.append(
|
147 |
-
|
148 |
{
|
149 |
**ToolModel.model_validate(tool).model_dump(),
|
150 |
"user": user.model_dump() if user else None,
|
@@ -155,7 +159,7 @@ class ToolsTable:
|
|
155 |
|
156 |
def get_tools_by_user_id(
|
157 |
self, user_id: str, permission: str = "write"
|
158 |
-
) -> list[
|
159 |
tools = self.get_tools()
|
160 |
|
161 |
return [
|
|
|
76 |
####################
|
77 |
|
78 |
|
79 |
+
class ToolUserModel(ToolModel):
|
80 |
+
user: Optional[UserResponse] = None
|
81 |
+
|
82 |
+
|
83 |
class ToolResponse(BaseModel):
|
84 |
id: str
|
85 |
user_id: str
|
|
|
142 |
except Exception:
|
143 |
return None
|
144 |
|
145 |
+
def get_tools(self) -> list[ToolUserModel]:
|
146 |
with get_db() as db:
|
147 |
tools = []
|
148 |
for tool in db.query(Tool).order_by(Tool.updated_at.desc()).all():
|
149 |
user = Users.get_user_by_id(tool.user_id)
|
150 |
tools.append(
|
151 |
+
ToolUserModel.model_validate(
|
152 |
{
|
153 |
**ToolModel.model_validate(tool).model_dump(),
|
154 |
"user": user.model_dump() if user else None,
|
|
|
159 |
|
160 |
def get_tools_by_user_id(
|
161 |
self, user_id: str, permission: str = "write"
|
162 |
+
) -> list[ToolUserModel]:
|
163 |
tools = self.get_tools()
|
164 |
|
165 |
return [
|
backend/open_webui/apps/webui/routers/configs.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
-
from open_webui.config import BannerModel
|
2 |
from fastapi import APIRouter, Depends, Request
|
3 |
from pydantic import BaseModel
|
4 |
-
from open_webui.utils.utils import get_admin_user, get_verified_user
|
5 |
|
|
|
6 |
|
|
|
7 |
from open_webui.config import get_config, save_config
|
|
|
|
|
8 |
|
9 |
router = APIRouter()
|
10 |
|
@@ -34,8 +36,32 @@ async def export_config(user=Depends(get_admin_user)):
|
|
34 |
return get_config()
|
35 |
|
36 |
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
|
40 |
|
41 |
class PromptSuggestion(BaseModel):
|
@@ -47,21 +73,8 @@ class SetDefaultSuggestionsForm(BaseModel):
|
|
47 |
suggestions: list[PromptSuggestion]
|
48 |
|
49 |
|
50 |
-
|
51 |
-
|
52 |
-
############################
|
53 |
-
|
54 |
-
|
55 |
-
@router.post("/default/models", response_model=str)
|
56 |
-
async def set_global_default_models(
|
57 |
-
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user)
|
58 |
-
):
|
59 |
-
request.app.state.config.DEFAULT_MODELS = form_data.models
|
60 |
-
return request.app.state.config.DEFAULT_MODELS
|
61 |
-
|
62 |
-
|
63 |
-
@router.post("/default/suggestions", response_model=list[PromptSuggestion])
|
64 |
-
async def set_global_default_suggestions(
|
65 |
request: Request,
|
66 |
form_data: SetDefaultSuggestionsForm,
|
67 |
user=Depends(get_admin_user),
|
|
|
|
|
1 |
from fastapi import APIRouter, Depends, Request
|
2 |
from pydantic import BaseModel
|
|
|
3 |
|
4 |
+
from typing import Optional
|
5 |
|
6 |
+
from open_webui.utils.utils import get_admin_user, get_verified_user
|
7 |
from open_webui.config import get_config, save_config
|
8 |
+
from open_webui.config import BannerModel
|
9 |
+
|
10 |
|
11 |
router = APIRouter()
|
12 |
|
|
|
36 |
return get_config()
|
37 |
|
38 |
|
39 |
+
############################
|
40 |
+
# SetDefaultModels
|
41 |
+
############################
|
42 |
+
class ModelsConfigForm(BaseModel):
|
43 |
+
DEFAULT_MODELS: Optional[str]
|
44 |
+
MODEL_ORDER_LIST: Optional[list[str]]
|
45 |
+
|
46 |
+
|
47 |
+
@router.get("/models", response_model=ModelsConfigForm)
|
48 |
+
async def get_models_config(request: Request, user=Depends(get_admin_user)):
|
49 |
+
return {
|
50 |
+
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
|
51 |
+
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
|
52 |
+
}
|
53 |
+
|
54 |
+
|
55 |
+
@router.post("/models", response_model=ModelsConfigForm)
|
56 |
+
async def set_models_config(
|
57 |
+
request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)
|
58 |
+
):
|
59 |
+
request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS
|
60 |
+
request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST
|
61 |
+
return {
|
62 |
+
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
|
63 |
+
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
|
64 |
+
}
|
65 |
|
66 |
|
67 |
class PromptSuggestion(BaseModel):
|
|
|
73 |
suggestions: list[PromptSuggestion]
|
74 |
|
75 |
|
76 |
+
@router.post("/suggestions", response_model=list[PromptSuggestion])
|
77 |
+
async def set_default_suggestions(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
request: Request,
|
79 |
form_data: SetDefaultSuggestionsForm,
|
80 |
user=Depends(get_admin_user),
|
backend/open_webui/config.py
CHANGED
@@ -583,6 +583,12 @@ OLLAMA_API_BASE_URL = os.environ.get(
|
|
583 |
)
|
584 |
|
585 |
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
586 |
|
587 |
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
588 |
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
|
@@ -696,6 +702,7 @@ ENABLE_LOGIN_FORM = PersistentConfig(
|
|
696 |
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
|
697 |
)
|
698 |
|
|
|
699 |
DEFAULT_LOCALE = PersistentConfig(
|
700 |
"DEFAULT_LOCALE",
|
701 |
"ui.default_locale",
|
@@ -740,13 +747,18 @@ DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
|
|
740 |
],
|
741 |
)
|
742 |
|
|
|
|
|
|
|
|
|
|
|
|
|
743 |
DEFAULT_USER_ROLE = PersistentConfig(
|
744 |
"DEFAULT_USER_ROLE",
|
745 |
"ui.default_user_role",
|
746 |
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
747 |
)
|
748 |
|
749 |
-
|
750 |
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
751 |
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
|
752 |
== "true"
|
@@ -969,7 +981,7 @@ QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
|
969 |
)
|
970 |
|
971 |
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
|
972 |
-
Analyze the chat history to determine the necessity of generating search queries. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
|
973 |
|
974 |
### Guidelines:
|
975 |
- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
|
@@ -977,7 +989,7 @@ Analyze the chat history to determine the necessity of generating search queries
|
|
977 |
- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
|
978 |
- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
|
979 |
- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
|
980 |
-
-
|
981 |
- Always prioritize providing actionable and broad queries that maximize informational coverage.
|
982 |
|
983 |
### Output:
|
@@ -992,6 +1004,66 @@ Strictly return in JSON format:
|
|
992 |
</chat_history>
|
993 |
"""
|
994 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
995 |
|
996 |
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
997 |
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
@@ -1253,6 +1325,12 @@ YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
|
|
1253 |
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
|
1254 |
)
|
1255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1256 |
|
1257 |
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
|
1258 |
"ENABLE_RAG_WEB_SEARCH",
|
|
|
583 |
)
|
584 |
|
585 |
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
586 |
+
if OLLAMA_BASE_URL:
|
587 |
+
# Remove trailing slash
|
588 |
+
OLLAMA_BASE_URL = (
|
589 |
+
OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith("/") else OLLAMA_BASE_URL
|
590 |
+
)
|
591 |
+
|
592 |
|
593 |
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
594 |
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
|
|
|
702 |
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
|
703 |
)
|
704 |
|
705 |
+
|
706 |
DEFAULT_LOCALE = PersistentConfig(
|
707 |
"DEFAULT_LOCALE",
|
708 |
"ui.default_locale",
|
|
|
747 |
],
|
748 |
)
|
749 |
|
750 |
+
MODEL_ORDER_LIST = PersistentConfig(
|
751 |
+
"MODEL_ORDER_LIST",
|
752 |
+
"ui.model_order_list",
|
753 |
+
[],
|
754 |
+
)
|
755 |
+
|
756 |
DEFAULT_USER_ROLE = PersistentConfig(
|
757 |
"DEFAULT_USER_ROLE",
|
758 |
"ui.default_user_role",
|
759 |
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
760 |
)
|
761 |
|
|
|
762 |
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
763 |
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
|
764 |
== "true"
|
|
|
981 |
)
|
982 |
|
983 |
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
|
984 |
+
Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
|
985 |
|
986 |
### Guidelines:
|
987 |
- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
|
|
|
989 |
- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
|
990 |
- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
|
991 |
- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
|
992 |
+
- Today's date is: {{CURRENT_DATE}}.
|
993 |
- Always prioritize providing actionable and broad queries that maximize informational coverage.
|
994 |
|
995 |
### Output:
|
|
|
1004 |
</chat_history>
|
1005 |
"""
|
1006 |
|
1007 |
+
ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig(
|
1008 |
+
"ENABLE_AUTOCOMPLETE_GENERATION",
|
1009 |
+
"task.autocomplete.enable",
|
1010 |
+
os.environ.get("ENABLE_AUTOCOMPLETE_GENERATION", "True").lower() == "true",
|
1011 |
+
)
|
1012 |
+
|
1013 |
+
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig(
|
1014 |
+
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH",
|
1015 |
+
"task.autocomplete.input_max_length",
|
1016 |
+
int(os.environ.get("AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH", "-1")),
|
1017 |
+
)
|
1018 |
+
|
1019 |
+
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
1020 |
+
"AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE",
|
1021 |
+
"task.autocomplete.prompt_template",
|
1022 |
+
os.environ.get("AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE", ""),
|
1023 |
+
)
|
1024 |
+
|
1025 |
+
|
1026 |
+
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
1027 |
+
You are an autocompletion system. Continue the text in `<text>` based on the **completion type** in `<type>` and the given language.
|
1028 |
+
|
1029 |
+
### **Instructions**:
|
1030 |
+
1. Analyze `<text>` for context and meaning.
|
1031 |
+
2. Use `<type>` to guide your output:
|
1032 |
+
- **General**: Provide a natural, concise continuation.
|
1033 |
+
- **Search Query**: Complete as if generating a realistic search query.
|
1034 |
+
3. Start as if you are directly continuing `<text>`. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text.
|
1035 |
+
4. Ensure the continuation:
|
1036 |
+
- Flows naturally from `<text>`.
|
1037 |
+
- Avoids repetition, overexplaining, or unrelated ideas.
|
1038 |
+
5. If unsure, return: `{ "text": "" }`.
|
1039 |
+
|
1040 |
+
### **Output Rules**:
|
1041 |
+
- Respond only in JSON format: `{ "text": "<your_completion>" }`.
|
1042 |
+
|
1043 |
+
### **Examples**:
|
1044 |
+
#### Example 1:
|
1045 |
+
Input:
|
1046 |
+
<type>General</type>
|
1047 |
+
<text>The sun was setting over the horizon, painting the sky</text>
|
1048 |
+
Output:
|
1049 |
+
{ "text": "with vibrant shades of orange and pink." }
|
1050 |
+
|
1051 |
+
#### Example 2:
|
1052 |
+
Input:
|
1053 |
+
<type>Search Query</type>
|
1054 |
+
<text>Top-rated restaurants in</text>
|
1055 |
+
Output:
|
1056 |
+
{ "text": "New York City for Italian cuisine." }
|
1057 |
+
|
1058 |
+
---
|
1059 |
+
### Context:
|
1060 |
+
<chat_history>
|
1061 |
+
{{MESSAGES:END:6}}
|
1062 |
+
</chat_history>
|
1063 |
+
<type>{{TYPE}}</type>
|
1064 |
+
<text>{{PROMPT}}</text>
|
1065 |
+
#### Output:
|
1066 |
+
"""
|
1067 |
|
1068 |
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
1069 |
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
|
|
1325 |
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
|
1326 |
)
|
1327 |
|
1328 |
+
YOUTUBE_LOADER_PROXY_URL = PersistentConfig(
|
1329 |
+
"YOUTUBE_LOADER_PROXY_URL",
|
1330 |
+
"rag.youtube_loader_proxy_url",
|
1331 |
+
os.getenv("YOUTUBE_LOADER_PROXY_URL", ""),
|
1332 |
+
)
|
1333 |
+
|
1334 |
|
1335 |
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
|
1336 |
"ENABLE_RAG_WEB_SEARCH",
|
backend/open_webui/constants.py
CHANGED
@@ -113,5 +113,6 @@ class TASKS(str, Enum):
|
|
113 |
TAGS_GENERATION = "tags_generation"
|
114 |
EMOJI_GENERATION = "emoji_generation"
|
115 |
QUERY_GENERATION = "query_generation"
|
|
|
116 |
FUNCTION_CALLING = "function_calling"
|
117 |
MOA_RESPONSE_GENERATION = "moa_response_generation"
|
|
|
113 |
TAGS_GENERATION = "tags_generation"
|
114 |
EMOJI_GENERATION = "emoji_generation"
|
115 |
QUERY_GENERATION = "query_generation"
|
116 |
+
AUTOCOMPLETE_GENERATION = "autocomplete_generation"
|
117 |
FUNCTION_CALLING = "function_calling"
|
118 |
MOA_RESPONSE_GENERATION = "moa_response_generation"
|
backend/open_webui/env.py
CHANGED
@@ -329,6 +329,9 @@ WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
|
329 |
)
|
330 |
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
|
331 |
|
|
|
|
|
|
|
332 |
|
333 |
####################################
|
334 |
# WEBUI_SECRET_KEY
|
@@ -373,7 +376,7 @@ else:
|
|
373 |
AIOHTTP_CLIENT_TIMEOUT = 300
|
374 |
|
375 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
|
376 |
-
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "
|
377 |
)
|
378 |
|
379 |
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
|
@@ -384,7 +387,7 @@ else:
|
|
384 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
385 |
)
|
386 |
except Exception:
|
387 |
-
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST =
|
388 |
|
389 |
####################################
|
390 |
# OFFLINE_MODE
|
|
|
329 |
)
|
330 |
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
|
331 |
|
332 |
+
BYPASS_MODEL_ACCESS_CONTROL = (
|
333 |
+
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
|
334 |
+
)
|
335 |
|
336 |
####################################
|
337 |
# WEBUI_SECRET_KEY
|
|
|
376 |
AIOHTTP_CLIENT_TIMEOUT = 300
|
377 |
|
378 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
|
379 |
+
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "5"
|
380 |
)
|
381 |
|
382 |
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
|
|
|
387 |
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
388 |
)
|
389 |
except Exception:
|
390 |
+
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
|
391 |
|
392 |
####################################
|
393 |
# OFFLINE_MODE
|
backend/open_webui/main.py
CHANGED
@@ -89,6 +89,10 @@ from open_webui.config import (
|
|
89 |
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE,
|
90 |
TITLE_GENERATION_PROMPT_TEMPLATE,
|
91 |
TAGS_GENERATION_PROMPT_TEMPLATE,
|
|
|
|
|
|
|
|
|
92 |
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
93 |
WEBHOOK_URL,
|
94 |
WEBUI_AUTH,
|
@@ -108,6 +112,7 @@ from open_webui.env import (
|
|
108 |
WEBUI_SESSION_COOKIE_SAME_SITE,
|
109 |
WEBUI_SESSION_COOKIE_SECURE,
|
110 |
WEBUI_URL,
|
|
|
111 |
RESET_CONFIG_ON_START,
|
112 |
OFFLINE_MODE,
|
113 |
)
|
@@ -127,6 +132,7 @@ from open_webui.utils.task import (
|
|
127 |
rag_template,
|
128 |
title_generation_template,
|
129 |
query_generation_template,
|
|
|
130 |
tags_generation_template,
|
131 |
emoji_generation_template,
|
132 |
moa_response_generation_template,
|
@@ -207,6 +213,11 @@ app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
|
207 |
|
208 |
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
209 |
|
|
|
|
|
|
|
|
|
|
|
210 |
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
211 |
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
212 |
|
@@ -215,6 +226,10 @@ app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION
|
|
215 |
app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
|
216 |
app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE
|
217 |
|
|
|
|
|
|
|
|
|
218 |
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
219 |
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
220 |
)
|
@@ -531,9 +546,16 @@ async def chat_completion_files_handler(
|
|
531 |
queries_response = queries_response["choices"][0]["message"]["content"]
|
532 |
|
533 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
534 |
queries_response = json.loads(queries_response)
|
535 |
except Exception as e:
|
536 |
-
queries_response = {"queries": []}
|
537 |
|
538 |
queries = queries_response.get("queries", [])
|
539 |
except Exception as e:
|
@@ -600,7 +622,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
|
600 |
)
|
601 |
|
602 |
model_info = Models.get_model_by_id(model["id"])
|
603 |
-
if user.role == "user":
|
604 |
if model.get("arena"):
|
605 |
if not has_access(
|
606 |
user.id,
|
@@ -1194,8 +1216,16 @@ async def get_models(user=Depends(get_verified_user)):
|
|
1194 |
if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
|
1195 |
]
|
1196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1197 |
# Filter out models that the user does not have access to
|
1198 |
-
if user.role == "user":
|
1199 |
filtered_models = []
|
1200 |
for model in models:
|
1201 |
if model.get("arena"):
|
@@ -1650,6 +1680,8 @@ async def get_task_config(user=Depends(get_verified_user)):
|
|
1650 |
"TASK_MODEL": app.state.config.TASK_MODEL,
|
1651 |
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
1652 |
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
|
|
|
|
1653 |
"TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
1654 |
"ENABLE_TAGS_GENERATION": app.state.config.ENABLE_TAGS_GENERATION,
|
1655 |
"ENABLE_SEARCH_QUERY_GENERATION": app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
|
@@ -1663,6 +1695,8 @@ class TaskConfigForm(BaseModel):
|
|
1663 |
TASK_MODEL: Optional[str]
|
1664 |
TASK_MODEL_EXTERNAL: Optional[str]
|
1665 |
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
|
|
|
|
1666 |
TAGS_GENERATION_PROMPT_TEMPLATE: str
|
1667 |
ENABLE_TAGS_GENERATION: bool
|
1668 |
ENABLE_SEARCH_QUERY_GENERATION: bool
|
@@ -1678,6 +1712,14 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
|
1678 |
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
1679 |
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
1680 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1681 |
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = (
|
1682 |
form_data.TAGS_GENERATION_PROMPT_TEMPLATE
|
1683 |
)
|
@@ -1700,6 +1742,8 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
|
1700 |
"TASK_MODEL": app.state.config.TASK_MODEL,
|
1701 |
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
1702 |
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
|
|
|
|
1703 |
"TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
1704 |
"ENABLE_TAGS_GENERATION": app.state.config.ENABLE_TAGS_GENERATION,
|
1705 |
"ENABLE_SEARCH_QUERY_GENERATION": app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
|
@@ -1927,7 +1971,7 @@ async def generate_queries(form_data: dict, user=Depends(get_verified_user)):
|
|
1927 |
f"generating {type} queries using model {task_model_id} for user {user.email}"
|
1928 |
)
|
1929 |
|
1930 |
-
if app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE != "":
|
1931 |
template = app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE
|
1932 |
else:
|
1933 |
template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE
|
@@ -1967,6 +2011,90 @@ async def generate_queries(form_data: dict, user=Depends(get_verified_user)):
|
|
1967 |
return await generate_chat_completions(form_data=payload, user=user)
|
1968 |
|
1969 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1970 |
@app.post("/api/task/emoji/completions")
|
1971 |
async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
1972 |
|
|
|
89 |
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE,
|
90 |
TITLE_GENERATION_PROMPT_TEMPLATE,
|
91 |
TAGS_GENERATION_PROMPT_TEMPLATE,
|
92 |
+
ENABLE_AUTOCOMPLETE_GENERATION,
|
93 |
+
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
94 |
+
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
95 |
+
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
96 |
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
97 |
WEBHOOK_URL,
|
98 |
WEBUI_AUTH,
|
|
|
112 |
WEBUI_SESSION_COOKIE_SAME_SITE,
|
113 |
WEBUI_SESSION_COOKIE_SECURE,
|
114 |
WEBUI_URL,
|
115 |
+
BYPASS_MODEL_ACCESS_CONTROL,
|
116 |
RESET_CONFIG_ON_START,
|
117 |
OFFLINE_MODE,
|
118 |
)
|
|
|
132 |
rag_template,
|
133 |
title_generation_template,
|
134 |
query_generation_template,
|
135 |
+
autocomplete_generation_template,
|
136 |
tags_generation_template,
|
137 |
emoji_generation_template,
|
138 |
moa_response_generation_template,
|
|
|
213 |
|
214 |
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
215 |
|
216 |
+
app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
|
217 |
+
app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = (
|
218 |
+
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
|
219 |
+
)
|
220 |
+
|
221 |
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
222 |
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
223 |
|
|
|
226 |
app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
|
227 |
app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE
|
228 |
|
229 |
+
app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = (
|
230 |
+
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
|
231 |
+
)
|
232 |
+
|
233 |
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
234 |
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
235 |
)
|
|
|
546 |
queries_response = queries_response["choices"][0]["message"]["content"]
|
547 |
|
548 |
try:
|
549 |
+
bracket_start = queries_response.find("{")
|
550 |
+
bracket_end = queries_response.rfind("}") + 1
|
551 |
+
|
552 |
+
if bracket_start == -1 or bracket_end == -1:
|
553 |
+
raise Exception("No JSON object found in the response")
|
554 |
+
|
555 |
+
queries_response = queries_response[bracket_start:bracket_end]
|
556 |
queries_response = json.loads(queries_response)
|
557 |
except Exception as e:
|
558 |
+
queries_response = {"queries": [queries_response]}
|
559 |
|
560 |
queries = queries_response.get("queries", [])
|
561 |
except Exception as e:
|
|
|
622 |
)
|
623 |
|
624 |
model_info = Models.get_model_by_id(model["id"])
|
625 |
+
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
626 |
if model.get("arena"):
|
627 |
if not has_access(
|
628 |
user.id,
|
|
|
1216 |
if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
|
1217 |
]
|
1218 |
|
1219 |
+
model_order_list = webui_app.state.config.MODEL_ORDER_LIST
|
1220 |
+
if model_order_list:
|
1221 |
+
model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)}
|
1222 |
+
# Sort models by order list priority, with fallback for those not in the list
|
1223 |
+
models.sort(
|
1224 |
+
key=lambda x: (model_order_dict.get(x["id"], float("inf")), x["name"])
|
1225 |
+
)
|
1226 |
+
|
1227 |
# Filter out models that the user does not have access to
|
1228 |
+
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
1229 |
filtered_models = []
|
1230 |
for model in models:
|
1231 |
if model.get("arena"):
|
|
|
1680 |
"TASK_MODEL": app.state.config.TASK_MODEL,
|
1681 |
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
1682 |
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
1683 |
+
"ENABLE_AUTOCOMPLETE_GENERATION": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
1684 |
+
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
1685 |
"TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
1686 |
"ENABLE_TAGS_GENERATION": app.state.config.ENABLE_TAGS_GENERATION,
|
1687 |
"ENABLE_SEARCH_QUERY_GENERATION": app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
|
|
|
1695 |
TASK_MODEL: Optional[str]
|
1696 |
TASK_MODEL_EXTERNAL: Optional[str]
|
1697 |
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
1698 |
+
ENABLE_AUTOCOMPLETE_GENERATION: bool
|
1699 |
+
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: int
|
1700 |
TAGS_GENERATION_PROMPT_TEMPLATE: str
|
1701 |
ENABLE_TAGS_GENERATION: bool
|
1702 |
ENABLE_SEARCH_QUERY_GENERATION: bool
|
|
|
1712 |
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
1713 |
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
1714 |
)
|
1715 |
+
|
1716 |
+
app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = (
|
1717 |
+
form_data.ENABLE_AUTOCOMPLETE_GENERATION
|
1718 |
+
)
|
1719 |
+
app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = (
|
1720 |
+
form_data.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
|
1721 |
+
)
|
1722 |
+
|
1723 |
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = (
|
1724 |
form_data.TAGS_GENERATION_PROMPT_TEMPLATE
|
1725 |
)
|
|
|
1742 |
"TASK_MODEL": app.state.config.TASK_MODEL,
|
1743 |
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
1744 |
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
1745 |
+
"ENABLE_AUTOCOMPLETE_GENERATION": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
1746 |
+
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
1747 |
"TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
1748 |
"ENABLE_TAGS_GENERATION": app.state.config.ENABLE_TAGS_GENERATION,
|
1749 |
"ENABLE_SEARCH_QUERY_GENERATION": app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
|
|
|
1971 |
f"generating {type} queries using model {task_model_id} for user {user.email}"
|
1972 |
)
|
1973 |
|
1974 |
+
if (app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != "":
|
1975 |
template = app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE
|
1976 |
else:
|
1977 |
template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE
|
|
|
2011 |
return await generate_chat_completions(form_data=payload, user=user)
|
2012 |
|
2013 |
|
2014 |
+
@app.post("/api/task/auto/completions")
|
2015 |
+
async def generate_autocompletion(form_data: dict, user=Depends(get_verified_user)):
|
2016 |
+
if not app.state.config.ENABLE_AUTOCOMPLETE_GENERATION:
|
2017 |
+
raise HTTPException(
|
2018 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2019 |
+
detail=f"Autocompletion generation is disabled",
|
2020 |
+
)
|
2021 |
+
|
2022 |
+
type = form_data.get("type")
|
2023 |
+
prompt = form_data.get("prompt")
|
2024 |
+
messages = form_data.get("messages")
|
2025 |
+
|
2026 |
+
if app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH > 0:
|
2027 |
+
if len(prompt) > app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH:
|
2028 |
+
raise HTTPException(
|
2029 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2030 |
+
detail=f"Input prompt exceeds maximum length of {app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}",
|
2031 |
+
)
|
2032 |
+
|
2033 |
+
model_list = await get_all_models()
|
2034 |
+
models = {model["id"]: model for model in model_list}
|
2035 |
+
|
2036 |
+
model_id = form_data["model"]
|
2037 |
+
if model_id not in models:
|
2038 |
+
raise HTTPException(
|
2039 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
2040 |
+
detail="Model not found",
|
2041 |
+
)
|
2042 |
+
|
2043 |
+
# Check if the user has a custom task model
|
2044 |
+
# If the user has a custom task model, use that model
|
2045 |
+
task_model_id = get_task_model_id(
|
2046 |
+
model_id,
|
2047 |
+
app.state.config.TASK_MODEL,
|
2048 |
+
app.state.config.TASK_MODEL_EXTERNAL,
|
2049 |
+
models,
|
2050 |
+
)
|
2051 |
+
|
2052 |
+
log.debug(
|
2053 |
+
f"generating autocompletion using model {task_model_id} for user {user.email}"
|
2054 |
+
)
|
2055 |
+
|
2056 |
+
if (app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != "":
|
2057 |
+
template = app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
|
2058 |
+
else:
|
2059 |
+
template = DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
|
2060 |
+
|
2061 |
+
content = autocomplete_generation_template(
|
2062 |
+
template, prompt, messages, type, {"name": user.name}
|
2063 |
+
)
|
2064 |
+
|
2065 |
+
payload = {
|
2066 |
+
"model": task_model_id,
|
2067 |
+
"messages": [{"role": "user", "content": content}],
|
2068 |
+
"stream": False,
|
2069 |
+
"metadata": {
|
2070 |
+
"task": str(TASKS.AUTOCOMPLETE_GENERATION),
|
2071 |
+
"task_body": form_data,
|
2072 |
+
"chat_id": form_data.get("chat_id", None),
|
2073 |
+
},
|
2074 |
+
}
|
2075 |
+
|
2076 |
+
print(payload)
|
2077 |
+
|
2078 |
+
# Handle pipeline filters
|
2079 |
+
try:
|
2080 |
+
payload = filter_pipeline(payload, user, models)
|
2081 |
+
except Exception as e:
|
2082 |
+
if len(e.args) > 1:
|
2083 |
+
return JSONResponse(
|
2084 |
+
status_code=e.args[0],
|
2085 |
+
content={"detail": e.args[1]},
|
2086 |
+
)
|
2087 |
+
else:
|
2088 |
+
return JSONResponse(
|
2089 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2090 |
+
content={"detail": str(e)},
|
2091 |
+
)
|
2092 |
+
if "chat_id" in payload:
|
2093 |
+
del payload["chat_id"]
|
2094 |
+
|
2095 |
+
return await generate_chat_completions(form_data=payload, user=user)
|
2096 |
+
|
2097 |
+
|
2098 |
@app.post("/api/task/emoji/completions")
|
2099 |
async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
2100 |
|
backend/open_webui/utils/security_headers.py
CHANGED
@@ -27,6 +27,7 @@ def set_security_headers() -> Dict[str, str]:
|
|
27 |
- x-download-options
|
28 |
- x-frame-options
|
29 |
- x-permitted-cross-domain-policies
|
|
|
30 |
|
31 |
Each environment variable is associated with a specific setter function
|
32 |
that constructs the header. If the environment variable is set, the
|
@@ -45,6 +46,7 @@ def set_security_headers() -> Dict[str, str]:
|
|
45 |
"XDOWNLOAD_OPTIONS": set_xdownload_options,
|
46 |
"XFRAME_OPTIONS": set_xframe,
|
47 |
"XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
|
|
|
48 |
}
|
49 |
|
50 |
for env_var, setter in header_setters.items():
|
@@ -124,3 +126,8 @@ def set_xpermitted_cross_domain_policies(value: str):
|
|
124 |
if not match:
|
125 |
value = "none"
|
126 |
return {"X-Permitted-Cross-Domain-Policies": value}
|
|
|
|
|
|
|
|
|
|
|
|
27 |
- x-download-options
|
28 |
- x-frame-options
|
29 |
- x-permitted-cross-domain-policies
|
30 |
+
- content-security-policy
|
31 |
|
32 |
Each environment variable is associated with a specific setter function
|
33 |
that constructs the header. If the environment variable is set, the
|
|
|
46 |
"XDOWNLOAD_OPTIONS": set_xdownload_options,
|
47 |
"XFRAME_OPTIONS": set_xframe,
|
48 |
"XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
|
49 |
+
"CONTENT_SECURITY_POLICY": set_content_security_policy,
|
50 |
}
|
51 |
|
52 |
for env_var, setter in header_setters.items():
|
|
|
126 |
if not match:
|
127 |
value = "none"
|
128 |
return {"X-Permitted-Cross-Domain-Policies": value}
|
129 |
+
|
130 |
+
|
131 |
+
# Set Content-Security-Policy response header
|
132 |
+
def set_content_security_policy(value: str):
|
133 |
+
return {"Content-Security-Policy": value}
|
backend/open_webui/utils/task.py
CHANGED
@@ -25,12 +25,14 @@ def prompt_template(
|
|
25 |
# Format the date to YYYY-MM-DD
|
26 |
formatted_date = current_date.strftime("%Y-%m-%d")
|
27 |
formatted_time = current_date.strftime("%I:%M:%S %p")
|
|
|
28 |
|
29 |
template = template.replace("{{CURRENT_DATE}}", formatted_date)
|
30 |
template = template.replace("{{CURRENT_TIME}}", formatted_time)
|
31 |
template = template.replace(
|
32 |
"{{CURRENT_DATETIME}}", f"{formatted_date} {formatted_time}"
|
33 |
)
|
|
|
34 |
|
35 |
if user_name:
|
36 |
# Replace {{USER_NAME}} in the template with the user's name
|
@@ -51,7 +53,9 @@ def prompt_template(
|
|
51 |
|
52 |
def replace_prompt_variable(template: str, prompt: str) -> str:
|
53 |
def replacement_function(match):
|
54 |
-
full_match = match.group(
|
|
|
|
|
55 |
start_length = match.group(1)
|
56 |
end_length = match.group(2)
|
57 |
middle_length = match.group(3)
|
@@ -71,20 +75,23 @@ def replace_prompt_variable(template: str, prompt: str) -> str:
|
|
71 |
return f"{start}...{end}"
|
72 |
return ""
|
73 |
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
template,
|
78 |
-
)
|
79 |
return template
|
80 |
|
81 |
|
82 |
-
def replace_messages_variable(
|
|
|
|
|
83 |
def replacement_function(match):
|
84 |
full_match = match.group(0)
|
85 |
start_length = match.group(1)
|
86 |
end_length = match.group(2)
|
87 |
middle_length = match.group(3)
|
|
|
|
|
|
|
88 |
|
89 |
# Process messages based on the number of messages required
|
90 |
if full_match == "{{MESSAGES}}":
|
@@ -120,7 +127,7 @@ def replace_messages_variable(template: str, messages: list[str]) -> str:
|
|
120 |
|
121 |
|
122 |
def rag_template(template: str, context: str, query: str):
|
123 |
-
if template == "":
|
124 |
template = DEFAULT_RAG_TEMPLATE
|
125 |
|
126 |
if "[context]" not in template and "{{CONTEXT}}" not in template:
|
@@ -210,6 +217,28 @@ def emoji_generation_template(
|
|
210 |
return template
|
211 |
|
212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
213 |
def query_generation_template(
|
214 |
template: str, messages: list[dict], user: Optional[dict] = None
|
215 |
) -> str:
|
|
|
25 |
# Format the date to YYYY-MM-DD
|
26 |
formatted_date = current_date.strftime("%Y-%m-%d")
|
27 |
formatted_time = current_date.strftime("%I:%M:%S %p")
|
28 |
+
formatted_weekday = current_date.strftime("%A")
|
29 |
|
30 |
template = template.replace("{{CURRENT_DATE}}", formatted_date)
|
31 |
template = template.replace("{{CURRENT_TIME}}", formatted_time)
|
32 |
template = template.replace(
|
33 |
"{{CURRENT_DATETIME}}", f"{formatted_date} {formatted_time}"
|
34 |
)
|
35 |
+
template = template.replace("{{CURRENT_WEEKDAY}}", formatted_weekday)
|
36 |
|
37 |
if user_name:
|
38 |
# Replace {{USER_NAME}} in the template with the user's name
|
|
|
53 |
|
54 |
def replace_prompt_variable(template: str, prompt: str) -> str:
|
55 |
def replacement_function(match):
|
56 |
+
full_match = match.group(
|
57 |
+
0
|
58 |
+
).lower() # Normalize to lowercase for consistent handling
|
59 |
start_length = match.group(1)
|
60 |
end_length = match.group(2)
|
61 |
middle_length = match.group(3)
|
|
|
75 |
return f"{start}...{end}"
|
76 |
return ""
|
77 |
|
78 |
+
# Updated regex pattern to make it case-insensitive with the `(?i)` flag
|
79 |
+
pattern = r"(?i){{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}"
|
80 |
+
template = re.sub(pattern, replacement_function, template)
|
|
|
|
|
81 |
return template
|
82 |
|
83 |
|
84 |
+
def replace_messages_variable(
|
85 |
+
template: str, messages: Optional[list[str]] = None
|
86 |
+
) -> str:
|
87 |
def replacement_function(match):
|
88 |
full_match = match.group(0)
|
89 |
start_length = match.group(1)
|
90 |
end_length = match.group(2)
|
91 |
middle_length = match.group(3)
|
92 |
+
# If messages is None, handle it as an empty list
|
93 |
+
if messages is None:
|
94 |
+
return ""
|
95 |
|
96 |
# Process messages based on the number of messages required
|
97 |
if full_match == "{{MESSAGES}}":
|
|
|
127 |
|
128 |
|
129 |
def rag_template(template: str, context: str, query: str):
|
130 |
+
if template.strip() == "":
|
131 |
template = DEFAULT_RAG_TEMPLATE
|
132 |
|
133 |
if "[context]" not in template and "{{CONTEXT}}" not in template:
|
|
|
217 |
return template
|
218 |
|
219 |
|
220 |
+
def autocomplete_generation_template(
|
221 |
+
template: str,
|
222 |
+
prompt: str,
|
223 |
+
messages: Optional[list[dict]] = None,
|
224 |
+
type: Optional[str] = None,
|
225 |
+
user: Optional[dict] = None,
|
226 |
+
) -> str:
|
227 |
+
template = template.replace("{{TYPE}}", type if type else "")
|
228 |
+
template = replace_prompt_variable(template, prompt)
|
229 |
+
template = replace_messages_variable(template, messages)
|
230 |
+
|
231 |
+
template = prompt_template(
|
232 |
+
template,
|
233 |
+
**(
|
234 |
+
{"user_name": user.get("name"), "user_location": user.get("location")}
|
235 |
+
if user
|
236 |
+
else {}
|
237 |
+
),
|
238 |
+
)
|
239 |
+
return template
|
240 |
+
|
241 |
+
|
242 |
def query_generation_template(
|
243 |
template: str, messages: list[dict], user: Optional[dict] = None
|
244 |
) -> str:
|
backend/requirements.txt
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
fastapi==0.111.0
|
2 |
uvicorn[standard]==0.30.6
|
3 |
pydantic==2.9.2
|
4 |
-
python-multipart==0.0.
|
5 |
|
6 |
Flask==3.0.3
|
7 |
Flask-Cors==5.0.0
|
@@ -11,13 +11,13 @@ python-jose==3.3.0
|
|
11 |
passlib[bcrypt]==1.7.4
|
12 |
|
13 |
requests==2.32.3
|
14 |
-
aiohttp==3.
|
15 |
async-timeout
|
16 |
aiocache
|
17 |
aiofiles
|
18 |
|
19 |
sqlalchemy==2.0.32
|
20 |
-
alembic==1.
|
21 |
peewee==3.17.6
|
22 |
peewee-migrate==1.12.2
|
23 |
psycopg2-binary==2.9.9
|
@@ -44,11 +44,11 @@ langchain-chroma==0.1.4
|
|
44 |
|
45 |
fake-useragent==1.5.1
|
46 |
chromadb==0.5.15
|
47 |
-
pymilvus==2.
|
48 |
qdrant-client~=1.12.0
|
49 |
opensearch-py==2.7.1
|
50 |
|
51 |
-
sentence-transformers==3.
|
52 |
colbert-ai==0.2.21
|
53 |
einops==0.8.0
|
54 |
|
|
|
1 |
fastapi==0.111.0
|
2 |
uvicorn[standard]==0.30.6
|
3 |
pydantic==2.9.2
|
4 |
+
python-multipart==0.0.18
|
5 |
|
6 |
Flask==3.0.3
|
7 |
Flask-Cors==5.0.0
|
|
|
11 |
passlib[bcrypt]==1.7.4
|
12 |
|
13 |
requests==2.32.3
|
14 |
+
aiohttp==3.11.8
|
15 |
async-timeout
|
16 |
aiocache
|
17 |
aiofiles
|
18 |
|
19 |
sqlalchemy==2.0.32
|
20 |
+
alembic==1.14.0
|
21 |
peewee==3.17.6
|
22 |
peewee-migrate==1.12.2
|
23 |
psycopg2-binary==2.9.9
|
|
|
44 |
|
45 |
fake-useragent==1.5.1
|
46 |
chromadb==0.5.15
|
47 |
+
pymilvus==2.5.0
|
48 |
qdrant-client~=1.12.0
|
49 |
opensearch-py==2.7.1
|
50 |
|
51 |
+
sentence-transformers==3.3.1
|
52 |
colbert-ai==0.2.21
|
53 |
einops==0.8.0
|
54 |
|
package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
{
|
2 |
"name": "open-webui",
|
3 |
-
"version": "0.4.
|
4 |
"lockfileVersion": 3,
|
5 |
"requires": true,
|
6 |
"packages": {
|
7 |
"": {
|
8 |
"name": "open-webui",
|
9 |
-
"version": "0.4.
|
10 |
"dependencies": {
|
11 |
"@codemirror/lang-javascript": "^6.2.2",
|
12 |
"@codemirror/lang-python": "^6.1.6",
|
@@ -1836,9 +1836,10 @@
|
|
1836 |
}
|
1837 |
},
|
1838 |
"node_modules/@polka/url": {
|
1839 |
-
"version": "1.0.0-next.
|
1840 |
-
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.
|
1841 |
-
"integrity": "sha512-
|
|
|
1842 |
},
|
1843 |
"node_modules/@popperjs/core": {
|
1844 |
"version": "2.11.8",
|
@@ -2257,22 +2258,23 @@
|
|
2257 |
}
|
2258 |
},
|
2259 |
"node_modules/@sveltejs/kit": {
|
2260 |
-
"version": "2.
|
2261 |
-
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.
|
2262 |
-
"integrity": "sha512-
|
2263 |
"hasInstallScript": true,
|
|
|
2264 |
"dependencies": {
|
2265 |
"@types/cookie": "^0.6.0",
|
2266 |
-
"cookie": "^0.
|
2267 |
"devalue": "^5.1.0",
|
2268 |
-
"esm-env": "^1.
|
2269 |
"import-meta-resolve": "^4.1.0",
|
2270 |
"kleur": "^4.1.5",
|
2271 |
"magic-string": "^0.30.5",
|
2272 |
"mrmime": "^2.0.0",
|
2273 |
"sade": "^1.8.1",
|
2274 |
"set-cookie-parser": "^2.6.0",
|
2275 |
-
"sirv": "^
|
2276 |
"tiny-glob": "^0.2.9"
|
2277 |
},
|
2278 |
"bin": {
|
@@ -2282,9 +2284,9 @@
|
|
2282 |
"node": ">=18.13"
|
2283 |
},
|
2284 |
"peerDependencies": {
|
2285 |
-
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1",
|
2286 |
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
2287 |
-
"vite": "^5.0.3"
|
2288 |
}
|
2289 |
},
|
2290 |
"node_modules/@sveltejs/vite-plugin-svelte": {
|
@@ -4391,9 +4393,10 @@
|
|
4391 |
"dev": true
|
4392 |
},
|
4393 |
"node_modules/cookie": {
|
4394 |
-
"version": "0.
|
4395 |
-
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.
|
4396 |
-
"integrity": "sha512-
|
|
|
4397 |
"engines": {
|
4398 |
"node": ">= 0.6"
|
4399 |
}
|
@@ -5690,9 +5693,10 @@
|
|
5690 |
}
|
5691 |
},
|
5692 |
"node_modules/esm-env": {
|
5693 |
-
"version": "1.
|
5694 |
-
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.
|
5695 |
-
"integrity": "sha512-
|
|
|
5696 |
},
|
5697 |
"node_modules/espree": {
|
5698 |
"version": "9.6.1",
|
@@ -8228,6 +8232,7 @@
|
|
8228 |
"version": "2.0.0",
|
8229 |
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
8230 |
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
|
|
|
8231 |
"engines": {
|
8232 |
"node": ">=10"
|
8233 |
}
|
@@ -10359,16 +10364,17 @@
|
|
10359 |
}
|
10360 |
},
|
10361 |
"node_modules/sirv": {
|
10362 |
-
"version": "
|
10363 |
-
"resolved": "https://registry.npmjs.org/sirv/-/sirv-
|
10364 |
-
"integrity": "sha512-
|
|
|
10365 |
"dependencies": {
|
10366 |
"@polka/url": "^1.0.0-next.24",
|
10367 |
"mrmime": "^2.0.0",
|
10368 |
"totalist": "^3.0.0"
|
10369 |
},
|
10370 |
"engines": {
|
10371 |
-
"node": ">=
|
10372 |
}
|
10373 |
},
|
10374 |
"node_modules/slash": {
|
@@ -11260,6 +11266,7 @@
|
|
11260 |
"version": "3.0.1",
|
11261 |
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
11262 |
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
|
|
11263 |
"engines": {
|
11264 |
"node": ">=6"
|
11265 |
}
|
|
|
1 |
{
|
2 |
"name": "open-webui",
|
3 |
+
"version": "0.4.7",
|
4 |
"lockfileVersion": 3,
|
5 |
"requires": true,
|
6 |
"packages": {
|
7 |
"": {
|
8 |
"name": "open-webui",
|
9 |
+
"version": "0.4.7",
|
10 |
"dependencies": {
|
11 |
"@codemirror/lang-javascript": "^6.2.2",
|
12 |
"@codemirror/lang-python": "^6.1.6",
|
|
|
1836 |
}
|
1837 |
},
|
1838 |
"node_modules/@polka/url": {
|
1839 |
+
"version": "1.0.0-next.28",
|
1840 |
+
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
|
1841 |
+
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
|
1842 |
+
"license": "MIT"
|
1843 |
},
|
1844 |
"node_modules/@popperjs/core": {
|
1845 |
"version": "2.11.8",
|
|
|
2258 |
}
|
2259 |
},
|
2260 |
"node_modules/@sveltejs/kit": {
|
2261 |
+
"version": "2.9.0",
|
2262 |
+
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.9.0.tgz",
|
2263 |
+
"integrity": "sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==",
|
2264 |
"hasInstallScript": true,
|
2265 |
+
"license": "MIT",
|
2266 |
"dependencies": {
|
2267 |
"@types/cookie": "^0.6.0",
|
2268 |
+
"cookie": "^0.6.0",
|
2269 |
"devalue": "^5.1.0",
|
2270 |
+
"esm-env": "^1.2.1",
|
2271 |
"import-meta-resolve": "^4.1.0",
|
2272 |
"kleur": "^4.1.5",
|
2273 |
"magic-string": "^0.30.5",
|
2274 |
"mrmime": "^2.0.0",
|
2275 |
"sade": "^1.8.1",
|
2276 |
"set-cookie-parser": "^2.6.0",
|
2277 |
+
"sirv": "^3.0.0",
|
2278 |
"tiny-glob": "^0.2.9"
|
2279 |
},
|
2280 |
"bin": {
|
|
|
2284 |
"node": ">=18.13"
|
2285 |
},
|
2286 |
"peerDependencies": {
|
2287 |
+
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
|
2288 |
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
2289 |
+
"vite": "^5.0.3 || ^6.0.0"
|
2290 |
}
|
2291 |
},
|
2292 |
"node_modules/@sveltejs/vite-plugin-svelte": {
|
|
|
4393 |
"dev": true
|
4394 |
},
|
4395 |
"node_modules/cookie": {
|
4396 |
+
"version": "0.6.0",
|
4397 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
4398 |
+
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
4399 |
+
"license": "MIT",
|
4400 |
"engines": {
|
4401 |
"node": ">= 0.6"
|
4402 |
}
|
|
|
5693 |
}
|
5694 |
},
|
5695 |
"node_modules/esm-env": {
|
5696 |
+
"version": "1.2.1",
|
5697 |
+
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
|
5698 |
+
"integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
|
5699 |
+
"license": "MIT"
|
5700 |
},
|
5701 |
"node_modules/espree": {
|
5702 |
"version": "9.6.1",
|
|
|
8232 |
"version": "2.0.0",
|
8233 |
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
8234 |
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
|
8235 |
+
"license": "MIT",
|
8236 |
"engines": {
|
8237 |
"node": ">=10"
|
8238 |
}
|
|
|
10364 |
}
|
10365 |
},
|
10366 |
"node_modules/sirv": {
|
10367 |
+
"version": "3.0.0",
|
10368 |
+
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz",
|
10369 |
+
"integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==",
|
10370 |
+
"license": "MIT",
|
10371 |
"dependencies": {
|
10372 |
"@polka/url": "^1.0.0-next.24",
|
10373 |
"mrmime": "^2.0.0",
|
10374 |
"totalist": "^3.0.0"
|
10375 |
},
|
10376 |
"engines": {
|
10377 |
+
"node": ">=18"
|
10378 |
}
|
10379 |
},
|
10380 |
"node_modules/slash": {
|
|
|
11266 |
"version": "3.0.1",
|
11267 |
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
11268 |
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
11269 |
+
"license": "MIT",
|
11270 |
"engines": {
|
11271 |
"node": ">=6"
|
11272 |
}
|
package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
{
|
2 |
"name": "open-webui",
|
3 |
-
"version": "0.4.
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
"dev": "npm run pyodide:fetch && vite dev --host",
|
|
|
1 |
{
|
2 |
"name": "open-webui",
|
3 |
+
"version": "0.4.7",
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
"dev": "npm run pyodide:fetch && vite dev --host",
|
pyproject.toml
CHANGED
@@ -9,7 +9,7 @@ dependencies = [
|
|
9 |
"fastapi==0.111.0",
|
10 |
"uvicorn[standard]==0.30.6",
|
11 |
"pydantic==2.9.2",
|
12 |
-
"python-multipart==0.0.
|
13 |
|
14 |
"Flask==3.0.3",
|
15 |
"Flask-Cors==5.0.0",
|
@@ -19,13 +19,13 @@ dependencies = [
|
|
19 |
"passlib[bcrypt]==1.7.4",
|
20 |
|
21 |
"requests==2.32.3",
|
22 |
-
"aiohttp==3.
|
23 |
"async-timeout",
|
24 |
"aiocache",
|
25 |
"aiofiles",
|
26 |
|
27 |
"sqlalchemy==2.0.32",
|
28 |
-
"alembic==1.
|
29 |
"peewee==3.17.6",
|
30 |
"peewee-migrate==1.12.2",
|
31 |
"psycopg2-binary==2.9.9",
|
@@ -51,11 +51,11 @@ dependencies = [
|
|
51 |
|
52 |
"fake-useragent==1.5.1",
|
53 |
"chromadb==0.5.15",
|
54 |
-
"pymilvus==2.
|
55 |
"qdrant-client~=1.12.0",
|
56 |
"opensearch-py==2.7.1",
|
57 |
|
58 |
-
"sentence-transformers==3.
|
59 |
"colbert-ai==0.2.21",
|
60 |
"einops==0.8.0",
|
61 |
|
|
|
9 |
"fastapi==0.111.0",
|
10 |
"uvicorn[standard]==0.30.6",
|
11 |
"pydantic==2.9.2",
|
12 |
+
"python-multipart==0.0.18",
|
13 |
|
14 |
"Flask==3.0.3",
|
15 |
"Flask-Cors==5.0.0",
|
|
|
19 |
"passlib[bcrypt]==1.7.4",
|
20 |
|
21 |
"requests==2.32.3",
|
22 |
+
"aiohttp==3.11.8",
|
23 |
"async-timeout",
|
24 |
"aiocache",
|
25 |
"aiofiles",
|
26 |
|
27 |
"sqlalchemy==2.0.32",
|
28 |
+
"alembic==1.14.0",
|
29 |
"peewee==3.17.6",
|
30 |
"peewee-migrate==1.12.2",
|
31 |
"psycopg2-binary==2.9.9",
|
|
|
51 |
|
52 |
"fake-useragent==1.5.1",
|
53 |
"chromadb==0.5.15",
|
54 |
+
"pymilvus==2.5.0",
|
55 |
"qdrant-client~=1.12.0",
|
56 |
"opensearch-py==2.7.1",
|
57 |
|
58 |
+
"sentence-transformers==3.3.1",
|
59 |
"colbert-ai==0.2.21",
|
60 |
"einops==0.8.0",
|
61 |
|
src/app.css
CHANGED
@@ -45,15 +45,15 @@ math {
|
|
45 |
}
|
46 |
|
47 |
.input-prose {
|
48 |
-
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
49 |
}
|
50 |
|
51 |
.input-prose-sm {
|
52 |
-
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line text-sm;
|
53 |
}
|
54 |
|
55 |
.markdown-prose {
|
56 |
-
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
57 |
}
|
58 |
|
59 |
.markdown a {
|
@@ -211,7 +211,15 @@ input[type='number'] {
|
|
211 |
float: left;
|
212 |
color: #adb5bd;
|
213 |
pointer-events: none;
|
214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
}
|
216 |
|
217 |
.tiptap > pre > code {
|
@@ -231,7 +239,6 @@ input[type='number'] {
|
|
231 |
@apply dark:bg-gray-800 bg-gray-100;
|
232 |
}
|
233 |
|
234 |
-
|
235 |
.tiptap p code {
|
236 |
color: #eb5757;
|
237 |
border-width: 0px;
|
|
|
45 |
}
|
46 |
|
47 |
.input-prose {
|
48 |
+
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
49 |
}
|
50 |
|
51 |
.input-prose-sm {
|
52 |
+
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line text-sm;
|
53 |
}
|
54 |
|
55 |
.markdown-prose {
|
56 |
+
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
57 |
}
|
58 |
|
59 |
.markdown a {
|
|
|
211 |
float: left;
|
212 |
color: #adb5bd;
|
213 |
pointer-events: none;
|
214 |
+
|
215 |
+
@apply line-clamp-1 absolute
|
216 |
+
}
|
217 |
+
|
218 |
+
.ai-autocompletion::after {
|
219 |
+
color: #a0a0a0;
|
220 |
+
|
221 |
+
content: attr(data-suggestion);
|
222 |
+
pointer-events: none;
|
223 |
}
|
224 |
|
225 |
.tiptap > pre > code {
|
|
|
239 |
@apply dark:bg-gray-800 bg-gray-100;
|
240 |
}
|
241 |
|
|
|
242 |
.tiptap p code {
|
243 |
color: #eb5757;
|
244 |
border-width: 0px;
|
src/app.html
CHANGED
@@ -2,9 +2,12 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="utf-8" />
|
5 |
-
<link rel="icon" href="
|
6 |
-
<link rel="
|
7 |
-
<link rel="
|
|
|
|
|
|
|
8 |
<meta
|
9 |
name="viewport"
|
10 |
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
|
6 |
+
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
|
7 |
+
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
8 |
+
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
9 |
+
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
|
10 |
+
<link rel="manifest" href="/favicon/site.webmanifest" />
|
11 |
<meta
|
12 |
name="viewport"
|
13 |
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
src/lib/apis/configs/index.ts
CHANGED
@@ -58,17 +58,44 @@ export const exportConfig = async (token: string) => {
|
|
58 |
return res;
|
59 |
};
|
60 |
|
61 |
-
export const
|
62 |
let error = null;
|
63 |
|
64 |
-
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
method: 'POST',
|
66 |
headers: {
|
67 |
'Content-Type': 'application/json',
|
68 |
Authorization: `Bearer ${token}`
|
69 |
},
|
70 |
body: JSON.stringify({
|
71 |
-
|
72 |
})
|
73 |
})
|
74 |
.then(async (res) => {
|
@@ -91,7 +118,7 @@ export const setDefaultModels = async (token: string, models: string) => {
|
|
91 |
export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => {
|
92 |
let error = null;
|
93 |
|
94 |
-
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/
|
95 |
method: 'POST',
|
96 |
headers: {
|
97 |
'Content-Type': 'application/json',
|
|
|
58 |
return res;
|
59 |
};
|
60 |
|
61 |
+
export const getModelsConfig = async (token: string) => {
|
62 |
let error = null;
|
63 |
|
64 |
+
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models`, {
|
65 |
+
method: 'GET',
|
66 |
+
headers: {
|
67 |
+
'Content-Type': 'application/json',
|
68 |
+
Authorization: `Bearer ${token}`
|
69 |
+
}
|
70 |
+
})
|
71 |
+
.then(async (res) => {
|
72 |
+
if (!res.ok) throw await res.json();
|
73 |
+
return res.json();
|
74 |
+
})
|
75 |
+
.catch((err) => {
|
76 |
+
console.log(err);
|
77 |
+
error = err.detail;
|
78 |
+
return null;
|
79 |
+
});
|
80 |
+
|
81 |
+
if (error) {
|
82 |
+
throw error;
|
83 |
+
}
|
84 |
+
|
85 |
+
return res;
|
86 |
+
};
|
87 |
+
|
88 |
+
export const setModelsConfig = async (token: string, config: object) => {
|
89 |
+
let error = null;
|
90 |
+
|
91 |
+
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models`, {
|
92 |
method: 'POST',
|
93 |
headers: {
|
94 |
'Content-Type': 'application/json',
|
95 |
Authorization: `Bearer ${token}`
|
96 |
},
|
97 |
body: JSON.stringify({
|
98 |
+
...config
|
99 |
})
|
100 |
})
|
101 |
.then(async (res) => {
|
|
|
118 |
export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => {
|
119 |
let error = null;
|
120 |
|
121 |
+
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/suggestions`, {
|
122 |
method: 'POST',
|
123 |
headers: {
|
124 |
'Content-Type': 'application/json',
|
src/lib/apis/index.ts
CHANGED
@@ -25,26 +25,6 @@ export const getModels = async (token: string = '', base: boolean = false) => {
|
|
25 |
}
|
26 |
|
27 |
let models = res?.data ?? [];
|
28 |
-
models = models
|
29 |
-
.filter((models) => models)
|
30 |
-
// Sort the models
|
31 |
-
.sort((a, b) => {
|
32 |
-
// Compare case-insensitively by name for models without position property
|
33 |
-
const lowerA = a.name.toLowerCase();
|
34 |
-
const lowerB = b.name.toLowerCase();
|
35 |
-
|
36 |
-
if (lowerA < lowerB) return -1;
|
37 |
-
if (lowerA > lowerB) return 1;
|
38 |
-
|
39 |
-
// If same case-insensitively, sort by original strings,
|
40 |
-
// lowercase will come before uppercase due to ASCII values
|
41 |
-
if (a.name < b.name) return -1;
|
42 |
-
if (a.name > b.name) return 1;
|
43 |
-
|
44 |
-
return 0; // They are equal
|
45 |
-
});
|
46 |
-
|
47 |
-
console.log(models);
|
48 |
return models;
|
49 |
};
|
50 |
|
@@ -387,15 +367,13 @@ export const generateQueries = async (
|
|
387 |
throw error;
|
388 |
}
|
389 |
|
390 |
-
|
391 |
-
|
392 |
-
const response = res?.choices[0]?.message?.content ?? '';
|
393 |
|
394 |
-
|
395 |
const jsonStartIndex = response.indexOf('{');
|
396 |
const jsonEndIndex = response.lastIndexOf('}');
|
397 |
|
398 |
-
// Step 4: Check if we found a valid JSON block (with both `{` and `}`)
|
399 |
if (jsonStartIndex !== -1 && jsonEndIndex !== -1) {
|
400 |
const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1);
|
401 |
|
@@ -410,12 +388,83 @@ export const generateQueries = async (
|
|
410 |
}
|
411 |
}
|
412 |
|
413 |
-
// If no valid JSON block found, return
|
414 |
-
return [];
|
415 |
} catch (e) {
|
416 |
// Catch and safely return empty array on any parsing errors
|
417 |
console.error('Failed to parse response: ', e);
|
418 |
-
return [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
419 |
}
|
420 |
};
|
421 |
|
|
|
25 |
}
|
26 |
|
27 |
let models = res?.data ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
return models;
|
29 |
};
|
30 |
|
|
|
367 |
throw error;
|
368 |
}
|
369 |
|
370 |
+
// Step 1: Safely extract the response string
|
371 |
+
const response = res?.choices[0]?.message?.content ?? '';
|
|
|
372 |
|
373 |
+
try {
|
374 |
const jsonStartIndex = response.indexOf('{');
|
375 |
const jsonEndIndex = response.lastIndexOf('}');
|
376 |
|
|
|
377 |
if (jsonStartIndex !== -1 && jsonEndIndex !== -1) {
|
378 |
const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1);
|
379 |
|
|
|
388 |
}
|
389 |
}
|
390 |
|
391 |
+
// If no valid JSON block found, return response as is
|
392 |
+
return [response];
|
393 |
} catch (e) {
|
394 |
// Catch and safely return empty array on any parsing errors
|
395 |
console.error('Failed to parse response: ', e);
|
396 |
+
return [response];
|
397 |
+
}
|
398 |
+
};
|
399 |
+
|
400 |
+
export const generateAutoCompletion = async (
|
401 |
+
token: string = '',
|
402 |
+
model: string,
|
403 |
+
prompt: string,
|
404 |
+
messages?: object[],
|
405 |
+
type: string = 'search query'
|
406 |
+
) => {
|
407 |
+
const controller = new AbortController();
|
408 |
+
let error = null;
|
409 |
+
|
410 |
+
const res = await fetch(`${WEBUI_BASE_URL}/api/task/auto/completions`, {
|
411 |
+
signal: controller.signal,
|
412 |
+
method: 'POST',
|
413 |
+
headers: {
|
414 |
+
Accept: 'application/json',
|
415 |
+
'Content-Type': 'application/json',
|
416 |
+
Authorization: `Bearer ${token}`
|
417 |
+
},
|
418 |
+
body: JSON.stringify({
|
419 |
+
model: model,
|
420 |
+
prompt: prompt,
|
421 |
+
...(messages && { messages: messages }),
|
422 |
+
type: type,
|
423 |
+
stream: false
|
424 |
+
})
|
425 |
+
})
|
426 |
+
.then(async (res) => {
|
427 |
+
if (!res.ok) throw await res.json();
|
428 |
+
return res.json();
|
429 |
+
})
|
430 |
+
.catch((err) => {
|
431 |
+
console.log(err);
|
432 |
+
if ('detail' in err) {
|
433 |
+
error = err.detail;
|
434 |
+
}
|
435 |
+
return null;
|
436 |
+
});
|
437 |
+
|
438 |
+
if (error) {
|
439 |
+
throw error;
|
440 |
+
}
|
441 |
+
|
442 |
+
const response = res?.choices[0]?.message?.content ?? '';
|
443 |
+
|
444 |
+
try {
|
445 |
+
const jsonStartIndex = response.indexOf('{');
|
446 |
+
const jsonEndIndex = response.lastIndexOf('}');
|
447 |
+
|
448 |
+
if (jsonStartIndex !== -1 && jsonEndIndex !== -1) {
|
449 |
+
const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1);
|
450 |
+
|
451 |
+
// Step 5: Parse the JSON block
|
452 |
+
const parsed = JSON.parse(jsonResponse);
|
453 |
+
|
454 |
+
// Step 6: If there's a "queries" key, return the queries array; otherwise, return an empty array
|
455 |
+
if (parsed && parsed.text) {
|
456 |
+
return parsed.text;
|
457 |
+
} else {
|
458 |
+
return '';
|
459 |
+
}
|
460 |
+
}
|
461 |
+
|
462 |
+
// If no valid JSON block found, return response as is
|
463 |
+
return response;
|
464 |
+
} catch (e) {
|
465 |
+
// Catch and safely return empty array on any parsing errors
|
466 |
+
console.error('Failed to parse response: ', e);
|
467 |
+
return response;
|
468 |
}
|
469 |
};
|
470 |
|
src/lib/apis/retrieval/index.ts
CHANGED
@@ -40,6 +40,7 @@ type ContentExtractConfigForm = {
|
|
40 |
type YoutubeConfigForm = {
|
41 |
language: string[];
|
42 |
translation?: string | null;
|
|
|
43 |
};
|
44 |
|
45 |
type RAGConfigForm = {
|
|
|
40 |
type YoutubeConfigForm = {
|
41 |
language: string[];
|
42 |
translation?: string | null;
|
43 |
+
proxy_url: string;
|
44 |
};
|
45 |
|
46 |
type RAGConfigForm = {
|
src/lib/components/admin/Settings/Evaluations.svelte
CHANGED
@@ -5,6 +5,7 @@
|
|
5 |
|
6 |
const dispatch = createEventDispatcher();
|
7 |
import { getModels } from '$lib/apis';
|
|
|
8 |
|
9 |
import Switch from '$lib/components/common/Switch.svelte';
|
10 |
import Spinner from '$lib/components/common/Spinner.svelte';
|
@@ -12,7 +13,6 @@
|
|
12 |
import Plus from '$lib/components/icons/Plus.svelte';
|
13 |
import Model from './Evaluations/Model.svelte';
|
14 |
import ArenaModelModal from './Evaluations/ArenaModelModal.svelte';
|
15 |
-
import { getConfig, updateConfig } from '$lib/apis/evaluations';
|
16 |
|
17 |
const i18n = getContext('i18n');
|
18 |
|
@@ -27,6 +27,7 @@
|
|
27 |
|
28 |
if (config) {
|
29 |
toast.success('Settings saved successfully');
|
|
|
30 |
}
|
31 |
};
|
32 |
|
|
|
5 |
|
6 |
const dispatch = createEventDispatcher();
|
7 |
import { getModels } from '$lib/apis';
|
8 |
+
import { getConfig, updateConfig } from '$lib/apis/evaluations';
|
9 |
|
10 |
import Switch from '$lib/components/common/Switch.svelte';
|
11 |
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
|
13 |
import Plus from '$lib/components/icons/Plus.svelte';
|
14 |
import Model from './Evaluations/Model.svelte';
|
15 |
import ArenaModelModal from './Evaluations/ArenaModelModal.svelte';
|
|
|
16 |
|
17 |
const i18n = getContext('i18n');
|
18 |
|
|
|
27 |
|
28 |
if (config) {
|
29 |
toast.success('Settings saved successfully');
|
30 |
+
models.set(await getModels(localStorage.token));
|
31 |
}
|
32 |
};
|
33 |
|
src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte
CHANGED
@@ -375,7 +375,7 @@
|
|
375 |
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
376 |
{#if edit}
|
377 |
<button
|
378 |
-
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-
|
379 |
type="button"
|
380 |
on:click={() => {
|
381 |
dispatch('delete', model);
|
@@ -387,7 +387,7 @@
|
|
387 |
{/if}
|
388 |
|
389 |
<button
|
390 |
-
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-
|
391 |
? ' cursor-not-allowed'
|
392 |
: ''}"
|
393 |
type="submit"
|
|
|
375 |
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
376 |
{#if edit}
|
377 |
<button
|
378 |
+
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-950 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
379 |
type="button"
|
380 |
on:click={() => {
|
381 |
dispatch('delete', model);
|
|
|
387 |
{/if}
|
388 |
|
389 |
<button
|
390 |
+
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-950 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
391 |
? ' cursor-not-allowed'
|
392 |
: ''}"
|
393 |
type="submit"
|
src/lib/components/admin/Settings/Interface.svelte
CHANGED
@@ -24,6 +24,8 @@
|
|
24 |
TASK_MODEL: '',
|
25 |
TASK_MODEL_EXTERNAL: '',
|
26 |
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
|
|
|
|
27 |
TAGS_GENERATION_PROMPT_TEMPLATE: '',
|
28 |
ENABLE_TAGS_GENERATION: true,
|
29 |
ENABLE_SEARCH_QUERY_GENERATION: true,
|
@@ -55,255 +57,207 @@
|
|
55 |
};
|
56 |
</script>
|
57 |
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
<div>
|
67 |
-
<div
|
68 |
-
<div class="
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
<svg
|
75 |
-
xmlns="http://www.w3.org/2000/svg"
|
76 |
-
fill="none"
|
77 |
-
viewBox="0 0 24 24"
|
78 |
-
stroke-width="1.5"
|
79 |
-
stroke="currentColor"
|
80 |
-
class="size-3.5"
|
81 |
-
>
|
82 |
-
<path
|
83 |
-
stroke-linecap="round"
|
84 |
-
stroke-linejoin="round"
|
85 |
-
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"
|
86 |
-
/>
|
87 |
-
</svg>
|
88 |
-
</Tooltip>
|
89 |
-
</div>
|
90 |
-
<div class="flex w-full gap-2">
|
91 |
-
<div class="flex-1">
|
92 |
-
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
93 |
-
<select
|
94 |
-
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
95 |
-
bind:value={taskConfig.TASK_MODEL}
|
96 |
-
placeholder={$i18n.t('Select a model')}
|
97 |
-
>
|
98 |
-
<option value="" selected>{$i18n.t('Current Model')}</option>
|
99 |
-
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
|
100 |
-
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
101 |
-
{model.name}
|
102 |
-
</option>
|
103 |
-
{/each}
|
104 |
-
</select>
|
105 |
-
</div>
|
106 |
-
|
107 |
-
<div class="flex-1">
|
108 |
-
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
109 |
-
<select
|
110 |
-
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
111 |
-
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
112 |
-
placeholder={$i18n.t('Select a model')}
|
113 |
>
|
114 |
-
<
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
</div>
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
<hr class=" dark:border-gray-850 my-3" />
|
139 |
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
</div>
|
144 |
|
145 |
-
<Switch bind:state={taskConfig.ENABLE_TAGS_GENERATION} />
|
146 |
-
</div>
|
147 |
-
|
148 |
-
{#if taskConfig.ENABLE_TAGS_GENERATION}
|
149 |
<div class="mt-3">
|
150 |
-
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('
|
151 |
|
152 |
<Tooltip
|
153 |
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
154 |
placement="top-start"
|
155 |
>
|
156 |
<Textarea
|
157 |
-
bind:value={taskConfig.
|
158 |
placeholder={$i18n.t(
|
159 |
'Leave empty to use the default prompt, or enter a custom prompt'
|
160 |
)}
|
161 |
/>
|
162 |
</Tooltip>
|
163 |
</div>
|
164 |
-
{/if}
|
165 |
-
|
166 |
-
<hr class=" dark:border-gray-850 my-3" />
|
167 |
|
168 |
-
|
169 |
-
<div class=" self-center text-xs font-medium">
|
170 |
-
{$i18n.t('Enable Retrieval Query Generation')}
|
171 |
-
</div>
|
172 |
|
173 |
-
<
|
174 |
-
|
|
|
|
|
175 |
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
</div>
|
180 |
|
181 |
-
|
182 |
-
|
|
|
|
|
|
|
183 |
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
</div>
|
197 |
-
</div>
|
198 |
|
199 |
-
|
|
|
|
|
|
|
|
|
|
|
200 |
|
201 |
-
|
202 |
-
<div class="flex w-full justify-between">
|
203 |
-
<div class=" self-center text-sm font-semibold">
|
204 |
-
{$i18n.t('Banners')}
|
205 |
</div>
|
206 |
|
207 |
-
|
208 |
-
class="
|
209 |
-
|
210 |
-
on:click={() => {
|
211 |
-
if (banners.length === 0 || banners.at(-1).content !== '') {
|
212 |
-
banners = [
|
213 |
-
...banners,
|
214 |
-
{
|
215 |
-
id: uuidv4(),
|
216 |
-
type: '',
|
217 |
-
title: '',
|
218 |
-
content: '',
|
219 |
-
dismissible: true,
|
220 |
-
timestamp: Math.floor(Date.now() / 1000)
|
221 |
-
}
|
222 |
-
];
|
223 |
-
}
|
224 |
-
}}
|
225 |
-
>
|
226 |
-
<svg
|
227 |
-
xmlns="http://www.w3.org/2000/svg"
|
228 |
-
viewBox="0 0 20 20"
|
229 |
-
fill="currentColor"
|
230 |
-
class="w-4 h-4"
|
231 |
-
>
|
232 |
-
<path
|
233 |
-
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"
|
234 |
-
/>
|
235 |
-
</svg>
|
236 |
-
</button>
|
237 |
-
</div>
|
238 |
-
<div class="flex flex-col space-y-1">
|
239 |
-
{#each banners as banner, bannerIdx}
|
240 |
-
<div class=" flex justify-between">
|
241 |
-
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
242 |
-
<select
|
243 |
-
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
244 |
-
bind:value={banner.type}
|
245 |
-
required
|
246 |
-
>
|
247 |
-
{#if banner.type == ''}
|
248 |
-
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
|
249 |
-
>
|
250 |
-
{/if}
|
251 |
-
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
252 |
-
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
253 |
-
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
254 |
-
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
255 |
-
</select>
|
256 |
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
|
|
|
|
|
|
|
|
|
|
261 |
/>
|
|
|
|
|
|
|
262 |
|
263 |
-
|
264 |
-
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
265 |
-
<Switch bind:state={banner.dismissible} />
|
266 |
-
</Tooltip>
|
267 |
-
</div>
|
268 |
-
</div>
|
269 |
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
on:click={() => {
|
274 |
-
banners.splice(bannerIdx, 1);
|
275 |
-
banners = banners;
|
276 |
-
}}
|
277 |
-
>
|
278 |
-
<svg
|
279 |
-
xmlns="http://www.w3.org/2000/svg"
|
280 |
-
viewBox="0 0 20 20"
|
281 |
-
fill="currentColor"
|
282 |
-
class="w-4 h-4"
|
283 |
-
>
|
284 |
-
<path
|
285 |
-
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"
|
286 |
-
/>
|
287 |
-
</svg>
|
288 |
-
</button>
|
289 |
</div>
|
290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
291 |
</div>
|
292 |
-
</div>
|
293 |
|
294 |
-
|
295 |
-
|
296 |
-
|
|
|
297 |
<div class=" self-center text-sm font-semibold">
|
298 |
-
{$i18n.t('
|
299 |
</div>
|
300 |
|
301 |
<button
|
302 |
class="p-1 px-3 text-xs flex rounded transition"
|
303 |
type="button"
|
304 |
on:click={() => {
|
305 |
-
if (
|
306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
307 |
}
|
308 |
}}
|
309 |
>
|
@@ -319,40 +273,45 @@
|
|
319 |
</svg>
|
320 |
</button>
|
321 |
</div>
|
322 |
-
<div class="
|
323 |
-
{#each
|
324 |
-
<div
|
325 |
-
class=" flex border
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
/>
|
340 |
</div>
|
341 |
-
|
342 |
-
<textarea
|
343 |
-
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
|
344 |
-
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
345 |
-
rows="3"
|
346 |
-
bind:value={prompt.content}
|
347 |
-
/>
|
348 |
</div>
|
349 |
|
350 |
<button
|
351 |
-
class="px-
|
352 |
type="button"
|
353 |
on:click={() => {
|
354 |
-
|
355 |
-
|
356 |
}}
|
357 |
>
|
358 |
<svg
|
@@ -369,22 +328,103 @@
|
|
369 |
</div>
|
370 |
{/each}
|
371 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
372 |
|
373 |
-
|
374 |
-
|
375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
376 |
</div>
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
TASK_MODEL: '',
|
25 |
TASK_MODEL_EXTERNAL: '',
|
26 |
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
27 |
+
ENABLE_AUTOCOMPLETE_GENERATION: true,
|
28 |
+
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: -1,
|
29 |
TAGS_GENERATION_PROMPT_TEMPLATE: '',
|
30 |
ENABLE_TAGS_GENERATION: true,
|
31 |
ENABLE_SEARCH_QUERY_GENERATION: true,
|
|
|
57 |
};
|
58 |
</script>
|
59 |
|
60 |
+
{#if taskConfig}
|
61 |
+
<form
|
62 |
+
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
63 |
+
on:submit|preventDefault={() => {
|
64 |
+
updateInterfaceHandler();
|
65 |
+
dispatch('save');
|
66 |
+
}}
|
67 |
+
>
|
68 |
+
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
69 |
+
<div>
|
70 |
+
<div class=" mb-2.5 text-sm font-medium flex items-center">
|
71 |
+
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
72 |
+
<Tooltip
|
73 |
+
content={$i18n.t(
|
74 |
+
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
75 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
>
|
77 |
+
<svg
|
78 |
+
xmlns="http://www.w3.org/2000/svg"
|
79 |
+
fill="none"
|
80 |
+
viewBox="0 0 24 24"
|
81 |
+
stroke-width="1.5"
|
82 |
+
stroke="currentColor"
|
83 |
+
class="size-3.5"
|
84 |
+
>
|
85 |
+
<path
|
86 |
+
stroke-linecap="round"
|
87 |
+
stroke-linejoin="round"
|
88 |
+
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"
|
89 |
+
/>
|
90 |
+
</svg>
|
91 |
+
</Tooltip>
|
92 |
</div>
|
93 |
+
<div class="flex w-full gap-2">
|
94 |
+
<div class="flex-1">
|
95 |
+
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
96 |
+
<select
|
97 |
+
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
98 |
+
bind:value={taskConfig.TASK_MODEL}
|
99 |
+
placeholder={$i18n.t('Select a model')}
|
100 |
+
>
|
101 |
+
<option value="" selected>{$i18n.t('Current Model')}</option>
|
102 |
+
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
|
103 |
+
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
104 |
+
{model.name}
|
105 |
+
</option>
|
106 |
+
{/each}
|
107 |
+
</select>
|
108 |
+
</div>
|
|
|
109 |
|
110 |
+
<div class="flex-1">
|
111 |
+
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
112 |
+
<select
|
113 |
+
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
114 |
+
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
115 |
+
placeholder={$i18n.t('Select a model')}
|
116 |
+
>
|
117 |
+
<option value="" selected>{$i18n.t('Current Model')}</option>
|
118 |
+
{#each $models as model}
|
119 |
+
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
120 |
+
{model.name}
|
121 |
+
</option>
|
122 |
+
{/each}
|
123 |
+
</select>
|
124 |
+
</div>
|
125 |
</div>
|
126 |
|
|
|
|
|
|
|
|
|
127 |
<div class="mt-3">
|
128 |
+
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
129 |
|
130 |
<Tooltip
|
131 |
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
132 |
placement="top-start"
|
133 |
>
|
134 |
<Textarea
|
135 |
+
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
136 |
placeholder={$i18n.t(
|
137 |
'Leave empty to use the default prompt, or enter a custom prompt'
|
138 |
)}
|
139 |
/>
|
140 |
</Tooltip>
|
141 |
</div>
|
|
|
|
|
|
|
142 |
|
143 |
+
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
|
|
|
|
|
|
144 |
|
145 |
+
<div class="my-3 flex w-full items-center justify-between">
|
146 |
+
<div class=" self-center text-xs font-medium">
|
147 |
+
{$i18n.t('Autocomplete Generation')}
|
148 |
+
</div>
|
149 |
|
150 |
+
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
|
151 |
+
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
|
152 |
+
</Tooltip>
|
153 |
</div>
|
154 |
|
155 |
+
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
|
156 |
+
<div class="mt-3">
|
157 |
+
<div class=" mb-2.5 text-xs font-medium">
|
158 |
+
{$i18n.t('Autocomplete Generation Input Max Length')}
|
159 |
+
</div>
|
160 |
|
161 |
+
<Tooltip
|
162 |
+
content={$i18n.t('Character limit for autocomplete generation input')}
|
163 |
+
placement="top-start"
|
164 |
+
>
|
165 |
+
<input
|
166 |
+
class="w-full outline-none bg-transparent"
|
167 |
+
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
|
168 |
+
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
|
169 |
+
/>
|
170 |
+
</Tooltip>
|
171 |
+
</div>
|
172 |
+
{/if}
|
|
|
|
|
173 |
|
174 |
+
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
175 |
+
|
176 |
+
<div class="my-3 flex w-full items-center justify-between">
|
177 |
+
<div class=" self-center text-xs font-medium">
|
178 |
+
{$i18n.t('Tags Generation')}
|
179 |
+
</div>
|
180 |
|
181 |
+
<Switch bind:state={taskConfig.ENABLE_TAGS_GENERATION} />
|
|
|
|
|
|
|
182 |
</div>
|
183 |
|
184 |
+
{#if taskConfig.ENABLE_TAGS_GENERATION}
|
185 |
+
<div class="mt-3">
|
186 |
+
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
|
188 |
+
<Tooltip
|
189 |
+
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
190 |
+
placement="top-start"
|
191 |
+
>
|
192 |
+
<Textarea
|
193 |
+
bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
|
194 |
+
placeholder={$i18n.t(
|
195 |
+
'Leave empty to use the default prompt, or enter a custom prompt'
|
196 |
+
)}
|
197 |
/>
|
198 |
+
</Tooltip>
|
199 |
+
</div>
|
200 |
+
{/if}
|
201 |
|
202 |
+
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
|
|
|
|
|
|
|
|
|
|
203 |
|
204 |
+
<div class="my-3 flex w-full items-center justify-between">
|
205 |
+
<div class=" self-center text-xs font-medium">
|
206 |
+
{$i18n.t('Retrieval Query Generation')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
</div>
|
208 |
+
|
209 |
+
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
|
210 |
+
</div>
|
211 |
+
|
212 |
+
<div class="my-3 flex w-full items-center justify-between">
|
213 |
+
<div class=" self-center text-xs font-medium">
|
214 |
+
{$i18n.t('Web Search Query Generation')}
|
215 |
+
</div>
|
216 |
+
|
217 |
+
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
|
218 |
+
</div>
|
219 |
+
|
220 |
+
<div class="">
|
221 |
+
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
|
222 |
+
|
223 |
+
<Tooltip
|
224 |
+
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
225 |
+
placement="top-start"
|
226 |
+
>
|
227 |
+
<Textarea
|
228 |
+
bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE}
|
229 |
+
placeholder={$i18n.t(
|
230 |
+
'Leave empty to use the default prompt, or enter a custom prompt'
|
231 |
+
)}
|
232 |
+
/>
|
233 |
+
</Tooltip>
|
234 |
+
</div>
|
235 |
</div>
|
|
|
236 |
|
237 |
+
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
238 |
+
|
239 |
+
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
|
240 |
+
<div class="flex w-full justify-between">
|
241 |
<div class=" self-center text-sm font-semibold">
|
242 |
+
{$i18n.t('Banners')}
|
243 |
</div>
|
244 |
|
245 |
<button
|
246 |
class="p-1 px-3 text-xs flex rounded transition"
|
247 |
type="button"
|
248 |
on:click={() => {
|
249 |
+
if (banners.length === 0 || banners.at(-1).content !== '') {
|
250 |
+
banners = [
|
251 |
+
...banners,
|
252 |
+
{
|
253 |
+
id: uuidv4(),
|
254 |
+
type: '',
|
255 |
+
title: '',
|
256 |
+
content: '',
|
257 |
+
dismissible: true,
|
258 |
+
timestamp: Math.floor(Date.now() / 1000)
|
259 |
+
}
|
260 |
+
];
|
261 |
}
|
262 |
}}
|
263 |
>
|
|
|
273 |
</svg>
|
274 |
</button>
|
275 |
</div>
|
276 |
+
<div class="flex flex-col space-y-1">
|
277 |
+
{#each banners as banner, bannerIdx}
|
278 |
+
<div class=" flex justify-between">
|
279 |
+
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
280 |
+
<select
|
281 |
+
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
282 |
+
bind:value={banner.type}
|
283 |
+
required
|
284 |
+
>
|
285 |
+
{#if banner.type == ''}
|
286 |
+
<option value="" selected disabled class="text-gray-900"
|
287 |
+
>{$i18n.t('Type')}</option
|
288 |
+
>
|
289 |
+
{/if}
|
290 |
+
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
291 |
+
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
292 |
+
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
293 |
+
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
294 |
+
</select>
|
295 |
+
|
296 |
+
<input
|
297 |
+
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
298 |
+
placeholder={$i18n.t('Content')}
|
299 |
+
bind:value={banner.content}
|
300 |
+
/>
|
301 |
|
302 |
+
<div class="relative top-1.5 -left-2">
|
303 |
+
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
304 |
+
<Switch bind:state={banner.dismissible} />
|
305 |
+
</Tooltip>
|
|
|
306 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
307 |
</div>
|
308 |
|
309 |
<button
|
310 |
+
class="px-2"
|
311 |
type="button"
|
312 |
on:click={() => {
|
313 |
+
banners.splice(bannerIdx, 1);
|
314 |
+
banners = banners;
|
315 |
}}
|
316 |
>
|
317 |
<svg
|
|
|
328 |
</div>
|
329 |
{/each}
|
330 |
</div>
|
331 |
+
</div>
|
332 |
+
|
333 |
+
{#if $user.role === 'admin'}
|
334 |
+
<div class=" space-y-3">
|
335 |
+
<div class="flex w-full justify-between mb-2">
|
336 |
+
<div class=" self-center text-sm font-semibold">
|
337 |
+
{$i18n.t('Default Prompt Suggestions')}
|
338 |
+
</div>
|
339 |
|
340 |
+
<button
|
341 |
+
class="p-1 px-3 text-xs flex rounded transition"
|
342 |
+
type="button"
|
343 |
+
on:click={() => {
|
344 |
+
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
345 |
+
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
346 |
+
}
|
347 |
+
}}
|
348 |
+
>
|
349 |
+
<svg
|
350 |
+
xmlns="http://www.w3.org/2000/svg"
|
351 |
+
viewBox="0 0 20 20"
|
352 |
+
fill="currentColor"
|
353 |
+
class="w-4 h-4"
|
354 |
+
>
|
355 |
+
<path
|
356 |
+
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"
|
357 |
+
/>
|
358 |
+
</svg>
|
359 |
+
</button>
|
360 |
</div>
|
361 |
+
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
362 |
+
{#each promptSuggestions as prompt, promptIdx}
|
363 |
+
<div
|
364 |
+
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
|
365 |
+
>
|
366 |
+
<div class="flex flex-col flex-1 pl-1">
|
367 |
+
<div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
|
368 |
+
<input
|
369 |
+
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
370 |
+
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
371 |
+
bind:value={prompt.title[0]}
|
372 |
+
/>
|
373 |
+
|
374 |
+
<input
|
375 |
+
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
376 |
+
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
377 |
+
bind:value={prompt.title[1]}
|
378 |
+
/>
|
379 |
+
</div>
|
380 |
+
|
381 |
+
<textarea
|
382 |
+
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
|
383 |
+
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
384 |
+
rows="3"
|
385 |
+
bind:value={prompt.content}
|
386 |
+
/>
|
387 |
+
</div>
|
388 |
+
|
389 |
+
<button
|
390 |
+
class="px-3"
|
391 |
+
type="button"
|
392 |
+
on:click={() => {
|
393 |
+
promptSuggestions.splice(promptIdx, 1);
|
394 |
+
promptSuggestions = promptSuggestions;
|
395 |
+
}}
|
396 |
+
>
|
397 |
+
<svg
|
398 |
+
xmlns="http://www.w3.org/2000/svg"
|
399 |
+
viewBox="0 0 20 20"
|
400 |
+
fill="currentColor"
|
401 |
+
class="w-4 h-4"
|
402 |
+
>
|
403 |
+
<path
|
404 |
+
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"
|
405 |
+
/>
|
406 |
+
</svg>
|
407 |
+
</button>
|
408 |
+
</div>
|
409 |
+
{/each}
|
410 |
+
</div>
|
411 |
+
|
412 |
+
{#if promptSuggestions.length > 0}
|
413 |
+
<div class="text-xs text-left w-full mt-2">
|
414 |
+
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
415 |
+
</div>
|
416 |
+
{/if}
|
417 |
+
</div>
|
418 |
+
{/if}
|
419 |
+
</div>
|
420 |
+
|
421 |
+
<div class="flex justify-end text-sm font-medium">
|
422 |
+
<button
|
423 |
+
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
424 |
+
type="submit"
|
425 |
+
>
|
426 |
+
{$i18n.t('Save')}
|
427 |
+
</button>
|
428 |
+
</div>
|
429 |
+
</form>
|
430 |
+
{/if}
|
src/lib/components/admin/Settings/Models.svelte
CHANGED
@@ -24,6 +24,8 @@
|
|
24 |
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
25 |
import { toast } from 'svelte-sonner';
|
26 |
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
|
|
|
27 |
|
28 |
let importFiles;
|
29 |
let modelsImportInputElement: HTMLInputElement;
|
@@ -35,12 +37,20 @@
|
|
35 |
|
36 |
let filteredModels = [];
|
37 |
let selectedModelId = null;
|
38 |
-
|
|
|
39 |
|
40 |
$: if (models) {
|
41 |
-
filteredModels = models
|
42 |
-
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}
|
45 |
|
46 |
let searchValue = '';
|
@@ -114,12 +124,11 @@
|
|
114 |
}).catch((error) => {
|
115 |
return null;
|
116 |
});
|
117 |
-
|
118 |
-
await init();
|
119 |
} else {
|
120 |
await toggleModelById(localStorage.token, model.id);
|
121 |
}
|
122 |
|
|
|
123 |
_models.set(await getModels(localStorage.token));
|
124 |
};
|
125 |
|
@@ -128,18 +137,7 @@
|
|
128 |
});
|
129 |
</script>
|
130 |
|
131 |
-
<
|
132 |
-
title={$i18n.t('Delete All Models')}
|
133 |
-
message={$i18n.t('This will delete all models including custom models and cannot be undone.')}
|
134 |
-
bind:show={showResetModal}
|
135 |
-
onConfirm={async () => {
|
136 |
-
const res = deleteAllModels(localStorage.token);
|
137 |
-
if (res) {
|
138 |
-
toast.success($i18n.t('All models deleted successfully'));
|
139 |
-
init();
|
140 |
-
}
|
141 |
-
}}
|
142 |
-
/>
|
143 |
|
144 |
{#if models !== null}
|
145 |
{#if selectedModelId === null}
|
@@ -154,17 +152,15 @@
|
|
154 |
</div>
|
155 |
|
156 |
<div>
|
157 |
-
<Tooltip content={$i18n.t('
|
158 |
<button
|
159 |
class=" px-2.5 py-1 rounded-full flex gap-1 items-center"
|
160 |
type="button"
|
161 |
on:click={() => {
|
162 |
-
|
163 |
}}
|
164 |
>
|
165 |
-
<
|
166 |
-
{$i18n.t('Reset')}
|
167 |
-
</div>
|
168 |
</button>
|
169 |
</Tooltip>
|
170 |
</div>
|
@@ -186,7 +182,7 @@
|
|
186 |
|
187 |
<div class=" my-2 mb-5" id="model-list">
|
188 |
{#if models.length > 0}
|
189 |
-
{#each filteredModels as model, modelIdx (
|
190 |
<div
|
191 |
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
|
192 |
id="model-item-{model.id}"
|
|
|
24 |
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
|
25 |
import { toast } from 'svelte-sonner';
|
26 |
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
27 |
+
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
28 |
+
import ConfigureModelsModal from './Models/ConfigureModelsModal.svelte';
|
29 |
|
30 |
let importFiles;
|
31 |
let modelsImportInputElement: HTMLInputElement;
|
|
|
37 |
|
38 |
let filteredModels = [];
|
39 |
let selectedModelId = null;
|
40 |
+
|
41 |
+
let showConfigModal = false;
|
42 |
|
43 |
$: if (models) {
|
44 |
+
filteredModels = models
|
45 |
+
.filter((m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase()))
|
46 |
+
.sort((a, b) => {
|
47 |
+
// // Check if either model is inactive and push them to the bottom
|
48 |
+
// if ((a.is_active ?? true) !== (b.is_active ?? true)) {
|
49 |
+
// return (b.is_active ?? true) - (a.is_active ?? true);
|
50 |
+
// }
|
51 |
+
// If both models' active states are the same, sort alphabetically
|
52 |
+
return a.name.localeCompare(b.name);
|
53 |
+
});
|
54 |
}
|
55 |
|
56 |
let searchValue = '';
|
|
|
124 |
}).catch((error) => {
|
125 |
return null;
|
126 |
});
|
|
|
|
|
127 |
} else {
|
128 |
await toggleModelById(localStorage.token, model.id);
|
129 |
}
|
130 |
|
131 |
+
await init();
|
132 |
_models.set(await getModels(localStorage.token));
|
133 |
};
|
134 |
|
|
|
137 |
});
|
138 |
</script>
|
139 |
|
140 |
+
<ConfigureModelsModal bind:show={showConfigModal} {init} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
|
142 |
{#if models !== null}
|
143 |
{#if selectedModelId === null}
|
|
|
152 |
</div>
|
153 |
|
154 |
<div>
|
155 |
+
<Tooltip content={$i18n.t('Configure')}>
|
156 |
<button
|
157 |
class=" px-2.5 py-1 rounded-full flex gap-1 items-center"
|
158 |
type="button"
|
159 |
on:click={() => {
|
160 |
+
showConfigModal = true;
|
161 |
}}
|
162 |
>
|
163 |
+
<Cog6 />
|
|
|
|
|
164 |
</button>
|
165 |
</Tooltip>
|
166 |
</div>
|
|
|
182 |
|
183 |
<div class=" my-2 mb-5" id="model-list">
|
184 |
{#if models.length > 0}
|
185 |
+
{#each filteredModels as model, modelIdx (model.id)}
|
186 |
<div
|
187 |
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
|
188 |
id="model-item-{model.id}"
|
src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte
ADDED
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script>
|
2 |
+
import { toast } from 'svelte-sonner';
|
3 |
+
|
4 |
+
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
5 |
+
const i18n = getContext('i18n');
|
6 |
+
const dispatch = createEventDispatcher();
|
7 |
+
|
8 |
+
import { models } from '$lib/stores';
|
9 |
+
import { deleteAllModels } from '$lib/apis/models';
|
10 |
+
|
11 |
+
import Modal from '$lib/components/common/Modal.svelte';
|
12 |
+
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
13 |
+
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
14 |
+
import ModelList from './ModelList.svelte';
|
15 |
+
import { getModelsConfig, setModelsConfig } from '$lib/apis/configs';
|
16 |
+
import Spinner from '$lib/components/common/Spinner.svelte';
|
17 |
+
import Minus from '$lib/components/icons/Minus.svelte';
|
18 |
+
import Plus from '$lib/components/icons/Plus.svelte';
|
19 |
+
|
20 |
+
export let show = false;
|
21 |
+
export let init = () => {};
|
22 |
+
|
23 |
+
let config = null;
|
24 |
+
|
25 |
+
let selectedModelId = '';
|
26 |
+
let defaultModelIds = [];
|
27 |
+
let modelIds = [];
|
28 |
+
|
29 |
+
let loading = false;
|
30 |
+
let showResetModal = false;
|
31 |
+
|
32 |
+
const submitHandler = async () => {
|
33 |
+
loading = true;
|
34 |
+
|
35 |
+
const res = await setModelsConfig(localStorage.token, {
|
36 |
+
DEFAULT_MODELS: defaultModelIds.join(','),
|
37 |
+
MODEL_ORDER_LIST: modelIds
|
38 |
+
});
|
39 |
+
|
40 |
+
if (res) {
|
41 |
+
toast.success($i18n.t('Models configuration saved successfully'));
|
42 |
+
init();
|
43 |
+
show = false;
|
44 |
+
} else {
|
45 |
+
toast.error($i18n.t('Failed to save models configuration'));
|
46 |
+
}
|
47 |
+
|
48 |
+
loading = false;
|
49 |
+
};
|
50 |
+
|
51 |
+
onMount(async () => {
|
52 |
+
config = await getModelsConfig(localStorage.token);
|
53 |
+
|
54 |
+
if (config?.DEFAULT_MODELS) {
|
55 |
+
defaultModelIds = (config?.DEFAULT_MODELS).split(',').filter((id) => id);
|
56 |
+
} else {
|
57 |
+
defaultModelIds = [];
|
58 |
+
}
|
59 |
+
const modelOrderList = config.MODEL_ORDER_LIST || [];
|
60 |
+
const allModelIds = $models.map((model) => model.id);
|
61 |
+
|
62 |
+
// Create a Set for quick lookup of ordered IDs
|
63 |
+
const orderedSet = new Set(modelOrderList);
|
64 |
+
|
65 |
+
modelIds = [
|
66 |
+
// Add all IDs from MODEL_ORDER_LIST that exist in allModelIds
|
67 |
+
...modelOrderList.filter((id) => orderedSet.has(id) && allModelIds.includes(id)),
|
68 |
+
// Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically
|
69 |
+
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
|
70 |
+
];
|
71 |
+
});
|
72 |
+
</script>
|
73 |
+
|
74 |
+
<ConfirmDialog
|
75 |
+
title={$i18n.t('Reset All Models')}
|
76 |
+
message={$i18n.t('This will delete all models including custom models and cannot be undone.')}
|
77 |
+
bind:show={showResetModal}
|
78 |
+
onConfirm={async () => {
|
79 |
+
const res = deleteAllModels(localStorage.token);
|
80 |
+
if (res) {
|
81 |
+
toast.success($i18n.t('All models deleted successfully'));
|
82 |
+
init();
|
83 |
+
}
|
84 |
+
}}
|
85 |
+
/>
|
86 |
+
|
87 |
+
<Modal size="sm" bind:show>
|
88 |
+
<div>
|
89 |
+
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
|
90 |
+
<div class=" text-lg font-medium self-center font-primary">
|
91 |
+
{$i18n.t('Configure Models')}
|
92 |
+
</div>
|
93 |
+
<button
|
94 |
+
class="self-center"
|
95 |
+
on:click={() => {
|
96 |
+
show = false;
|
97 |
+
}}
|
98 |
+
>
|
99 |
+
<svg
|
100 |
+
xmlns="http://www.w3.org/2000/svg"
|
101 |
+
viewBox="0 0 20 20"
|
102 |
+
fill="currentColor"
|
103 |
+
class="w-5 h-5"
|
104 |
+
>
|
105 |
+
<path
|
106 |
+
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"
|
107 |
+
/>
|
108 |
+
</svg>
|
109 |
+
</button>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
113 |
+
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
114 |
+
{#if config}
|
115 |
+
<form
|
116 |
+
class="flex flex-col w-full"
|
117 |
+
on:submit|preventDefault={() => {
|
118 |
+
submitHandler();
|
119 |
+
}}
|
120 |
+
>
|
121 |
+
<div>
|
122 |
+
<div class="flex flex-col w-full">
|
123 |
+
<div class="mb-1 flex justify-between">
|
124 |
+
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
|
125 |
+
</div>
|
126 |
+
|
127 |
+
<ModelList bind:modelIds />
|
128 |
+
</div>
|
129 |
+
</div>
|
130 |
+
|
131 |
+
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
132 |
+
|
133 |
+
<div>
|
134 |
+
<div class="flex flex-col w-full">
|
135 |
+
<div class="mb-1 flex justify-between">
|
136 |
+
<div class="text-xs text-gray-500">{$i18n.t('Default Models')}</div>
|
137 |
+
</div>
|
138 |
+
|
139 |
+
{#if defaultModelIds.length > 0}
|
140 |
+
<div class="flex flex-col">
|
141 |
+
{#each defaultModelIds as modelId, modelIdx}
|
142 |
+
<div class=" flex gap-2 w-full justify-between items-center">
|
143 |
+
<div class=" text-sm flex-1 py-1 rounded-lg">
|
144 |
+
{$models.find((model) => model.id === modelId)?.name}
|
145 |
+
</div>
|
146 |
+
<div class="flex-shrink-0">
|
147 |
+
<button
|
148 |
+
type="button"
|
149 |
+
on:click={() => {
|
150 |
+
defaultModelIds = defaultModelIds.filter(
|
151 |
+
(_, idx) => idx !== modelIdx
|
152 |
+
);
|
153 |
+
}}
|
154 |
+
>
|
155 |
+
<Minus strokeWidth="2" className="size-3.5" />
|
156 |
+
</button>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
{/each}
|
160 |
+
</div>
|
161 |
+
{:else}
|
162 |
+
<div class="text-gray-500 text-xs text-center py-2">
|
163 |
+
{$i18n.t('No models selected')}
|
164 |
+
</div>
|
165 |
+
{/if}
|
166 |
+
|
167 |
+
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
168 |
+
|
169 |
+
<div class="flex items-center">
|
170 |
+
<select
|
171 |
+
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
172 |
+
? ''
|
173 |
+
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
174 |
+
bind:value={selectedModelId}
|
175 |
+
>
|
176 |
+
<option value="">{$i18n.t('Select a model')}</option>
|
177 |
+
{#each $models as model}
|
178 |
+
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
179 |
+
>{model.name}</option
|
180 |
+
>
|
181 |
+
{/each}
|
182 |
+
</select>
|
183 |
+
|
184 |
+
<div>
|
185 |
+
<button
|
186 |
+
type="button"
|
187 |
+
on:click={() => {
|
188 |
+
if (selectedModelId === '') {
|
189 |
+
return;
|
190 |
+
}
|
191 |
+
|
192 |
+
if (defaultModelIds.includes(selectedModelId)) {
|
193 |
+
return;
|
194 |
+
}
|
195 |
+
|
196 |
+
defaultModelIds = [...defaultModelIds, selectedModelId];
|
197 |
+
selectedModelId = '';
|
198 |
+
}}
|
199 |
+
>
|
200 |
+
<Plus className="size-3.5" strokeWidth="2" />
|
201 |
+
</button>
|
202 |
+
</div>
|
203 |
+
</div>
|
204 |
+
</div>
|
205 |
+
</div>
|
206 |
+
|
207 |
+
<div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
|
208 |
+
<Tooltip content={$i18n.t('This will delete all models including custom models')}>
|
209 |
+
<button
|
210 |
+
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-950 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
211 |
+
type="button"
|
212 |
+
on:click={() => {
|
213 |
+
showResetModal = true;
|
214 |
+
}}
|
215 |
+
>
|
216 |
+
<!-- {$i18n.t('Delete All Models')} -->
|
217 |
+
{$i18n.t('Reset All Models')}
|
218 |
+
</button>
|
219 |
+
</Tooltip>
|
220 |
+
|
221 |
+
<button
|
222 |
+
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
223 |
+
? ' cursor-not-allowed'
|
224 |
+
: ''}"
|
225 |
+
type="submit"
|
226 |
+
disabled={loading}
|
227 |
+
>
|
228 |
+
{$i18n.t('Save')}
|
229 |
+
|
230 |
+
{#if loading}
|
231 |
+
<div class="ml-2 self-center">
|
232 |
+
<svg
|
233 |
+
class=" w-4 h-4"
|
234 |
+
viewBox="0 0 24 24"
|
235 |
+
fill="currentColor"
|
236 |
+
xmlns="http://www.w3.org/2000/svg"
|
237 |
+
><style>
|
238 |
+
.spinner_ajPY {
|
239 |
+
transform-origin: center;
|
240 |
+
animation: spinner_AtaB 0.75s infinite linear;
|
241 |
+
}
|
242 |
+
@keyframes spinner_AtaB {
|
243 |
+
100% {
|
244 |
+
transform: rotate(360deg);
|
245 |
+
}
|
246 |
+
}
|
247 |
+
</style><path
|
248 |
+
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"
|
249 |
+
opacity=".25"
|
250 |
+
/><path
|
251 |
+
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"
|
252 |
+
class="spinner_ajPY"
|
253 |
+
/></svg
|
254 |
+
>
|
255 |
+
</div>
|
256 |
+
{/if}
|
257 |
+
</button>
|
258 |
+
</div>
|
259 |
+
</form>
|
260 |
+
{:else}
|
261 |
+
<div>
|
262 |
+
<Spinner />
|
263 |
+
</div>
|
264 |
+
{/if}
|
265 |
+
</div>
|
266 |
+
</div>
|
267 |
+
</div>
|
268 |
+
</Modal>
|
src/lib/components/admin/Settings/Models/ModelList.svelte
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import Sortable from 'sortablejs';
|
3 |
+
|
4 |
+
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
5 |
+
const i18n = getContext('i18n');
|
6 |
+
|
7 |
+
import { models } from '$lib/stores';
|
8 |
+
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
9 |
+
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
10 |
+
|
11 |
+
export let modelIds = [];
|
12 |
+
|
13 |
+
let sortable = null;
|
14 |
+
let modelListElement = null;
|
15 |
+
|
16 |
+
const positionChangeHandler = () => {
|
17 |
+
const modelList = Array.from(modelListElement.children).map((child) =>
|
18 |
+
child.id.replace('model-item-', '')
|
19 |
+
);
|
20 |
+
|
21 |
+
modelIds = modelList;
|
22 |
+
};
|
23 |
+
|
24 |
+
onMount(() => {
|
25 |
+
sortable = Sortable.create(modelListElement, {
|
26 |
+
animation: 150,
|
27 |
+
onUpdate: async (event) => {
|
28 |
+
positionChangeHandler();
|
29 |
+
}
|
30 |
+
});
|
31 |
+
});
|
32 |
+
</script>
|
33 |
+
|
34 |
+
{#if modelIds.length > 0}
|
35 |
+
<div class="flex flex-col -translate-x-1" bind:this={modelListElement}>
|
36 |
+
{#each modelIds as modelId, modelIdx (modelId)}
|
37 |
+
<div class=" flex gap-2 w-full justify-between items-center" id="model-item-{modelId}">
|
38 |
+
<Tooltip content={modelId} placement="top-start">
|
39 |
+
<div class="flex items-center gap-1">
|
40 |
+
<EllipsisVertical className="size-4 cursor-move" />
|
41 |
+
|
42 |
+
<div class=" text-sm flex-1 py-1 rounded-lg">
|
43 |
+
{#if $models.find((model) => model.id === modelId)}
|
44 |
+
{$models.find((model) => model.id === modelId).name}
|
45 |
+
{:else}
|
46 |
+
{modelId}
|
47 |
+
{/if}
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
</Tooltip>
|
51 |
+
</div>
|
52 |
+
{/each}
|
53 |
+
</div>
|
54 |
+
{:else}
|
55 |
+
<div class="text-gray-500 text-xs text-center py-2">
|
56 |
+
{$i18n.t('No models found')}
|
57 |
+
</div>
|
58 |
+
{/if}
|
src/lib/components/admin/Settings/WebSearch.svelte
CHANGED
@@ -29,13 +29,15 @@
|
|
29 |
|
30 |
let youtubeLanguage = 'en';
|
31 |
let youtubeTranslation = null;
|
|
|
32 |
|
33 |
const submitHandler = async () => {
|
34 |
const res = await updateRAGConfig(localStorage.token, {
|
35 |
web: webConfig,
|
36 |
youtube: {
|
37 |
language: youtubeLanguage.split(',').map((lang) => lang.trim()),
|
38 |
-
translation: youtubeTranslation
|
|
|
39 |
}
|
40 |
});
|
41 |
};
|
@@ -48,6 +50,7 @@
|
|
48 |
|
49 |
youtubeLanguage = res.youtube.language.join(',');
|
50 |
youtubeTranslation = res.youtube.translation;
|
|
|
51 |
}
|
52 |
});
|
53 |
</script>
|
@@ -358,6 +361,21 @@
|
|
358 |
</div>
|
359 |
</div>
|
360 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
361 |
</div>
|
362 |
{/if}
|
363 |
</div>
|
|
|
29 |
|
30 |
let youtubeLanguage = 'en';
|
31 |
let youtubeTranslation = null;
|
32 |
+
let youtubeProxyUrl = '';
|
33 |
|
34 |
const submitHandler = async () => {
|
35 |
const res = await updateRAGConfig(localStorage.token, {
|
36 |
web: webConfig,
|
37 |
youtube: {
|
38 |
language: youtubeLanguage.split(',').map((lang) => lang.trim()),
|
39 |
+
translation: youtubeTranslation,
|
40 |
+
proxy_url: youtubeProxyUrl
|
41 |
}
|
42 |
});
|
43 |
};
|
|
|
50 |
|
51 |
youtubeLanguage = res.youtube.language.join(',');
|
52 |
youtubeTranslation = res.youtube.translation;
|
53 |
+
youtubeProxyUrl = res.youtube.proxy_url;
|
54 |
}
|
55 |
});
|
56 |
</script>
|
|
|
361 |
</div>
|
362 |
</div>
|
363 |
</div>
|
364 |
+
|
365 |
+
<div>
|
366 |
+
<div class=" py-0.5 flex w-full justify-between">
|
367 |
+
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
|
368 |
+
<div class=" flex-1 self-center">
|
369 |
+
<input
|
370 |
+
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
371 |
+
type="text"
|
372 |
+
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
|
373 |
+
bind:value={youtubeProxyUrl}
|
374 |
+
autocomplete="off"
|
375 |
+
/>
|
376 |
+
</div>
|
377 |
+
</div>
|
378 |
+
</div>
|
379 |
</div>
|
380 |
{/if}
|
381 |
</div>
|
src/lib/components/admin/Users/UserList/UserChatsModal.svelte
CHANGED
@@ -9,13 +9,14 @@
|
|
9 |
|
10 |
import Modal from '$lib/components/common/Modal.svelte';
|
11 |
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
|
12 |
|
13 |
const i18n = getContext('i18n');
|
14 |
|
15 |
export let show = false;
|
16 |
export let user;
|
17 |
|
18 |
-
let chats =
|
19 |
|
20 |
const deleteChatHandler = async (chatId) => {
|
21 |
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
@@ -31,6 +32,8 @@
|
|
31 |
chats = await getChatListByUserId(localStorage.token, user.id);
|
32 |
}
|
33 |
})();
|
|
|
|
|
34 |
}
|
35 |
|
36 |
let sortKey = 'updated_at'; // default sort key
|
@@ -46,33 +49,32 @@
|
|
46 |
</script>
|
47 |
|
48 |
<Modal size="lg" bind:show>
|
49 |
-
<div>
|
50 |
-
<div class="
|
51 |
-
|
52 |
-
{$i18n.t("{{user}}'s Chats", { user: user.name })}
|
53 |
-
</div>
|
54 |
-
<button
|
55 |
-
class="self-center"
|
56 |
-
on:click={() => {
|
57 |
-
show = false;
|
58 |
-
}}
|
59 |
-
>
|
60 |
-
<svg
|
61 |
-
xmlns="http://www.w3.org/2000/svg"
|
62 |
-
viewBox="0 0 20 20"
|
63 |
-
fill="currentColor"
|
64 |
-
class="w-5 h-5"
|
65 |
-
>
|
66 |
-
<path
|
67 |
-
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"
|
68 |
-
/>
|
69 |
-
</svg>
|
70 |
-
</button>
|
71 |
</div>
|
72 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
|
74 |
-
|
75 |
-
|
|
|
76 |
{#if chats.length > 0}
|
77 |
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
|
78 |
<div class="relative overflow-x-auto">
|
@@ -176,7 +178,9 @@
|
|
176 |
{$i18n.t('has no conversations.')}
|
177 |
</div>
|
178 |
{/if}
|
179 |
-
|
|
|
|
|
180 |
</div>
|
181 |
</div>
|
182 |
</Modal>
|
|
|
9 |
|
10 |
import Modal from '$lib/components/common/Modal.svelte';
|
11 |
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
12 |
+
import Spinner from '$lib/components/common/Spinner.svelte';
|
13 |
|
14 |
const i18n = getContext('i18n');
|
15 |
|
16 |
export let show = false;
|
17 |
export let user;
|
18 |
|
19 |
+
let chats = null;
|
20 |
|
21 |
const deleteChatHandler = async (chatId) => {
|
22 |
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
|
|
32 |
chats = await getChatListByUserId(localStorage.token, user.id);
|
33 |
}
|
34 |
})();
|
35 |
+
} else {
|
36 |
+
chats = null;
|
37 |
}
|
38 |
|
39 |
let sortKey = 'updated_at'; // default sort key
|
|
|
49 |
</script>
|
50 |
|
51 |
<Modal size="lg" bind:show>
|
52 |
+
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
53 |
+
<div class=" text-lg font-medium self-center capitalize">
|
54 |
+
{$i18n.t("{{user}}'s Chats", { user: user.name })}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
</div>
|
56 |
+
<button
|
57 |
+
class="self-center"
|
58 |
+
on:click={() => {
|
59 |
+
show = false;
|
60 |
+
}}
|
61 |
+
>
|
62 |
+
<svg
|
63 |
+
xmlns="http://www.w3.org/2000/svg"
|
64 |
+
viewBox="0 0 20 20"
|
65 |
+
fill="currentColor"
|
66 |
+
class="w-5 h-5"
|
67 |
+
>
|
68 |
+
<path
|
69 |
+
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"
|
70 |
+
/>
|
71 |
+
</svg>
|
72 |
+
</button>
|
73 |
+
</div>
|
74 |
|
75 |
+
<div class="flex flex-col md:flex-row w-full px-5 pt-2 pb-4 md:space-x-4 dark:text-gray-200">
|
76 |
+
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
77 |
+
{#if chats}
|
78 |
{#if chats.length > 0}
|
79 |
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
|
80 |
<div class="relative overflow-x-auto">
|
|
|
178 |
{$i18n.t('has no conversations.')}
|
179 |
</div>
|
180 |
{/if}
|
181 |
+
{:else}
|
182 |
+
<Spinner />
|
183 |
+
{/if}
|
184 |
</div>
|
185 |
</div>
|
186 |
</Modal>
|
src/lib/components/chat/Chat.svelte
CHANGED
@@ -2284,7 +2284,7 @@
|
|
2284 |
</div>
|
2285 |
</div>
|
2286 |
|
2287 |
-
<div class=" pb-[
|
2288 |
<MessageInput
|
2289 |
{history}
|
2290 |
{selectedModels}
|
@@ -2319,9 +2319,9 @@
|
|
2319 |
/>
|
2320 |
|
2321 |
<div
|
2322 |
-
class="absolute bottom-1
|
2323 |
>
|
2324 |
-
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
2325 |
</div>
|
2326 |
</div>
|
2327 |
{:else}
|
|
|
2284 |
</div>
|
2285 |
</div>
|
2286 |
|
2287 |
+
<div class=" pb-[1rem]">
|
2288 |
<MessageInput
|
2289 |
{history}
|
2290 |
{selectedModels}
|
|
|
2319 |
/>
|
2320 |
|
2321 |
<div
|
2322 |
+
class="absolute bottom-1 text-xs text-gray-500 text-center line-clamp-1 right-0 left-0"
|
2323 |
>
|
2324 |
+
<!-- {$i18n.t('LLMs can make mistakes. Verify important information.')} -->
|
2325 |
</div>
|
2326 |
</div>
|
2327 |
{:else}
|
src/lib/components/chat/MessageInput.svelte
CHANGED
@@ -18,7 +18,7 @@
|
|
18 |
showControls
|
19 |
} from '$lib/stores';
|
20 |
|
21 |
-
import { blobToFile, findWordIndices } from '$lib/utils';
|
22 |
import { transcribeAudio } from '$lib/apis/audio';
|
23 |
import { uploadFile } from '$lib/apis/files';
|
24 |
import { getTools } from '$lib/apis/tools';
|
@@ -34,6 +34,8 @@
|
|
34 |
import Commands from './MessageInput/Commands.svelte';
|
35 |
import XMark from '../icons/XMark.svelte';
|
36 |
import RichTextInput from '../common/RichTextInput.svelte';
|
|
|
|
|
37 |
|
38 |
const i18n = getContext('i18n');
|
39 |
|
@@ -47,6 +49,9 @@
|
|
47 |
export let atSelectedModel: Model | undefined;
|
48 |
export let selectedModels: [''];
|
49 |
|
|
|
|
|
|
|
50 |
export let history;
|
51 |
|
52 |
export let prompt = '';
|
@@ -266,8 +271,8 @@
|
|
266 |
|
267 |
{#if loaded}
|
268 |
<div class="w-full font-primary">
|
269 |
-
<div class="
|
270 |
-
<div class="flex flex-col px-
|
271 |
<div class="relative">
|
272 |
{#if autoScroll === false && history?.currentId}
|
273 |
<div
|
@@ -300,7 +305,7 @@
|
|
300 |
<div class="w-full relative">
|
301 |
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled}
|
302 |
<div
|
303 |
-
class="px-
|
304 |
>
|
305 |
{#if selectedToolIds.length > 0}
|
306 |
<div class="flex items-center justify-between w-full">
|
@@ -405,7 +410,7 @@
|
|
405 |
</div>
|
406 |
|
407 |
<div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
|
408 |
-
<div class="max-w-6xl px-
|
409 |
<div class="">
|
410 |
<input
|
411 |
bind:this={filesInputElement}
|
@@ -457,7 +462,7 @@
|
|
457 |
}}
|
458 |
>
|
459 |
<div
|
460 |
-
class="flex-1 flex flex-col relative w-full rounded-3xl px-1
|
461 |
dir={$settings?.chatDirection ?? 'LTR'}
|
462 |
>
|
463 |
{#if files.length > 0}
|
@@ -542,7 +547,7 @@
|
|
542 |
{/if}
|
543 |
|
544 |
<div class=" flex">
|
545 |
-
<div class="
|
546 |
<InputMenu
|
547 |
bind:webSearchEnabled
|
548 |
bind:selectedToolIds
|
@@ -557,18 +562,18 @@
|
|
557 |
}}
|
558 |
>
|
559 |
<button
|
560 |
-
class="bg-
|
561 |
type="button"
|
562 |
aria-label="More"
|
563 |
>
|
564 |
<svg
|
565 |
xmlns="http://www.w3.org/2000/svg"
|
566 |
-
viewBox="0 0
|
567 |
fill="currentColor"
|
568 |
class="size-5"
|
569 |
>
|
570 |
<path
|
571 |
-
d="
|
572 |
/>
|
573 |
</svg>
|
574 |
</button>
|
@@ -577,10 +582,11 @@
|
|
577 |
|
578 |
{#if $settings?.richTextInput ?? true}
|
579 |
<div
|
580 |
-
class="scrollbar-hidden text-left bg-
|
581 |
>
|
582 |
<RichTextInput
|
583 |
bind:this={chatInputElement}
|
|
|
584 |
id="chat-input"
|
585 |
messageInput={true}
|
586 |
shiftEnter={!$mobile ||
|
@@ -591,29 +597,27 @@
|
|
591 |
)}
|
592 |
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
593 |
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
594 |
-
|
595 |
-
|
596 |
-
|
597 |
-
|
598 |
-
if (commandsContainerElement) {
|
599 |
-
e.preventDefault();
|
600 |
-
|
601 |
-
const commandOptionButton = [
|
602 |
-
...document.getElementsByClassName('selected-command-option-button')
|
603 |
-
]?.at(-1);
|
604 |
-
|
605 |
-
if (commandOptionButton) {
|
606 |
-
commandOptionButton?.click();
|
607 |
-
return;
|
608 |
-
}
|
609 |
}
|
610 |
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
|
616 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
617 |
}}
|
618 |
on:keydown={async (e) => {
|
619 |
e = e.detail.event;
|
@@ -657,34 +661,70 @@
|
|
657 |
editButton?.click();
|
658 |
}
|
659 |
|
660 |
-
if (commandsContainerElement
|
661 |
-
e.
|
662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
663 |
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
commandOptionButton.scrollIntoView({ block: 'center' });
|
668 |
-
}
|
669 |
|
670 |
-
|
671 |
-
|
672 |
-
|
|
|
|
|
673 |
|
674 |
-
|
675 |
-
|
676 |
-
]?.at(-1);
|
677 |
-
commandOptionButton.scrollIntoView({ block: 'center' });
|
678 |
-
}
|
679 |
|
680 |
-
|
681 |
-
|
|
|
682 |
|
683 |
-
|
684 |
-
|
685 |
-
]?.at(-1);
|
686 |
|
687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
688 |
}
|
689 |
|
690 |
if (e.key === 'Escape') {
|
@@ -867,7 +907,7 @@
|
|
867 |
}
|
868 |
|
869 |
e.target.style.height = '';
|
870 |
-
e.target.style.height = Math.min(e.target.scrollHeight,
|
871 |
}
|
872 |
|
873 |
if (e.key === 'Escape') {
|
@@ -880,11 +920,11 @@
|
|
880 |
rows="1"
|
881 |
on:input={async (e) => {
|
882 |
e.target.style.height = '';
|
883 |
-
e.target.style.height = Math.min(e.target.scrollHeight,
|
884 |
}}
|
885 |
on:focus={async (e) => {
|
886 |
e.target.style.height = '';
|
887 |
-
e.target.style.height = Math.min(e.target.scrollHeight,
|
888 |
}}
|
889 |
on:paste={async (e) => {
|
890 |
const clipboardData = e.clipboardData || window.clipboardData;
|
@@ -927,12 +967,12 @@
|
|
927 |
/>
|
928 |
{/if}
|
929 |
|
930 |
-
<div class="self-end mb-
|
931 |
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
|
932 |
<Tooltip content={$i18n.t('Record voice')}>
|
933 |
<button
|
934 |
id="voice-input-button"
|
935 |
-
class=" text-gray-600 dark:text-gray-300 hover:
|
936 |
type="button"
|
937 |
on:click={async () => {
|
938 |
try {
|
@@ -976,112 +1016,113 @@
|
|
976 |
</button>
|
977 |
</Tooltip>
|
978 |
{/if}
|
979 |
-
</div>
|
980 |
-
</div>
|
981 |
-
</div>
|
982 |
-
<div class="flex items-end w-10">
|
983 |
-
{#if !history.currentId || history.messages[history.currentId]?.done == true}
|
984 |
-
{#if prompt === ''}
|
985 |
-
<div class=" flex items-center mb-1">
|
986 |
-
<Tooltip content={$i18n.t('Call')}>
|
987 |
-
<button
|
988 |
-
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-2 self-center"
|
989 |
-
type="button"
|
990 |
-
on:click={async () => {
|
991 |
-
if (selectedModels.length > 1) {
|
992 |
-
toast.error($i18n.t('Select only one model to call'));
|
993 |
|
994 |
-
|
995 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
996 |
|
997 |
-
|
998 |
-
|
999 |
-
$i18n.t('Call feature is not supported when using Web STT engine')
|
1000 |
-
);
|
1001 |
|
1002 |
-
|
1003 |
-
|
1004 |
-
|
1005 |
-
|
1006 |
-
|
1007 |
-
|
1008 |
-
});
|
1009 |
-
// If the user grants the permission, proceed to show the call overlay
|
1010 |
|
1011 |
-
|
1012 |
-
|
1013 |
-
|
1014 |
-
|
|
|
|
|
|
|
|
|
1015 |
|
1016 |
-
|
|
|
|
|
|
|
1017 |
|
1018 |
-
|
1019 |
-
|
1020 |
-
|
1021 |
-
|
1022 |
-
|
1023 |
-
|
1024 |
-
|
1025 |
-
|
1026 |
-
|
1027 |
-
|
1028 |
-
|
1029 |
-
|
1030 |
-
|
1031 |
-
|
1032 |
-
|
1033 |
-
|
1034 |
-
|
1035 |
-
|
1036 |
-
<
|
1037 |
-
|
1038 |
-
|
1039 |
-
|
1040 |
-
|
1041 |
-
|
1042 |
-
|
1043 |
-
|
1044 |
-
|
1045 |
-
|
1046 |
-
|
1047 |
-
|
1048 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1049 |
>
|
1050 |
-
<
|
1051 |
-
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
|
1056 |
-
|
1057 |
-
|
1058 |
-
|
1059 |
-
|
1060 |
-
|
1061 |
-
|
1062 |
-
|
1063 |
-
|
1064 |
-
|
1065 |
-
|
1066 |
-
stopResponse();
|
1067 |
-
}}
|
1068 |
-
>
|
1069 |
-
<svg
|
1070 |
-
xmlns="http://www.w3.org/2000/svg"
|
1071 |
-
viewBox="0 0 24 24"
|
1072 |
-
fill="currentColor"
|
1073 |
-
class="size-6"
|
1074 |
-
>
|
1075 |
-
<path
|
1076 |
-
fill-rule="evenodd"
|
1077 |
-
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
|
1078 |
-
clip-rule="evenodd"
|
1079 |
-
/>
|
1080 |
-
</svg>
|
1081 |
-
</button>
|
1082 |
-
</Tooltip>
|
1083 |
</div>
|
1084 |
-
|
1085 |
</div>
|
1086 |
</form>
|
1087 |
{/if}
|
|
|
18 |
showControls
|
19 |
} from '$lib/stores';
|
20 |
|
21 |
+
import { blobToFile, createMessagesList, findWordIndices } from '$lib/utils';
|
22 |
import { transcribeAudio } from '$lib/apis/audio';
|
23 |
import { uploadFile } from '$lib/apis/files';
|
24 |
import { getTools } from '$lib/apis/tools';
|
|
|
34 |
import Commands from './MessageInput/Commands.svelte';
|
35 |
import XMark from '../icons/XMark.svelte';
|
36 |
import RichTextInput from '../common/RichTextInput.svelte';
|
37 |
+
import { generateAutoCompletion } from '$lib/apis';
|
38 |
+
import { error, text } from '@sveltejs/kit';
|
39 |
|
40 |
const i18n = getContext('i18n');
|
41 |
|
|
|
49 |
export let atSelectedModel: Model | undefined;
|
50 |
export let selectedModels: [''];
|
51 |
|
52 |
+
let selectedModelIds = [];
|
53 |
+
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
|
54 |
+
|
55 |
export let history;
|
56 |
|
57 |
export let prompt = '';
|
|
|
271 |
|
272 |
{#if loaded}
|
273 |
<div class="w-full font-primary">
|
274 |
+
<div class=" mx-auto inset-x-0 bg-transparent flex justify-center">
|
275 |
+
<div class="flex flex-col px-3 max-w-6xl w-full">
|
276 |
<div class="relative">
|
277 |
{#if autoScroll === false && history?.currentId}
|
278 |
<div
|
|
|
305 |
<div class="w-full relative">
|
306 |
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled}
|
307 |
<div
|
308 |
+
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
|
309 |
>
|
310 |
{#if selectedToolIds.length > 0}
|
311 |
<div class="flex items-center justify-between w-full">
|
|
|
410 |
</div>
|
411 |
|
412 |
<div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
|
413 |
+
<div class="max-w-6xl px-2.5 mx-auto inset-x-0">
|
414 |
<div class="">
|
415 |
<input
|
416 |
bind:this={filesInputElement}
|
|
|
462 |
}}
|
463 |
>
|
464 |
<div
|
465 |
+
class="flex-1 flex flex-col relative w-full rounded-3xl px-1 bg-gray-50 dark:bg-gray-400/5 dark:text-gray-100"
|
466 |
dir={$settings?.chatDirection ?? 'LTR'}
|
467 |
>
|
468 |
{#if files.length > 0}
|
|
|
547 |
{/if}
|
548 |
|
549 |
<div class=" flex">
|
550 |
+
<div class="ml-1 self-end mb-1.5 flex space-x-1">
|
551 |
<InputMenu
|
552 |
bind:webSearchEnabled
|
553 |
bind:selectedToolIds
|
|
|
562 |
}}
|
563 |
>
|
564 |
<button
|
565 |
+
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
|
566 |
type="button"
|
567 |
aria-label="More"
|
568 |
>
|
569 |
<svg
|
570 |
xmlns="http://www.w3.org/2000/svg"
|
571 |
+
viewBox="0 0 20 20"
|
572 |
fill="currentColor"
|
573 |
class="size-5"
|
574 |
>
|
575 |
<path
|
576 |
+
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
577 |
/>
|
578 |
</svg>
|
579 |
</button>
|
|
|
582 |
|
583 |
{#if $settings?.richTextInput ?? true}
|
584 |
<div
|
585 |
+
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
586 |
>
|
587 |
<RichTextInput
|
588 |
bind:this={chatInputElement}
|
589 |
+
bind:value={prompt}
|
590 |
id="chat-input"
|
591 |
messageInput={true}
|
592 |
shiftEnter={!$mobile ||
|
|
|
597 |
)}
|
598 |
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
599 |
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
600 |
+
autocomplete={true}
|
601 |
+
generateAutoCompletion={async (text) => {
|
602 |
+
if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
|
603 |
+
toast.error($i18n.t('Please select a model first.'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
604 |
}
|
605 |
|
606 |
+
const res = await generateAutoCompletion(
|
607 |
+
localStorage.token,
|
608 |
+
selectedModelIds.at(0),
|
609 |
+
text,
|
610 |
+
history?.currentId
|
611 |
+
? createMessagesList(history, history.currentId)
|
612 |
+
: null
|
613 |
+
).catch((error) => {
|
614 |
+
console.log(error);
|
615 |
+
|
616 |
+
return null;
|
617 |
+
});
|
618 |
+
|
619 |
+
console.log(res);
|
620 |
+
return res;
|
621 |
}}
|
622 |
on:keydown={async (e) => {
|
623 |
e = e.detail.event;
|
|
|
661 |
editButton?.click();
|
662 |
}
|
663 |
|
664 |
+
if (commandsContainerElement) {
|
665 |
+
if (commandsContainerElement && e.key === 'ArrowUp') {
|
666 |
+
e.preventDefault();
|
667 |
+
commandsElement.selectUp();
|
668 |
+
|
669 |
+
const commandOptionButton = [
|
670 |
+
...document.getElementsByClassName('selected-command-option-button')
|
671 |
+
]?.at(-1);
|
672 |
+
commandOptionButton.scrollIntoView({ block: 'center' });
|
673 |
+
}
|
674 |
|
675 |
+
if (commandsContainerElement && e.key === 'ArrowDown') {
|
676 |
+
e.preventDefault();
|
677 |
+
commandsElement.selectDown();
|
|
|
|
|
678 |
|
679 |
+
const commandOptionButton = [
|
680 |
+
...document.getElementsByClassName('selected-command-option-button')
|
681 |
+
]?.at(-1);
|
682 |
+
commandOptionButton.scrollIntoView({ block: 'center' });
|
683 |
+
}
|
684 |
|
685 |
+
if (commandsContainerElement && e.key === 'Tab') {
|
686 |
+
e.preventDefault();
|
|
|
|
|
|
|
687 |
|
688 |
+
const commandOptionButton = [
|
689 |
+
...document.getElementsByClassName('selected-command-option-button')
|
690 |
+
]?.at(-1);
|
691 |
|
692 |
+
commandOptionButton?.click();
|
693 |
+
}
|
|
|
694 |
|
695 |
+
if (commandsContainerElement && e.key === 'Enter') {
|
696 |
+
e.preventDefault();
|
697 |
+
|
698 |
+
const commandOptionButton = [
|
699 |
+
...document.getElementsByClassName('selected-command-option-button')
|
700 |
+
]?.at(-1);
|
701 |
+
|
702 |
+
if (commandOptionButton) {
|
703 |
+
commandOptionButton?.click();
|
704 |
+
} else {
|
705 |
+
document.getElementById('send-message-button')?.click();
|
706 |
+
}
|
707 |
+
}
|
708 |
+
} else {
|
709 |
+
if (
|
710 |
+
!$mobile ||
|
711 |
+
!(
|
712 |
+
'ontouchstart' in window ||
|
713 |
+
navigator.maxTouchPoints > 0 ||
|
714 |
+
navigator.msMaxTouchPoints > 0
|
715 |
+
)
|
716 |
+
) {
|
717 |
+
// Prevent Enter key from creating a new line
|
718 |
+
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
719 |
+
if (e.keyCode === 13 && !e.shiftKey) {
|
720 |
+
e.preventDefault();
|
721 |
+
}
|
722 |
+
|
723 |
+
// Submit the prompt when Enter key is pressed
|
724 |
+
if (prompt !== '' && e.keyCode === 13 && !e.shiftKey) {
|
725 |
+
dispatch('submit', prompt);
|
726 |
+
}
|
727 |
+
}
|
728 |
}
|
729 |
|
730 |
if (e.key === 'Escape') {
|
|
|
907 |
}
|
908 |
|
909 |
e.target.style.height = '';
|
910 |
+
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
911 |
}
|
912 |
|
913 |
if (e.key === 'Escape') {
|
|
|
920 |
rows="1"
|
921 |
on:input={async (e) => {
|
922 |
e.target.style.height = '';
|
923 |
+
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
924 |
}}
|
925 |
on:focus={async (e) => {
|
926 |
e.target.style.height = '';
|
927 |
+
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
928 |
}}
|
929 |
on:paste={async (e) => {
|
930 |
const clipboardData = e.clipboardData || window.clipboardData;
|
|
|
967 |
/>
|
968 |
{/if}
|
969 |
|
970 |
+
<div class="self-end mb-1.5 flex space-x-1 mr-1">
|
971 |
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
|
972 |
<Tooltip content={$i18n.t('Record voice')}>
|
973 |
<button
|
974 |
id="voice-input-button"
|
975 |
+
class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 mr-0.5 self-center"
|
976 |
type="button"
|
977 |
on:click={async () => {
|
978 |
try {
|
|
|
1016 |
</button>
|
1017 |
</Tooltip>
|
1018 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1019 |
|
1020 |
+
{#if !history.currentId || history.messages[history.currentId]?.done == true}
|
1021 |
+
{#if prompt === ''}
|
1022 |
+
<div class=" flex items-center">
|
1023 |
+
<Tooltip content={$i18n.t('Call')}>
|
1024 |
+
<button
|
1025 |
+
class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-2 self-center"
|
1026 |
+
type="button"
|
1027 |
+
on:click={async () => {
|
1028 |
+
if (selectedModels.length > 1) {
|
1029 |
+
toast.error($i18n.t('Select only one model to call'));
|
1030 |
|
1031 |
+
return;
|
1032 |
+
}
|
|
|
|
|
1033 |
|
1034 |
+
if ($config.audio.stt.engine === 'web') {
|
1035 |
+
toast.error(
|
1036 |
+
$i18n.t(
|
1037 |
+
'Call feature is not supported when using Web STT engine'
|
1038 |
+
)
|
1039 |
+
);
|
|
|
|
|
1040 |
|
1041 |
+
return;
|
1042 |
+
}
|
1043 |
+
// check if user has access to getUserMedia
|
1044 |
+
try {
|
1045 |
+
let stream = await navigator.mediaDevices.getUserMedia({
|
1046 |
+
audio: true
|
1047 |
+
});
|
1048 |
+
// If the user grants the permission, proceed to show the call overlay
|
1049 |
|
1050 |
+
if (stream) {
|
1051 |
+
const tracks = stream.getTracks();
|
1052 |
+
tracks.forEach((track) => track.stop());
|
1053 |
+
}
|
1054 |
|
1055 |
+
stream = null;
|
1056 |
+
|
1057 |
+
showCallOverlay.set(true);
|
1058 |
+
showControls.set(true);
|
1059 |
+
} catch (err) {
|
1060 |
+
// If the user denies the permission or an error occurs, show an error message
|
1061 |
+
toast.error(
|
1062 |
+
$i18n.t('Permission denied when accessing media devices')
|
1063 |
+
);
|
1064 |
+
}
|
1065 |
+
}}
|
1066 |
+
aria-label="Call"
|
1067 |
+
>
|
1068 |
+
<Headphone className="size-5" />
|
1069 |
+
</button>
|
1070 |
+
</Tooltip>
|
1071 |
+
</div>
|
1072 |
+
{:else}
|
1073 |
+
<div class=" flex items-center">
|
1074 |
+
<Tooltip content={$i18n.t('Send message')}>
|
1075 |
+
<button
|
1076 |
+
id="send-message-button"
|
1077 |
+
class="{prompt !== ''
|
1078 |
+
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
1079 |
+
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
|
1080 |
+
type="submit"
|
1081 |
+
disabled={prompt === ''}
|
1082 |
+
>
|
1083 |
+
<svg
|
1084 |
+
xmlns="http://www.w3.org/2000/svg"
|
1085 |
+
viewBox="0 0 16 16"
|
1086 |
+
fill="currentColor"
|
1087 |
+
class="size-6"
|
1088 |
+
>
|
1089 |
+
<path
|
1090 |
+
fill-rule="evenodd"
|
1091 |
+
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
1092 |
+
clip-rule="evenodd"
|
1093 |
+
/>
|
1094 |
+
</svg>
|
1095 |
+
</button>
|
1096 |
+
</Tooltip>
|
1097 |
+
</div>
|
1098 |
+
{/if}
|
1099 |
+
{:else}
|
1100 |
+
<div class=" flex items-center">
|
1101 |
+
<Tooltip content={$i18n.t('Stop')}>
|
1102 |
+
<button
|
1103 |
+
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"
|
1104 |
+
on:click={() => {
|
1105 |
+
stopResponse();
|
1106 |
+
}}
|
1107 |
>
|
1108 |
+
<svg
|
1109 |
+
xmlns="http://www.w3.org/2000/svg"
|
1110 |
+
viewBox="0 0 24 24"
|
1111 |
+
fill="currentColor"
|
1112 |
+
class="size-6"
|
1113 |
+
>
|
1114 |
+
<path
|
1115 |
+
fill-rule="evenodd"
|
1116 |
+
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
|
1117 |
+
clip-rule="evenodd"
|
1118 |
+
/>
|
1119 |
+
</svg>
|
1120 |
+
</button>
|
1121 |
+
</Tooltip>
|
1122 |
+
</div>
|
1123 |
+
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1124 |
</div>
|
1125 |
+
</div>
|
1126 |
</div>
|
1127 |
</form>
|
1128 |
{/if}
|
src/lib/components/chat/MessageInput/Commands.svelte
CHANGED
@@ -106,7 +106,7 @@
|
|
106 |
{:else}
|
107 |
<div
|
108 |
id="commands-container"
|
109 |
-
class="
|
110 |
>
|
111 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
112 |
<div
|
|
|
106 |
{:else}
|
107 |
<div
|
108 |
id="commands-container"
|
109 |
+
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
110 |
>
|
111 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
112 |
<div
|
src/lib/components/chat/MessageInput/Commands/Knowledge.svelte
CHANGED
@@ -159,7 +159,7 @@
|
|
159 |
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
160 |
<div
|
161 |
id="commands-container"
|
162 |
-
class="
|
163 |
>
|
164 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
165 |
<div
|
|
|
159 |
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
160 |
<div
|
161 |
id="commands-container"
|
162 |
+
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
163 |
>
|
164 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
165 |
<div
|
src/lib/components/chat/MessageInput/Commands/Models.svelte
CHANGED
@@ -68,7 +68,7 @@
|
|
68 |
{#if filteredItems.length > 0}
|
69 |
<div
|
70 |
id="commands-container"
|
71 |
-
class="
|
72 |
>
|
73 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
74 |
<div
|
|
|
68 |
{#if filteredItems.length > 0}
|
69 |
<div
|
70 |
id="commands-container"
|
71 |
+
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
72 |
>
|
73 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
74 |
<div
|
src/lib/components/chat/MessageInput/Commands/Prompts.svelte
CHANGED
@@ -137,7 +137,7 @@
|
|
137 |
{#if filteredPrompts.length > 0}
|
138 |
<div
|
139 |
id="commands-container"
|
140 |
-
class="
|
141 |
>
|
142 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
143 |
<div
|
|
|
137 |
{#if filteredPrompts.length > 0}
|
138 |
<div
|
139 |
id="commands-container"
|
140 |
+
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
141 |
>
|
142 |
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
143 |
<div
|
src/lib/components/chat/Messages/Error.svelte
CHANGED
@@ -4,9 +4,9 @@
|
|
4 |
export let content = '';
|
5 |
</script>
|
6 |
|
7 |
-
<div class="flex my-2 gap-2.5 border px-4 py-3 border-red-
|
8 |
<div class=" self-start mt-0.5">
|
9 |
-
<Info className="size-5" />
|
10 |
</div>
|
11 |
|
12 |
<div class=" self-center text-sm">
|
|
|
4 |
export let content = '';
|
5 |
</script>
|
6 |
|
7 |
+
<div class="flex my-2 gap-2.5 border px-4 py-3 border-red-600/10 bg-red-600/10 rounded-lg">
|
8 |
<div class=" self-start mt-0.5">
|
9 |
+
<Info className="size-5 text-red-700 dark:text-red-400" />
|
10 |
</div>
|
11 |
|
12 |
<div class=" self-center text-sm">
|
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte
CHANGED
@@ -60,7 +60,7 @@
|
|
60 |
<!-- {JSON.stringify(tokens)} -->
|
61 |
{#each tokens as token, tokenIdx (tokenIdx)}
|
62 |
{#if token.type === 'hr'}
|
63 |
-
<hr />
|
64 |
{:else if token.type === 'heading'}
|
65 |
<svelte:element this={headerComponent(token.depth)}>
|
66 |
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
|
@@ -89,11 +89,9 @@
|
|
89 |
{/if}
|
90 |
{:else if token.type === 'table'}
|
91 |
<div class="relative w-full group">
|
92 |
-
<div
|
93 |
-
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-lg"
|
94 |
-
>
|
95 |
<table
|
96 |
-
class="
|
97 |
>
|
98 |
<thead
|
99 |
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
|
@@ -102,15 +100,17 @@
|
|
102 |
{#each token.header as header, headerIdx}
|
103 |
<th
|
104 |
scope="col"
|
105 |
-
class="!px-
|
106 |
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
107 |
>
|
108 |
-
<div class="flex gap-1.5
|
109 |
-
<
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
114 |
</div>
|
115 |
</th>
|
116 |
{/each}
|
@@ -121,10 +121,10 @@
|
|
121 |
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
122 |
{#each row ?? [] as cell, cellIdx}
|
123 |
<td
|
124 |
-
class="!px-
|
125 |
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
|
126 |
>
|
127 |
-
<div class="flex">
|
128 |
<MarkdownInlineTokens
|
129 |
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
130 |
tokens={cell.tokens}
|
|
|
60 |
<!-- {JSON.stringify(tokens)} -->
|
61 |
{#each tokens as token, tokenIdx (tokenIdx)}
|
62 |
{#if token.type === 'hr'}
|
63 |
+
<hr class=" border-gray-50 dark:border-gray-850" />
|
64 |
{:else if token.type === 'heading'}
|
65 |
<svelte:element this={headerComponent(token.depth)}>
|
66 |
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
|
|
|
89 |
{/if}
|
90 |
{:else if token.type === 'table'}
|
91 |
<div class="relative w-full group">
|
92 |
+
<div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
|
|
|
|
|
93 |
<table
|
94 |
+
class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
|
95 |
>
|
96 |
<thead
|
97 |
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
|
|
|
100 |
{#each token.header as header, headerIdx}
|
101 |
<th
|
102 |
scope="col"
|
103 |
+
class="!px-3 !py-1.5 cursor-pointer select-none border border-gray-50 dark:border-gray-850"
|
104 |
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
105 |
>
|
106 |
+
<div class="flex flex-col gap-1.5 text-left">
|
107 |
+
<div class="flex-shrink-0 break-normal">
|
108 |
+
<MarkdownInlineTokens
|
109 |
+
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
110 |
+
tokens={header.tokens}
|
111 |
+
{onSourceClick}
|
112 |
+
/>
|
113 |
+
</div>
|
114 |
</div>
|
115 |
</th>
|
116 |
{/each}
|
|
|
121 |
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
122 |
{#each row ?? [] as cell, cellIdx}
|
123 |
<td
|
124 |
+
class="!px-3 !py-1.5 text-gray-900 dark:text-white w-max border border-gray-50 dark:border-gray-850"
|
125 |
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
|
126 |
>
|
127 |
+
<div class="flex flex-col break-normal">
|
128 |
<MarkdownInlineTokens
|
129 |
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
130 |
tokens={cell.tokens}
|
src/lib/components/chat/Messages/ResponseMessage.svelte
CHANGED
@@ -658,7 +658,7 @@
|
|
658 |
/>
|
659 |
{/if}
|
660 |
|
661 |
-
{#if message
|
662 |
<Error content={message?.error?.content ?? message.content} />
|
663 |
{/if}
|
664 |
|
|
|
658 |
/>
|
659 |
{/if}
|
660 |
|
661 |
+
{#if message?.error}
|
662 |
<Error content={message?.error?.content ?? message.content} />
|
663 |
{/if}
|
664 |
|
src/lib/components/chat/ModelSelector.svelte
CHANGED
@@ -5,9 +5,7 @@
|
|
5 |
import Selector from './ModelSelector/Selector.svelte';
|
6 |
import Tooltip from '../common/Tooltip.svelte';
|
7 |
|
8 |
-
import { setDefaultModels } from '$lib/apis/configs';
|
9 |
import { updateUserSettings } from '$lib/apis/users';
|
10 |
-
|
11 |
const i18n = getContext('i18n');
|
12 |
|
13 |
export let selectedModels = [''];
|
|
|
5 |
import Selector from './ModelSelector/Selector.svelte';
|
6 |
import Tooltip from '../common/Tooltip.svelte';
|
7 |
|
|
|
8 |
import { updateUserSettings } from '$lib/apis/users';
|
|
|
9 |
const i18n = getContext('i18n');
|
10 |
|
11 |
export let selectedModels = [''];
|
src/lib/components/chat/Settings/General.svelte
CHANGED
@@ -55,6 +55,7 @@
|
|
55 |
mirostat_tau: null,
|
56 |
top_k: null,
|
57 |
top_p: null,
|
|
|
58 |
stop: null,
|
59 |
tfs_z: null,
|
60 |
num_ctx: null,
|
@@ -340,6 +341,7 @@
|
|
340 |
mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined,
|
341 |
top_k: params.top_k !== null ? params.top_k : undefined,
|
342 |
top_p: params.top_p !== null ? params.top_p : undefined,
|
|
|
343 |
tfs_z: params.tfs_z !== null ? params.tfs_z : undefined,
|
344 |
num_ctx: params.num_ctx !== null ? params.num_ctx : undefined,
|
345 |
num_batch: params.num_batch !== null ? params.num_batch : undefined,
|
|
|
55 |
mirostat_tau: null,
|
56 |
top_k: null,
|
57 |
top_p: null,
|
58 |
+
min_p: null,
|
59 |
stop: null,
|
60 |
tfs_z: null,
|
61 |
num_ctx: null,
|
|
|
341 |
mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined,
|
342 |
top_k: params.top_k !== null ? params.top_k : undefined,
|
343 |
top_p: params.top_p !== null ? params.top_p : undefined,
|
344 |
+
min_p: params.min_p !== null ? params.min_p : undefined,
|
345 |
tfs_z: params.tfs_z !== null ? params.tfs_z : undefined,
|
346 |
num_ctx: params.num_ctx !== null ? params.num_ctx : undefined,
|
347 |
num_batch: params.num_batch !== null ? params.num_batch : undefined,
|
src/lib/components/common/FileItem.svelte
CHANGED
@@ -5,6 +5,7 @@
|
|
5 |
import FileItemModal from './FileItemModal.svelte';
|
6 |
import GarbageBin from '../icons/GarbageBin.svelte';
|
7 |
import Spinner from './Spinner.svelte';
|
|
|
8 |
|
9 |
const i18n = getContext('i18n');
|
10 |
const dispatch = createEventDispatcher();
|
@@ -18,6 +19,7 @@
|
|
18 |
|
19 |
export let item = null;
|
20 |
export let edit = false;
|
|
|
21 |
|
22 |
export let name: string;
|
23 |
export let type: string;
|
@@ -31,7 +33,9 @@
|
|
31 |
{/if}
|
32 |
|
33 |
<button
|
34 |
-
class="relative group p-1.5 {className} flex items-center {colorClassName}
|
|
|
|
|
35 |
type="button"
|
36 |
on:click={async () => {
|
37 |
if (item?.file?.data?.content) {
|
@@ -49,48 +53,66 @@
|
|
49 |
dispatch('click');
|
50 |
}}
|
51 |
>
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
<Spinner />
|
71 |
-
{/if}
|
72 |
-
</div>
|
73 |
-
|
74 |
-
<div class="flex flex-col justify-center -space-y-0.5 ml-1 px-2.5 w-full">
|
75 |
-
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
|
76 |
-
{name}
|
77 |
-
</div>
|
78 |
-
|
79 |
-
<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
|
80 |
-
{#if type === 'file'}
|
81 |
-
{$i18n.t('File')}
|
82 |
-
{:else if type === 'doc'}
|
83 |
-
{$i18n.t('Document')}
|
84 |
-
{:else if type === 'collection'}
|
85 |
-
{$i18n.t('Collection')}
|
86 |
{:else}
|
87 |
-
<
|
88 |
-
{/if}
|
89 |
-
{#if size}
|
90 |
-
<span class="capitalize">{formatFileSize(size)}</span>
|
91 |
{/if}
|
92 |
</div>
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
{#if dismissible}
|
96 |
<div class=" absolute -top-1 -right-1">
|
|
|
5 |
import FileItemModal from './FileItemModal.svelte';
|
6 |
import GarbageBin from '../icons/GarbageBin.svelte';
|
7 |
import Spinner from './Spinner.svelte';
|
8 |
+
import Tooltip from './Tooltip.svelte';
|
9 |
|
10 |
const i18n = getContext('i18n');
|
11 |
const dispatch = createEventDispatcher();
|
|
|
19 |
|
20 |
export let item = null;
|
21 |
export let edit = false;
|
22 |
+
export let small = false;
|
23 |
|
24 |
export let name: string;
|
25 |
export let type: string;
|
|
|
33 |
{/if}
|
34 |
|
35 |
<button
|
36 |
+
class="relative group p-1.5 {className} flex items-center gap-1 {colorClassName} {small
|
37 |
+
? 'rounded-xl'
|
38 |
+
: 'rounded-2xl'} text-left"
|
39 |
type="button"
|
40 |
on:click={async () => {
|
41 |
if (item?.file?.data?.content) {
|
|
|
53 |
dispatch('click');
|
54 |
}}
|
55 |
>
|
56 |
+
{#if !small}
|
57 |
+
<div class="p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl">
|
58 |
+
{#if !loading}
|
59 |
+
<svg
|
60 |
+
xmlns="http://www.w3.org/2000/svg"
|
61 |
+
viewBox="0 0 24 24"
|
62 |
+
fill="currentColor"
|
63 |
+
class=" size-5"
|
64 |
+
>
|
65 |
+
<path
|
66 |
+
fill-rule="evenodd"
|
67 |
+
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"
|
68 |
+
clip-rule="evenodd"
|
69 |
+
/>
|
70 |
+
<path
|
71 |
+
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"
|
72 |
+
/>
|
73 |
+
</svg>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
{:else}
|
75 |
+
<Spinner />
|
|
|
|
|
|
|
76 |
{/if}
|
77 |
</div>
|
78 |
+
{/if}
|
79 |
+
|
80 |
+
{#if !small}
|
81 |
+
<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
|
82 |
+
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
|
83 |
+
{name}
|
84 |
+
</div>
|
85 |
+
|
86 |
+
<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
|
87 |
+
{#if type === 'file'}
|
88 |
+
{$i18n.t('File')}
|
89 |
+
{:else if type === 'doc'}
|
90 |
+
{$i18n.t('Document')}
|
91 |
+
{:else if type === 'collection'}
|
92 |
+
{$i18n.t('Collection')}
|
93 |
+
{:else}
|
94 |
+
<span class=" capitalize line-clamp-1">{type}</span>
|
95 |
+
{/if}
|
96 |
+
{#if size}
|
97 |
+
<span class="capitalize">{formatFileSize(size)}</span>
|
98 |
+
{/if}
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
{:else}
|
102 |
+
<Tooltip content={name} className="flex flex-col w-full" placement="top-start">
|
103 |
+
<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
|
104 |
+
<div class=" dark:text-gray-100 text-sm flex justify-between items-center">
|
105 |
+
{#if loading}
|
106 |
+
<div class=" shrink-0 mr-2">
|
107 |
+
<Spinner className="size-4" />
|
108 |
+
</div>
|
109 |
+
{/if}
|
110 |
+
<div class="font-medium line-clamp-1 flex-1">{name}</div>
|
111 |
+
<div class="text-gray-500 text-xs capitalize shrink-0">{formatFileSize(size)}</div>
|
112 |
+
</div>
|
113 |
+
</div>
|
114 |
+
</Tooltip>
|
115 |
+
{/if}
|
116 |
|
117 |
{#if dismissible}
|
118 |
<div class=" absolute -top-1 -right-1">
|
src/lib/components/common/Modal.svelte
CHANGED
@@ -6,7 +6,9 @@
|
|
6 |
|
7 |
export let show = true;
|
8 |
export let size = 'md';
|
9 |
-
|
|
|
|
|
10 |
|
11 |
let modalElement = null;
|
12 |
let mounted = false;
|
@@ -65,7 +67,7 @@
|
|
65 |
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
66 |
<div
|
67 |
bind:this={modalElement}
|
68 |
-
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[9999] overflow-
|
69 |
in:fade={{ duration: 10 }}
|
70 |
on:mousedown={() => {
|
71 |
show = false;
|
@@ -74,7 +76,7 @@
|
|
74 |
<div
|
75 |
class=" m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
|
76 |
? 'mx-2'
|
77 |
-
: ''} shadow-3xl
|
78 |
in:flyAndScale
|
79 |
on:mousedown={(e) => {
|
80 |
e.stopPropagation();
|
|
|
6 |
|
7 |
export let show = true;
|
8 |
export let size = 'md';
|
9 |
+
|
10 |
+
export let containerClassName = 'p-3';
|
11 |
+
export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl';
|
12 |
|
13 |
let modalElement = null;
|
14 |
let mounted = false;
|
|
|
67 |
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
68 |
<div
|
69 |
bind:this={modalElement}
|
70 |
+
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-[9999] overflow-y-auto overscroll-contain"
|
71 |
in:fade={{ duration: 10 }}
|
72 |
on:mousedown={() => {
|
73 |
show = false;
|
|
|
76 |
<div
|
77 |
class=" m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
|
78 |
? 'mx-2'
|
79 |
+
: ''} shadow-3xl min-h-fit scrollbar-hidden {className}"
|
80 |
in:flyAndScale
|
81 |
on:mousedown={(e) => {
|
82 |
e.stopPropagation();
|
src/lib/components/common/RichTextInput.svelte
CHANGED
@@ -2,7 +2,8 @@
|
|
2 |
import { marked } from 'marked';
|
3 |
import TurndownService from 'turndown';
|
4 |
const turndownService = new TurndownService({
|
5 |
-
codeBlockStyle: 'fenced'
|
|
|
6 |
});
|
7 |
turndownService.escape = (string) => string;
|
8 |
|
@@ -10,16 +11,18 @@
|
|
10 |
import { createEventDispatcher } from 'svelte';
|
11 |
const eventDispatch = createEventDispatcher();
|
12 |
|
13 |
-
import { EditorState, Plugin, TextSelection } from 'prosemirror-state';
|
|
|
14 |
|
15 |
import { Editor } from '@tiptap/core';
|
16 |
|
|
|
|
|
17 |
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
18 |
import Placeholder from '@tiptap/extension-placeholder';
|
19 |
import Highlight from '@tiptap/extension-highlight';
|
20 |
import Typography from '@tiptap/extension-typography';
|
21 |
import StarterKit from '@tiptap/starter-kit';
|
22 |
-
|
23 |
import { all, createLowlight } from 'lowlight';
|
24 |
|
25 |
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
@@ -32,6 +35,9 @@
|
|
32 |
export let value = '';
|
33 |
export let id = '';
|
34 |
|
|
|
|
|
|
|
35 |
export let messageInput = false;
|
36 |
export let shiftEnter = false;
|
37 |
export let largeTextAsFile = false;
|
@@ -120,10 +126,23 @@
|
|
120 |
};
|
121 |
|
122 |
onMount(async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
async function tryParse(value, attempts = 3, interval = 100) {
|
124 |
try {
|
125 |
// Try parsing the value
|
126 |
-
return marked.parse(value)
|
|
|
|
|
127 |
} catch (error) {
|
128 |
// If no attempts remain, fallback to plain text
|
129 |
if (attempts <= 1) {
|
@@ -147,20 +166,48 @@
|
|
147 |
}),
|
148 |
Highlight,
|
149 |
Typography,
|
150 |
-
Placeholder.configure({ placeholder })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
],
|
152 |
content: content,
|
153 |
-
autofocus: true,
|
154 |
onTransaction: () => {
|
155 |
// force re-render so `editor.isActive` works as expected
|
156 |
editor = editor;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
|
158 |
-
const newValue = turndownService.turndown(editor.getHTML());
|
159 |
if (value !== newValue) {
|
160 |
value = newValue;
|
161 |
|
162 |
-
if
|
163 |
-
|
|
|
|
|
|
|
164 |
}
|
165 |
}
|
166 |
},
|
@@ -171,22 +218,21 @@
|
|
171 |
eventDispatch('focus', { event });
|
172 |
return false;
|
173 |
},
|
174 |
-
|
175 |
-
eventDispatch('
|
176 |
return false;
|
177 |
},
|
178 |
-
|
179 |
keydown: (view, event) => {
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
|
|
|
|
186 |
}
|
187 |
-
}
|
188 |
|
189 |
-
if (messageInput) {
|
190 |
if (event.key === 'Enter') {
|
191 |
// Check if the current selection is inside a structured block (like codeBlock or list)
|
192 |
const { state } = view;
|
@@ -217,22 +263,12 @@
|
|
217 |
|
218 |
// Handle shift + Enter for a line break
|
219 |
if (shiftEnter) {
|
220 |
-
if (event.key === 'Enter' && event.shiftKey) {
|
221 |
editor.commands.setHardBreak(); // Insert a hard break
|
222 |
view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor
|
223 |
event.preventDefault();
|
224 |
return true;
|
225 |
}
|
226 |
-
if (event.key === 'Enter') {
|
227 |
-
eventDispatch('enter', { event });
|
228 |
-
event.preventDefault();
|
229 |
-
return true;
|
230 |
-
}
|
231 |
-
}
|
232 |
-
if (event.key === 'Enter') {
|
233 |
-
eventDispatch('enter', { event });
|
234 |
-
event.preventDefault();
|
235 |
-
return true;
|
236 |
}
|
237 |
}
|
238 |
eventDispatch('keydown', { event });
|
@@ -286,7 +322,9 @@
|
|
286 |
}
|
287 |
});
|
288 |
|
289 |
-
|
|
|
|
|
290 |
});
|
291 |
|
292 |
onDestroy(() => {
|
@@ -296,8 +334,23 @@
|
|
296 |
});
|
297 |
|
298 |
// Update the editor content if the external `value` changes
|
299 |
-
$: if (
|
300 |
-
editor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
301 |
selectTemplate();
|
302 |
}
|
303 |
</script>
|
|
|
2 |
import { marked } from 'marked';
|
3 |
import TurndownService from 'turndown';
|
4 |
const turndownService = new TurndownService({
|
5 |
+
codeBlockStyle: 'fenced',
|
6 |
+
headingStyle: 'atx'
|
7 |
});
|
8 |
turndownService.escape = (string) => string;
|
9 |
|
|
|
11 |
import { createEventDispatcher } from 'svelte';
|
12 |
const eventDispatch = createEventDispatcher();
|
13 |
|
14 |
+
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
15 |
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
16 |
|
17 |
import { Editor } from '@tiptap/core';
|
18 |
|
19 |
+
import { AIAutocompletion } from './RichTextInput/AutoCompletion.js';
|
20 |
+
|
21 |
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
22 |
import Placeholder from '@tiptap/extension-placeholder';
|
23 |
import Highlight from '@tiptap/extension-highlight';
|
24 |
import Typography from '@tiptap/extension-typography';
|
25 |
import StarterKit from '@tiptap/starter-kit';
|
|
|
26 |
import { all, createLowlight } from 'lowlight';
|
27 |
|
28 |
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
|
|
35 |
export let value = '';
|
36 |
export let id = '';
|
37 |
|
38 |
+
export let preserveBreaks = false;
|
39 |
+
export let generateAutoCompletion: Function = async () => null;
|
40 |
+
export let autocomplete = false;
|
41 |
export let messageInput = false;
|
42 |
export let shiftEnter = false;
|
43 |
export let largeTextAsFile = false;
|
|
|
126 |
};
|
127 |
|
128 |
onMount(async () => {
|
129 |
+
console.log(value);
|
130 |
+
|
131 |
+
if (preserveBreaks) {
|
132 |
+
turndownService.addRule('preserveBreaks', {
|
133 |
+
filter: 'br', // Target <br> elements
|
134 |
+
replacement: function (content) {
|
135 |
+
return '<br/>';
|
136 |
+
}
|
137 |
+
});
|
138 |
+
}
|
139 |
+
|
140 |
async function tryParse(value, attempts = 3, interval = 100) {
|
141 |
try {
|
142 |
// Try parsing the value
|
143 |
+
return marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), {
|
144 |
+
breaks: false
|
145 |
+
});
|
146 |
} catch (error) {
|
147 |
// If no attempts remain, fallback to plain text
|
148 |
if (attempts <= 1) {
|
|
|
166 |
}),
|
167 |
Highlight,
|
168 |
Typography,
|
169 |
+
Placeholder.configure({ placeholder }),
|
170 |
+
...(autocomplete
|
171 |
+
? [
|
172 |
+
AIAutocompletion.configure({
|
173 |
+
generateCompletion: async (text) => {
|
174 |
+
if (text.trim().length === 0) {
|
175 |
+
return null;
|
176 |
+
}
|
177 |
+
|
178 |
+
const suggestion = await generateAutoCompletion(text).catch(() => null);
|
179 |
+
if (!suggestion || suggestion.trim().length === 0) {
|
180 |
+
return null;
|
181 |
+
}
|
182 |
+
|
183 |
+
return suggestion;
|
184 |
+
}
|
185 |
+
})
|
186 |
+
]
|
187 |
+
: [])
|
188 |
],
|
189 |
content: content,
|
190 |
+
autofocus: messageInput ? true : false,
|
191 |
onTransaction: () => {
|
192 |
// force re-render so `editor.isActive` works as expected
|
193 |
editor = editor;
|
194 |
+
const newValue = turndownService
|
195 |
+
.turndown(
|
196 |
+
(preserveBreaks
|
197 |
+
? editor.getHTML().replace(/<p><\/p>/g, '<br/>')
|
198 |
+
: editor.getHTML()
|
199 |
+
).replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
|
200 |
+
)
|
201 |
+
.replace(/\u00a0/g, ' ');
|
202 |
|
|
|
203 |
if (value !== newValue) {
|
204 |
value = newValue;
|
205 |
|
206 |
+
// check if the node is paragraph as well
|
207 |
+
if (editor.isActive('paragraph')) {
|
208 |
+
if (value === '') {
|
209 |
+
editor.commands.clearContent();
|
210 |
+
}
|
211 |
}
|
212 |
}
|
213 |
},
|
|
|
218 |
eventDispatch('focus', { event });
|
219 |
return false;
|
220 |
},
|
221 |
+
keyup: (view, event) => {
|
222 |
+
eventDispatch('keyup', { event });
|
223 |
return false;
|
224 |
},
|
|
|
225 |
keydown: (view, event) => {
|
226 |
+
if (messageInput) {
|
227 |
+
// Handle Tab Key
|
228 |
+
if (event.key === 'Tab') {
|
229 |
+
const handled = selectNextTemplate(view.state, view.dispatch);
|
230 |
+
if (handled) {
|
231 |
+
event.preventDefault();
|
232 |
+
return true;
|
233 |
+
}
|
234 |
}
|
|
|
235 |
|
|
|
236 |
if (event.key === 'Enter') {
|
237 |
// Check if the current selection is inside a structured block (like codeBlock or list)
|
238 |
const { state } = view;
|
|
|
263 |
|
264 |
// Handle shift + Enter for a line break
|
265 |
if (shiftEnter) {
|
266 |
+
if (event.key === 'Enter' && event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
267 |
editor.commands.setHardBreak(); // Insert a hard break
|
268 |
view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor
|
269 |
event.preventDefault();
|
270 |
return true;
|
271 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
}
|
273 |
}
|
274 |
eventDispatch('keydown', { event });
|
|
|
322 |
}
|
323 |
});
|
324 |
|
325 |
+
if (messageInput) {
|
326 |
+
selectTemplate();
|
327 |
+
}
|
328 |
});
|
329 |
|
330 |
onDestroy(() => {
|
|
|
334 |
});
|
335 |
|
336 |
// Update the editor content if the external `value` changes
|
337 |
+
$: if (
|
338 |
+
editor &&
|
339 |
+
value !==
|
340 |
+
turndownService
|
341 |
+
.turndown(
|
342 |
+
(preserveBreaks
|
343 |
+
? editor.getHTML().replace(/<p><\/p>/g, '<br/>')
|
344 |
+
: editor.getHTML()
|
345 |
+
).replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
|
346 |
+
)
|
347 |
+
.replace(/\u00a0/g, ' ')
|
348 |
+
) {
|
349 |
+
editor.commands.setContent(
|
350 |
+
marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), {
|
351 |
+
breaks: false
|
352 |
+
})
|
353 |
+
); // Update editor content
|
354 |
selectTemplate();
|
355 |
}
|
356 |
</script>
|
src/lib/components/common/RichTextInput/AutoCompletion.js
ADDED
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/*
|
2 |
+
Here we initialize the plugin with keyword mapping.
|
3 |
+
Intended to handle user interactions seamlessly.
|
4 |
+
|
5 |
+
Observe the keydown events for proactive suggestions.
|
6 |
+
Provide a mechanism for accepting AI suggestions.
|
7 |
+
Evaluate each input change with debounce logic.
|
8 |
+
Next, we implement touch and mouse interactions.
|
9 |
+
|
10 |
+
Anchor the user experience to intuitive behavior.
|
11 |
+
Intelligently reset suggestions on new input.
|
12 |
+
*/
|
13 |
+
|
14 |
+
import { Extension } from '@tiptap/core';
|
15 |
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
16 |
+
|
17 |
+
export const AIAutocompletion = Extension.create({
|
18 |
+
name: 'aiAutocompletion',
|
19 |
+
|
20 |
+
addOptions() {
|
21 |
+
return {
|
22 |
+
generateCompletion: () => Promise.resolve(''),
|
23 |
+
debounceTime: 1000
|
24 |
+
};
|
25 |
+
},
|
26 |
+
|
27 |
+
addGlobalAttributes() {
|
28 |
+
return [
|
29 |
+
{
|
30 |
+
types: ['paragraph'],
|
31 |
+
attributes: {
|
32 |
+
class: {
|
33 |
+
default: null,
|
34 |
+
parseHTML: (element) => element.getAttribute('class'),
|
35 |
+
renderHTML: (attributes) => {
|
36 |
+
if (!attributes.class) return {};
|
37 |
+
return { class: attributes.class };
|
38 |
+
}
|
39 |
+
},
|
40 |
+
'data-prompt': {
|
41 |
+
default: null,
|
42 |
+
parseHTML: (element) => element.getAttribute('data-prompt'),
|
43 |
+
renderHTML: (attributes) => {
|
44 |
+
if (!attributes['data-prompt']) return {};
|
45 |
+
return { 'data-prompt': attributes['data-prompt'] };
|
46 |
+
}
|
47 |
+
},
|
48 |
+
'data-suggestion': {
|
49 |
+
default: null,
|
50 |
+
parseHTML: (element) => element.getAttribute('data-suggestion'),
|
51 |
+
renderHTML: (attributes) => {
|
52 |
+
if (!attributes['data-suggestion']) return {};
|
53 |
+
return { 'data-suggestion': attributes['data-suggestion'] };
|
54 |
+
}
|
55 |
+
}
|
56 |
+
}
|
57 |
+
}
|
58 |
+
];
|
59 |
+
},
|
60 |
+
|
61 |
+
addProseMirrorPlugins() {
|
62 |
+
let debounceTimer = null;
|
63 |
+
let loading = false;
|
64 |
+
|
65 |
+
let touchStartX = 0;
|
66 |
+
let touchStartY = 0;
|
67 |
+
|
68 |
+
return [
|
69 |
+
new Plugin({
|
70 |
+
key: new PluginKey('aiAutocompletion'),
|
71 |
+
props: {
|
72 |
+
handleKeyDown: (view, event) => {
|
73 |
+
const { state, dispatch } = view;
|
74 |
+
const { selection } = state;
|
75 |
+
const { $head } = selection;
|
76 |
+
|
77 |
+
if ($head.parent.type.name !== 'paragraph') return false;
|
78 |
+
|
79 |
+
const node = $head.parent;
|
80 |
+
|
81 |
+
if (event.key === 'Tab') {
|
82 |
+
// if (!node.attrs['data-suggestion']) {
|
83 |
+
// // Generate completion
|
84 |
+
// if (loading) return true
|
85 |
+
// loading = true
|
86 |
+
// const prompt = node.textContent
|
87 |
+
// this.options.generateCompletion(prompt).then(suggestion => {
|
88 |
+
// if (suggestion && suggestion.trim() !== '') {
|
89 |
+
// dispatch(state.tr.setNodeMarkup($head.before(), null, {
|
90 |
+
// ...node.attrs,
|
91 |
+
// class: 'ai-autocompletion',
|
92 |
+
// 'data-prompt': prompt,
|
93 |
+
// 'data-suggestion': suggestion,
|
94 |
+
// }))
|
95 |
+
// }
|
96 |
+
// // If suggestion is empty or null, do nothing
|
97 |
+
// }).finally(() => {
|
98 |
+
// loading = false
|
99 |
+
// })
|
100 |
+
// }
|
101 |
+
|
102 |
+
if (node.attrs['data-suggestion']) {
|
103 |
+
// Accept suggestion
|
104 |
+
const suggestion = node.attrs['data-suggestion'];
|
105 |
+
dispatch(
|
106 |
+
state.tr.insertText(suggestion, $head.pos).setNodeMarkup($head.before(), null, {
|
107 |
+
...node.attrs,
|
108 |
+
class: null,
|
109 |
+
'data-prompt': null,
|
110 |
+
'data-suggestion': null
|
111 |
+
})
|
112 |
+
);
|
113 |
+
return true;
|
114 |
+
}
|
115 |
+
} else {
|
116 |
+
if (node.attrs['data-suggestion']) {
|
117 |
+
// Reset suggestion on any other key press
|
118 |
+
dispatch(
|
119 |
+
state.tr.setNodeMarkup($head.before(), null, {
|
120 |
+
...node.attrs,
|
121 |
+
class: null,
|
122 |
+
'data-prompt': null,
|
123 |
+
'data-suggestion': null
|
124 |
+
})
|
125 |
+
);
|
126 |
+
}
|
127 |
+
|
128 |
+
// Start debounce logic for AI generation only if the cursor is at the end of the paragraph
|
129 |
+
if (selection.empty && $head.pos === $head.end()) {
|
130 |
+
// Set up debounce for AI generation
|
131 |
+
if (this.options.debounceTime !== null) {
|
132 |
+
clearTimeout(debounceTimer);
|
133 |
+
|
134 |
+
// Capture current position
|
135 |
+
const currentPos = $head.before();
|
136 |
+
|
137 |
+
debounceTimer = setTimeout(() => {
|
138 |
+
const newState = view.state;
|
139 |
+
const newSelection = newState.selection;
|
140 |
+
const newNode = newState.doc.nodeAt(currentPos);
|
141 |
+
|
142 |
+
// Check if the node still exists and is still a paragraph
|
143 |
+
if (
|
144 |
+
newNode &&
|
145 |
+
newNode.type.name === 'paragraph' &&
|
146 |
+
newSelection.$head.pos === newSelection.$head.end() &&
|
147 |
+
newSelection.$head.pos === currentPos + newNode.nodeSize - 1
|
148 |
+
) {
|
149 |
+
const prompt = newNode.textContent;
|
150 |
+
|
151 |
+
if (prompt.trim() !== '') {
|
152 |
+
if (loading) return true;
|
153 |
+
loading = true;
|
154 |
+
this.options
|
155 |
+
.generateCompletion(prompt)
|
156 |
+
.then((suggestion) => {
|
157 |
+
if (suggestion && suggestion.trim() !== '') {
|
158 |
+
if (
|
159 |
+
view.state.selection.$head.pos === view.state.selection.$head.end()
|
160 |
+
) {
|
161 |
+
view.dispatch(
|
162 |
+
newState.tr.setNodeMarkup(currentPos, null, {
|
163 |
+
...newNode.attrs,
|
164 |
+
class: 'ai-autocompletion',
|
165 |
+
'data-prompt': prompt,
|
166 |
+
'data-suggestion': suggestion
|
167 |
+
})
|
168 |
+
);
|
169 |
+
}
|
170 |
+
}
|
171 |
+
})
|
172 |
+
.finally(() => {
|
173 |
+
loading = false;
|
174 |
+
});
|
175 |
+
}
|
176 |
+
}
|
177 |
+
}, this.options.debounceTime);
|
178 |
+
}
|
179 |
+
}
|
180 |
+
}
|
181 |
+
return false;
|
182 |
+
},
|
183 |
+
handleDOMEvents: {
|
184 |
+
touchstart: (view, event) => {
|
185 |
+
touchStartX = event.touches[0].clientX;
|
186 |
+
touchStartY = event.touches[0].clientY;
|
187 |
+
return false;
|
188 |
+
},
|
189 |
+
touchend: (view, event) => {
|
190 |
+
const touchEndX = event.changedTouches[0].clientX;
|
191 |
+
const touchEndY = event.changedTouches[0].clientY;
|
192 |
+
|
193 |
+
const deltaX = touchEndX - touchStartX;
|
194 |
+
const deltaY = touchEndY - touchStartY;
|
195 |
+
|
196 |
+
// Check if the swipe was primarily horizontal and to the right
|
197 |
+
if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 50) {
|
198 |
+
const { state, dispatch } = view;
|
199 |
+
const { selection } = state;
|
200 |
+
const { $head } = selection;
|
201 |
+
const node = $head.parent;
|
202 |
+
|
203 |
+
if (node.type.name === 'paragraph' && node.attrs['data-suggestion']) {
|
204 |
+
const suggestion = node.attrs['data-suggestion'];
|
205 |
+
dispatch(
|
206 |
+
state.tr.insertText(suggestion, $head.pos).setNodeMarkup($head.before(), null, {
|
207 |
+
...node.attrs,
|
208 |
+
class: null,
|
209 |
+
'data-prompt': null,
|
210 |
+
'data-suggestion': null
|
211 |
+
})
|
212 |
+
);
|
213 |
+
return true;
|
214 |
+
}
|
215 |
+
}
|
216 |
+
return false;
|
217 |
+
},
|
218 |
+
// Add mousedown behavior
|
219 |
+
// mouseup: (view, event) => {
|
220 |
+
// const { state, dispatch } = view;
|
221 |
+
// const { selection } = state;
|
222 |
+
// const { $head } = selection;
|
223 |
+
// const node = $head.parent;
|
224 |
+
|
225 |
+
// // Reset debounce timer on mouse click
|
226 |
+
// clearTimeout(debounceTimer);
|
227 |
+
|
228 |
+
// // If a suggestion exists and the cursor moves, remove the suggestion
|
229 |
+
// if (
|
230 |
+
// node.type.name === 'paragraph' &&
|
231 |
+
// node.attrs['data-suggestion'] &&
|
232 |
+
// view.state.selection.$head.pos !== view.state.selection.$head.end()
|
233 |
+
// ) {
|
234 |
+
// dispatch(
|
235 |
+
// state.tr.setNodeMarkup($head.before(), null, {
|
236 |
+
// ...node.attrs,
|
237 |
+
// class: null,
|
238 |
+
// 'data-prompt': null,
|
239 |
+
// 'data-suggestion': null
|
240 |
+
// })
|
241 |
+
// );
|
242 |
+
// }
|
243 |
+
|
244 |
+
// return false;
|
245 |
+
// }
|
246 |
+
mouseup: (view, event) => {
|
247 |
+
const { state, dispatch } = view;
|
248 |
+
|
249 |
+
// Reset debounce timer on mouse click
|
250 |
+
clearTimeout(debounceTimer);
|
251 |
+
|
252 |
+
// Iterate over all nodes in the document
|
253 |
+
const tr = state.tr;
|
254 |
+
state.doc.descendants((node, pos) => {
|
255 |
+
if (node.type.name === 'paragraph' && node.attrs['data-suggestion']) {
|
256 |
+
// Remove suggestion from this paragraph
|
257 |
+
tr.setNodeMarkup(pos, null, {
|
258 |
+
...node.attrs,
|
259 |
+
class: null,
|
260 |
+
'data-prompt': null,
|
261 |
+
'data-suggestion': null
|
262 |
+
});
|
263 |
+
}
|
264 |
+
});
|
265 |
+
|
266 |
+
// Apply the transaction if any changes were made
|
267 |
+
if (tr.docChanged) {
|
268 |
+
dispatch(tr);
|
269 |
+
}
|
270 |
+
|
271 |
+
return false;
|
272 |
+
}
|
273 |
+
}
|
274 |
+
}
|
275 |
+
})
|
276 |
+
];
|
277 |
+
}
|
278 |
+
});
|
src/lib/components/common/Textarea.svelte
CHANGED
@@ -3,38 +3,65 @@
|
|
3 |
|
4 |
export let value = '';
|
5 |
export let placeholder = '';
|
6 |
-
export let rows = 1;
|
7 |
-
export let required = false;
|
8 |
export let className =
|
9 |
'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none h-full';
|
10 |
|
11 |
let textareaElement;
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
// Adjust height on mount and after setting the element.
|
14 |
onMount(async () => {
|
15 |
await tick();
|
16 |
-
adjustHeight();
|
17 |
});
|
18 |
|
19 |
-
//
|
20 |
-
|
|
|
|
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
// Reset height to calculate the correct scroll height
|
26 |
-
textareaElement.style.height = 'auto';
|
27 |
-
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
|
28 |
-
}
|
29 |
-
};
|
30 |
</script>
|
31 |
|
32 |
-
<
|
|
|
33 |
bind:this={textareaElement}
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
export let value = '';
|
5 |
export let placeholder = '';
|
|
|
|
|
6 |
export let className =
|
7 |
'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none h-full';
|
8 |
|
9 |
let textareaElement;
|
10 |
|
11 |
+
$: if (textareaElement) {
|
12 |
+
if (textareaElement.innerText !== value && value !== '') {
|
13 |
+
textareaElement.innerText = value ?? '';
|
14 |
+
}
|
15 |
+
}
|
16 |
+
|
17 |
// Adjust height on mount and after setting the element.
|
18 |
onMount(async () => {
|
19 |
await tick();
|
|
|
20 |
});
|
21 |
|
22 |
+
// Handle paste event to ensure only plaintext is pasted
|
23 |
+
function handlePaste(event: ClipboardEvent) {
|
24 |
+
event.preventDefault(); // Prevent the default paste action
|
25 |
+
const clipboardData = event.clipboardData?.getData('text/plain'); // Get plaintext from clipboard
|
26 |
|
27 |
+
// Insert plaintext into the textarea
|
28 |
+
document.execCommand('insertText', false, clipboardData);
|
29 |
+
}
|
|
|
|
|
|
|
|
|
|
|
30 |
</script>
|
31 |
|
32 |
+
<div
|
33 |
+
contenteditable="true"
|
34 |
bind:this={textareaElement}
|
35 |
+
class="{className} whitespace-pre-wrap relative {value
|
36 |
+
? !value.trim()
|
37 |
+
? 'placeholder'
|
38 |
+
: ''
|
39 |
+
: 'placeholder'}"
|
40 |
+
style="field-sizing: content; -moz-user-select: text !important;"
|
41 |
+
on:input={() => {
|
42 |
+
const text = textareaElement.innerText;
|
43 |
+
if (text === '\n') {
|
44 |
+
value = '';
|
45 |
+
return;
|
46 |
+
}
|
47 |
+
|
48 |
+
value = text;
|
49 |
+
}}
|
50 |
+
on:paste={handlePaste}
|
51 |
+
data-placeholder={placeholder}
|
52 |
/>
|
53 |
+
|
54 |
+
<style>
|
55 |
+
.placeholder::before {
|
56 |
+
/* abolute */
|
57 |
+
position: absolute;
|
58 |
+
content: attr(data-placeholder);
|
59 |
+
color: #adb5bd;
|
60 |
+
overflow: hidden;
|
61 |
+
display: -webkit-box;
|
62 |
+
-webkit-box-orient: vertical;
|
63 |
+
-webkit-line-clamp: 1;
|
64 |
+
pointer-events: none;
|
65 |
+
touch-action: none;
|
66 |
+
}
|
67 |
+
</style>
|