coolmanx commited on
Commit
b13cb37
·
verified ·
1 Parent(s): 47e51fe

Upload 647 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. CHANGELOG.md +45 -0
  2. CODE_OF_CONDUCT.md +60 -38
  3. backend/open_webui/apps/audio/main.py +5 -5
  4. backend/open_webui/apps/ollama/main.py +40 -15
  5. backend/open_webui/apps/openai/main.py +2 -1
  6. backend/open_webui/apps/retrieval/loaders/youtube.py +21 -2
  7. backend/open_webui/apps/retrieval/main.py +13 -5
  8. backend/open_webui/apps/retrieval/utils.py +1 -1
  9. backend/open_webui/apps/webui/main.py +2 -0
  10. backend/open_webui/apps/webui/models/tools.py +7 -3
  11. backend/open_webui/apps/webui/routers/configs.py +32 -19
  12. backend/open_webui/config.py +81 -3
  13. backend/open_webui/constants.py +1 -0
  14. backend/open_webui/env.py +5 -2
  15. backend/open_webui/main.py +132 -4
  16. backend/open_webui/utils/security_headers.py +7 -0
  17. backend/open_webui/utils/task.py +37 -8
  18. backend/requirements.txt +5 -5
  19. package-lock.json +30 -23
  20. package.json +1 -1
  21. pyproject.toml +5 -5
  22. src/app.css +12 -5
  23. src/app.html +6 -3
  24. src/lib/apis/configs/index.ts +31 -4
  25. src/lib/apis/index.ts +77 -28
  26. src/lib/apis/retrieval/index.ts +1 -0
  27. src/lib/components/admin/Settings/Evaluations.svelte +2 -1
  28. src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte +2 -2
  29. src/lib/components/admin/Settings/Interface.svelte +291 -251
  30. src/lib/components/admin/Settings/Models.svelte +20 -24
  31. src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte +268 -0
  32. src/lib/components/admin/Settings/Models/ModelList.svelte +58 -0
  33. src/lib/components/admin/Settings/WebSearch.svelte +19 -1
  34. src/lib/components/admin/Users/UserList/UserChatsModal.svelte +31 -27
  35. src/lib/components/chat/Chat.svelte +3 -3
  36. src/lib/components/chat/MessageInput.svelte +198 -157
  37. src/lib/components/chat/MessageInput/Commands.svelte +1 -1
  38. src/lib/components/chat/MessageInput/Commands/Knowledge.svelte +1 -1
  39. src/lib/components/chat/MessageInput/Commands/Models.svelte +1 -1
  40. src/lib/components/chat/MessageInput/Commands/Prompts.svelte +1 -1
  41. src/lib/components/chat/Messages/Error.svelte +2 -2
  42. src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +14 -14
  43. src/lib/components/chat/Messages/ResponseMessage.svelte +1 -1
  44. src/lib/components/chat/ModelSelector.svelte +0 -2
  45. src/lib/components/chat/Settings/General.svelte +2 -0
  46. src/lib/components/common/FileItem.svelte +62 -40
  47. src/lib/components/common/Modal.svelte +5 -3
  48. src/lib/components/common/RichTextInput.svelte +87 -34
  49. src/lib/components/common/RichTextInput/AutoCompletion.js +278 -0
  50. 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
- We as members, contributors, and leaders pledge to make participation in our
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 pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
 
 
 
 
 
 
 
 
13
 
14
  ## Our Standards
15
 
16
- Examples of behavior that contribute to a positive environment for our community include:
17
 
18
- - Demonstrating empathy and kindness toward other people
19
- - Being respectful of differing opinions, viewpoints, and experiences
20
- - Giving and gracefully accepting constructive feedback
21
- - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
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 sexualized language or imagery, and sexual attention or advances of any kind
27
- - Trolling, insulting or derogatory comments, and personal or political attacks
28
- - Public or private harassment
29
- - Publishing others' private information, such as a physical or email address, without their explicit permission
30
- - **Spamming of any kind**
31
- - Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
32
- - Other conduct which could reasonably be considered inappropriate in a professional setting
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  ## Enforcement Responsibilities
35
 
36
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
37
 
38
  ## Scope
39
 
40
- This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
 
 
41
 
42
- ## Enforcement
43
 
44
- Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [email protected]. All complaints will be reviewed and investigated promptly and fairly.
45
 
46
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
47
 
48
  ## Enforcement Guidelines
49
 
50
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
 
 
 
 
 
 
 
 
 
51
 
52
- ### 1. Temporary Ban
53
 
