coolmanx commited on
Commit
aeb6625
·
verified ·
1 Parent(s): 52db962

up to 0.3.35

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 +34 -0
  2. Dockerfile +1 -1
  3. backend/open_webui/apps/audio/main.py +2 -1
  4. backend/open_webui/apps/ollama/main.py +2 -2
  5. backend/open_webui/apps/retrieval/main.py +7 -1
  6. backend/open_webui/apps/retrieval/vector/dbs/chroma.py +13 -2
  7. backend/open_webui/apps/webui/models/files.py +2 -0
  8. backend/open_webui/apps/webui/routers/evaluations.py +27 -15
  9. backend/open_webui/apps/webui/routers/files.py +7 -1
  10. backend/open_webui/apps/webui/routers/knowledge.py +36 -8
  11. backend/open_webui/config.py +2 -0
  12. backend/open_webui/main.py +13 -2
  13. backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py +33 -8
  14. backend/open_webui/storage/provider.py +6 -5
  15. backend/open_webui/utils/oauth.py +2 -2
  16. backend/open_webui/utils/security_headers.py +1 -1
  17. backend/requirements.txt +1 -1
  18. package-lock.json +2 -2
  19. package.json +1 -1
  20. src/app.css +1 -1
  21. src/lib/apis/evaluations/index.ts +31 -0
  22. src/lib/components/admin/Evaluations.svelte +75 -31
  23. src/lib/components/admin/Settings/Interface.svelte +2 -2
  24. src/lib/components/chat/Chat.svelte +3 -9
  25. src/lib/components/chat/MessageInput.svelte +240 -52
  26. src/lib/components/chat/MessageInput/Commands/Prompts.svelte +12 -3
  27. src/lib/components/chat/MessageInput/VoiceRecording.svelte +161 -127
  28. src/lib/components/chat/Messages/MultiResponseMessages.svelte +19 -19
  29. src/lib/components/chat/Placeholder.svelte +7 -2
  30. src/lib/components/chat/Settings/Interface.svelte +99 -39
  31. src/lib/components/common/RichTextInput.svelte +22 -0
  32. src/lib/components/icons/DocumentArrowDown.svelte +19 -0
  33. src/lib/components/icons/SparklesSolid.svelte +12 -0
  34. src/lib/components/layout/Sidebar.svelte +20 -22
  35. src/lib/components/layout/Sidebar/ChatItem.svelte +18 -2
  36. src/lib/components/layout/Sidebar/RecursiveFolder.svelte +16 -3
  37. src/lib/components/playground/Notes.svelte +121 -0
  38. src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte +2 -2
  39. src/lib/i18n/locales/ar-BH/translation.json +2 -0
  40. src/lib/i18n/locales/bg-BG/translation.json +2 -0
  41. src/lib/i18n/locales/bn-BD/translation.json +2 -0
  42. src/lib/i18n/locales/ca-ES/translation.json +2 -0
  43. src/lib/i18n/locales/ceb-PH/translation.json +2 -0
  44. src/lib/i18n/locales/da-DK/translation.json +2 -0
  45. src/lib/i18n/locales/de-DE/translation.json +2 -0
  46. src/lib/i18n/locales/dg-DG/translation.json +2 -0
  47. src/lib/i18n/locales/en-GB/translation.json +2 -0
  48. src/lib/i18n/locales/en-US/translation.json +2 -0
  49. src/lib/i18n/locales/es-ES/translation.json +2 -0
  50. src/lib/i18n/locales/fa-IR/translation.json +2 -0
CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ 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.3.33] - 2024-10-24
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.3.35] - 2024-10-26
9
+
10
+ ### Added
11
+
12
+ - **📁 Robust File Handling**: Enhanced file input handling for chat. If the content extraction fails or is empty, users will now receive a clear warning, preventing silent failures and ensuring you always know what's happening with your uploads.
13
+ - **🌍 New Language Support**: Introduced Hungarian translations and updated French translations, expanding the platform's language accessibility for a more global user base.
14
+
15
+ ### Fixed
16
+
17
+ - **📚 Knowledge Base Loading Issue**: Resolved a critical bug where the Knowledge Base was not loading, ensuring smooth access to your stored documents and improving information retrieval in RAG-enhanced workflows.
18
+ - **🛠️ Tool Parameters Issue**: Fixed an error where tools were not functioning correctly when required parameters were missing, ensuring reliable tool performance and more efficient task completions.
19
+ - **🔗 Merged Response Loss in Multi-Model Chats**: Addressed an issue where responses in multi-model chat workflows were being deleted after follow-up queries, improving consistency and ensuring smoother interactions across models.
20
+
21
+ ## [0.3.34] - 2024-10-26
22
+
23
+ ### Added
24
+
25
+ - **🔧 Feedback Export Enhancements**: Feedback history data can now be exported to JSON, allowing for seamless integration in RLHF processing and further analysis.
26
+ - **🗂️ Embedding Model Lazy Loading**: Search functionality for leaderboard reranking is now more efficient, as embedding models are lazy-loaded only when needed, optimizing performance.
27
+ - **🎨 Rich Text Input Toggle**: Users can now switch back to legacy textarea input for chat if they prefer simpler text input, though rich text is still the default until deprecation.
28
+ - **🛠️ Improved Tool Calling Mechanism**: Enhanced method for parsing and calling tools, improving the reliability and robustness of tool function calls.
29
+ - **🌐 Globalization Enhancements**: Updates to internationalization (i18n) support, further refining multi-language compatibility and accuracy.
30
+
31
+ ### Fixed
32
+
33
+ - **🖥️ Folder Rename Fix for Firefox**: Addressed a persistent issue where users could not rename folders by pressing enter in Firefox, now ensuring seamless folder management across browsers.
34
+ - **🔠 Tiktoken Model Text Splitter Issue**: Resolved an issue where the tiktoken text splitter wasn’t working in Docker installations, restoring full functionality for tokenized text editing.
35
+ - **💼 S3 File Upload Issue**: Fixed a problem affecting S3 file uploads, ensuring smooth operations for those who store files on cloud storage.
36
+ - **🔒 Strict-Transport-Security Crash**: Resolved a crash when setting the Strict-Transport-Security (HSTS) header, improving stability and security enhancements.
37
+ - **🚫 OIDC Boolean Access Fix**: Addressed an issue with boolean values not being accessed correctly during OIDC logins, ensuring login reliability.
38
+ - **⚙️ Rich Text Paste Behavior**: Refined paste behavior in rich text input to make it smoother and more intuitive when pasting various content types.
39
+ - **🔨 Model Exclusion for Arena Fix**: Corrected the filter function that was not properly excluding models from the arena, improving model management.
40
+ - **🏷️ "Tags Generation Prompt" Fix**: Addressed an issue preventing custom "tags generation prompts" from registering properly, ensuring custom prompt work seamlessly.
41
+
42
  ## [0.3.33] - 2024-10-24
43
 
44
  ### Added
Dockerfile CHANGED
@@ -77,7 +77,7 @@ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
77
  SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
78
 
79
  ## Tiktoken model settings ##
80
- ENV TIKTOKEN_ENCODING_NAME="$USE_TIKTOKEN_ENCODING_NAME" \
81
  TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
82
 
83
  ## Hugging Face download cache ##
 
77
  SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
78
 
79
  ## Tiktoken model settings ##
80
+ ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
81
  TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
82
 
83
  ## Hugging Face download cache ##
backend/open_webui/apps/audio/main.py CHANGED
@@ -522,7 +522,8 @@ def transcription(
522
  else:
523
  data = transcribe(file_path)
524
 
525
- return data
 
526
  except Exception as e:
527
  log.exception(e)
528
  raise HTTPException(
 
522
  else:
523
  data = transcribe(file_path)
524
 
525
+ file_path = file_path.split("/")[-1]
526
+ return {**data, "filename": file_path}
527
  except Exception as e:
528
  log.exception(e)
529
  raise HTTPException(
backend/open_webui/apps/ollama/main.py CHANGED
@@ -692,7 +692,7 @@ class GenerateCompletionForm(BaseModel):
692
  options: Optional[dict] = None
693
  system: Optional[str] = None
694
  template: Optional[str] = None
695
- context: Optional[str] = None
696
  stream: Optional[bool] = True
697
  raw: Optional[bool] = None
698
  keep_alive: Optional[Union[int, str]] = None
@@ -739,7 +739,7 @@ class GenerateChatCompletionForm(BaseModel):
739
  format: Optional[str] = None
740
  options: Optional[dict] = None
741
  template: Optional[str] = None
742
- stream: Optional[bool] = None
743
  keep_alive: Optional[Union[int, str]] = None
744
 
745
 
 
692
  options: Optional[dict] = None
693
  system: Optional[str] = None
694
  template: Optional[str] = None
695
+ context: Optional[list[int]] = None
696
  stream: Optional[bool] = True
697
  raw: Optional[bool] = None
698
  keep_alive: Optional[Union[int, str]] = None
 
739
  format: Optional[str] = None
740
  options: Optional[dict] = None
741
  template: Optional[str] = None
742
+ stream: Optional[bool] = True
743
  keep_alive: Optional[Union[int, str]] = None
744
 
745
 
backend/open_webui/apps/retrieval/main.py CHANGED
@@ -14,6 +14,7 @@ from typing import Iterator, Optional, Sequence, Union
14
  from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
15
  from fastapi.middleware.cors import CORSMiddleware
16
  from pydantic import BaseModel
 
17
 
18
 
19
  from open_webui.storage.provider import Storage
@@ -666,8 +667,13 @@ def save_docs_to_vector_db(
666
  add_start_index=True,
667
  )
668
  elif app.state.config.TEXT_SPLITTER == "token":
 
 
 
 
 
669
  text_splitter = TokenTextSplitter(
670
- encoding_name=app.state.config.TIKTOKEN_ENCODING_NAME,
671
  chunk_size=app.state.config.CHUNK_SIZE,
672
  chunk_overlap=app.state.config.CHUNK_OVERLAP,
673
  add_start_index=True,
 
14
  from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
15
  from fastapi.middleware.cors import CORSMiddleware
16
  from pydantic import BaseModel
17
+ import tiktoken
18
 
19
 
20
  from open_webui.storage.provider import Storage
 
667
  add_start_index=True,
668
  )
669
  elif app.state.config.TEXT_SPLITTER == "token":
670
+ log.info(
671
+ f"Using token text splitter: {app.state.config.TIKTOKEN_ENCODING_NAME}"
672
+ )
673
+
674
+ tiktoken.get_encoding(str(app.state.config.TIKTOKEN_ENCODING_NAME))
675
  text_splitter = TokenTextSplitter(
676
+ encoding_name=str(app.state.config.TIKTOKEN_ENCODING_NAME),
677
  chunk_size=app.state.config.CHUNK_SIZE,
678
  chunk_overlap=app.state.config.CHUNK_OVERLAP,
679
  add_start_index=True,
backend/open_webui/apps/retrieval/vector/dbs/chroma.py CHANGED
@@ -13,11 +13,22 @@ from open_webui.config import (
13
  CHROMA_HTTP_SSL,
14
  CHROMA_TENANT,
15
  CHROMA_DATABASE,
 
 
16
  )
17
 
18
 
19
  class ChromaClient:
20
  def __init__(self):
 
 
 
 
 
 
 
 
 
21
  if CHROMA_HTTP_HOST != "":
22
  self.client = chromadb.HttpClient(
23
  host=CHROMA_HTTP_HOST,
@@ -26,12 +37,12 @@ class ChromaClient:
26
  ssl=CHROMA_HTTP_SSL,
27
  tenant=CHROMA_TENANT,
28
  database=CHROMA_DATABASE,
29
- settings=Settings(allow_reset=True, anonymized_telemetry=False),
30
  )
31
  else:
32
  self.client = chromadb.PersistentClient(
33
  path=CHROMA_DATA_PATH,
34
- settings=Settings(allow_reset=True, anonymized_telemetry=False),
35
  tenant=CHROMA_TENANT,
36
  database=CHROMA_DATABASE,
37
  )
 
13
  CHROMA_HTTP_SSL,
14
  CHROMA_TENANT,
15
  CHROMA_DATABASE,
16
+ CHROMA_CLIENT_AUTH_PROVIDER,
17
+ CHROMA_CLIENT_AUTH_CREDENTIALS,
18
  )
19
 
20
 
21
  class ChromaClient:
22
  def __init__(self):
23
+ settings_dict = {
24
+ "allow_reset": True,
25
+ "anonymized_telemetry": False,
26
+ }
27
+ if CHROMA_CLIENT_AUTH_PROVIDER is not None:
28
+ settings_dict["chroma_client_auth_provider"] = CHROMA_CLIENT_AUTH_PROVIDER
29
+ if CHROMA_CLIENT_AUTH_CREDENTIALS is not None:
30
+ settings_dict["chroma_client_auth_credentials"] = CHROMA_CLIENT_AUTH_CREDENTIALS
31
+
32
  if CHROMA_HTTP_HOST != "":
33
  self.client = chromadb.HttpClient(
34
  host=CHROMA_HTTP_HOST,
 
37
  ssl=CHROMA_HTTP_SSL,
38
  tenant=CHROMA_TENANT,
39
  database=CHROMA_DATABASE,
40
+ settings=Settings(**settings_dict),
41
  )
42
  else:
43
  self.client = chromadb.PersistentClient(
44
  path=CHROMA_DATA_PATH,
45
+ settings=Settings(**settings_dict),
46
  tenant=CHROMA_TENANT,
47
  database=CHROMA_DATABASE,
48
  )
backend/open_webui/apps/webui/models/files.py CHANGED
@@ -73,6 +73,8 @@ class FileModelResponse(BaseModel):
73
  created_at: int # timestamp in epoch
74
  updated_at: int # timestamp in epoch
75
 
 
 
76
 
77
  class FileMetadataResponse(BaseModel):
78
  id: str
 
73
  created_at: int # timestamp in epoch
74
  updated_at: int # timestamp in epoch
75
 
76
+ model_config = ConfigDict(extra="allow")
77
+
78
 
79
  class FileMetadataResponse(BaseModel):
80
  id: str
backend/open_webui/apps/webui/routers/evaluations.py CHANGED
@@ -5,6 +5,7 @@ from pydantic import BaseModel
5
  from open_webui.apps.webui.models.users import Users, UserModel
6
  from open_webui.apps.webui.models.feedbacks import (
7
  FeedbackModel,
 
8
  FeedbackForm,
9
  Feedbacks,
10
  )
@@ -55,27 +56,15 @@ async def update_config(
55
  }
56
 
57
 
58
- @router.get("/feedbacks", response_model=list[FeedbackModel])
59
- async def get_feedbacks(user=Depends(get_verified_user)):
60
- feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id)
61
- return feedbacks
62
-
63
-
64
- @router.delete("/feedbacks", response_model=bool)
65
- async def delete_feedbacks(user=Depends(get_verified_user)):
66
- success = Feedbacks.delete_feedbacks_by_user_id(user.id)
67
- return success
68
-
69
-
70
- class FeedbackUserModel(FeedbackModel):
71
  user: Optional[UserModel] = None
72
 
73
 
74
- @router.get("/feedbacks/all", response_model=list[FeedbackUserModel])
75
  async def get_all_feedbacks(user=Depends(get_admin_user)):
76
  feedbacks = Feedbacks.get_all_feedbacks()
77
  return [
78
- FeedbackUserModel(
79
  **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
80
  )
81
  for feedback in feedbacks
@@ -88,6 +77,29 @@ async def delete_all_feedbacks(user=Depends(get_admin_user)):
88
  return success
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  @router.post("/feedback", response_model=FeedbackModel)
92
  async def create_feedback(
93
  request: Request,
 
5
  from open_webui.apps.webui.models.users import Users, UserModel
6
  from open_webui.apps.webui.models.feedbacks import (
7
  FeedbackModel,
8
+ FeedbackResponse,
9
  FeedbackForm,
10
  Feedbacks,
11
  )
 
56
  }
57
 
58
 
59
+ class FeedbackUserResponse(FeedbackResponse):
 
 
 
 
 
 
 
 
 
 
 
 
60
  user: Optional[UserModel] = None
61
 
62
 
63
+ @router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
64
  async def get_all_feedbacks(user=Depends(get_admin_user)):
65
  feedbacks = Feedbacks.get_all_feedbacks()
66
  return [
67
+ FeedbackUserResponse(
68
  **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
69
  )
70
  for feedback in feedbacks
 
77
  return success
78
 
79
 
80
+ @router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
81
+ async def get_all_feedbacks(user=Depends(get_admin_user)):
82
+ feedbacks = Feedbacks.get_all_feedbacks()
83
+ return [
84
+ FeedbackModel(
85
+ **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
86
+ )
87
+ for feedback in feedbacks
88
+ ]
89
+
90
+
91
+ @router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
92
+ async def get_feedbacks(user=Depends(get_verified_user)):
93
+ feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id)
94
+ return feedbacks
95
+
96
+
97
+ @router.delete("/feedbacks", response_model=bool)
98
+ async def delete_feedbacks(user=Depends(get_verified_user)):
99
+ success = Feedbacks.delete_feedbacks_by_user_id(user.id)
100
+ return success
101
+
102
+
103
  @router.post("/feedback", response_model=FeedbackModel)
104
  async def create_feedback(
105
  request: Request,
backend/open_webui/apps/webui/routers/files.py CHANGED
@@ -38,7 +38,7 @@ router = APIRouter()
38
  ############################
39
 
40
 
41
- @router.post("/")
42
  def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
43
  log.info(f"file.content_type: {file.content_type}")
44
  try:
@@ -73,6 +73,12 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
73
  except Exception as e:
74
  log.exception(e)
75
  log.error(f"Error processing file: {file_item.id}")
 
 
 
 
 
 
76
 
77
  if file_item:
78
  return file_item
 
38
  ############################
39
 
40
 
41
+ @router.post("/", response_model=FileModelResponse)
42
  def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
43
  log.info(f"file.content_type: {file.content_type}")
44
  try:
 
73
  except Exception as e:
74
  log.exception(e)
75
  log.error(f"Error processing file: {file_item.id}")
76
+ file_item = FileModelResponse(
77
+ **{
78
+ **file_item.model_dump(),
79
+ "error": str(e.detail) if hasattr(e, "detail") else str(e),
80
+ }
81
+ )
82
 
83
  if file_item:
84
  return file_item
backend/open_webui/apps/webui/routers/knowledge.py CHANGED
@@ -47,15 +47,43 @@ async def get_knowledge_items(
47
  detail=ERROR_MESSAGES.NOT_FOUND,
48
  )
49
  else:
50
- return [
51
- KnowledgeResponse(
52
- **knowledge.model_dump(),
53
- files=Files.get_file_metadatas_by_ids(
54
- knowledge.data.get("file_ids", []) if knowledge.data else []
55
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  )
57
- for knowledge in Knowledges.get_knowledge_items()
58
- ]
59
 
60
 
61
  ############################
 
47
  detail=ERROR_MESSAGES.NOT_FOUND,
48
  )
49
  else:
50
+ knowledge_bases = []
51
+
52
+ for knowledge in Knowledges.get_knowledge_items():
53
+
54
+ files = []
55
+ if knowledge.data:
56
+ files = Files.get_file_metadatas_by_ids(
57
+ knowledge.data.get("file_ids", [])
58
+ )
59
+
60
+ # Check if all files exist
61
+ if len(files) != len(knowledge.data.get("file_ids", [])):
62
+ missing_files = list(
63
+ set(knowledge.data.get("file_ids", []))
64
+ - set([file.id for file in files])
65
+ )
66
+ if missing_files:
67
+ data = knowledge.data or {}
68
+ file_ids = data.get("file_ids", [])
69
+
70
+ for missing_file in missing_files:
71
+ file_ids.remove(missing_file)
72
+
73
+ data["file_ids"] = file_ids
74
+ Knowledges.update_knowledge_by_id(
75
+ id=knowledge.id, form_data=KnowledgeUpdateForm(data=data)
76
+ )
77
+
78
+ files = Files.get_file_metadatas_by_ids(file_ids)
79
+
80
+ knowledge_bases.append(
81
+ KnowledgeResponse(
82
+ **knowledge.model_dump(),
83
+ files=files,
84
+ )
85
  )
86
+ return knowledge_bases
 
87
 
88
 
89
  ############################
backend/open_webui/config.py CHANGED
@@ -937,6 +937,8 @@ CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
937
  CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
938
  CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
939
  CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
 
 
940
  # Comma-separated list of header=value pairs
941
  CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
942
  if CHROMA_HTTP_HEADERS:
 
937
  CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
938
  CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
939
  CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
940
+ CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
941
+ CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "")
942
  # Comma-separated list of header=value pairs
943
  CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
944
  if CHROMA_HTTP_HEADERS:
backend/open_webui/main.py CHANGED
@@ -439,9 +439,20 @@ async def chat_completion_tools_handler(
439
  tool_function_params = result.get("parameters", {})
440
 
441
  try:
442
- tool_output = await tools[tool_function_name]["callable"](
443
- **tool_function_params
 
 
 
444
  )
 
 
 
 
 
 
 
 
445
  except Exception as e:
446
  tool_output = str(e)
447
 
 
439
  tool_function_params = result.get("parameters", {})
440
 
441
  try:
442
+ required_params = (
443
+ tools[tool_function_name]
444
+ .get("spec", {})
445
+ .get("parameters", {})
446
+ .get("required", [])
447
  )
448
+ tool_function = tools[tool_function_name]["callable"]
449
+ tool_function_params = {
450
+ k: v
451
+ for k, v in tool_function_params.items()
452
+ if k in required_params
453
+ }
454
+ tool_output = await tool_function(**tool_function_params)
455
+
456
  except Exception as e:
457
  tool_output = str(e)
458
 
backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py CHANGED
@@ -19,17 +19,41 @@ depends_on = None
19
 
20
 
21
  def upgrade():
22
- # Step 1: Rename current 'chat' column to 'old_chat'
23
- op.alter_column("chat", "chat", new_column_name="old_chat", existing_type=sa.Text)
24
 
25
- # Step 2: Add new 'chat' column of type JSON
26
- op.add_column("chat", sa.Column("chat", sa.JSON(), nullable=True))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  # Step 3: Migrate data from 'old_chat' to 'chat'
29
  chat_table = table(
30
  "chat",
31
- sa.Column("id", sa.String, primary_key=True),
32
- sa.Column("old_chat", sa.Text),
33
  sa.Column("chat", sa.JSON()),
34
  )
35
 
@@ -50,6 +74,7 @@ def upgrade():
50
  )
51
 
52
  # Step 4: Drop 'old_chat' column
 
53
  op.drop_column("chat", "old_chat")
54
 
55
 
@@ -60,7 +85,7 @@ def downgrade():
60
  # Step 2: Convert 'chat' JSON data back to text and store in 'old_chat'
61
  chat_table = table(
62
  "chat",
63
- sa.Column("id", sa.String, primary_key=True),
64
  sa.Column("chat", sa.JSON()),
65
  sa.Column("old_chat", sa.Text()),
66
  )
@@ -79,4 +104,4 @@ def downgrade():
79
  op.drop_column("chat", "chat")
80
 
81
  # Step 4: Rename 'old_chat' back to 'chat'
82
- op.alter_column("chat", "old_chat", new_column_name="chat", existing_type=sa.Text)
 
19
 
20
 
21
  def upgrade():
22
+ conn = op.get_bind()
23
+ inspector = sa.inspect(conn)
24
 
25
+ columns = inspector.get_columns("chat")
26
+ column_dict = {col["name"]: col for col in columns}
27
+
28
+ chat_column = column_dict.get("chat")
29
+ old_chat_exists = "old_chat" in column_dict
30
+
31
+ if chat_column:
32
+ if isinstance(chat_column["type"], sa.Text):
33
+ print("Converting 'chat' column to JSON")
34
+
35
+ if old_chat_exists:
36
+ print("Dropping old 'old_chat' column")
37
+ op.drop_column("chat", "old_chat")
38
+
39
+ # Step 1: Rename current 'chat' column to 'old_chat'
40
+ print("Renaming 'chat' column to 'old_chat'")
41
+ op.alter_column(
42
+ "chat", "chat", new_column_name="old_chat", existing_type=sa.Text()
43
+ )
44
+
45
+ # Step 2: Add new 'chat' column of type JSON
46
+ print("Adding new 'chat' column of type JSON")
47
+ op.add_column("chat", sa.Column("chat", sa.JSON(), nullable=True))
48
+ else:
49
+ # If the column is already JSON, no need to do anything
50
+ pass
51
 
52
  # Step 3: Migrate data from 'old_chat' to 'chat'
53
  chat_table = table(
54
  "chat",
55
+ sa.Column("id", sa.String(), primary_key=True),
56
+ sa.Column("old_chat", sa.Text()),
57
  sa.Column("chat", sa.JSON()),
58
  )
59
 
 
74
  )
75
 
76
  # Step 4: Drop 'old_chat' column
77
+ print("Dropping 'old_chat' column")
78
  op.drop_column("chat", "old_chat")
79
 
80
 
 
85
  # Step 2: Convert 'chat' JSON data back to text and store in 'old_chat'
86
  chat_table = table(
87
  "chat",
88
+ sa.Column("id", sa.String(), primary_key=True),
89
  sa.Column("chat", sa.JSON()),
90
  sa.Column("old_chat", sa.Text()),
91
  )
 
104
  op.drop_column("chat", "chat")
105
 
106
  # Step 4: Rename 'old_chat' back to 'chat'
107
+ op.alter_column("chat", "old_chat", new_column_name="chat", existing_type=sa.Text())
backend/open_webui/storage/provider.py CHANGED
@@ -44,14 +44,14 @@ class StorageProvider:
44
  )
45
  self.bucket_name = S3_BUCKET_NAME
46
 
47
- def _upload_to_s3(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
48
  """Handles uploading of the file to S3 storage."""
49
  if not self.s3_client:
50
  raise RuntimeError("S3 Client is not initialized.")
51
 
52
  try:
53
- self.s3_client.upload_fileobj(file, self.bucket_name, filename)
54
- return file.read(), f"s3://{self.bucket_name}/{filename}"
55
  except ClientError as e:
56
  raise RuntimeError(f"Error uploading file to S3: {e}")
57
 
@@ -132,10 +132,11 @@ class StorageProvider:
132
  contents = file.read()
133
  if not contents:
134
  raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
 
135
 
136
  if self.storage_provider == "s3":
137
- return self._upload_to_s3(file, filename)
138
- return self._upload_to_local(contents, filename)
139
 
140
  def get_file(self, file_path: str) -> str:
141
  """Downloads a file either from S3 or the local file system and returns the file path."""
 
44
  )
45
  self.bucket_name = S3_BUCKET_NAME
46
 
47
+ def _upload_to_s3(self, file_path: str, filename: str) -> Tuple[bytes, str]:
48
  """Handles uploading of the file to S3 storage."""
49
  if not self.s3_client:
50
  raise RuntimeError("S3 Client is not initialized.")
51
 
52
  try:
53
+ self.s3_client.upload_file(file_path, self.bucket_name, filename)
54
+ return open(file_path, "rb").read(), file_path
55
  except ClientError as e:
56
  raise RuntimeError(f"Error uploading file to S3: {e}")
57
 
 
132
  contents = file.read()
133
  if not contents:
134
  raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
135
+ contents, file_path = self._upload_to_local(contents, filename)
136
 
137
  if self.storage_provider == "s3":
138
+ return self._upload_to_s3(file_path, filename)
139
+ return contents, file_path
140
 
141
  def get_file(self, file_path: str) -> str:
142
  """Downloads a file either from S3 or the local file system and returns the file path."""
backend/open_webui/utils/oauth.py CHANGED
@@ -162,7 +162,7 @@ class OAuthManager:
162
 
163
  if not user:
164
  # If the user does not exist, check if merging is enabled
165
- if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value:
166
  # Check if the user exists by email
167
  user = Users.get_user_by_email(email)
168
  if user:
@@ -176,7 +176,7 @@ class OAuthManager:
176
 
177
  if not user:
178
  # If the user does not exist, check if signups are enabled
179
- if auth_manager_config.ENABLE_OAUTH_SIGNUP.value:
180
  # Check if an existing user with the same email already exists
181
  existing_user = Users.get_user_by_email(
182
  user_data.get("email", "").lower()
 
162
 
163
  if not user:
164
  # If the user does not exist, check if merging is enabled
165
+ if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
166
  # Check if the user exists by email
167
  user = Users.get_user_by_email(email)
168
  if user:
 
176
 
177
  if not user:
178
  # If the user does not exist, check if signups are enabled
179
+ if auth_manager_config.ENABLE_OAUTH_SIGNUP:
180
  # Check if an existing user with the same email already exists
181
  existing_user = Users.get_user_by_email(
182
  user_data.get("email", "").lower()
backend/open_webui/utils/security_headers.py CHANGED
@@ -60,7 +60,7 @@ def set_hsts(value: str):
60
  pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
61
  match = re.match(pattern, value, re.IGNORECASE)
62
  if not match:
63
- return "max-age=31536000;includeSubDomains"
64
  return {"Strict-Transport-Security": value}
65
 
66
 
 
60
  pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
61
  match = re.match(pattern, value, re.IGNORECASE)
62
  if not match:
63
+ value = "max-age=31536000;includeSubDomains"
64
  return {"Strict-Transport-Security": value}
65
 
66
 
backend/requirements.txt CHANGED
@@ -40,7 +40,7 @@ langchain-community==0.2.12
40
  langchain-chroma==0.1.4
41
 
42
  fake-useragent==1.5.1
43
- chromadb==0.5.9
44
  pymilvus==2.4.7
45
  qdrant-client~=1.12.0
46
 
 
40
  langchain-chroma==0.1.4
41
 
42
  fake-useragent==1.5.1
43
+ chromadb==0.5.15
44
  pymilvus==2.4.7
45
  qdrant-client~=1.12.0
46
 
package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
  "name": "open-webui",
3
- "version": "0.3.33",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "open-webui",
9
- "version": "0.3.33",
10
  "dependencies": {
11
  "@codemirror/lang-javascript": "^6.2.2",
12
  "@codemirror/lang-python": "^6.1.6",
 
1
  {
2
  "name": "open-webui",
3
+ "version": "0.3.35",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "open-webui",
9
+ "version": "0.3.35",
10
  "dependencies": {
11
  "@codemirror/lang-javascript": "^6.2.2",
12
  "@codemirror/lang-python": "^6.1.6",
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "open-webui",
3
- "version": "0.3.33",
4
  "private": true,
5
  "scripts": {
6
  "dev": "npm run pyodide:fetch && vite dev --host",
 
1
  {
2
  "name": "open-webui",
3
+ "version": "0.3.35",
4
  "private": true,
5
  "scripts": {
6
  "dev": "npm run pyodide:fetch && vite dev --host",
src/app.css CHANGED
@@ -189,7 +189,7 @@ input[type='number'] {
189
  }
190
 
191
  .ProseMirror {
192
- @apply h-full min-h-fit max-h-full;
193
  }
194
 
195
  .ProseMirror:focus {
 
189
  }
190
 
191
  .ProseMirror {
192
+ @apply h-full min-h-fit max-h-full whitespace-pre-wrap;
193
  }
194
 
195
  .ProseMirror:focus {
src/lib/apis/evaluations/index.ts CHANGED
@@ -93,6 +93,37 @@ export const getAllFeedbacks = async (token: string = '') => {
93
  return res;
94
  };
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  export const createNewFeedback = async (token: string, feedback: object) => {
97
  let error = null;
98
 
 
93
  return res;
94
  };
95
 
96
+ export const exportAllFeedbacks = async (token: string = '') => {
97
+ let error = null;
98
+
99
+ const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/all/export`, {
100
+ method: 'GET',
101
+ headers: {
102
+ Accept: 'application/json',
103
+ 'Content-Type': 'application/json',
104
+ authorization: `Bearer ${token}`
105
+ }
106
+ })
107
+ .then(async (res) => {
108
+ if (!res.ok) throw await res.json();
109
+ return res.json();
110
+ })
111
+ .then((json) => {
112
+ return json;
113
+ })
114
+ .catch((err) => {
115
+ error = err.detail;
116
+ console.log(err);
117
+ return null;
118
+ });
119
+
120
+ if (error) {
121
+ throw error;
122
+ }
123
+
124
+ return res;
125
+ };
126
+
127
  export const createNewFeedback = async (token: string, feedback: object) => {
128
  let error = null;
129
 
src/lib/components/admin/Evaluations.svelte CHANGED
@@ -1,4 +1,7 @@
1
  <script lang="ts">
 
 
 
2
  import { onMount, getContext } from 'svelte';
3
  import dayjs from 'dayjs';
4
  import relativeTime from 'dayjs/plugin/relativeTime';
@@ -12,7 +15,7 @@
12
  let model = null;
13
 
14
  import { models } from '$lib/stores';
15
- import { deleteFeedbackById, getAllFeedbacks } from '$lib/apis/evaluations';
16
 
17
  import FeedbackMenu from './Evaluations/FeedbackMenu.svelte';
18
  import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
@@ -23,6 +26,10 @@
23
  import Share from '../icons/Share.svelte';
24
  import CloudArrowUp from '../icons/CloudArrowUp.svelte';
25
  import { toast } from 'svelte-sonner';
 
 
 
 
26
 
27
  const i18n = getContext('i18n');
28
 
@@ -35,6 +42,7 @@
35
  let tagEmbeddings = new Map();
36
 
37
  let loaded = false;
 
38
  let debounceTimer;
39
 
40
  $: paginatedFeedbacks = feedbacks.slice((page - 1) * 10, page * 10);
@@ -91,6 +99,8 @@
91
  if (a.rating !== '-' && b.rating !== '-') return b.rating - a.rating;
92
  return a.name.localeCompare(b.name);
93
  });
 
 
94
  };
95
 
96
  function calculateModelStats(
@@ -206,6 +216,25 @@
206
  //
207
  //////////////////////
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  const getEmbeddings = async (text: string) => {
210
  const tokens = await tokenizer(text);
211
  const output = await model(tokens);
@@ -227,6 +256,8 @@
227
  };
228
 
229
  const debouncedQueryHandler = async () => {
 
 
230
  if (query.trim() === '') {
231
  rankHandler();
232
  return;
@@ -294,26 +325,23 @@
294
  window.addEventListener('message', messageHandler, false);
295
  };
296
 
297
- onMount(async () => {
298
- feedbacks = await getAllFeedbacks(localStorage.token);
299
- loaded = true;
300
-
301
- // Check if the tokenizer and model are already loaded and stored in the window object
302
- if (!window.tokenizer) {
303
- window.tokenizer = await AutoTokenizer.from_pretrained(EMBEDDING_MODEL);
304
- }
305
 
306
- if (!window.model) {
307
- window.model = await AutoModel.from_pretrained(EMBEDDING_MODEL);
 
 
 
308
  }
 
309
 
310
- // Use the tokenizer and model from the window object
311
- tokenizer = window.tokenizer;
312
- model = window.model;
313
-
314
- // Pre-compute embeddings for all unique tags
315
- const allTags = new Set(feedbacks.flatMap((feedback) => feedback.data.tags || []));
316
- await getTagEmbeddings(Array.from(allTags));
317
 
318
  rankHandler();
319
  });
@@ -343,6 +371,9 @@
343
  class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
344
  bind:value={query}
345
  placeholder={$i18n.t('Search')}
 
 
 
346
  />
347
  </div>
348
  </Tooltip>
@@ -352,13 +383,22 @@
352
  <div
353
  class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"
354
  >
 
 
 
 
 
 
 
355
  {#if (rankedModels ?? []).length === 0}
356
  <div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
357
  {$i18n.t('No models found')}
358
  </div>
359
  {:else}
360
  <table
361
- class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
 
 
362
  >
363
  <thead
364
  class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
@@ -463,6 +503,21 @@
463
 
464
  <span class="text-lg font-medium text-gray-500 dark:text-gray-300">{feedbacks.length}</span>
465
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  </div>
467
 
468
  <div
@@ -606,18 +661,7 @@
606
  </div>
607
 
608
  <div class=" self-center">
609
- <svg
610
- xmlns="http://www.w3.org/2000/svg"
611
- viewBox="0 0 16 16"
612
- fill="currentColor"
613
- class="w-3.5 h-3.5"
614
- >
615
- <path
616
- fill-rule="evenodd"
617
- d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
618
- clip-rule="evenodd"
619
- />
620
- </svg>
621
  </div>
622
  </button>
623
  </Tooltip>
 
1
  <script lang="ts">
2
+ import fileSaver from 'file-saver';
3
+ const { saveAs } = fileSaver;
4
+
5
  import { onMount, getContext } from 'svelte';
6
  import dayjs from 'dayjs';
7
  import relativeTime from 'dayjs/plugin/relativeTime';
 
15
  let model = null;
16
 
17
  import { models } from '$lib/stores';
18
+ import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations';
19
 
20
  import FeedbackMenu from './Evaluations/FeedbackMenu.svelte';
21
  import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 
26
  import Share from '../icons/Share.svelte';
27
  import CloudArrowUp from '../icons/CloudArrowUp.svelte';
28
  import { toast } from 'svelte-sonner';
29
+ import Spinner from '../common/Spinner.svelte';
30
+ import DocumentArrowUpSolid from '../icons/DocumentArrowUpSolid.svelte';
31
+ import DocumentArrowDown from '../icons/DocumentArrowDown.svelte';
32
+ import ArrowDownTray from '../icons/ArrowDownTray.svelte';
33
 
34
  const i18n = getContext('i18n');
35
 
 
42
  let tagEmbeddings = new Map();
43
 
44
  let loaded = false;
45
+ let loadingLeaderboard = true;
46
  let debounceTimer;
47
 
48
  $: paginatedFeedbacks = feedbacks.slice((page - 1) * 10, page * 10);
 
99
  if (a.rating !== '-' && b.rating !== '-') return b.rating - a.rating;
100
  return a.name.localeCompare(b.name);
101
  });
102
+
103
+ loadingLeaderboard = false;
104
  };
105
 
106
  function calculateModelStats(
 
216
  //
217
  //////////////////////
218
 
219
+ const loadEmbeddingModel = async () => {
220
+ // Check if the tokenizer and model are already loaded and stored in the window object
221
+ if (!window.tokenizer) {
222
+ window.tokenizer = await AutoTokenizer.from_pretrained(EMBEDDING_MODEL);
223
+ }
224
+
225
+ if (!window.model) {
226
+ window.model = await AutoModel.from_pretrained(EMBEDDING_MODEL);
227
+ }
228
+
229
+ // Use the tokenizer and model from the window object
230
+ tokenizer = window.tokenizer;
231
+ model = window.model;
232
+
233
+ // Pre-compute embeddings for all unique tags
234
+ const allTags = new Set(feedbacks.flatMap((feedback) => feedback.data.tags || []));
235
+ await getTagEmbeddings(Array.from(allTags));
236
+ };
237
+
238
  const getEmbeddings = async (text: string) => {
239
  const tokens = await tokenizer(text);
240
  const output = await model(tokens);
 
256
  };
257
 
258
  const debouncedQueryHandler = async () => {
259
+ loadingLeaderboard = true;
260
+
261
  if (query.trim() === '') {
262
  rankHandler();
263
  return;
 
325
  window.addEventListener('message', messageHandler, false);
326
  };
327
 
328
+ const exportHandler = async () => {
329
+ const _feedbacks = await exportAllFeedbacks(localStorage.token).catch((err) => {
330
+ toast.error(err);
331
+ return null;
332
+ });
 
 
 
333
 
334
+ if (_feedbacks) {
335
+ let blob = new Blob([JSON.stringify(_feedbacks)], {
336
+ type: 'application/json'
337
+ });
338
+ saveAs(blob, `feedback-history-export-${Date.now()}.json`);
339
  }
340
+ };
341
 
342
+ onMount(async () => {
343
+ feedbacks = await getAllFeedbacks(localStorage.token);
344
+ loaded = true;
 
 
 
 
345
 
346
  rankHandler();
347
  });
 
371
  class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
372
  bind:value={query}
373
  placeholder={$i18n.t('Search')}
374
+ on:focus={() => {
375
+ loadEmbeddingModel();
376
+ }}
377
  />
378
  </div>
379
  </Tooltip>
 
383
  <div
384
  class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"
385
  >
386
+ {#if loadingLeaderboard}
387
+ <div class=" absolute top-0 bottom-0 left-0 right-0 flex">
388
+ <div class="m-auto">
389
+ <Spinner />
390
+ </div>
391
+ </div>
392
+ {/if}
393
  {#if (rankedModels ?? []).length === 0}
394
  <div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
395
  {$i18n.t('No models found')}
396
  </div>
397
  {:else}
398
  <table
399
+ class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded {loadingLeaderboard
400
+ ? 'opacity-20'
401
+ : ''}"
402
  >
403
  <thead
404
  class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
 
503
 
504
  <span class="text-lg font-medium text-gray-500 dark:text-gray-300">{feedbacks.length}</span>
505
  </div>
506
+
507
+ <div>
508
+ <div>
509
+ <Tooltip content={$i18n.t('Export')}>
510
+ <button
511
+ class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
512
+ on:click={() => {
513
+ exportHandler();
514
+ }}
515
+ >
516
+ <ArrowDownTray className="size-3" />
517
+ </button>
518
+ </Tooltip>
519
+ </div>
520
+ </div>
521
  </div>
522
 
523
  <div
 
661
  </div>
662
 
663
  <div class=" self-center">
664
+ <CloudArrowUp className="size-3" strokeWidth="3" />
 
 
 
 
 
 
 
 
 
 
 
665
  </div>
666
  </button>
667
  </Tooltip>
src/lib/components/admin/Settings/Interface.svelte CHANGED
@@ -24,7 +24,7 @@
24
  TASK_MODEL: '',
25
  TASK_MODEL_EXTERNAL: '',
26
  TITLE_GENERATION_PROMPT_TEMPLATE: '',
27
- TAG_GENERATION_PROMPT_TEMPLATE: '',
28
  ENABLE_SEARCH_QUERY: true,
29
  SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
30
  };
@@ -141,7 +141,7 @@
141
  placement="top-start"
142
  >
143
  <Textarea
144
- bind:value={taskConfig.TAG_GENERATION_PROMPT_TEMPLATE}
145
  placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
146
  />
147
  </Tooltip>
 
24
  TASK_MODEL: '',
25
  TASK_MODEL_EXTERNAL: '',
26
  TITLE_GENERATION_PROMPT_TEMPLATE: '',
27
+ TAGS_GENERATION_PROMPT_TEMPLATE: '',
28
  ENABLE_SEARCH_QUERY: true,
29
  SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
30
  };
 
141
  placement="top-start"
142
  >
143
  <Textarea
144
+ bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
145
  placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
146
  />
147
  </Tooltip>
src/lib/components/chat/Chat.svelte CHANGED
@@ -1070,7 +1070,7 @@
1070
  // Prepare the base message object
1071
  const baseMessage = {
1072
  role: message.role,
1073
- content: message.content
1074
  };
1075
 
1076
  // Extract and format image URLs if any exist
@@ -1535,10 +1535,7 @@
1535
  content: [
1536
  {
1537
  type: 'text',
1538
- text:
1539
- arr.length - 1 !== idx
1540
- ? message.content
1541
- : (message?.raContent ?? message.content)
1542
  },
1543
  ...message.files
1544
  .filter((file) => file.type === 'image')
@@ -1551,10 +1548,7 @@
1551
  ]
1552
  }
1553
  : {
1554
- content:
1555
- arr.length - 1 !== idx
1556
- ? message.content
1557
- : (message?.raContent ?? message.content)
1558
  })
1559
  })),
1560
  seed: params?.seed ?? $settings?.params?.seed ?? undefined,
 
1070
  // Prepare the base message object
1071
  const baseMessage = {
1072
  role: message.role,
1073
+ content: message?.merged?.content ?? message.content
1074
  };
1075
 
1076
  // Extract and format image URLs if any exist
 
1535
  content: [
1536
  {
1537
  type: 'text',
1538
+ text: message?.merged?.content ?? message.content
 
 
 
1539
  },
1540
  ...message.files
1541
  .filter((file) => file.type === 'image')
 
1548
  ]
1549
  }
1550
  : {
1551
+ content: message?.merged?.content ?? message.content
 
 
 
1552
  })
1553
  })),
1554
  seed: params?.seed ?? $settings?.params?.seed ?? undefined,
src/lib/components/chat/MessageInput.svelte CHANGED
@@ -1,5 +1,7 @@
1
  <script lang="ts">
2
  import { toast } from 'svelte-sonner';
 
 
3
  import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
4
  const dispatch = createEventDispatcher();
5
 
@@ -89,6 +91,7 @@
89
  const uploadFileHandler = async (file) => {
90
  console.log(file);
91
 
 
92
  const fileItem = {
93
  type: 'file',
94
  file: '',
@@ -98,10 +101,16 @@
98
  collection_name: '',
99
  status: 'uploading',
100
  size: file.size,
101
- error: ''
 
102
  };
103
- files = [...files, fileItem];
104
 
 
 
 
 
 
 
105
  // Check if the file is an audio file and transcribe/convert it to text file
106
  if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
107
  const res = await transcribeAudio(localStorage.token, file).catch((error) => {
@@ -124,6 +133,10 @@
124
  const uploadedFile = await uploadFile(localStorage.token, file);
125
 
126
  if (uploadedFile) {
 
 
 
 
127
  fileItem.status = 'uploaded';
128
  fileItem.file = uploadedFile;
129
  fileItem.id = uploadedFile.id;
@@ -132,11 +145,11 @@
132
 
133
  files = files;
134
  } else {
135
- files = files.filter((item) => item.status !== null);
136
  }
137
  } catch (e) {
138
  toast.error(e);
139
- files = files.filter((item) => item.status !== null);
140
  }
141
  };
142
 
@@ -361,8 +374,8 @@
361
  document.getElementById('chat-input')?.focus();
362
  }}
363
  on:confirm={async (e) => {
364
- const response = e.detail;
365
- prompt = `${prompt}${response} `;
366
 
367
  recording = false;
368
 
@@ -509,54 +522,202 @@
509
  </InputMenu>
510
  </div>
511
 
512
- <div
513
- bind:this={chatInputContainerElement}
514
- id="chat-input-container"
515
- 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-[48px] overflow-auto"
516
- >
517
- <RichTextInput
518
- bind:this={chatInputElement}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  id="chat-input"
520
- trim={true}
 
521
  placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
522
  bind:value={prompt}
523
- shiftEnter={!$mobile ||
524
- !(
525
- 'ontouchstart' in window ||
526
- navigator.maxTouchPoints > 0 ||
527
- navigator.msMaxTouchPoints > 0
528
- )}
529
- on:enter={async (e) => {
530
- if (prompt !== '') {
531
- dispatch('submit', prompt);
532
- }
533
- }}
534
- on:input={async (e) => {
535
- if (chatInputContainerElement) {
536
- chatInputContainerElement.style.height = '';
537
- chatInputContainerElement.style.height =
538
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
539
- }
540
- }}
541
- on:focus={async (e) => {
542
- if (chatInputContainerElement) {
543
- chatInputContainerElement.style.height = '';
544
- chatInputContainerElement.style.height =
545
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
546
- }
547
- }}
548
  on:keypress={(e) => {
549
- e = e.detail.event;
550
- }}
551
- on:keydown={async (e) => {
552
- e = e.detail.event;
 
 
 
 
 
 
 
 
553
 
554
- if (chatInputContainerElement) {
555
- chatInputContainerElement.style.height = '';
556
- chatInputContainerElement.style.height =
557
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
558
  }
559
-
 
560
  const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
561
  const commandsContainerElement =
562
  document.getElementById('commands-container');
@@ -640,6 +801,26 @@
640
  ]?.at(-1);
641
 
642
  commandOptionButton?.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  }
644
 
645
  if (e.key === 'Escape') {
@@ -647,10 +828,17 @@
647
  atSelectedModel = undefined;
648
  }
649
  }}
 
 
 
 
 
 
 
 
 
 
650
  on:paste={async (e) => {
651
- e = e.detail.event;
652
- console.log(e);
653
-
654
  const clipboardData = e.clipboardData || window.clipboardData;
655
 
656
  if (clipboardData && clipboardData.items) {
@@ -675,7 +863,7 @@
675
  }
676
  }}
677
  />
678
- </div>
679
 
680
  <div class="self-end mb-2 flex space-x-1 mr-1">
681
  {#if !history?.currentId || history.messages[history.currentId]?.done == true}
 
1
  <script lang="ts">
2
  import { toast } from 'svelte-sonner';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
  import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
6
  const dispatch = createEventDispatcher();
7
 
 
91
  const uploadFileHandler = async (file) => {
92
  console.log(file);
93
 
94
+ const tempItemId = uuidv4();
95
  const fileItem = {
96
  type: 'file',
97
  file: '',
 
101
  collection_name: '',
102
  status: 'uploading',
103
  size: file.size,
104
+ error: '',
105
+ itemId: tempItemId
106
  };
 
107
 
108
+ if (fileItem.size == 0) {
109
+ toast.error($i18n.t('You cannot upload an empty file.'));
110
+ return null;
111
+ }
112
+
113
+ files = [...files, fileItem];
114
  // Check if the file is an audio file and transcribe/convert it to text file
115
  if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
116
  const res = await transcribeAudio(localStorage.token, file).catch((error) => {
 
133
  const uploadedFile = await uploadFile(localStorage.token, file);
134
 
135
  if (uploadedFile) {
136
+ if (uploadedFile.error) {
137
+ toast.warning(uploadedFile.error);
138
+ }
139
+
140
  fileItem.status = 'uploaded';
141
  fileItem.file = uploadedFile;
142
  fileItem.id = uploadedFile.id;
 
145
 
146
  files = files;
147
  } else {
148
+ files = files.filter((item) => item?.itemId !== tempItemId);
149
  }
150
  } catch (e) {
151
  toast.error(e);
152
+ files = files.filter((item) => item?.itemId !== tempItemId);
153
  }
154
  };
155
 
 
374
  document.getElementById('chat-input')?.focus();
375
  }}
376
  on:confirm={async (e) => {
377
+ const { text, filename } = e.detail;
378
+ prompt = `${prompt}${text} `;
379
 
380
  recording = false;
381
 
 
522
  </InputMenu>
523
  </div>
524
 
525
+ {#if $settings?.richTextInput ?? true}
526
+ <div
527
+ bind:this={chatInputContainerElement}
528
+ id="chat-input-container"
529
+ 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-[48px] overflow-auto"
530
+ >
531
+ <RichTextInput
532
+ bind:this={chatInputElement}
533
+ id="chat-input"
534
+ trim={true}
535
+ placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
536
+ bind:value={prompt}
537
+ shiftEnter={!$mobile ||
538
+ !(
539
+ 'ontouchstart' in window ||
540
+ navigator.maxTouchPoints > 0 ||
541
+ navigator.msMaxTouchPoints > 0
542
+ )}
543
+ on:enter={async (e) => {
544
+ if (prompt !== '') {
545
+ dispatch('submit', prompt);
546
+ }
547
+ }}
548
+ on:input={async (e) => {
549
+ if (chatInputContainerElement) {
550
+ chatInputContainerElement.style.height = '';
551
+ chatInputContainerElement.style.height =
552
+ Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
553
+ }
554
+ }}
555
+ on:focus={async (e) => {
556
+ if (chatInputContainerElement) {
557
+ chatInputContainerElement.style.height = '';
558
+ chatInputContainerElement.style.height =
559
+ Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
560
+ }
561
+ }}
562
+ on:keypress={(e) => {
563
+ e = e.detail.event;
564
+ }}
565
+ on:keydown={async (e) => {
566
+ e = e.detail.event;
567
+
568
+ if (chatInputContainerElement) {
569
+ chatInputContainerElement.style.height = '';
570
+ chatInputContainerElement.style.height =
571
+ Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
572
+ }
573
+
574
+ const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
575
+ const commandsContainerElement =
576
+ document.getElementById('commands-container');
577
+
578
+ // Command/Ctrl + Shift + Enter to submit a message pair
579
+ if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
580
+ e.preventDefault();
581
+ createMessagePair(prompt);
582
+ }
583
+
584
+ // Check if Ctrl + R is pressed
585
+ if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
586
+ e.preventDefault();
587
+ console.log('regenerate');
588
+
589
+ const regenerateButton = [
590
+ ...document.getElementsByClassName('regenerate-response-button')
591
+ ]?.at(-1);
592
+
593
+ regenerateButton?.click();
594
+ }
595
+
596
+ if (prompt === '' && e.key == 'ArrowUp') {
597
+ e.preventDefault();
598
+
599
+ const userMessageElement = [
600
+ ...document.getElementsByClassName('user-message')
601
+ ]?.at(-1);
602
+
603
+ const editButton = [
604
+ ...document.getElementsByClassName('edit-user-message-button')
605
+ ]?.at(-1);
606
+
607
+ console.log(userMessageElement);
608
+
609
+ userMessageElement.scrollIntoView({ block: 'center' });
610
+ editButton?.click();
611
+ }
612
+
613
+ if (commandsContainerElement && e.key === 'ArrowUp') {
614
+ e.preventDefault();
615
+ commandsElement.selectUp();
616
+
617
+ const commandOptionButton = [
618
+ ...document.getElementsByClassName('selected-command-option-button')
619
+ ]?.at(-1);
620
+ commandOptionButton.scrollIntoView({ block: 'center' });
621
+ }
622
+
623
+ if (commandsContainerElement && e.key === 'ArrowDown') {
624
+ e.preventDefault();
625
+ commandsElement.selectDown();
626
+
627
+ const commandOptionButton = [
628
+ ...document.getElementsByClassName('selected-command-option-button')
629
+ ]?.at(-1);
630
+ commandOptionButton.scrollIntoView({ block: 'center' });
631
+ }
632
+
633
+ if (commandsContainerElement && e.key === 'Enter') {
634
+ e.preventDefault();
635
+
636
+ const commandOptionButton = [
637
+ ...document.getElementsByClassName('selected-command-option-button')
638
+ ]?.at(-1);
639
+
640
+ if (e.shiftKey) {
641
+ prompt = `${prompt}\n`;
642
+ } else if (commandOptionButton) {
643
+ commandOptionButton?.click();
644
+ } else {
645
+ document.getElementById('send-message-button')?.click();
646
+ }
647
+ }
648
+
649
+ if (commandsContainerElement && e.key === 'Tab') {
650
+ e.preventDefault();
651
+
652
+ const commandOptionButton = [
653
+ ...document.getElementsByClassName('selected-command-option-button')
654
+ ]?.at(-1);
655
+
656
+ commandOptionButton?.click();
657
+ }
658
+
659
+ if (e.key === 'Escape') {
660
+ console.log('Escape');
661
+ atSelectedModel = undefined;
662
+ }
663
+ }}
664
+ on:paste={async (e) => {
665
+ e = e.detail.event;
666
+ console.log(e);
667
+
668
+ const clipboardData = e.clipboardData || window.clipboardData;
669
+
670
+ if (clipboardData && clipboardData.items) {
671
+ for (const item of clipboardData.items) {
672
+ if (item.type.indexOf('image') !== -1) {
673
+ const blob = item.getAsFile();
674
+ const reader = new FileReader();
675
+
676
+ reader.onload = function (e) {
677
+ files = [
678
+ ...files,
679
+ {
680
+ type: 'image',
681
+ url: `${e.target.result}`
682
+ }
683
+ ];
684
+ };
685
+
686
+ reader.readAsDataURL(blob);
687
+ }
688
+ }
689
+ }
690
+ }}
691
+ />
692
+ </div>
693
+ {:else}
694
+ <textarea
695
  id="chat-input"
696
+ bind:this={chatInputElement}
697
+ class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
698
  placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
699
  bind:value={prompt}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  on:keypress={(e) => {
701
+ if (
702
+ !$mobile ||
703
+ !(
704
+ 'ontouchstart' in window ||
705
+ navigator.maxTouchPoints > 0 ||
706
+ navigator.msMaxTouchPoints > 0
707
+ )
708
+ ) {
709
+ // Prevent Enter key from creating a new line
710
+ if (e.key === 'Enter' && !e.shiftKey) {
711
+ e.preventDefault();
712
+ }
713
 
714
+ // Submit the prompt when Enter key is pressed
715
+ if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
716
+ dispatch('submit', prompt);
717
+ }
718
  }
719
+ }}
720
+ on:keydown={async (e) => {
721
  const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
722
  const commandsContainerElement =
723
  document.getElementById('commands-container');
 
801
  ]?.at(-1);
802
 
803
  commandOptionButton?.click();
804
+ } else if (e.key === 'Tab') {
805
+ const words = findWordIndices(prompt);
806
+
807
+ if (words.length > 0) {
808
+ const word = words.at(0);
809
+ const fullPrompt = prompt;
810
+
811
+ prompt = prompt.substring(0, word?.endIndex + 1);
812
+ await tick();
813
+
814
+ e.target.scrollTop = e.target.scrollHeight;
815
+ prompt = fullPrompt;
816
+ await tick();
817
+
818
+ e.preventDefault();
819
+ e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
820
+ }
821
+
822
+ e.target.style.height = '';
823
+ e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
824
  }
825
 
826
  if (e.key === 'Escape') {
 
828
  atSelectedModel = undefined;
829
  }
830
  }}
831
+ rows="1"
832
+ on:input={async (e) => {
833
+ e.target.style.height = '';
834
+ e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
835
+ user = null;
836
+ }}
837
+ on:focus={async (e) => {
838
+ e.target.style.height = '';
839
+ e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
840
+ }}
841
  on:paste={async (e) => {
 
 
 
842
  const clipboardData = e.clipboardData || window.clipboardData;
843
 
844
  if (clipboardData && clipboardData.items) {
 
863
  }
864
  }}
865
  />
866
+ {/if}
867
 
868
  <div class="self-end mb-2 flex space-x-1 mr-1">
869
  {#if !history?.currentId || history.messages[history.currentId]?.done == true}
src/lib/components/chat/MessageInput/Commands/Prompts.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import { prompts } from '$lib/stores';
3
  import {
4
  findWordIndices,
5
  getUserPosition,
@@ -78,6 +78,12 @@
78
  text = text.replaceAll('{{USER_LOCATION}}', String(location));
79
  }
80
 
 
 
 
 
 
 
81
  if (command.content.includes('{{USER_LANGUAGE}}')) {
82
  const language = localStorage.getItem('locale') || 'en-US';
83
  text = text.replaceAll('{{USER_LANGUAGE}}', language);
@@ -114,13 +120,16 @@
114
  const chatInputElement = document.getElementById('chat-input');
115
 
116
  await tick();
117
-
118
  if (chatInputContainerElement) {
119
  chatInputContainerElement.style.height = '';
120
  chatInputContainerElement.style.height =
121
  Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
 
122
 
123
- chatInputElement?.focus();
 
 
 
124
  }
125
  };
126
  </script>
 
1
  <script lang="ts">
2
+ import { prompts, user } from '$lib/stores';
3
  import {
4
  findWordIndices,
5
  getUserPosition,
 
78
  text = text.replaceAll('{{USER_LOCATION}}', String(location));
79
  }
80
 
81
+ if (command.content.includes('{{USER_NAME}}')) {
82
+ console.log($user);
83
+ const name = $user.name || 'User';
84
+ text = text.replaceAll('{{USER_NAME}}', name);
85
+ }
86
+
87
  if (command.content.includes('{{USER_LANGUAGE}}')) {
88
  const language = localStorage.getItem('locale') || 'en-US';
89
  text = text.replaceAll('{{USER_LANGUAGE}}', language);
 
120
  const chatInputElement = document.getElementById('chat-input');
121
 
122
  await tick();
 
123
  if (chatInputContainerElement) {
124
  chatInputContainerElement.style.height = '';
125
  chatInputContainerElement.style.height =
126
  Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
127
+ }
128
 
129
+ await tick();
130
+ if (chatInputElement) {
131
+ chatInputElement.focus();
132
+ chatInputElement.dispatchEvent(new Event('input'));
133
  }
134
  };
135
  </script>
src/lib/components/chat/MessageInput/VoiceRecording.svelte CHANGED
@@ -1,6 +1,6 @@
1
  <script lang="ts">
2
  import { toast } from 'svelte-sonner';
3
- import { createEventDispatcher, tick, getContext } from 'svelte';
4
  import { config, settings } from '$lib/stores';
5
  import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
6
 
@@ -52,7 +52,7 @@
52
  let audioChunks = [];
53
 
54
  const MIN_DECIBELS = -45;
55
- const VISUALIZER_BUFFER_LENGTH = 300;
56
 
57
  let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
58
 
@@ -142,8 +142,8 @@
142
  });
143
 
144
  if (res) {
145
- console.log(res.text);
146
- dispatch('confirm', res.text);
147
  }
148
  };
149
 
@@ -278,12 +278,40 @@
278
 
279
  stream = null;
280
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  </script>
282
 
283
  <div
 
284
  class="{loading
285
  ? ' bg-gray-100/50 dark:bg-gray-850/50'
286
- : 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex {className}"
287
  >
288
  <div class="flex items-center mr-1">
289
  <button
@@ -318,146 +346,152 @@
318
  class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
319
  dir="rtl"
320
  >
321
- <div class="flex-1 flex items-center gap-0.5 h-6">
 
 
322
  {#each visualizerData.slice().reverse() as rms}
323
- <div
324
- class="w-[2px]
 
325
 
326
  {loading
327
- ? ' bg-gray-500 dark:bg-gray-400 '
328
- : 'bg-indigo-500 dark:bg-indigo-400 '}
329
 
330
  inline-block h-full"
331
- style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
332
- />
 
333
  {/each}
334
  </div>
335
  </div>
336
 
337
- <div class=" mx-1.5 pr-1 flex justify-center items-center">
338
- <div
339
- class="text-sm
 
340
 
341
 
342
  {loading ? ' text-gray-500 dark:text-gray-400 ' : ' text-indigo-400 '}
343
  font-medium flex-1 mx-auto text-center"
344
- >
345
- {formatSeconds(durationSeconds)}
 
346
  </div>
347
- </div>
348
 
349
- <div class="flex items-center mr-1">
350
- {#if loading}
351
- <div class=" text-gray-500 rounded-full cursor-not-allowed">
352
- <svg
353
- width="24"
354
- height="24"
355
- viewBox="0 0 24 24"
356
- xmlns="http://www.w3.org/2000/svg"
357
- fill="currentColor"
358
- ><style>
359
- .spinner_OSmW {
360
- transform-origin: center;
361
- animation: spinner_T6mA 0.75s step-end infinite;
362
- }
363
- @keyframes spinner_T6mA {
364
- 8.3% {
365
- transform: rotate(30deg);
366
- }
367
- 16.6% {
368
- transform: rotate(60deg);
369
- }
370
- 25% {
371
- transform: rotate(90deg);
372
- }
373
- 33.3% {
374
- transform: rotate(120deg);
375
- }
376
- 41.6% {
377
- transform: rotate(150deg);
378
- }
379
- 50% {
380
- transform: rotate(180deg);
381
- }
382
- 58.3% {
383
- transform: rotate(210deg);
384
- }
385
- 66.6% {
386
- transform: rotate(240deg);
387
  }
388
- 75% {
389
- transform: rotate(270deg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
- 83.3% {
392
- transform: rotate(300deg);
393
- }
394
- 91.6% {
395
- transform: rotate(330deg);
396
- }
397
- 100% {
398
- transform: rotate(360deg);
399
- }
400
- }
401
- </style><g class="spinner_OSmW"
402
- ><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
403
- x="11"
404
- y="1"
405
- width="2"
406
- height="5"
407
- transform="rotate(30 12 12)"
408
- opacity=".29"
409
- /><rect
410
- x="11"
411
- y="1"
412
- width="2"
413
- height="5"
414
- transform="rotate(60 12 12)"
415
- opacity=".43"
416
- /><rect
417
- x="11"
418
- y="1"
419
- width="2"
420
- height="5"
421
- transform="rotate(90 12 12)"
422
- opacity=".57"
423
- /><rect
424
- x="11"
425
- y="1"
426
- width="2"
427
- height="5"
428
- transform="rotate(120 12 12)"
429
- opacity=".71"
430
- /><rect
431
- x="11"
432
- y="1"
433
- width="2"
434
- height="5"
435
- transform="rotate(150 12 12)"
436
- opacity=".86"
437
- /><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
438
- ></svg
439
  >
440
- </div>
441
- {:else}
442
- <button
443
- type="button"
444
- class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
445
- on:click={async () => {
446
- await confirmRecording();
447
- }}
448
- >
449
- <svg
450
- xmlns="http://www.w3.org/2000/svg"
451
- fill="none"
452
- viewBox="0 0 24 24"
453
- stroke-width="2.5"
454
- stroke="currentColor"
455
- class="size-4"
456
- >
457
- <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
458
- </svg>
459
- </button>
460
- {/if}
461
  </div>
462
  </div>
463
 
 
1
  <script lang="ts">
2
  import { toast } from 'svelte-sonner';
3
+ import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
4
  import { config, settings } from '$lib/stores';
5
  import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
6
 
 
52
  let audioChunks = [];
53
 
54
  const MIN_DECIBELS = -45;
55
+ let VISUALIZER_BUFFER_LENGTH = 300;
56
 
57
  let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
58
 
 
142
  });
143
 
144
  if (res) {
145
+ console.log(res);
146
+ dispatch('confirm', res);
147
  }
148
  };
149
 
 
278
 
279
  stream = null;
280
  };
281
+
282
+ let resizeObserver;
283
+ let containerWidth;
284
+
285
+ let maxVisibleItems = 300;
286
+ $: maxVisibleItems = Math.floor(containerWidth / 5); // 2px width + 0.5px gap
287
+
288
+ onMount(() => {
289
+ // listen to width changes
290
+ resizeObserver = new ResizeObserver(() => {
291
+ VISUALIZER_BUFFER_LENGTH = Math.floor(window.innerWidth / 4);
292
+ if (visualizerData.length > VISUALIZER_BUFFER_LENGTH) {
293
+ visualizerData = visualizerData.slice(visualizerData.length - VISUALIZER_BUFFER_LENGTH);
294
+ } else {
295
+ visualizerData = Array(VISUALIZER_BUFFER_LENGTH - visualizerData.length)
296
+ .fill(0)
297
+ .concat(visualizerData);
298
+ }
299
+ });
300
+
301
+ resizeObserver.observe(document.body);
302
+ });
303
+
304
+ onDestroy(() => {
305
+ // remove resize observer
306
+ resizeObserver.disconnect();
307
+ });
308
  </script>
309
 
310
  <div
311
+ bind:clientWidth={containerWidth}
312
  class="{loading
313
  ? ' bg-gray-100/50 dark:bg-gray-850/50'
314
+ : 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex justify-between {className}"
315
  >
316
  <div class="flex items-center mr-1">
317
  <button
 
346
  class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
347
  dir="rtl"
348
  >
349
+ <div
350
+ class="flex items-center gap-0.5 h-6 w-full max-w-full overflow-hidden overflow-x-hidden flex-wrap"
351
+ >
352
  {#each visualizerData.slice().reverse() as rms}
353
+ <div class="flex items-center h-full">
354
+ <div
355
+ class="w-[2px] flex-shrink-0
356
 
357
  {loading
358
+ ? ' bg-gray-500 dark:bg-gray-400 '
359
+ : 'bg-indigo-500 dark:bg-indigo-400 '}
360
 
361
  inline-block h-full"
362
+ style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
363
+ />
364
+ </div>
365
  {/each}
366
  </div>
367
  </div>
368
 
369
+ <div class="flex">
370
+ <div class=" mx-1.5 pr-1 flex justify-center items-center">
371
+ <div
372
+ class="text-sm
373
 
374
 
375
  {loading ? ' text-gray-500 dark:text-gray-400 ' : ' text-indigo-400 '}
376
  font-medium flex-1 mx-auto text-center"
377
+ >
378
+ {formatSeconds(durationSeconds)}
379
+ </div>
380
  </div>
 
381
 
382
+ <div class="flex items-center">
383
+ {#if loading}
384
+ <div class=" text-gray-500 rounded-full cursor-not-allowed">
385
+ <svg
386
+ width="24"
387
+ height="24"
388
+ viewBox="0 0 24 24"
389
+ xmlns="http://www.w3.org/2000/svg"
390
+ fill="currentColor"
391
+ ><style>
392
+ .spinner_OSmW {
393
+ transform-origin: center;
394
+ animation: spinner_T6mA 0.75s step-end infinite;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  }
396
+ @keyframes spinner_T6mA {
397
+ 8.3% {
398
+ transform: rotate(30deg);
399
+ }
400
+ 16.6% {
401
+ transform: rotate(60deg);
402
+ }
403
+ 25% {
404
+ transform: rotate(90deg);
405
+ }
406
+ 33.3% {
407
+ transform: rotate(120deg);
408
+ }
409
+ 41.6% {
410
+ transform: rotate(150deg);
411
+ }
412
+ 50% {
413
+ transform: rotate(180deg);
414
+ }
415
+ 58.3% {
416
+ transform: rotate(210deg);
417
+ }
418
+ 66.6% {
419
+ transform: rotate(240deg);
420
+ }
421
+ 75% {
422
+ transform: rotate(270deg);
423
+ }
424
+ 83.3% {
425
+ transform: rotate(300deg);
426
+ }
427
+ 91.6% {
428
+ transform: rotate(330deg);
429
+ }
430
+ 100% {
431
+ transform: rotate(360deg);
432
+ }
433
  }
434
+ </style><g class="spinner_OSmW"
435
+ ><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
436
+ x="11"
437
+ y="1"
438
+ width="2"
439
+ height="5"
440
+ transform="rotate(30 12 12)"
441
+ opacity=".29"
442
+ /><rect
443
+ x="11"
444
+ y="1"
445
+ width="2"
446
+ height="5"
447
+ transform="rotate(60 12 12)"
448
+ opacity=".43"
449
+ /><rect
450
+ x="11"
451
+ y="1"
452
+ width="2"
453
+ height="5"
454
+ transform="rotate(90 12 12)"
455
+ opacity=".57"
456
+ /><rect
457
+ x="11"
458
+ y="1"
459
+ width="2"
460
+ height="5"
461
+ transform="rotate(120 12 12)"
462
+ opacity=".71"
463
+ /><rect
464
+ x="11"
465
+ y="1"
466
+ width="2"
467
+ height="5"
468
+ transform="rotate(150 12 12)"
469
+ opacity=".86"
470
+ /><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
471
+ ></svg
472
+ >
473
+ </div>
474
+ {:else}
475
+ <button
476
+ type="button"
477
+ class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
478
+ on:click={async () => {
479
+ await confirmRecording();
480
+ }}
 
481
  >
482
+ <svg
483
+ xmlns="http://www.w3.org/2000/svg"
484
+ fill="none"
485
+ viewBox="0 0 24 24"
486
+ stroke-width="2.5"
487
+ stroke="currentColor"
488
+ class="size-4"
489
+ >
490
+ <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
491
+ </svg>
492
+ </button>
493
+ {/if}
494
+ </div>
 
 
 
 
 
 
 
 
495
  </div>
496
  </div>
497
 
src/lib/components/chat/Messages/MultiResponseMessages.svelte CHANGED
@@ -16,7 +16,6 @@
16
  import Markdown from './Markdown.svelte';
17
  import Name from './Name.svelte';
18
  import Skeleton from './Skeleton.svelte';
19
-
20
  const i18n = getContext('i18n');
21
 
22
  export let chatId;
@@ -155,7 +154,6 @@
155
  await tick();
156
 
157
  const messageElement = document.getElementById(`message-${messageId}`);
158
- console.log(messageElement);
159
  if (messageElement) {
160
  messageElement.scrollIntoView({ block: 'start' });
161
  }
@@ -237,7 +235,7 @@
237
  {/each}
238
  </div>
239
 
240
- {#if !readOnly && isLastMessage}
241
  {#if !Object.keys(groupedMessageIds).find((modelIdx) => {
242
  const { messageIds } = groupedMessageIds[modelIdx];
243
  const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
@@ -272,22 +270,24 @@
272
  {/if}
273
  </div>
274
 
275
- <div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
276
- <Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
277
- <button
278
- type="button"
279
- id="merge-response-button"
280
- class="{true
281
- ? 'visible'
282
- : 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
283
- on:click={() => {
284
- mergeResponsesHandler();
285
- }}
286
- >
287
- <Merge className=" size-5 " />
288
- </button>
289
- </Tooltip>
290
- </div>
 
 
291
  </div>
292
  {/if}
293
  {/if}
 
16
  import Markdown from './Markdown.svelte';
17
  import Name from './Name.svelte';
18
  import Skeleton from './Skeleton.svelte';
 
19
  const i18n = getContext('i18n');
20
 
21
  export let chatId;
 
154
  await tick();
155
 
156
  const messageElement = document.getElementById(`message-${messageId}`);
 
157
  if (messageElement) {
158
  messageElement.scrollIntoView({ block: 'start' });
159
  }
 
235
  {/each}
236
  </div>
237
 
238
+ {#if !readOnly}
239
  {#if !Object.keys(groupedMessageIds).find((modelIdx) => {
240
  const { messageIds } = groupedMessageIds[modelIdx];
241
  const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
 
270
  {/if}
271
  </div>
272
 
273
+ {#if isLastMessage}
274
+ <div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
275
+ <Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
276
+ <button
277
+ type="button"
278
+ id="merge-response-button"
279
+ class="{true
280
+ ? 'visible'
281
+ : 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
282
+ on:click={() => {
283
+ mergeResponsesHandler();
284
+ }}
285
+ >
286
+ <Merge className=" size-5 " />
287
+ </button>
288
+ </Tooltip>
289
+ </div>
290
+ {/if}
291
  </div>
292
  {/if}
293
  {/if}
src/lib/components/chat/Placeholder.svelte CHANGED
@@ -58,13 +58,18 @@
58
  await tick();
59
 
60
  const chatInputContainerElement = document.getElementById('chat-input-container');
 
 
61
  if (chatInputContainerElement) {
62
  chatInputContainerElement.style.height = '';
63
  chatInputContainerElement.style.height =
64
  Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
 
65
 
66
- const chatInputElement = document.getElementById('chat-input');
67
- chatInputElement?.focus();
 
 
68
  }
69
 
70
  await tick();
 
58
  await tick();
59
 
60
  const chatInputContainerElement = document.getElementById('chat-input-container');
61
+ const chatInputElement = document.getElementById('chat-input');
62
+
63
  if (chatInputContainerElement) {
64
  chatInputContainerElement.style.height = '';
65
  chatInputContainerElement.style.height =
66
  Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
67
+ }
68
 
69
+ await tick();
70
+ if (chatInputElement) {
71
+ chatInputElement.focus();
72
+ chatInputElement.dispatchEvent(new Event('input'));
73
  }
74
 
75
  await tick();
src/lib/components/chat/Settings/Interface.svelte CHANGED
@@ -30,11 +30,15 @@
30
  // Interface
31
  let defaultModelId = '';
32
  let showUsername = false;
 
33
 
34
  let landingPageMode = '';
35
  let chatBubble = true;
36
  let chatDirection: 'LTR' | 'RTL' = 'LTR';
 
 
37
  let showUpdateToast = true;
 
38
 
39
  let showEmojiInCall = false;
40
  let voiceInterruption = false;
@@ -70,6 +74,11 @@
70
  saveSettings({ showUpdateToast: showUpdateToast });
71
  };
72
 
 
 
 
 
 
73
  const toggleShowUsername = async () => {
74
  showUsername = !showUsername;
75
  saveSettings({ showUsername: showUsername });
@@ -125,6 +134,11 @@
125
  saveSettings({ autoTags });
126
  };
127
 
 
 
 
 
 
128
  const toggleResponseAutoCopy = async () => {
129
  const permission = await navigator.clipboard
130
  .readText()
@@ -168,10 +182,12 @@
168
 
169
  showUsername = $settings.showUsername ?? false;
170
  showUpdateToast = $settings.showUpdateToast ?? true;
 
171
 
172
  showEmojiInCall = $settings.showEmojiInCall ?? false;
173
  voiceInterruption = $settings.voiceInterruption ?? false;
174
 
 
175
  landingPageMode = $settings.landingPageMode ?? '';
176
  chatBubble = $settings.chatBubble ?? true;
177
  widescreenMode = $settings.widescreenMode ?? false;
@@ -376,22 +392,44 @@
376
  </button>
377
  </div>
378
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  {/if}
380
 
 
 
381
  <div>
382
  <div class=" py-0.5 flex w-full justify-between">
383
- <div class=" self-center text-xs">
384
- {$i18n.t('Fluidly stream large external response chunks')}
385
- </div>
386
 
387
  <button
388
  class="p-1 px-3 text-xs flex rounded transition"
389
  on:click={() => {
390
- toggleSplitLargeChunks();
391
  }}
392
  type="button"
393
  >
394
- {#if splitLargeChunks === true}
395
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
396
  {:else}
397
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -402,18 +440,16 @@
402
 
403
  <div>
404
  <div class=" py-0.5 flex w-full justify-between">
405
- <div class=" self-center text-xs">
406
- {$i18n.t('Scroll to bottom when switching between branches')}
407
- </div>
408
 
409
  <button
410
  class="p-1 px-3 text-xs flex rounded transition"
411
  on:click={() => {
412
- togglesScrollOnBranchChange();
413
  }}
414
  type="button"
415
  >
416
- {#if scrollOnBranchChange === true}
417
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
418
  {:else}
419
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -425,44 +461,39 @@
425
  <div>
426
  <div class=" py-0.5 flex w-full justify-between">
427
  <div class=" self-center text-xs">
428
- {$i18n.t('Chat Background Image')}
429
  </div>
430
 
431
  <button
432
  class="p-1 px-3 text-xs flex rounded transition"
433
  on:click={() => {
434
- if (backgroundImageUrl !== null) {
435
- backgroundImageUrl = null;
436
- saveSettings({ backgroundImageUrl });
437
- } else {
438
- filesInputElement.click();
439
- }
440
  }}
441
  type="button"
442
  >
443
- {#if backgroundImageUrl !== null}
444
- <span class="ml-2 self-center">{$i18n.t('Reset')}</span>
445
  {:else}
446
- <span class="ml-2 self-center">{$i18n.t('Upload')}</span>
447
  {/if}
448
  </button>
449
  </div>
450
  </div>
451
 
452
- <div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
453
-
454
  <div>
455
  <div class=" py-0.5 flex w-full justify-between">
456
- <div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
 
 
457
 
458
  <button
459
  class="p-1 px-3 text-xs flex rounded transition"
460
  on:click={() => {
461
- toggleTitleAutoGenerate();
462
  }}
463
  type="button"
464
  >
465
- {#if titleAutoGenerate === true}
466
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
467
  {:else}
468
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -473,16 +504,43 @@
473
 
474
  <div>
475
  <div class=" py-0.5 flex w-full justify-between">
476
- <div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
 
 
477
 
478
  <button
479
  class="p-1 px-3 text-xs flex rounded transition"
480
  on:click={() => {
481
- toggleAutoTags();
 
 
 
 
 
482
  }}
483
  type="button"
484
  >
485
- {#if autoTags === true}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
487
  {:else}
488
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -493,18 +551,16 @@
493
 
494
  <div>
495
  <div class=" py-0.5 flex w-full justify-between">
496
- <div class=" self-center text-xs">
497
- {$i18n.t('Response AutoCopy to Clipboard')}
498
- </div>
499
 
500
  <button
501
  class="p-1 px-3 text-xs flex rounded transition"
502
  on:click={() => {
503
- toggleResponseAutoCopy();
504
  }}
505
  type="button"
506
  >
507
- {#if responseAutoCopy === true}
508
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
509
  {:else}
510
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -515,16 +571,18 @@
515
 
516
  <div>
517
  <div class=" py-0.5 flex w-full justify-between">
518
- <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
 
 
519
 
520
  <button
521
  class="p-1 px-3 text-xs flex rounded transition"
522
  on:click={() => {
523
- toggleUserLocation();
524
  }}
525
  type="button"
526
  >
527
- {#if userLocation === true}
528
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
529
  {:else}
530
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -535,16 +593,18 @@
535
 
536
  <div>
537
  <div class=" py-0.5 flex w-full justify-between">
538
- <div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>
 
 
539
 
540
  <button
541
  class="p-1 px-3 text-xs flex rounded transition"
542
  on:click={() => {
543
- toggleHapticFeedback();
544
  }}
545
  type="button"
546
  >
547
- {#if hapticFeedback === true}
548
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
549
  {:else}
550
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
30
  // Interface
31
  let defaultModelId = '';
32
  let showUsername = false;
33
+ let richTextInput = true;
34
 
35
  let landingPageMode = '';
36
  let chatBubble = true;
37
  let chatDirection: 'LTR' | 'RTL' = 'LTR';
38
+
39
+ // Admin - Show Update Available Toast
40
  let showUpdateToast = true;
41
+ let showChangelog = true;
42
 
43
  let showEmojiInCall = false;
44
  let voiceInterruption = false;
 
74
  saveSettings({ showUpdateToast: showUpdateToast });
75
  };
76
 
77
+ const toggleShowChangelog = async () => {
78
+ showChangelog = !showChangelog;
79
+ saveSettings({ showChangelog: showChangelog });
80
+ };
81
+
82
  const toggleShowUsername = async () => {
83
  showUsername = !showUsername;
84
  saveSettings({ showUsername: showUsername });
 
134
  saveSettings({ autoTags });
135
  };
136
 
137
+ const toggleRichTextInput = async () => {
138
+ richTextInput = !richTextInput;
139
+ saveSettings({ richTextInput });
140
+ };
141
+
142
  const toggleResponseAutoCopy = async () => {
143
  const permission = await navigator.clipboard
144
  .readText()
 
182
 
183
  showUsername = $settings.showUsername ?? false;
184
  showUpdateToast = $settings.showUpdateToast ?? true;
185
+ showChangelog = $settings.showChangelog ?? true;
186
 
187
  showEmojiInCall = $settings.showEmojiInCall ?? false;
188
  voiceInterruption = $settings.voiceInterruption ?? false;
189
 
190
+ richTextInput = $settings.richTextInput ?? true;
191
  landingPageMode = $settings.landingPageMode ?? '';
192
  chatBubble = $settings.chatBubble ?? true;
193
  widescreenMode = $settings.widescreenMode ?? false;
 
392
  </button>
393
  </div>
394
  </div>
395
+
396
+ <div>
397
+ <div class=" py-0.5 flex w-full justify-between">
398
+ <div class=" self-center text-xs">
399
+ {$i18n.t(`Show "What's New" modal on login`)}
400
+ </div>
401
+
402
+ <button
403
+ class="p-1 px-3 text-xs flex rounded transition"
404
+ on:click={() => {
405
+ toggleShowChangelog();
406
+ }}
407
+ type="button"
408
+ >
409
+ {#if showChangelog === true}
410
+ <span class="ml-2 self-center">{$i18n.t('On')}</span>
411
+ {:else}
412
+ <span class="ml-2 self-center">{$i18n.t('Off')}</span>
413
+ {/if}
414
+ </button>
415
+ </div>
416
+ </div>
417
  {/if}
418
 
419
+ <div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
420
+
421
  <div>
422
  <div class=" py-0.5 flex w-full justify-between">
423
+ <div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
 
 
424
 
425
  <button
426
  class="p-1 px-3 text-xs flex rounded transition"
427
  on:click={() => {
428
+ toggleTitleAutoGenerate();
429
  }}
430
  type="button"
431
  >
432
+ {#if titleAutoGenerate === true}
433
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
434
  {:else}
435
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
440
 
441
  <div>
442
  <div class=" py-0.5 flex w-full justify-between">
443
+ <div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
 
 
444
 
445
  <button
446
  class="p-1 px-3 text-xs flex rounded transition"
447
  on:click={() => {
448
+ toggleAutoTags();
449
  }}
450
  type="button"
451
  >
452
+ {#if autoTags === true}
453
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
454
  {:else}
455
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
461
  <div>
462
  <div class=" py-0.5 flex w-full justify-between">
463
  <div class=" self-center text-xs">
464
+ {$i18n.t('Response AutoCopy to Clipboard')}
465
  </div>
466
 
467
  <button
468
  class="p-1 px-3 text-xs flex rounded transition"
469
  on:click={() => {
470
+ toggleResponseAutoCopy();
 
 
 
 
 
471
  }}
472
  type="button"
473
  >
474
+ {#if responseAutoCopy === true}
475
+ <span class="ml-2 self-center">{$i18n.t('On')}</span>
476
  {:else}
477
+ <span class="ml-2 self-center">{$i18n.t('Off')}</span>
478
  {/if}
479
  </button>
480
  </div>
481
  </div>
482
 
 
 
483
  <div>
484
  <div class=" py-0.5 flex w-full justify-between">
485
+ <div class=" self-center text-xs">
486
+ {$i18n.t('Rich Text Input for Chat')}
487
+ </div>
488
 
489
  <button
490
  class="p-1 px-3 text-xs flex rounded transition"
491
  on:click={() => {
492
+ toggleRichTextInput();
493
  }}
494
  type="button"
495
  >
496
+ {#if richTextInput === true}
497
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
498
  {:else}
499
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
504
 
505
  <div>
506
  <div class=" py-0.5 flex w-full justify-between">
507
+ <div class=" self-center text-xs">
508
+ {$i18n.t('Chat Background Image')}
509
+ </div>
510
 
511
  <button
512
  class="p-1 px-3 text-xs flex rounded transition"
513
  on:click={() => {
514
+ if (backgroundImageUrl !== null) {
515
+ backgroundImageUrl = null;
516
+ saveSettings({ backgroundImageUrl });
517
+ } else {
518
+ filesInputElement.click();
519
+ }
520
  }}
521
  type="button"
522
  >
523
+ {#if backgroundImageUrl !== null}
524
+ <span class="ml-2 self-center">{$i18n.t('Reset')}</span>
525
+ {:else}
526
+ <span class="ml-2 self-center">{$i18n.t('Upload')}</span>
527
+ {/if}
528
+ </button>
529
+ </div>
530
+ </div>
531
+
532
+ <div>
533
+ <div class=" py-0.5 flex w-full justify-between">
534
+ <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
535
+
536
+ <button
537
+ class="p-1 px-3 text-xs flex rounded transition"
538
+ on:click={() => {
539
+ toggleUserLocation();
540
+ }}
541
+ type="button"
542
+ >
543
+ {#if userLocation === true}
544
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
545
  {:else}
546
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
551
 
552
  <div>
553
  <div class=" py-0.5 flex w-full justify-between">
554
+ <div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>
 
 
555
 
556
  <button
557
  class="p-1 px-3 text-xs flex rounded transition"
558
  on:click={() => {
559
+ toggleHapticFeedback();
560
  }}
561
  type="button"
562
  >
563
+ {#if hapticFeedback === true}
564
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
565
  {:else}
566
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
571
 
572
  <div>
573
  <div class=" py-0.5 flex w-full justify-between">
574
+ <div class=" self-center text-xs">
575
+ {$i18n.t('Fluidly stream large external response chunks')}
576
+ </div>
577
 
578
  <button
579
  class="p-1 px-3 text-xs flex rounded transition"
580
  on:click={() => {
581
+ toggleSplitLargeChunks();
582
  }}
583
  type="button"
584
  >
585
+ {#if splitLargeChunks === true}
586
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
587
  {:else}
588
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
 
593
 
594
  <div>
595
  <div class=" py-0.5 flex w-full justify-between">
596
+ <div class=" self-center text-xs">
597
+ {$i18n.t('Scroll to bottom when switching between branches')}
598
+ </div>
599
 
600
  <button
601
  class="p-1 px-3 text-xs flex rounded transition"
602
  on:click={() => {
603
+ togglesScrollOnBranchChange();
604
  }}
605
  type="button"
606
  >
607
+ {#if scrollOnBranchChange === true}
608
  <span class="ml-2 self-center">{$i18n.t('On')}</span>
609
  {:else}
610
  <span class="ml-2 self-center">{$i18n.t('Off')}</span>
src/lib/components/common/RichTextInput.svelte CHANGED
@@ -288,6 +288,12 @@
288
  return false;
289
  }
290
 
 
 
 
 
 
 
291
  onMount(() => {
292
  const initialDoc = markdownToProseMirrorDoc(value || ''); // Convert the initial content
293
 
@@ -403,6 +409,22 @@
403
  },
404
  paste: (view, event) => {
405
  if (event.clipboardData) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  // Check if the pasted content contains image files
407
  const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
408
  file.type.startsWith('image/')
 
288
  return false;
289
  }
290
 
291
+ // Replace tabs with four spaces
292
+ function handleTabIndentation(text: string): string {
293
+ // Replace each tab character with four spaces
294
+ return text.replace(/\t/g, ' ');
295
+ }
296
+
297
  onMount(() => {
298
  const initialDoc = markdownToProseMirrorDoc(value || ''); // Convert the initial content
299
 
 
409
  },
410
  paste: (view, event) => {
411
  if (event.clipboardData) {
412
+ // Extract plain text from clipboard and paste it without formatting
413
+ const plainText = event.clipboardData.getData('text/plain');
414
+ if (plainText) {
415
+ const modifiedText = handleTabIndentation(plainText);
416
+ console.log(modifiedText);
417
+
418
+ // Replace the current selection with the plain text content
419
+ const tr = view.state.tr.replaceSelectionWith(
420
+ view.state.schema.text(modifiedText),
421
+ false
422
+ );
423
+ view.dispatch(tr.scrollIntoView());
424
+ event.preventDefault(); // Prevent the default paste behavior
425
+ return true;
426
+ }
427
+
428
  // Check if the pasted content contains image files
429
  const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
430
  file.type.startsWith('image/')
src/lib/components/icons/DocumentArrowDown.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let className = 'size-4';
3
+ export let strokeWidth = '1.5';
4
+ </script>
5
+
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ stroke-width={strokeWidth}
11
+ stroke="currentColor"
12
+ class={className}
13
+ >
14
+ <path
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
18
+ />
19
+ </svg>
src/lib/components/icons/SparklesSolid.svelte ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let className = 'w-4 h-4';
3
+ export let strokeWidth = '1.5';
4
+ </script>
5
+
6
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M9 4.5a.75.75 0 0 1 .721.544l.813 2.846a3.75 3.75 0 0 0 2.576 2.576l2.846.813a.75.75 0 0 1 0 1.442l-2.846.813a3.75 3.75 0 0 0-2.576 2.576l-.813 2.846a.75.75 0 0 1-1.442 0l-.813-2.846a3.75 3.75 0 0 0-2.576-2.576l-2.846-.813a.75.75 0 0 1 0-1.442l2.846-.813A3.75 3.75 0 0 0 7.466 7.89l.813-2.846A.75.75 0 0 1 9 4.5ZM18 1.5a.75.75 0 0 1 .728.568l.258 1.036c.236.94.97 1.674 1.91 1.91l1.036.258a.75.75 0 0 1 0 1.456l-1.036.258c-.94.236-1.674.97-1.91 1.91l-.258 1.036a.75.75 0 0 1-1.456 0l-.258-1.036a2.625 2.625 0 0 0-1.91-1.91l-1.036-.258a.75.75 0 0 1 0-1.456l1.036-.258a2.625 2.625 0 0 0 1.91-1.91l.258-1.036A.75.75 0 0 1 18 1.5ZM16.5 15a.75.75 0 0 1 .712.513l.394 1.183c.15.447.5.799.948.948l1.183.395a.75.75 0 0 1 0 1.422l-1.183.395c-.447.15-.799.5-.948.948l-.395 1.183a.75.75 0 0 1-1.422 0l-.395-1.183a1.5 1.5 0 0 0-.948-.948l-1.183-.395a.75.75 0 0 1 0-1.422l1.183-.395c.447-.15.799-.5.948-.948l.395-1.183A.75.75 0 0 1 16.5 15Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
src/lib/components/layout/Sidebar.svelte CHANGED
@@ -571,10 +571,15 @@
571
  importChatHandler(e.detail, true);
572
  }}
573
  on:drop={async (e) => {
574
- const { type, id } = e.detail;
575
 
576
  if (type === 'chat') {
577
- const chat = await getChatById(localStorage.token, id);
 
 
 
 
 
578
 
579
  if (chat) {
580
  console.log(chat);
@@ -587,19 +592,13 @@
587
  toast.error(error);
588
  return null;
589
  });
590
-
591
- if (res) {
592
- initChatList();
593
- }
594
  }
595
 
596
  if (!chat.pinned) {
597
- const res = await toggleChatPinnedStatusById(localStorage.token, id);
598
-
599
- if (res) {
600
- initChatList();
601
- }
602
  }
 
 
603
  }
604
  }
605
  }}
@@ -660,10 +659,15 @@
660
  importChatHandler(e.detail);
661
  }}
662
  on:drop={async (e) => {
663
- const { type, id } = e.detail;
664
 
665
  if (type === 'chat') {
666
- const chat = await getChatById(localStorage.token, id);
 
 
 
 
 
667
 
668
  if (chat) {
669
  console.log(chat);
@@ -674,19 +678,13 @@
674
  return null;
675
  }
676
  );
677
-
678
- if (res) {
679
- initChatList();
680
- }
681
  }
682
 
683
  if (chat.pinned) {
684
- const res = await toggleChatPinnedStatusById(localStorage.token, id);
685
-
686
- if (res) {
687
- initChatList();
688
- }
689
  }
 
 
690
  }
691
  } else if (type === 'folder') {
692
  if (folders[id].parent_id === null) {
 
571
  importChatHandler(e.detail, true);
572
  }}
573
  on:drop={async (e) => {
574
+ const { type, id, item } = e.detail;
575
 
576
  if (type === 'chat') {
577
+ let chat = await getChatById(localStorage.token, id).catch((error) => {
578
+ return null;
579
+ });
580
+ if (!chat && item) {
581
+ chat = await importChat(localStorage.token, item.chat, item?.meta ?? {});
582
+ }
583
 
584
  if (chat) {
585
  console.log(chat);
 
592
  toast.error(error);
593
  return null;
594
  });
 
 
 
 
595
  }
596
 
597
  if (!chat.pinned) {
598
+ const res = await toggleChatPinnedStatusById(localStorage.token, chat.id);
 
 
 
 
599
  }
600
+
601
+ initChatList();
602
  }
603
  }
604
  }}
 
659
  importChatHandler(e.detail);
660
  }}
661
  on:drop={async (e) => {
662
+ const { type, id, item } = e.detail;
663
 
664
  if (type === 'chat') {
665
+ let chat = await getChatById(localStorage.token, id).catch((error) => {
666
+ return null;
667
+ });
668
+ if (!chat && item) {
669
+ chat = await importChat(localStorage.token, item.chat, item?.meta ?? {});
670
+ }
671
 
672
  if (chat) {
673
  console.log(chat);
 
678
  return null;
679
  }
680
  );
 
 
 
 
681
  }
682
 
683
  if (chat.pinned) {
684
+ const res = await toggleChatPinnedStatusById(localStorage.token, chat, id);
 
 
 
 
685
  }
686
+
687
+ initChatList();
688
  }
689
  } else if (type === 'folder') {
690
  if (folders[id].parent_id === null) {
src/lib/components/layout/Sidebar/ChatItem.svelte CHANGED
@@ -11,6 +11,7 @@
11
  cloneChatById,
12
  deleteChatById,
13
  getAllTags,
 
14
  getChatList,
15
  getChatListByTagName,
16
  getPinnedChatList,
@@ -46,7 +47,21 @@
46
  export let selected = false;
47
  export let shiftKey = false;
48
 
 
 
49
  let mouseOver = false;
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  let showShareChatModal = false;
52
  let confirmEdit = false;
@@ -133,7 +148,8 @@
133
  'text/plain',
134
  JSON.stringify({
135
  type: 'chat',
136
- id: id
 
137
  })
138
  );
139
 
@@ -204,7 +220,7 @@
204
  </DragGhost>
205
  {/if}
206
 
207
- <div bind:this={itemElement} class=" w-full {className} relative group" draggable="true">
208
  {#if confirmEdit}
209
  <div
210
  class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
 
11
  cloneChatById,
12
  deleteChatById,
13
  getAllTags,
14
+ getChatById,
15
  getChatList,
16
  getChatListByTagName,
17
  getPinnedChatList,
 
47
  export let selected = false;
48
  export let shiftKey = false;
49
 
50
+ let chat = null;
51
+
52
  let mouseOver = false;
53
+ let draggable = false;
54
+ $: if (mouseOver) {
55
+ loadChat();
56
+ }
57
+
58
+ const loadChat = async () => {
59
+ if (!chat) {
60
+ draggable = false;
61
+ chat = await getChatById(localStorage.token, id);
62
+ draggable = true;
63
+ }
64
+ };
65
 
66
  let showShareChatModal = false;
67
  let confirmEdit = false;
 
148
  'text/plain',
149
  JSON.stringify({
150
  type: 'chat',
151
+ id: id,
152
+ item: chat
153
  })
154
  );
155
 
 
220
  </DragGhost>
221
  {/if}
222
 
223
+ <div bind:this={itemElement} class=" w-full {className} relative group" {draggable}>
224
  {#if confirmEdit}
225
  <div
226
  class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
src/lib/components/layout/Sidebar/RecursiveFolder.svelte CHANGED
@@ -22,7 +22,12 @@
22
  updateFolderParentIdById
23
  } from '$lib/apis/folders';
24
  import { toast } from 'svelte-sonner';
25
- import { getChatsByFolderId, updateChatFolderIdById } from '$lib/apis/chats';
 
 
 
 
 
26
  import ChatItem from './ChatItem.svelte';
27
  import FolderMenu from './Folders/FolderMenu.svelte';
28
  import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@@ -101,7 +106,7 @@
101
  const data = JSON.parse(dataTransfer);
102
  console.log(data);
103
 
104
- const { type, id } = data;
105
 
106
  if (type === 'folder') {
107
  open = true;
@@ -122,8 +127,15 @@
122
  } else if (type === 'chat') {
123
  open = true;
124
 
 
 
 
 
 
 
 
125
  // Move the chat
126
- const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch(
127
  (error) => {
128
  toast.error(error);
129
  return null;
@@ -396,6 +408,7 @@
396
  }}
397
  on:keydown={(e) => {
398
  if (e.key === 'Enter') {
 
399
  edit = false;
400
  }
401
  }}
 
22
  updateFolderParentIdById
23
  } from '$lib/apis/folders';
24
  import { toast } from 'svelte-sonner';
25
+ import {
26
+ getChatById,
27
+ getChatsByFolderId,
28
+ importChat,
29
+ updateChatFolderIdById
30
+ } from '$lib/apis/chats';
31
  import ChatItem from './ChatItem.svelte';
32
  import FolderMenu from './Folders/FolderMenu.svelte';
33
  import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 
106
  const data = JSON.parse(dataTransfer);
107
  console.log(data);
108
 
109
+ const { type, id, item } = data;
110
 
111
  if (type === 'folder') {
112
  open = true;
 
127
  } else if (type === 'chat') {
128
  open = true;
129
 
130
+ let chat = await getChatById(localStorage.token, id).catch((error) => {
131
+ return null;
132
+ });
133
+ if (!chat && item) {
134
+ chat = await importChat(localStorage.token, item.chat, item?.meta ?? {});
135
+ }
136
+
137
  // Move the chat
138
+ const res = await updateChatFolderIdById(localStorage.token, chat.id, folderId).catch(
139
  (error) => {
140
  toast.error(error);
141
  return null;
 
408
  }}
409
  on:keydown={(e) => {
410
  if (e.key === 'Enter') {
411
+ nameUpdateHandler();
412
  edit = false;
413
  }
414
  }}
src/lib/components/playground/Notes.svelte ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { getContext } from 'svelte';
3
+ const i18n = getContext('i18n');
4
+
5
+ import RichTextInput from '../common/RichTextInput.svelte';
6
+ import Spinner from '../common/Spinner.svelte';
7
+ import Sparkles from '../icons/Sparkles.svelte';
8
+ import SparklesSolid from '../icons/SparklesSolid.svelte';
9
+ import Mic from '../icons/Mic.svelte';
10
+ import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
11
+ import Tooltip from '../common/Tooltip.svelte';
12
+ import { toast } from 'svelte-sonner';
13
+
14
+ let name = '';
15
+ let content = '';
16
+
17
+ let voiceInput = false;
18
+ let loading = false;
19
+ </script>
20
+
21
+ <div class="relative flex-1 w-full h-full flex justify-center overflow-auto px-5 py-1">
22
+ {#if loading}
23
+ <div class=" absolute top-0 bottom-0 left-0 right-0 flex">
24
+ <div class="m-auto">
25
+ <Spinner />
26
+ </div>
27
+ </div>
28
+ {/if}
29
+
30
+ <div class=" w-full flex flex-col gap-2 {loading ? 'opacity-20' : ''}">
31
+ <div class="flex-shrink-0 w-full flex justify-between items-center">
32
+ <div class="w-full">
33
+ <input
34
+ class="w-full text-2xl font-medium bg-transparent outline-none"
35
+ type="text"
36
+ bind:value={name}
37
+ placeholder={$i18n.t('Title')}
38
+ required
39
+ />
40
+ </div>
41
+ </div>
42
+
43
+ <div class=" flex-1 w-full h-full">
44
+ <RichTextInput
45
+ className=" input-prose-sm"
46
+ bind:value={content}
47
+ placeholder={$i18n.t('Write something...')}
48
+ />
49
+ </div>
50
+ </div>
51
+
52
+ <div class="absolute bottom-0 left-0 right-0 p-5 max-w-full flex justify-end">
53
+ <div class="flex gap-0.5 justify-end w-full">
54
+ {#if voiceInput}
55
+ <div class="flex-1 w-full">
56
+ <VoiceRecording
57
+ bind:recording={voiceInput}
58
+ className="p-1 w-full max-w-full"
59
+ on:cancel={() => {
60
+ voiceInput = false;
61
+ }}
62
+ on:confirm={(e) => {
63
+ const { text, filename } = e.detail;
64
+
65
+ // url is hostname + /cache/audio/transcription/ + filename
66
+ const url = `${window.location.origin}/cache/audio/transcription/${filename}`;
67
+
68
+ // Open in new tab
69
+
70
+ if (content.trim() !== '') {
71
+ content = `${content}\n\n${text}\n\nRecording: ${url}\n\n`;
72
+ } else {
73
+ content = `${content}${text}\n\nRecording: ${url}\n\n`;
74
+ }
75
+
76
+ voiceInput = false;
77
+ }}
78
+ />
79
+ </div>
80
+ {:else}
81
+ <Tooltip content={$i18n.t('Voice Input')}>
82
+ <button
83
+ class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition shadow-xl"
84
+ type="button"
85
+ on:click={async () => {
86
+ try {
87
+ let stream = await navigator.mediaDevices
88
+ .getUserMedia({ audio: true })
89
+ .catch(function (err) {
90
+ toast.error(
91
+ $i18n.t(`Permission denied when accessing microphone: {{error}}`, {
92
+ error: err
93
+ })
94
+ );
95
+ return null;
96
+ });
97
+
98
+ if (stream) {
99
+ voiceInput = true;
100
+ const tracks = stream.getTracks();
101
+ tracks.forEach((track) => track.stop());
102
+ }
103
+ stream = null;
104
+ } catch {
105
+ toast.error($i18n.t('Permission denied when accessing microphone'));
106
+ }
107
+ }}
108
+ >
109
+ <Mic className="size-4" />
110
+ </button>
111
+ </Tooltip>
112
+ {/if}
113
+
114
+ <!-- <button
115
+ class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition shadow-xl"
116
+ >
117
+ <SparklesSolid className="size-4" />
118
+ </button> -->
119
+ </div>
120
+ </div>
121
+ </div>
src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte CHANGED
@@ -85,8 +85,8 @@
85
  voiceInput = false;
86
  }}
87
  on:confirm={(e) => {
88
- const response = e.detail;
89
- content = `${content}${response} `;
90
 
91
  voiceInput = false;
92
  }}
 
85
  voiceInput = false;
86
  }}
87
  on:confirm={(e) => {
88
+ const { text, filename } = e.detail;
89
+ content = `${content}${text} `;
90
 
91
  voiceInput = false;
92
  }}
src/lib/i18n/locales/ar-BH/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "ليس صحيحا من حيث الواقع",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.",
 
493
  "Notifications": "إشعارات",
494
  "November": "نوفمبر",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "منصب",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "ليس صحيحا من حيث الواقع",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.",
493
+ "Notes": "",
494
  "Notifications": "إشعارات",
495
  "November": "نوفمبر",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "منصب",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/bg-BG/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "Не е фактологически правилно",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Забележка: Ако зададете минимален резултат, търсенето ще върне само документи с резултат, по-голям или равен на минималния резултат.",
 
493
  "Notifications": "Десктоп Известия",
494
  "November": "Ноември",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Роля",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "Не е фактологически правилно",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Забележка: Ако зададете минимален резултат, търсенето ще върне само документи с резултат, по-голям или равен на минималния резултат.",
493
+ "Notes": "",
494
  "Notifications": "Десктоп Известия",
495
  "November": "Ноември",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Роля",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/bn-BD/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "তথ্যগত দিক থেকে সঠিক নয়",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "দ্রষ্টব্য: আপনি যদি ন্যূনতম স্কোর সেট করেন তবে অনুসন্ধানটি কেবলমাত্র ন্যূনতম স্কোরের চেয়ে বেশি বা সমান স্কোর সহ নথিগুলি ফেরত দেবে।",
 
493
  "Notifications": "নোটিফিকেশনসমূহ",
494
  "November": "নভেম্বর",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "পদবি",
592
  "Rosé Pine": "রোজ পাইন",
 
490
  "Not factually correct": "তথ্যগত দিক থেকে সঠিক নয়",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "দ্রষ্টব্য: আপনি যদি ন্যূনতম স্কোর সেট করেন তবে অনুসন্ধানটি কেবলমাত্র ন্যূনতম স্কোরের চেয়ে বেশি বা সমান স্কোর সহ নথিগুলি ফেরত দেবে।",
493
+ "Notes": "",
494
  "Notifications": "নোটিফিকেশনসমূহ",
495
  "November": "নভেম্বর",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "পদবি",
594
  "Rosé Pine": "রোজ পাইন",
src/lib/i18n/locales/ca-ES/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "No és clarament correcte",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
 
493
  "Notifications": "Notificacions",
494
  "November": "Novembre",
495
  "num_gpu (Ollama)": "num_gpu (Ollama)",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de resposta no es poden activar perquè els permisos del lloc web han estat rebutjats. Comprova les preferències del navegador per donar l'accés necessari.",
588
  "Response splitting": "Divisió de la resposta",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Rol",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "No és clarament correcte",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
493
+ "Notes": "",
494
  "Notifications": "Notificacions",
495
  "November": "Novembre",
496
  "num_gpu (Ollama)": "num_gpu (Ollama)",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de resposta no es poden activar perquè els permisos del lloc web han estat rebutjats. Comprova les preferències del navegador per donar l'accés necessari.",
589
  "Response splitting": "Divisió de la resposta",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Rol",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/ceb-PH/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
 
493
  "Notifications": "Mga pahibalo sa desktop",
494
  "November": "",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Papel",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
493
+ "Notes": "",
494
  "Notifications": "Mga pahibalo sa desktop",
495
  "November": "",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Papel",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/da-DK/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "Ikke faktuelt korrekt",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Bemærk: Hvis du angiver en minimumscore, returnerer søgningen kun dokumenter med en score, der er større end eller lig med minimumscoren.",
 
493
  "Notifications": "Notifikationer",
494
  "November": "November",
495
  "num_gpu (Ollama)": "num_gpu (Ollama)",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Svarnotifikationer kan ikke aktiveres, da webstedets tilladelser er blevet nægtet. Besøg dine browserindstillinger for at give den nødvendige adgang.",
588
  "Response splitting": "Svaropdeling",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Rolle",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "Ikke faktuelt korrekt",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Bemærk: Hvis du angiver en minimumscore, returnerer søgningen kun dokumenter med en score, der er større end eller lig med minimumscoren.",
493
+ "Notes": "",
494
  "Notifications": "Notifikationer",
495
  "November": "November",
496
  "num_gpu (Ollama)": "num_gpu (Ollama)",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Svarnotifikationer kan ikke aktiveres, da webstedets tilladelser er blevet nægtet. Besøg dine browserindstillinger for at give den nødvendige adgang.",
589
  "Response splitting": "Svaropdeling",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Rolle",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/de-DE/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "Nicht sachlich korrekt",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Hinweis: Wenn Sie eine Mindestpunktzahl festlegen, werden in der Suche nur Dokumente mit einer Punktzahl größer oder gleich der Mindestpunktzahl zurückgegeben.",
 
493
  "Notifications": "Benachrichtigungen",
494
  "November": "November",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Benachrichtigungen können nicht aktiviert werden, da die Website-Berechtigungen abgelehnt wurden. Bitte besuchen Sie Ihre Browser-Einstellungen, um den erforderlichen Zugriff zu gewähren.",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Rolle",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "Nicht sachlich korrekt",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Hinweis: Wenn Sie eine Mindestpunktzahl festlegen, werden in der Suche nur Dokumente mit einer Punktzahl größer oder gleich der Mindestpunktzahl zurückgegeben.",
493
+ "Notes": "",
494
  "Notifications": "Benachrichtigungen",
495
  "November": "November",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Benachrichtigungen können nicht aktiviert werden, da die Website-Berechtigungen abgelehnt wurden. Bitte besuchen Sie Ihre Browser-Einstellungen, um den erforderlichen Zugriff zu gewähren.",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Rolle",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/dg-DG/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
 
493
  "Notifications": "Notifications",
494
  "November": "",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Role",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
493
+ "Notes": "",
494
  "Notifications": "Notifications",
495
  "November": "",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Role",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/en-GB/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
 
493
  "Notifications": "",
494
  "November": "",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "",
592
  "Rosé Pine": "",
 
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
493
+ "Notes": "",
494
  "Notifications": "",
495
  "November": "",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "",
594
  "Rosé Pine": "",
src/lib/i18n/locales/en-US/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
 
493
  "Notifications": "",
494
  "November": "",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "",
592
  "Rosé Pine": "",
 
490
  "Not factually correct": "",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
493
+ "Notes": "",
494
  "Notifications": "",
495
  "November": "",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "",
594
  "Rosé Pine": "",
src/lib/i18n/locales/es-ES/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "No es correcto en todos los aspectos",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si estableces una puntuación mínima, la búsqueda sólo devolverá documentos con una puntuación mayor o igual a la puntuación mínima.",
 
493
  "Notifications": "Notificaciones",
494
  "November": "Noviembre",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Las notificaciones de respuesta no pueden activarse debido a que los permisos del sitio web han sido denegados. Por favor, visite las configuraciones de su navegador para otorgar el acceso necesario.",
588
  "Response splitting": "División de respuestas",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "Rol",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "No es correcto en todos los aspectos",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si estableces una puntuación mínima, la búsqueda sólo devolverá documentos con una puntuación mayor o igual a la puntuación mínima.",
493
+ "Notes": "",
494
  "Notifications": "Notificaciones",
495
  "November": "Noviembre",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Las notificaciones de respuesta no pueden activarse debido a que los permisos del sitio web han sido denegados. Por favor, visite las configuraciones de su navegador para otorgar el acceso necesario.",
589
  "Response splitting": "División de respuestas",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "Rol",
594
  "Rosé Pine": "Rosé Pine",
src/lib/i18n/locales/fa-IR/translation.json CHANGED
@@ -490,6 +490,7 @@
490
  "Not factually correct": "اشتباهی فکری نیست",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "توجه: اگر حداقل نمره را تعیین کنید، جستجو تنها اسنادی را با نمره بیشتر یا برابر با حداقل نمره باز می گرداند.",
 
493
  "Notifications": "اعلان",
494
  "November": "نوامبر",
495
  "num_gpu (Ollama)": "",
@@ -587,6 +588,7 @@
587
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
588
  "Response splitting": "",
589
  "Result": "",
 
590
  "RK": "",
591
  "Role": "نقش",
592
  "Rosé Pine": "Rosé Pine",
 
490
  "Not factually correct": "اشتباهی فکری نیست",
491
  "Not helpful": "",
492
  "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "توجه: اگر حداقل نمره را تعیین کنید، جستجو تنها اسنادی را با نمره بیشتر یا برابر با حداقل نمره باز می گرداند.",
493
+ "Notes": "",
494
  "Notifications": "اعلان",
495
  "November": "نوامبر",
496
  "num_gpu (Ollama)": "",
 
588
  "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
589
  "Response splitting": "",
590
  "Result": "",
591
+ "Rich Text Input for Chat": "",
592
  "RK": "",
593
  "Role": "نقش",
594
  "Rosé Pine": "Rosé Pine",