54
- **Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
55
 
56
- **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
57
 
58
- ### 2. Permanent Ban
59
 
60
- **Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
61
 
62
- **Consequence**: A permanent ban from any sort of public interaction within the community.
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, 'status', 500),
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, 'status', 500),
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- headers = dict(r.headers)
214
  if content_type:
215
- headers["Content-Type"] = content_type
216
  return StreamingResponse(
217
  r.content,
218
  status_code=r.status,
219
- headers=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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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
- api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
 
 
 
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(self.video_id)
 
 
81
  except Exception as e:
82
- print(e)
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
- return False
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, language=app.state.config.YOUTUBE_LOADER_LANGUAGE
 
 
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
- query=form_data.query,
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
- query=form_data.query,
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[ToolUserResponse]:
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
- ToolUserResponse.model_validate(
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[ToolUserResponse]:
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
- class SetDefaultModelsForm(BaseModel):
38
- models: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
 
41
  class PromptSuggestion(BaseModel):
@@ -47,21 +73,8 @@ class SetDefaultSuggestionsForm(BaseModel):
47
  suggestions: list[PromptSuggestion]
48
 
49
 
50
- ############################
51
- # SetDefaultModels
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
- - Assume today's date is: {{CURRENT_DATE}}.
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", "3"
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 = 3
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(0)
 
 
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
- template = re.sub(
75
- r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
76
- replacement_function,
77
- template,
78
- )
79
  return template
80
 
81
 
82
- def replace_messages_variable(template: str, messages: list[str]) -> str:
 
 
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.17
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.10.8
15
  async-timeout
16
  aiocache
17
  aiofiles
18
 
19
  sqlalchemy==2.0.32
20
- alembic==1.13.2
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.4.9
48
  qdrant-client~=1.12.0
49
  opensearch-py==2.7.1
50
 
51
- sentence-transformers==3.2.0
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",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "open-webui",
9
- "version": "0.4.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.25",
1840
- "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
1841
- "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ=="
 
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.6.2",
2261
- "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.6.2.tgz",
2262
- "integrity": "sha512-ruogrSPXjckn5poUiZU8VYNCSPHq66SFR1AATvOikQxtP6LNI4niAZVX/AWZRe/EPDG3oY2DNJ9c5z7u0t2NAQ==",
2263
  "hasInstallScript": true,
 
2264
  "dependencies": {
2265
  "@types/cookie": "^0.6.0",
2266
- "cookie": "^0.7.0",
2267
  "devalue": "^5.1.0",
2268
- "esm-env": "^1.0.0",
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": "^2.0.4",
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.7.1",
4395
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
4396
- "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
 
4397
  "engines": {
4398
  "node": ">= 0.6"
4399
  }
@@ -5690,9 +5693,10 @@
5690
  }
5691
  },
5692
  "node_modules/esm-env": {
5693
- "version": "1.0.0",
5694
- "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
5695
- "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
 
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": "2.0.4",
10363
- "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
10364
- "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
 
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": ">= 10"
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",
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.17",
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.10.8",
23
  "async-timeout",
24
  "aiocache",
25
  "aiofiles",
26
 
27
  "sqlalchemy==2.0.32",
28
- "alembic==1.13.2",
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.4.9",
55
  "qdrant-client~=1.12.0",
56
  "opensearch-py==2.7.1",
57
 
58
- "sentence-transformers==3.2.0",
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
- height: 0;
 
 
 
 
 
 
 
 
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="%sveltekit.assets%/favicon.png" />
6
- <link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png" />
7
- <link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
 
 
 
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 setDefaultModels = async (token: string, models: string) => {
62
  let error = null;
63
 
64
- const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/models`, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  method: 'POST',
66
  headers: {
67
  'Content-Type': 'application/json',
68
  Authorization: `Bearer ${token}`
69
  },
70
  body: JSON.stringify({
71
- models: models
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/default/suggestions`, {
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
- try {
391
- // Step 1: Safely extract the response string
392
- const response = res?.choices[0]?.message?.content ?? '';
393
 
394
- // Step 3: Find the relevant JSON block within the response
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 an empty array
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-900 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,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-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
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
- <form
59
- class="flex flex-col h-full justify-between space-y-3 text-sm"
60
- on:submit|preventDefault={() => {
61
- updateInterfaceHandler();
62
- dispatch('save');
63
- }}
64
- >
65
- <div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
66
- <div>
67
- <div class=" mb-2.5 text-sm font-medium flex items-center">
68
- <div class=" mr-1">{$i18n.t('Set Task Model')}</div>
69
- <Tooltip
70
- content={$i18n.t(
71
- 'A task model is used when performing tasks such as generating titles for chats and web search queries'
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
- <option value="" selected>{$i18n.t('Current Model')}</option>
115
- {#each $models as model}
116
- <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
117
- {model.name}
118
- </option>
119
- {/each}
120
- </select>
 
 
 
 
 
 
 
 
121
  </div>
122
- </div>
123
-
124
- <div class="mt-3">
125
- <div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
126
-
127
- <Tooltip
128
- content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
129
- placement="top-start"
130
- >
131
- <Textarea
132
- bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
133
- placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
134
- />
135
- </Tooltip>
136
- </div>
137
-
138
- <hr class=" dark:border-gray-850 my-3" />
139
 
140
- <div class="my-3 flex w-full items-center justify-between">
141
- <div class=" self-center text-xs font-medium">
142
- {$i18n.t('Enable Tags Generation')}
 
 
 
 
 
 
 
 
 
 
 
 
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('Tags Generation Prompt')}</div>
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.TAGS_GENERATION_PROMPT_TEMPLATE}
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
- <div class="my-3 flex w-full items-center justify-between">
169
- <div class=" self-center text-xs font-medium">
170
- {$i18n.t('Enable Retrieval Query Generation')}
171
- </div>
172
 
173
- <Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
174
- </div>
 
 
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('Enable Web Search Query Generation')}
179
  </div>
180
 
181
- <Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
182
- </div>
 
 
 
183
 
184
- <div class="">
185
- <div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
186
-
187
- <Tooltip
188
- content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
189
- placement="top-start"
190
- >
191
- <Textarea
192
- bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE}
193
- placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
194
- />
195
- </Tooltip>
196
- </div>
197
- </div>
198
 
199
- <hr class=" dark:border-gray-850 my-3" />
 
 
 
 
 
200
 
201
- <div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
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
- <button
208
- class="p-1 px-3 text-xs flex rounded transition"
209
- type="button"
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
- <input
258
- class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
259
- placeholder={$i18n.t('Content')}
260
- bind:value={banner.content}
 
 
 
 
 
261
  />
 
 
 
262
 
263
- <div class="relative top-1.5 -left-2">
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
- <button
271
- class="px-2"
272
- type="button"
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
- {/each}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  </div>
292
- </div>
293
 
294
- {#if $user.role === 'admin'}
295
- <div class=" space-y-3">
296
- <div class="flex w-full justify-between mb-2">
 
297
  <div class=" self-center text-sm font-semibold">
298
- {$i18n.t('Default Prompt Suggestions')}
299
  </div>
300
 
301
  <button
302
  class="p-1 px-3 text-xs flex rounded transition"
303
  type="button"
304
  on:click={() => {
305
- if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
306
- promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
 
 
 
 
 
 
 
 
 
 
307
  }
308
  }}
309
  >
@@ -319,40 +273,45 @@
319
  </svg>
320
  </button>
321
  </div>
322
- <div class="grid lg:grid-cols-2 flex-col gap-1.5">
323
- {#each promptSuggestions as prompt, promptIdx}
324
- <div
325
- class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
326
- >
327
- <div class="flex flex-col flex-1 pl-1">
328
- <div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
329
- <input
330
- class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
331
- placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
332
- bind:value={prompt.title[0]}
333
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
- <input
336
- class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
337
- placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
338
- bind:value={prompt.title[1]}
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-3"
352
  type="button"
353
  on:click={() => {
354
- promptSuggestions.splice(promptIdx, 1);
355
- promptSuggestions = promptSuggestions;
356
  }}
357
  >
358
  <svg
@@ -369,22 +328,103 @@
369
  </div>
370
  {/each}
371
  </div>
 
 
 
 
 
 
 
 
372
 
373
- {#if promptSuggestions.length > 0}
374
- <div class="text-xs text-left w-full mt-2">
375
- {$i18n.t('Adjusting these settings will apply changes universally to all users.')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  </div>
377
- {/if}
378
- </div>
379
- {/if}
380
- </div>
381
-
382
- <div class="flex justify-end text-sm font-medium">
383
- <button
384
- 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"
385
- type="submit"
386
- >
387
- {$i18n.t('Save')}
388
- </button>
389
- </div>
390
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- let showResetModal = false;
 
39
 
40
  $: if (models) {
41
- filteredModels = models.filter(
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
- <ConfirmDialog
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('This will delete all models including custom models')}>
158
  <button
159
  class=" px-2.5 py-1 rounded-full flex gap-1 items-center"
160
  type="button"
161
  on:click={() => {
162
- showResetModal = true;
163
  }}
164
  >
165
- <div class="text-xs flex-shrink-0">
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 (`${model.id}-${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=" flex justify-between dark:text-gray-300 px-5 py-4">
51
- <div class=" text-lg font-medium self-center capitalize">
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
- <hr class=" dark:border-gray-850" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- <div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
75
- <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
 
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
- </div>
 
 
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-[1.6rem]">
2288
  <MessageInput
2289
  {history}
2290
  {selectedModels}
@@ -2319,9 +2319,9 @@
2319
  />
2320
 
2321
  <div
2322
- class="absolute bottom-1.5 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}
 
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=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
270
- <div class="flex flex-col px-2.5 max-w-6xl w-full">
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-4 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"
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-4 mx-auto inset-x-0">
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.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
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=" ml-0.5 self-end mb-1.5 flex space-x-1">
546
  <InputMenu
547
  bind:webSearchEnabled
548
  bind:selectedToolIds
@@ -557,18 +562,18 @@
557
  }}
558
  >
559
  <button
560
- class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
561
  type="button"
562
  aria-label="More"
563
  >
564
  <svg
565
  xmlns="http://www.w3.org/2000/svg"
566
- viewBox="0 0 16 16"
567
  fill="currentColor"
568
  class="size-5"
569
  >
570
  <path
571
- d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
572
  />
573
  </svg>
574
  </button>
@@ -577,10 +582,11 @@
577
 
578
  {#if $settings?.richTextInput ?? true}
579
  <div
580
- class="scrollbar-hidden text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-60 overflow-auto"
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
- bind:value={prompt}
595
- on:enter={async (e) => {
596
- const commandsContainerElement =
597
- document.getElementById('commands-container');
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
- if (prompt !== '') {
612
- dispatch('submit', prompt);
613
- }
614
- }}
615
- on:keypress={(e) => {
616
- e = e.detail.event;
 
 
 
 
 
 
 
 
 
617
  }}
618
  on:keydown={async (e) => {
619
  e = e.detail.event;
@@ -657,34 +661,70 @@
657
  editButton?.click();
658
  }
659
 
660
- if (commandsContainerElement && e.key === 'ArrowUp') {
661
- e.preventDefault();
662
- commandsElement.selectUp();
 
 
 
 
 
 
 
663
 
664
- const commandOptionButton = [
665
- ...document.getElementsByClassName('selected-command-option-button')
666
- ]?.at(-1);
667
- commandOptionButton.scrollIntoView({ block: 'center' });
668
- }
669
 
670
- if (commandsContainerElement && e.key === 'ArrowDown') {
671
- e.preventDefault();
672
- commandsElement.selectDown();
 
 
673
 
674
- const commandOptionButton = [
675
- ...document.getElementsByClassName('selected-command-option-button')
676
- ]?.at(-1);
677
- commandOptionButton.scrollIntoView({ block: 'center' });
678
- }
679
 
680
- if (commandsContainerElement && e.key === 'Tab') {
681
- e.preventDefault();
 
682
 
683
- const commandOptionButton = [
684
- ...document.getElementsByClassName('selected-command-option-button')
685
- ]?.at(-1);
686
 
687
- commandOptionButton?.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, 200) + 'px';
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, 200) + 'px';
884
  }}
885
  on:focus={async (e) => {
886
  e.target.style.height = '';
887
- e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
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-2 flex space-x-1 mr-1">
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:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
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
- return;
995
- }
 
 
 
 
 
 
 
 
996
 
997
- if ($config.audio.stt.engine === 'web') {
998
- toast.error(
999
- $i18n.t('Call feature is not supported when using Web STT engine')
1000
- );
1001
 
1002
- return;
1003
- }
1004
- // check if user has access to getUserMedia
1005
- try {
1006
- let stream = await navigator.mediaDevices.getUserMedia({
1007
- audio: true
1008
- });
1009
- // If the user grants the permission, proceed to show the call overlay
1010
 
1011
- if (stream) {
1012
- const tracks = stream.getTracks();
1013
- tracks.forEach((track) => track.stop());
1014
- }
 
 
 
 
1015
 
1016
- stream = null;
 
 
 
1017
 
1018
- showCallOverlay.set(true);
1019
- showControls.set(true);
1020
- } catch (err) {
1021
- // If the user denies the permission or an error occurs, show an error message
1022
- toast.error(
1023
- $i18n.t('Permission denied when accessing media devices')
1024
- );
1025
- }
1026
- }}
1027
- aria-label="Call"
1028
- >
1029
- <Headphone className="size-6" />
1030
- </button>
1031
- </Tooltip>
1032
- </div>
1033
- {:else}
1034
- <div class=" flex items-center mb-1">
1035
- <Tooltip content={$i18n.t('Send message')}>
1036
- <button
1037
- id="send-message-button"
1038
- class="{prompt !== ''
1039
- ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
1040
- : 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 m-0.5 self-center"
1041
- type="submit"
1042
- disabled={prompt === ''}
1043
- >
1044
- <svg
1045
- xmlns="http://www.w3.org/2000/svg"
1046
- viewBox="0 0 16 16"
1047
- fill="currentColor"
1048
- class="size-6"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
  >
1050
- <path
1051
- fill-rule="evenodd"
1052
- 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"
1053
- clip-rule="evenodd"
1054
- />
1055
- </svg>
1056
- </button>
1057
- </Tooltip>
1058
- </div>
1059
- {/if}
1060
- {:else}
1061
- <div class=" flex items-center mb-1.5">
1062
- <Tooltip content={$i18n.t('Stop')}>
1063
- <button
1064
- 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"
1065
- on:click={() => {
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
- {/if}
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="pl-3 pr-14 mb-3 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
 
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="pl-3 pr-14 mb-3 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
 
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="pl-3 pr-14 mb-3 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
 
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="pl-3 pr-14 mb-3 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
 
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-800 bg-red-800/30 rounded-lg">
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="table-auto w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
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-2 !py-1.5 cursor-pointer select-none border border-gray-50 dark:border-gray-850"
106
  style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
107
  >
108
- <div class="flex gap-1.5 items-center">
109
- <MarkdownInlineTokens
110
- id={`${id}-${tokenIdx}-header-${headerIdx}`}
111
- tokens={header.tokens}
112
- {onSourceClick}
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-2 !py-1.5 font-medium 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">
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.error}
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} rounded-2xl text-left"
 
 
35
  type="button"
36
  on:click={async () => {
37
  if (item?.file?.data?.content) {
@@ -49,48 +53,66 @@
49
  dispatch('click');
50
  }}
51
  >
52
- <div class="p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl">
53
- {#if !loading}
54
- <svg
55
- xmlns="http://www.w3.org/2000/svg"
56
- viewBox="0 0 24 24"
57
- fill="currentColor"
58
- class=" size-5"
59
- >
60
- <path
61
- fill-rule="evenodd"
62
- 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"
63
- clip-rule="evenodd"
64
- />
65
- <path
66
- 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"
67
- />
68
- </svg>
69
- {:else}
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
- <span class=" capitalize line-clamp-1">{type}</span>
88
- {/if}
89
- {#if size}
90
- <span class="capitalize">{formatFileSize(size)}</span>
91
  {/if}
92
  </div>
93
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl';
 
 
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-hidden overscroll-contain"
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 max-h-[100dvh] overflow-y-auto scrollbar-hidden {className}"
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 (value === '') {
163
- editor.commands.clearContent();
 
 
 
164
  }
165
  }
166
  },
@@ -171,22 +218,21 @@
171
  eventDispatch('focus', { event });
172
  return false;
173
  },
174
- keypress: (view, event) => {
175
- eventDispatch('keypress', { event });
176
  return false;
177
  },
178
-
179
  keydown: (view, event) => {
180
- // Handle Tab Key
181
- if (event.key === 'Tab') {
182
- const handled = selectNextTemplate(view.state, view.dispatch);
183
- if (handled) {
184
- event.preventDefault();
185
- return true;
 
 
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
- selectTemplate();
 
 
290
  });
291
 
292
  onDestroy(() => {
@@ -296,8 +334,23 @@
296
  });
297
 
298
  // Update the editor content if the external `value` changes
299
- $: if (editor && value !== turndownService.turndown(editor.getHTML())) {
300
- editor.commands.setContent(marked.parse(value)); // Update editor content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // This reactive statement runs whenever `value` changes
20
- $: adjustHeight();
 
 
21
 
22
- // Adjust height to match content
23
- const adjustHeight = () => {
24
- if (textareaElement) {
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
- <textarea
 
33
  bind:this={textareaElement}
34
- bind:value
35
- {placeholder}
36
- on:input={adjustHeight}
37
- class={className}
38
- {rows}
39
- {required}
 
 
 
 
 
 
 
 
 
 
 
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>