coolmanx commited on
Commit
38fcb55
1 Parent(s): 2780a47
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. CHANGELOG.md +17 -0
  2. backend/open_webui/apps/ollama/main.py +4 -2
  3. backend/open_webui/apps/openai/main.py +6 -0
  4. backend/open_webui/apps/retrieval/main.py +45 -11
  5. backend/open_webui/apps/retrieval/utils.py +7 -27
  6. backend/open_webui/apps/retrieval/web/mojeek.py +40 -0
  7. backend/open_webui/apps/webui/routers/auths.py +14 -4
  8. backend/open_webui/apps/webui/routers/files.py +1 -1
  9. backend/open_webui/apps/webui/routers/tools.py +25 -29
  10. backend/open_webui/config.py +27 -10
  11. backend/open_webui/main.py +67 -36
  12. backend/open_webui/utils/tools.py +79 -88
  13. package-lock.json +945 -18
  14. package.json +9 -1
  15. src/app.css +72 -5
  16. src/lib/apis/streaming/index.ts +4 -4
  17. src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte +1 -1
  18. src/lib/components/admin/Settings/Models.svelte +1 -1
  19. src/lib/components/admin/Settings/WebSearch.svelte +12 -0
  20. src/lib/components/chat/Chat.svelte +13 -13
  21. src/lib/components/chat/MessageInput.svelte +20 -51
  22. src/lib/components/chat/Messages/Citations.svelte +36 -27
  23. src/lib/components/chat/Messages/ContentRenderer.svelte +29 -0
  24. src/lib/components/chat/Messages/Markdown.svelte +5 -1
  25. src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte +8 -4
  26. src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +15 -4
  27. src/lib/components/chat/Messages/Markdown/Source.svelte +25 -0
  28. src/lib/components/chat/Messages/RateComment.svelte +1 -1
  29. src/lib/components/chat/Messages/ResponseMessage.svelte +12 -3
  30. src/lib/components/chat/Messages/UserMessage.svelte +1 -5
  31. src/lib/components/common/RichTextInput.svelte +201 -412
  32. src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +1 -1
  33. src/lib/i18n/locales/ar-BH/translation.json +2 -0
  34. src/lib/i18n/locales/bg-BG/translation.json +2 -0
  35. src/lib/i18n/locales/bn-BD/translation.json +2 -0
  36. src/lib/i18n/locales/ca-ES/translation.json +2 -0
  37. src/lib/i18n/locales/ceb-PH/translation.json +2 -0
  38. src/lib/i18n/locales/cs-CZ/translation.json +2 -0
  39. src/lib/i18n/locales/da-DK/translation.json +2 -0
  40. src/lib/i18n/locales/de-DE/translation.json +2 -0
  41. src/lib/i18n/locales/dg-DG/translation.json +2 -0
  42. src/lib/i18n/locales/en-GB/translation.json +2 -0
  43. src/lib/i18n/locales/en-US/translation.json +2 -0
  44. src/lib/i18n/locales/es-ES/translation.json +2 -0
  45. src/lib/i18n/locales/fa-IR/translation.json +2 -0
  46. src/lib/i18n/locales/fi-FI/translation.json +2 -0
  47. src/lib/i18n/locales/fr-CA/translation.json +2 -0
  48. src/lib/i18n/locales/fr-FR/translation.json +2 -0
  49. src/lib/i18n/locales/he-IL/translation.json +2 -0
  50. src/lib/i18n/locales/hi-IN/translation.json +2 -0
CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ## [0.4.2] - 2024-11-20
9
 
10
  ### Fixed
 
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
 
8
+ ## [0.4.3] - 2024-11-21
9
+
10
+ ### Added
11
+
12
+ - **📚 Inline Citations for RAG Results**: Get seamless inline citations for Retrieval-Augmented Generation (RAG) responses using the default RAG prompt. Note: This feature only supports newly uploaded files, improving traceability and providing source clarity.
13
+ - **🎨 Better Rich Text Input Support**: Enjoy smoother and more reliable rich text formatting for chats, enhancing communication quality.
14
+ - **⚡ Faster Model Retrieval**: Implemented caching optimizations for faster model loading, providing a noticeable speed boost across workflows. Further improvements are on the way!
15
+
16
+ ### Fixed
17
+
18
+ - **🔗 Pipelines Feature Restored**: Resolved a critical issue that previously prevented Pipelines from functioning, ensuring seamless workflows.
19
+ - **✏️ Missing Suffix Field in Ollama Form**: Added the missing "suffix" field to the Ollama generate form, enhancing customization options.
20
+
21
+ ### Changed
22
+
23
+ - **🗂️ Renamed "Citations" to "Sources"**: Improved clarity and consistency by renaming the "citations" field to "sources" in messages.
24
+
25
  ## [0.4.2] - 2024-11-20
26
 
27
  ### Fixed
backend/open_webui/apps/ollama/main.py CHANGED
@@ -9,6 +9,8 @@ from typing import Optional, Union
9
  from urllib.parse import urlparse
10
 
11
  import aiohttp
 
 
12
  import requests
13
  from open_webui.apps.webui.models.models import Models
14
  from open_webui.config import (
@@ -256,6 +258,7 @@ def merge_models_lists(model_lists):
256
  return list(merged_models.values())
257
 
258
 
 
259
  async def get_all_models():
260
  log.info("get_all_models()")
261
  if app.state.config.ENABLE_OLLAMA_API:
@@ -295,8 +298,6 @@ async def get_all_models():
295
  for model in response.get("models", []):
296
  model["model"] = f"{prefix_id}.{model['model']}"
297
 
298
- print(responses)
299
-
300
  models = {
301
  "models": merge_models_lists(
302
  map(
@@ -837,6 +838,7 @@ async def generate_ollama_batch_embeddings(
837
  class GenerateCompletionForm(BaseModel):
838
  model: str
839
  prompt: str
 
840
  images: Optional[list[str]] = None
841
  format: Optional[str] = None
842
  options: Optional[dict] = None
 
9
  from urllib.parse import urlparse
10
 
11
  import aiohttp
12
+ from aiocache import cached
13
+
14
  import requests
15
  from open_webui.apps.webui.models.models import Models
16
  from open_webui.config import (
 
258
  return list(merged_models.values())
259
 
260
 
261
+ @cached(ttl=3)
262
  async def get_all_models():
263
  log.info("get_all_models()")
264
  if app.state.config.ENABLE_OLLAMA_API:
 
298
  for model in response.get("models", []):
299
  model["model"] = f"{prefix_id}.{model['model']}"
300
 
 
 
301
  models = {
302
  "models": merge_models_lists(
303
  map(
 
838
  class GenerateCompletionForm(BaseModel):
839
  model: str
840
  prompt: str
841
+ suffix: Optional[str] = None
842
  images: Optional[list[str]] = None
843
  format: Optional[str] = None
844
  options: Optional[dict] = None
backend/open_webui/apps/openai/main.py CHANGED
@@ -6,7 +6,10 @@ from pathlib import Path
6
  from typing import Literal, Optional, overload
7
 
8
  import aiohttp
 
9
  import requests
 
 
10
  from open_webui.apps.webui.models.models import Models
11
  from open_webui.config import (
12
  CACHE_DIR,
@@ -302,6 +305,8 @@ async def get_all_models_responses() -> list:
302
  }
303
 
304
  tasks.append(asyncio.ensure_future(asyncio.sleep(0, model_list)))
 
 
305
 
306
  responses = await asyncio.gather(*tasks)
307
 
@@ -323,6 +328,7 @@ async def get_all_models_responses() -> list:
323
  return responses
324
 
325
 
 
326
  async def get_all_models() -> dict[str, list]:
327
  log.info("get_all_models()")
328
 
 
6
  from typing import Literal, Optional, overload
7
 
8
  import aiohttp
9
+ from aiocache import cached
10
  import requests
11
+
12
+
13
  from open_webui.apps.webui.models.models import Models
14
  from open_webui.config import (
15
  CACHE_DIR,
 
305
  }
306
 
307
  tasks.append(asyncio.ensure_future(asyncio.sleep(0, model_list)))
308
+ else:
309
+ tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
310
 
311
  responses = await asyncio.gather(*tasks)
312
 
 
328
  return responses
329
 
330
 
331
+ @cached(ttl=3)
332
  async def get_all_models() -> dict[str, list]:
333
  log.info("get_all_models()")
334
 
backend/open_webui/apps/retrieval/main.py CHANGED
@@ -29,6 +29,7 @@ from open_webui.apps.retrieval.loaders.youtube import YoutubeLoader
29
  from open_webui.apps.retrieval.web.main import SearchResult
30
  from open_webui.apps.retrieval.web.utils import get_web_loader
31
  from open_webui.apps.retrieval.web.brave import search_brave
 
32
  from open_webui.apps.retrieval.web.duckduckgo import search_duckduckgo
33
  from open_webui.apps.retrieval.web.google_pse import search_google_pse
34
  from open_webui.apps.retrieval.web.jina_search import search_jina
@@ -53,6 +54,7 @@ from open_webui.apps.retrieval.utils import (
53
  from open_webui.apps.webui.models.files import Files
54
  from open_webui.config import (
55
  BRAVE_SEARCH_API_KEY,
 
56
  TIKTOKEN_ENCODING_NAME,
57
  RAG_TEXT_SPLITTER,
58
  CHUNK_OVERLAP,
@@ -180,6 +182,7 @@ app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
180
  app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
181
  app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
182
  app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
 
183
  app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
184
  app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
185
  app.state.config.SERPER_API_KEY = SERPER_API_KEY
@@ -478,6 +481,7 @@ async def get_rag_config(user=Depends(get_admin_user)):
478
  "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
479
  "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
480
  "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
 
481
  "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
482
  "serpstack_https": app.state.config.SERPSTACK_HTTPS,
483
  "serper_api_key": app.state.config.SERPER_API_KEY,
@@ -523,6 +527,7 @@ class WebSearchConfig(BaseModel):
523
  google_pse_api_key: Optional[str] = None
524
  google_pse_engine_id: Optional[str] = None
525
  brave_search_api_key: Optional[str] = None
 
526
  serpstack_api_key: Optional[str] = None
527
  serpstack_https: Optional[bool] = None
528
  serper_api_key: Optional[str] = None
@@ -593,6 +598,9 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
593
  app.state.config.BRAVE_SEARCH_API_KEY = (
594
  form_data.web.search.brave_search_api_key
595
  )
 
 
 
596
  app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
597
  app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
598
  app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
@@ -643,6 +651,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
643
  "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
644
  "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
645
  "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
 
646
  "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
647
  "serpstack_https": app.state.config.SERPSTACK_HTTPS,
648
  "serper_api_key": app.state.config.SERPER_API_KEY,
@@ -884,19 +893,17 @@ def process_file(
884
  # Update the content in the file
885
  # Usage: /files/{file_id}/data/content/update
886
 
887
- VECTOR_DB_CLIENT.delete(
888
- collection_name=f"file-{file.id}",
889
- filter={"file_id": file.id},
890
- )
891
 
892
  docs = [
893
  Document(
894
  page_content=form_data.content,
895
  metadata={
896
- "name": file.meta.get("name", file.filename),
 
897
  "created_by": file.user_id,
898
  "file_id": file.id,
899
- **file.meta,
900
  },
901
  )
902
  ]
@@ -923,10 +930,11 @@ def process_file(
923
  Document(
924
  page_content=file.data.get("content", ""),
925
  metadata={
926
- "name": file.meta.get("name", file.filename),
 
927
  "created_by": file.user_id,
928
  "file_id": file.id,
929
- **file.meta,
930
  },
931
  )
932
  ]
@@ -946,15 +954,30 @@ def process_file(
946
  docs = loader.load(
947
  file.filename, file.meta.get("content_type"), file_path
948
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949
  else:
950
  docs = [
951
  Document(
952
  page_content=file.data.get("content", ""),
953
  metadata={
 
954
  "name": file.filename,
955
  "created_by": file.user_id,
956
  "file_id": file.id,
957
- **file.meta,
958
  },
959
  )
960
  ]
@@ -975,7 +998,7 @@ def process_file(
975
  collection_name=collection_name,
976
  metadata={
977
  "file_id": file.id,
978
- "name": file.meta.get("name", file.filename),
979
  "hash": hash,
980
  },
981
  add=(True if form_data.collection_name else False),
@@ -992,7 +1015,7 @@ def process_file(
992
  return {
993
  "status": True,
994
  "collection_name": collection_name,
995
- "filename": file.meta.get("name", file.filename),
996
  "content": text_content,
997
  }
998
  except Exception as e:
@@ -1131,6 +1154,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
1131
  - SEARXNG_QUERY_URL
1132
  - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
1133
  - BRAVE_SEARCH_API_KEY
 
1134
  - SERPSTACK_API_KEY
1135
  - SERPER_API_KEY
1136
  - SERPLY_API_KEY
@@ -1177,6 +1201,16 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
1177
  )
1178
  else:
1179
  raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
 
 
 
 
 
 
 
 
 
 
1180
  elif engine == "serpstack":
1181
  if app.state.config.SERPSTACK_API_KEY:
1182
  return search_serpstack(
 
29
  from open_webui.apps.retrieval.web.main import SearchResult
30
  from open_webui.apps.retrieval.web.utils import get_web_loader
31
  from open_webui.apps.retrieval.web.brave import search_brave
32
+ from open_webui.apps.retrieval.web.mojeek import search_mojeek
33
  from open_webui.apps.retrieval.web.duckduckgo import search_duckduckgo
34
  from open_webui.apps.retrieval.web.google_pse import search_google_pse
35
  from open_webui.apps.retrieval.web.jina_search import search_jina
 
54
  from open_webui.apps.webui.models.files import Files
55
  from open_webui.config import (
56
  BRAVE_SEARCH_API_KEY,
57
+ MOJEEK_SEARCH_API_KEY,
58
  TIKTOKEN_ENCODING_NAME,
59
  RAG_TEXT_SPLITTER,
60
  CHUNK_OVERLAP,
 
182
  app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
183
  app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
184
  app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
185
+ app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY
186
  app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
187
  app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
188
  app.state.config.SERPER_API_KEY = SERPER_API_KEY
 
481
  "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
482
  "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
483
  "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
484
+ "mojeek_search_api_key": app.state.config.MOJEEK_SEARCH_API_KEY,
485
  "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
486
  "serpstack_https": app.state.config.SERPSTACK_HTTPS,
487
  "serper_api_key": app.state.config.SERPER_API_KEY,
 
527
  google_pse_api_key: Optional[str] = None
528
  google_pse_engine_id: Optional[str] = None
529
  brave_search_api_key: Optional[str] = None
530
+ mojeek_search_api_key: Optional[str] = None
531
  serpstack_api_key: Optional[str] = None
532
  serpstack_https: Optional[bool] = None
533
  serper_api_key: Optional[str] = None
 
598
  app.state.config.BRAVE_SEARCH_API_KEY = (
599
  form_data.web.search.brave_search_api_key
600
  )
601
+ app.state.config.MOJEEK_SEARCH_API_KEY = (
602
+ form_data.web.search.mojeek_search_api_key
603
+ )
604
  app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
605
  app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
606
  app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
 
651
  "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
652
  "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
653
  "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
654
+ "mojeek_search_api_key": app.state.config.MOJEEK_SEARCH_API_KEY,
655
  "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
656
  "serpstack_https": app.state.config.SERPSTACK_HTTPS,
657
  "serper_api_key": app.state.config.SERPER_API_KEY,
 
893
  # Update the content in the file
894
  # Usage: /files/{file_id}/data/content/update
895
 
896
+ VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
 
 
 
897
 
898
  docs = [
899
  Document(
900
  page_content=form_data.content,
901
  metadata={
902
+ **file.meta,
903
+ "name": file.filename,
904
  "created_by": file.user_id,
905
  "file_id": file.id,
906
+ "source": file.filename,
907
  },
908
  )
909
  ]
 
930
  Document(
931
  page_content=file.data.get("content", ""),
932
  metadata={
933
+ **file.meta,
934
+ "name": file.filename,
935
  "created_by": file.user_id,
936
  "file_id": file.id,
937
+ "source": file.filename,
938
  },
939
  )
940
  ]
 
954
  docs = loader.load(
955
  file.filename, file.meta.get("content_type"), file_path
956
  )
957
+
958
+ docs = [
959
+ Document(
960
+ page_content=doc.page_content,
961
+ metadata={
962
+ **doc.metadata,
963
+ "name": file.filename,
964
+ "created_by": file.user_id,
965
+ "file_id": file.id,
966
+ "source": file.filename,
967
+ },
968
+ )
969
+ for doc in docs
970
+ ]
971
  else:
972
  docs = [
973
  Document(
974
  page_content=file.data.get("content", ""),
975
  metadata={
976
+ **file.meta,
977
  "name": file.filename,
978
  "created_by": file.user_id,
979
  "file_id": file.id,
980
+ "source": file.filename,
981
  },
982
  )
983
  ]
 
998
  collection_name=collection_name,
999
  metadata={
1000
  "file_id": file.id,
1001
+ "name": file.filename,
1002
  "hash": hash,
1003
  },
1004
  add=(True if form_data.collection_name else False),
 
1015
  return {
1016
  "status": True,
1017
  "collection_name": collection_name,
1018
+ "filename": file.filename,
1019
  "content": text_content,
1020
  }
1021
  except Exception as e:
 
1154
  - SEARXNG_QUERY_URL
1155
  - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
1156
  - BRAVE_SEARCH_API_KEY
1157
+ - MOJEEK_SEARCH_API_KEY
1158
  - SERPSTACK_API_KEY
1159
  - SERPER_API_KEY
1160
  - SERPLY_API_KEY
 
1201
  )
1202
  else:
1203
  raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
1204
+ elif engine == "mojeek":
1205
+ if app.state.config.MOJEEK_SEARCH_API_KEY:
1206
+ return search_mojeek(
1207
+ app.state.config.MOJEEK_SEARCH_API_KEY,
1208
+ query,
1209
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
1210
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
1211
+ )
1212
+ else:
1213
+ raise Exception("No MOJEEK_SEARCH_API_KEY found in environment variables")
1214
  elif engine == "serpstack":
1215
  if app.state.config.SERPSTACK_API_KEY:
1216
  return search_serpstack(
backend/open_webui/apps/retrieval/utils.py CHANGED
@@ -307,7 +307,7 @@ def get_embedding_function(
307
  return lambda query: generate_multiple(query, func)
308
 
309
 
310
- def get_rag_context(
311
  files,
312
  queries,
313
  embedding_function,
@@ -387,43 +387,24 @@ def get_rag_context(
387
  del file["data"]
388
  relevant_contexts.append({**context, "file": file})
389
 
390
- contexts = []
391
- citations = []
392
  for context in relevant_contexts:
393
  try:
394
  if "documents" in context:
395
- file_names = list(
396
- set(
397
- [
398
- metadata["name"]
399
- for metadata in context["metadatas"][0]
400
- if metadata is not None and "name" in metadata
401
- ]
402
- )
403
- )
404
- contexts.append(
405
- ((", ".join(file_names) + ":\n\n") if file_names else "")
406
- + "\n\n".join(
407
- [text for text in context["documents"][0] if text is not None]
408
- )
409
- )
410
-
411
  if "metadatas" in context:
412
- citation = {
413
  "source": context["file"],
414
  "document": context["documents"][0],
415
  "metadata": context["metadatas"][0],
416
  }
417
  if "distances" in context and context["distances"]:
418
- citation["distances"] = context["distances"][0]
419
- citations.append(citation)
 
420
  except Exception as e:
421
  log.exception(e)
422
 
423
- print("contexts", contexts)
424
- print("citations", citations)
425
-
426
- return contexts, citations
427
 
428
 
429
  def get_model_path(model: str, update_model: bool = False):
@@ -502,7 +483,6 @@ def generate_ollama_batch_embeddings(
502
  r.raise_for_status()
503
  data = r.json()
504
 
505
- print(data)
506
  if "embeddings" in data:
507
  return data["embeddings"]
508
  else:
 
307
  return lambda query: generate_multiple(query, func)
308
 
309
 
310
+ def get_sources_from_files(
311
  files,
312
  queries,
313
  embedding_function,
 
387
  del file["data"]
388
  relevant_contexts.append({**context, "file": file})
389
 
390
+ sources = []
 
391
  for context in relevant_contexts:
392
  try:
393
  if "documents" in context:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  if "metadatas" in context:
395
+ source = {
396
  "source": context["file"],
397
  "document": context["documents"][0],
398
  "metadata": context["metadatas"][0],
399
  }
400
  if "distances" in context and context["distances"]:
401
+ source["distances"] = context["distances"][0]
402
+
403
+ sources.append(source)
404
  except Exception as e:
405
  log.exception(e)
406
 
407
+ return sources
 
 
 
408
 
409
 
410
  def get_model_path(model: str, update_model: bool = False):
 
483
  r.raise_for_status()
484
  data = r.json()
485
 
 
486
  if "embeddings" in data:
487
  return data["embeddings"]
488
  else:
backend/open_webui/apps/retrieval/web/mojeek.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import requests
5
+ from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
6
+ from open_webui.env import SRC_LOG_LEVELS
7
+
8
+ log = logging.getLogger(__name__)
9
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
10
+
11
+
12
+ def search_mojeek(
13
+ api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
14
+ ) -> list[SearchResult]:
15
+ """Search using Mojeek's Search API and return the results as a list of SearchResult objects.
16
+
17
+ Args:
18
+ api_key (str): A Mojeek Search API key
19
+ query (str): The query to search for
20
+ """
21
+ url = "https://api.mojeek.com/search"
22
+ headers = {
23
+ "Accept": "application/json",
24
+ }
25
+ params = {"q": query, "api_key": api_key, "fmt": "json", "t": count}
26
+
27
+ response = requests.get(url, headers=headers, params=params)
28
+ response.raise_for_status()
29
+ json_response = response.json()
30
+ results = json_response.get("response", {}).get("results", [])
31
+ print(results)
32
+ if filter_list:
33
+ results = get_filtered_results(results, filter_list)
34
+
35
+ return [
36
+ SearchResult(
37
+ link=result["url"], title=result.get("title"), snippet=result.get("desc")
38
+ )
39
+ for result in results
40
+ ]
backend/open_webui/apps/webui/routers/auths.py CHANGED
@@ -238,10 +238,20 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
238
 
239
  user = Users.get_user_by_email(mail)
240
  if not user:
241
-
242
  try:
243
- hashed = get_password_hash(form_data.password)
244
- user = Auths.insert_new_auth(mail, hashed, cn)
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  if not user:
247
  raise HTTPException(
@@ -253,7 +263,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
253
  except Exception as err:
254
  raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
255
 
256
- user = Auths.authenticate_user(mail, password=str(form_data.password))
257
 
258
  if user:
259
  token = create_token(
 
238
 
239
  user = Users.get_user_by_email(mail)
240
  if not user:
 
241
  try:
242
+ role = (
243
+ "admin"
244
+ if Users.get_num_users() == 0
245
+ else request.app.state.config.DEFAULT_USER_ROLE
246
+ )
247
+
248
+ user = Auths.insert_new_auth(
249
+ mail,
250
+ str(uuid.uuid4()),
251
+ cn,
252
+ None,
253
+ role,
254
+ )
255
 
256
  if not user:
257
  raise HTTPException(
 
263
  except Exception as err:
264
  raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
265
 
266
+ user = Auths.authenticate_user_by_trusted_header(mail)
267
 
268
  if user:
269
  token = create_token(
backend/open_webui/apps/webui/routers/files.py CHANGED
@@ -56,7 +56,7 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
56
  FileForm(
57
  **{
58
  "id": id,
59
- "filename": filename,
60
  "path": file_path,
61
  "meta": {
62
  "name": name,
 
56
  FileForm(
57
  **{
58
  "id": id,
59
+ "filename": name,
60
  "path": file_path,
61
  "meta": {
62
  "name": name,
backend/open_webui/apps/webui/routers/tools.py CHANGED
@@ -1,4 +1,3 @@
1
- import os
2
  from pathlib import Path
3
  from typing import Optional
4
 
@@ -10,7 +9,7 @@ from open_webui.apps.webui.models.tools import (
10
  Tools,
11
  )
12
  from open_webui.apps.webui.utils import load_tools_module_by_id, replace_imports
13
- from open_webui.config import CACHE_DIR, DATA_DIR
14
  from open_webui.constants import ERROR_MESSAGES
15
  from fastapi import APIRouter, Depends, HTTPException, Request, status
16
  from open_webui.utils.tools import get_tools_specs
@@ -300,38 +299,35 @@ async def update_tools_valves_by_id(
300
  request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
301
  ):
302
  tools = Tools.get_tool_by_id(id)
303
- if tools:
304
- if id in request.app.state.TOOLS:
305
- tools_module = request.app.state.TOOLS[id]
306
- else:
307
- tools_module, _ = load_tools_module_by_id(id)
308
- request.app.state.TOOLS[id] = tools_module
309
-
310
- if hasattr(tools_module, "Valves"):
311
- Valves = tools_module.Valves
312
-
313
- try:
314
- form_data = {k: v for k, v in form_data.items() if v is not None}
315
- valves = Valves(**form_data)
316
- Tools.update_tool_valves_by_id(id, valves.model_dump())
317
- return valves.model_dump()
318
- except Exception as e:
319
- print(e)
320
- raise HTTPException(
321
- status_code=status.HTTP_400_BAD_REQUEST,
322
- detail=ERROR_MESSAGES.DEFAULT(str(e)),
323
- )
324
- else:
325
- raise HTTPException(
326
- status_code=status.HTTP_401_UNAUTHORIZED,
327
- detail=ERROR_MESSAGES.NOT_FOUND,
328
- )
329
-
330
  else:
 
 
 
 
331
  raise HTTPException(
332
  status_code=status.HTTP_401_UNAUTHORIZED,
333
  detail=ERROR_MESSAGES.NOT_FOUND,
334
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
 
337
  ############################
 
 
1
  from pathlib import Path
2
  from typing import Optional
3
 
 
9
  Tools,
10
  )
11
  from open_webui.apps.webui.utils import load_tools_module_by_id, replace_imports
12
+ from open_webui.config import CACHE_DIR
13
  from open_webui.constants import ERROR_MESSAGES
14
  from fastapi import APIRouter, Depends, HTTPException, Request, status
15
  from open_webui.utils.tools import get_tools_specs
 
299
  request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
300
  ):
301
  tools = Tools.get_tool_by_id(id)
302
+ if not tools:
303
+ raise HTTPException(
304
+ status_code=status.HTTP_401_UNAUTHORIZED,
305
+ detail=ERROR_MESSAGES.NOT_FOUND,
306
+ )
307
+ if id in request.app.state.TOOLS:
308
+ tools_module = request.app.state.TOOLS[id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  else:
310
+ tools_module, _ = load_tools_module_by_id(id)
311
+ request.app.state.TOOLS[id] = tools_module
312
+
313
+ if not hasattr(tools_module, "Valves"):
314
  raise HTTPException(
315
  status_code=status.HTTP_401_UNAUTHORIZED,
316
  detail=ERROR_MESSAGES.NOT_FOUND,
317
  )
318
+ Valves = tools_module.Valves
319
+
320
+ try:
321
+ form_data = {k: v for k, v in form_data.items() if v is not None}
322
+ valves = Valves(**form_data)
323
+ Tools.update_tool_valves_by_id(id, valves.model_dump())
324
+ return valves.model_dump()
325
+ except Exception as e:
326
+ print(e)
327
+ raise HTTPException(
328
+ status_code=status.HTTP_400_BAD_REQUEST,
329
+ detail=ERROR_MESSAGES.DEFAULT(str(e)),
330
+ )
331
 
332
 
333
  ############################
backend/open_webui/config.py CHANGED
@@ -1181,21 +1181,32 @@ CHUNK_OVERLAP = PersistentConfig(
1181
  int(os.environ.get("CHUNK_OVERLAP", "100")),
1182
  )
1183
 
1184
- DEFAULT_RAG_TEMPLATE = """You are given a user query, some textual context and rules, all inside xml tags. You have to answer the query based on the context while respecting the rules.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1185
 
1186
  <context>
1187
  {{CONTEXT}}
1188
  </context>
1189
 
1190
- <rules>
1191
- - If you don't know, just say so.
1192
- - If you are not sure, ask for clarification.
1193
- - Answer in the same language as the user query.
1194
- - If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
1195
- - If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
1196
- - Answer directly and without using xml tags.
1197
- </rules>
1198
-
1199
  <user_query>
1200
  {{QUERY}}
1201
  </user_query>
@@ -1290,6 +1301,12 @@ BRAVE_SEARCH_API_KEY = PersistentConfig(
1290
  os.getenv("BRAVE_SEARCH_API_KEY", ""),
1291
  )
1292
 
 
 
 
 
 
 
1293
  SERPSTACK_API_KEY = PersistentConfig(
1294
  "SERPSTACK_API_KEY",
1295
  "rag.web.search.serpstack_api_key",
 
1181
  int(os.environ.get("CHUNK_OVERLAP", "100")),
1182
  )
1183
 
1184
+ DEFAULT_RAG_TEMPLATE = """### Task:
1185
+ Respond to the user query using the provided context, incorporating inline citations in the format [source_id] **only when the <source_id> tag is explicitly provided** in the context.
1186
+
1187
+ ### Guidelines:
1188
+ - If you don't know the answer, clearly state that.
1189
+ - If uncertain, ask the user for clarification.
1190
+ - Respond in the same language as the user's query.
1191
+ - If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
1192
+ - If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
1193
+ - **Only include inline citations using [source_id] when a <source_id> tag is explicitly provided in the context.**
1194
+ - Do not cite if the <source_id> tag is not provided in the context.
1195
+ - Do not use XML tags in your response.
1196
+ - Ensure citations are concise and directly related to the information provided.
1197
+
1198
+ ### Example of Citation:
1199
+ If the user asks about a specific topic and the information is found in "whitepaper.pdf" with a provided <source_id>, the response should include the citation like so:
1200
+ * "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]."
1201
+ If no <source_id> is present, the response should omit the citation.
1202
+
1203
+ ### Output:
1204
+ Provide a clear and direct response to the user's query, including inline citations in the format [source_id] only when the <source_id> tag is present in the context.
1205
 
1206
  <context>
1207
  {{CONTEXT}}
1208
  </context>
1209
 
 
 
 
 
 
 
 
 
 
1210
  <user_query>
1211
  {{QUERY}}
1212
  </user_query>
 
1301
  os.getenv("BRAVE_SEARCH_API_KEY", ""),
1302
  )
1303
 
1304
+ MOJEEK_SEARCH_API_KEY = PersistentConfig(
1305
+ "MOJEEK_SEARCH_API_KEY",
1306
+ "rag.web.search.mojeek_search_api_key",
1307
+ os.getenv("MOJEEK_SEARCH_API_KEY", ""),
1308
+ )
1309
+
1310
  SERPSTACK_API_KEY = PersistentConfig(
1311
  "SERPSTACK_API_KEY",
1312
  "rag.web.search.serpstack_api_key",
backend/open_webui/main.py CHANGED
@@ -49,7 +49,7 @@ from open_webui.apps.openai.main import (
49
  get_all_models_responses as get_openai_models_responses,
50
  )
51
  from open_webui.apps.retrieval.main import app as retrieval_app
52
- from open_webui.apps.retrieval.utils import get_rag_context, rag_template
53
  from open_webui.apps.socket.main import (
54
  app as socket_app,
55
  periodic_usage_pool_cleanup,
@@ -380,8 +380,7 @@ async def chat_completion_tools_handler(
380
  return body, {}
381
 
382
  skip_files = False
383
- contexts = []
384
- citations = []
385
 
386
  task_model_id = get_task_model_id(
387
  body["model"],
@@ -463,21 +462,39 @@ async def chat_completion_tools_handler(
463
  except Exception as e:
464
  tool_output = str(e)
465
 
466
- if tools[tool_function_name]["citation"]:
467
- citations.append(
468
- {
469
- "source": {
470
- "name": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
471
- },
472
- "document": [tool_output],
473
- "metadata": [{"source": tool_function_name}],
474
- }
475
- )
476
- if tools[tool_function_name]["file_handler"]:
477
- skip_files = True
478
 
479
  if isinstance(tool_output, str):
480
- contexts.append(tool_output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  except Exception as e:
482
  log.exception(f"Error: {e}")
483
  content = None
@@ -485,19 +502,18 @@ async def chat_completion_tools_handler(
485
  log.exception(f"Error: {e}")
486
  content = None
487
 
488
- log.debug(f"tool_contexts: {contexts}")
489
 
490
  if skip_files and "files" in body.get("metadata", {}):
491
  del body["metadata"]["files"]
492
 
493
- return body, {"contexts": contexts, "citations": citations}
494
 
495
 
496
  async def chat_completion_files_handler(
497
  body: dict, user: UserModel
498
  ) -> tuple[dict, dict[str, list]]:
499
- contexts = []
500
- citations = []
501
 
502
  try:
503
  queries_response = await generate_queries(
@@ -525,7 +541,7 @@ async def chat_completion_files_handler(
525
  print(f"{queries=}")
526
 
527
  if files := body.get("metadata", {}).get("files", None):
528
- contexts, citations = get_rag_context(
529
  files=files,
530
  queries=queries,
531
  embedding_function=retrieval_app.state.EMBEDDING_FUNCTION,
@@ -535,9 +551,8 @@ async def chat_completion_files_handler(
535
  hybrid_search=retrieval_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
536
  )
537
 
538
- log.debug(f"rag_contexts: {contexts}, citations: {citations}")
539
-
540
- return body, {"contexts": contexts, "citations": citations}
541
 
542
 
543
  def is_chat_completion_request(request):
@@ -638,8 +653,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
638
  # Initialize data_items to store additional data to be sent to the client
639
  # Initialize contexts and citation
640
  data_items = []
641
- contexts = []
642
- citations = []
643
 
644
  try:
645
  body, flags = await chat_completion_filter_functions_handler(
@@ -665,21 +679,36 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
665
  body, flags = await chat_completion_tools_handler(
666
  body, user, models, extra_params
667
  )
668
- contexts.extend(flags.get("contexts", []))
669
- citations.extend(flags.get("citations", []))
670
  except Exception as e:
671
  log.exception(e)
672
 
673
  try:
674
  body, flags = await chat_completion_files_handler(body, user)
675
- contexts.extend(flags.get("contexts", []))
676
- citations.extend(flags.get("citations", []))
677
  except Exception as e:
678
  log.exception(e)
679
 
680
  # If context is not empty, insert it into the messages
681
- if len(contexts) > 0:
682
- context_string = "/n".join(contexts).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  prompt = get_last_user_message(body["messages"])
684
 
685
  if prompt is None:
@@ -710,8 +739,11 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
710
  )
711
 
712
  # If there are citations, add them to the data_items
713
- if len(citations) > 0:
714
- data_items.append({"citations": citations})
 
 
 
715
 
716
  modified_body_bytes = json.dumps(body).encode("utf-8")
717
  # Replace the request body with the modified one
@@ -1020,7 +1052,7 @@ async def get_all_base_models():
1020
  return models
1021
 
1022
 
1023
- @cached(ttl=1)
1024
  async def get_all_models():
1025
  models = await get_all_base_models()
1026
 
@@ -1313,7 +1345,6 @@ async def generate_chat_completions(
1313
 
1314
  @app.post("/api/chat/completed")
1315
  async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
1316
-
1317
  model_list = await get_all_models()
1318
  models = {model["id"]: model for model in model_list}
1319
 
 
49
  get_all_models_responses as get_openai_models_responses,
50
  )
51
  from open_webui.apps.retrieval.main import app as retrieval_app
52
+ from open_webui.apps.retrieval.utils import get_sources_from_files, rag_template
53
  from open_webui.apps.socket.main import (
54
  app as socket_app,
55
  periodic_usage_pool_cleanup,
 
380
  return body, {}
381
 
382
  skip_files = False
383
+ sources = []
 
384
 
385
  task_model_id = get_task_model_id(
386
  body["model"],
 
462
  except Exception as e:
463
  tool_output = str(e)
464
 
465
+ print(tools[tool_function_name]["citation"])
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  if isinstance(tool_output, str):
468
+ if tools[tool_function_name]["citation"]:
469
+ sources.append(
470
+ {
471
+ "source": {
472
+ "name": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
473
+ },
474
+ "document": [tool_output],
475
+ "metadata": [
476
+ {
477
+ "source": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
478
+ }
479
+ ],
480
+ }
481
+ )
482
+ else:
483
+ sources.append(
484
+ {
485
+ "source": {},
486
+ "document": [tool_output],
487
+ "metadata": [
488
+ {
489
+ "source": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
490
+ }
491
+ ],
492
+ }
493
+ )
494
+
495
+ if tools[tool_function_name]["file_handler"]:
496
+ skip_files = True
497
+
498
  except Exception as e:
499
  log.exception(f"Error: {e}")
500
  content = None
 
502
  log.exception(f"Error: {e}")
503
  content = None
504
 
505
+ log.debug(f"tool_contexts: {sources}")
506
 
507
  if skip_files and "files" in body.get("metadata", {}):
508
  del body["metadata"]["files"]
509
 
510
+ return body, {"sources": sources}
511
 
512
 
513
  async def chat_completion_files_handler(
514
  body: dict, user: UserModel
515
  ) -> tuple[dict, dict[str, list]]:
516
+ sources = []
 
517
 
518
  try:
519
  queries_response = await generate_queries(
 
541
  print(f"{queries=}")
542
 
543
  if files := body.get("metadata", {}).get("files", None):
544
+ sources = get_sources_from_files(
545
  files=files,
546
  queries=queries,
547
  embedding_function=retrieval_app.state.EMBEDDING_FUNCTION,
 
551
  hybrid_search=retrieval_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
552
  )
553
 
554
+ log.debug(f"rag_contexts:sources: {sources}")
555
+ return body, {"sources": sources}
 
556
 
557
 
558
  def is_chat_completion_request(request):
 
653
  # Initialize data_items to store additional data to be sent to the client
654
  # Initialize contexts and citation
655
  data_items = []
656
+ sources = []
 
657
 
658
  try:
659
  body, flags = await chat_completion_filter_functions_handler(
 
679
  body, flags = await chat_completion_tools_handler(
680
  body, user, models, extra_params
681
  )
682
+ sources.extend(flags.get("sources", []))
 
683
  except Exception as e:
684
  log.exception(e)
685
 
686
  try:
687
  body, flags = await chat_completion_files_handler(body, user)
688
+ sources.extend(flags.get("sources", []))
 
689
  except Exception as e:
690
  log.exception(e)
691
 
692
  # If context is not empty, insert it into the messages
693
+ if len(sources) > 0:
694
+ context_string = ""
695
+ for source_idx, source in enumerate(sources):
696
+ source_id = source.get("source", {}).get("name", "")
697
+
698
+ if "document" in source:
699
+ for doc_idx, doc_context in enumerate(source["document"]):
700
+ metadata = source.get("metadata")
701
+
702
+ if metadata:
703
+ doc_source_id = metadata[doc_idx].get("source", source_id)
704
+
705
+ if source_id:
706
+ context_string += f"<source><source_id>{doc_source_id}</source_id><source_context>{doc_context}</source_context></source>\n"
707
+ else:
708
+ # If there is no source_id, then do not include the source_id tag
709
+ context_string += f"<source><source_context>{doc_context}</source_context></source>\n"
710
+
711
+ context_string = context_string.strip()
712
  prompt = get_last_user_message(body["messages"])
713
 
714
  if prompt is None:
 
739
  )
740
 
741
  # If there are citations, add them to the data_items
742
+ sources = [
743
+ source for source in sources if source.get("source", {}).get("name", "")
744
+ ]
745
+ if len(sources) > 0:
746
+ data_items.append({"sources": sources})
747
 
748
  modified_body_bytes = json.dumps(body).encode("utf-8")
749
  # Replace the request body with the modified one
 
1052
  return models
1053
 
1054
 
1055
+ @cached(ttl=3)
1056
  async def get_all_models():
1057
  models = await get_all_base_models()
1058
 
 
1345
 
1346
  @app.post("/api/chat/completed")
1347
  async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
 
1348
  model_list = await get_all_models()
1349
  models = {model["id"]: model for model in model_list}
1350
 
backend/open_webui/utils/tools.py CHANGED
@@ -1,11 +1,14 @@
1
  import inspect
2
  import logging
3
- from typing import Awaitable, Callable, get_type_hints
 
 
4
 
 
5
  from open_webui.apps.webui.models.tools import Tools
6
  from open_webui.apps.webui.models.users import UserModel
7
  from open_webui.apps.webui.utils import load_tools_module_by_id
8
- from open_webui.utils.schemas import json_schema_to_model
9
 
10
  log = logging.getLogger(__name__)
11
 
@@ -14,17 +17,16 @@ def apply_extra_params_to_tool_function(
14
  function: Callable, extra_params: dict
15
  ) -> Callable[..., Awaitable]:
16
  sig = inspect.signature(function)
17
- extra_params = {
18
- key: value for key, value in extra_params.items() if key in sig.parameters
19
- }
20
- is_coroutine = inspect.iscoroutinefunction(function)
 
21
 
22
- async def new_function(**kwargs):
23
- extra_kwargs = kwargs | extra_params
24
- if is_coroutine:
25
- return await function(**extra_kwargs)
26
- return function(**extra_kwargs)
27
 
 
28
  return new_function
29
 
30
 
@@ -55,11 +57,6 @@ def get_tools(
55
  )
56
 
57
  for spec in tools.specs:
58
- # TODO: Fix hack for OpenAI API
59
- for val in spec.get("parameters", {}).get("properties", {}).values():
60
- if val["type"] == "str":
61
- val["type"] = "string"
62
-
63
  # Remove internal parameters
64
  spec["parameters"]["properties"] = {
65
  key: val
@@ -72,15 +69,12 @@ def get_tools(
72
  # convert to function that takes only model params and inserts custom params
73
  original_func = getattr(module, function_name)
74
  callable = apply_extra_params_to_tool_function(original_func, extra_params)
75
- if hasattr(original_func, "__doc__"):
76
- callable.__doc__ = original_func.__doc__
77
-
78
  # TODO: This needs to be a pydantic model
79
  tool_dict = {
80
  "toolkit_id": tool_id,
81
  "callable": callable,
82
  "spec": spec,
83
- "pydantic_model": json_schema_to_model(spec),
84
  "file_handler": hasattr(module, "file_handler") and module.file_handler,
85
  "citation": hasattr(module, "citation") and module.citation,
86
  }
@@ -96,78 +90,75 @@ def get_tools(
96
  return tools_dict
97
 
98
 
99
- def doc_to_dict(docstring):
100
- lines = docstring.split("\n")
101
- description = lines[1].strip()
102
- param_dict = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- for line in lines:
105
- if ":param" in line:
106
- line = line.replace(":param", "").strip()
107
- param, desc = line.split(":", 1)
108
- param_dict[param.strip()] = desc.strip()
109
- ret_dict = {"description": description, "params": param_dict}
110
- return ret_dict
111
 
 
 
 
112
 
113
- def get_tools_specs(tools) -> list[dict]:
114
- function_list = [
115
- {"name": func, "function": getattr(tools, func)}
116
- for func in dir(tools)
117
- if callable(getattr(tools, func))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  and not func.startswith("__")
119
- and not inspect.isclass(getattr(tools, func))
120
  ]
121
 
122
- specs = []
123
- for function_item in function_list:
124
- function_name = function_item["name"]
125
- function = function_item["function"]
126
-
127
- function_doc = doc_to_dict(function.__doc__ or function_name)
128
- specs.append(
129
- {
130
- "name": function_name,
131
- # TODO: multi-line desc?
132
- "description": function_doc.get("description", function_name),
133
- "parameters": {
134
- "type": "object",
135
- "properties": {
136
- param_name: {
137
- "type": param_annotation.__name__.lower(),
138
- **(
139
- {
140
- "enum": (
141
- str(param_annotation.__args__)
142
- if hasattr(param_annotation, "__args__")
143
- else None
144
- )
145
- }
146
- if hasattr(param_annotation, "__args__")
147
- else {}
148
- ),
149
- "description": function_doc.get("params", {}).get(
150
- param_name, param_name
151
- ),
152
- }
153
- for param_name, param_annotation in get_type_hints(
154
- function
155
- ).items()
156
- if param_name != "return"
157
- and not (
158
- param_name.startswith("__") and param_name.endswith("__")
159
- )
160
- },
161
- "required": [
162
- name
163
- for name, param in inspect.signature(
164
- function
165
- ).parameters.items()
166
- if param.default is param.empty
167
- and not (name.startswith("__") and name.endswith("__"))
168
- ],
169
- },
170
- }
171
- )
172
 
173
- return specs
 
 
 
 
1
  import inspect
2
  import logging
3
+ import re
4
+ from typing import Any, Awaitable, Callable, get_type_hints
5
+ from functools import update_wrapper, partial
6
 
7
+ from langchain_core.utils.function_calling import convert_to_openai_function
8
  from open_webui.apps.webui.models.tools import Tools
9
  from open_webui.apps.webui.models.users import UserModel
10
  from open_webui.apps.webui.utils import load_tools_module_by_id
11
+ from pydantic import BaseModel, Field, create_model
12
 
13
  log = logging.getLogger(__name__)
14
 
 
17
  function: Callable, extra_params: dict
18
  ) -> Callable[..., Awaitable]:
19
  sig = inspect.signature(function)
20
+ extra_params = {k: v for k, v in extra_params.items() if k in sig.parameters}
21
+ partial_func = partial(function, **extra_params)
22
+ if inspect.iscoroutinefunction(function):
23
+ update_wrapper(partial_func, function)
24
+ return partial_func
25
 
26
+ async def new_function(*args, **kwargs):
27
+ return partial_func(*args, **kwargs)
 
 
 
28
 
29
+ update_wrapper(new_function, function)
30
  return new_function
31
 
32
 
 
57
  )
58
 
59
  for spec in tools.specs:
 
 
 
 
 
60
  # Remove internal parameters
61
  spec["parameters"]["properties"] = {
62
  key: val
 
69
  # convert to function that takes only model params and inserts custom params
70
  original_func = getattr(module, function_name)
71
  callable = apply_extra_params_to_tool_function(original_func, extra_params)
 
 
 
72
  # TODO: This needs to be a pydantic model
73
  tool_dict = {
74
  "toolkit_id": tool_id,
75
  "callable": callable,
76
  "spec": spec,
77
+ "pydantic_model": function_to_pydantic_model(callable),
78
  "file_handler": hasattr(module, "file_handler") and module.file_handler,
79
  "citation": hasattr(module, "citation") and module.citation,
80
  }
 
90
  return tools_dict
91
 
92
 
93
+ def parse_docstring(docstring):
94
+ """
95
+ Parse a function's docstring to extract parameter descriptions in reST format.
96
+
97
+ Args:
98
+ docstring (str): The docstring to parse.
99
+
100
+ Returns:
101
+ dict: A dictionary where keys are parameter names and values are descriptions.
102
+ """
103
+ if not docstring:
104
+ return {}
105
+
106
+ # Regex to match `:param name: description` format
107
+ param_pattern = re.compile(r":param (\w+):\s*(.+)")
108
+ param_descriptions = {}
109
+
110
+ for line in docstring.splitlines():
111
+ match = param_pattern.match(line.strip())
112
+ if match:
113
+ param_name, param_description = match.groups()
114
+ param_descriptions[param_name] = param_description
115
+
116
+ return param_descriptions
117
+
118
 
119
+ def function_to_pydantic_model(func: Callable) -> type[BaseModel]:
120
+ """
121
+ Converts a Python function's type hints and docstring to a Pydantic model,
122
+ including support for nested types, default values, and descriptions.
 
 
 
123
 
124
+ Args:
125
+ func: The function whose type hints and docstring should be converted.
126
+ model_name: The name of the generated Pydantic model.
127
 
128
+ Returns:
129
+ A Pydantic model class.
130
+ """
131
+ type_hints = get_type_hints(func)
132
+ signature = inspect.signature(func)
133
+ parameters = signature.parameters
134
+
135
+ docstring = func.__doc__
136
+ descriptions = parse_docstring(docstring)
137
+
138
+ field_defs = {}
139
+ for name, param in parameters.items():
140
+ type_hint = type_hints.get(name, Any)
141
+ default_value = param.default if param.default is not param.empty else ...
142
+ description = descriptions.get(name, None)
143
+ if not description:
144
+ field_defs[name] = type_hint, default_value
145
+ continue
146
+ field_defs[name] = type_hint, Field(default_value, description=description)
147
+
148
+ return create_model(func.__name__, **field_defs)
149
+
150
+
151
+ def get_callable_attributes(tool: object) -> list[Callable]:
152
+ return [
153
+ getattr(tool, func)
154
+ for func in dir(tool)
155
+ if callable(getattr(tool, func))
156
  and not func.startswith("__")
157
+ and not inspect.isclass(getattr(tool, func))
158
  ]
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ def get_tools_specs(tool_class: object) -> list[dict]:
162
+ function_list = get_callable_attributes(tool_class)
163
+ models = map(function_to_pydantic_model, function_list)
164
+ return [convert_to_openai_function(tool) for tool in models]
package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
  "name": "open-webui",
3
- "version": "0.4.2",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "open-webui",
9
- "version": "0.4.2",
10
  "dependencies": {
11
  "@codemirror/lang-javascript": "^6.2.2",
12
  "@codemirror/lang-python": "^6.1.6",
@@ -16,6 +16,13 @@
16
  "@mediapipe/tasks-vision": "^0.10.17",
17
  "@pyscript/core": "^0.4.32",
18
  "@sveltejs/adapter-node": "^2.0.0",
 
 
 
 
 
 
 
19
  "@xyflow/svelte": "^0.1.19",
20
  "async": "^3.2.5",
21
  "bits-ui": "^0.19.7",
@@ -73,6 +80,7 @@
73
  "postcss": "^8.4.31",
74
  "prettier": "^3.3.3",
75
  "prettier-plugin-svelte": "^3.2.6",
 
76
  "svelte": "^4.2.18",
77
  "svelte-check": "^3.8.5",
78
  "svelte-confetti": "^1.3.2",
@@ -136,6 +144,13 @@
136
  "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
137
  "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
138
  },
 
 
 
 
 
 
 
139
  "node_modules/@codemirror/autocomplete": {
140
  "version": "6.16.2",
141
  "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz",
@@ -1901,6 +1916,12 @@
1901
  "type-checked-collections": "^0.1.7"
1902
  }
1903
  },
 
 
 
 
 
 
1904
  "node_modules/@rollup/plugin-commonjs": {
1905
  "version": "25.0.7",
1906
  "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
@@ -2326,6 +2347,391 @@
2326
  "tailwindcss": ">=3.0.0 || insiders"
2327
  }
2328
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2329
  "node_modules/@types/cookie": {
2330
  "version": "0.6.0",
2331
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -2405,6 +2811,16 @@
2405
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
2406
  "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
2407
  },
 
 
 
 
 
 
 
 
 
 
2408
  "node_modules/@types/json-schema": {
2409
  "version": "7.0.15",
2410
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -3425,6 +3841,13 @@
3425
  "ieee754": "^1.2.1"
3426
  }
3427
  },
 
 
 
 
 
 
 
3428
  "node_modules/buffer-crc32": {
3429
  "version": "0.2.13",
3430
  "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -3898,6 +4321,13 @@
3898
  "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
3899
  "dev": true
3900
  },
 
 
 
 
 
 
 
3901
  "node_modules/colors": {
3902
  "version": "1.4.0",
3903
  "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@@ -4760,6 +5190,20 @@
4760
  "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
4761
  "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="
4762
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4763
  "node_modules/didyoumean": {
4764
  "version": "1.2.2",
4765
  "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -5047,7 +5491,6 @@
5047
  "version": "4.0.0",
5048
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
5049
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
5050
- "dev": true,
5051
  "engines": {
5052
  "node": ">=10"
5053
  },
@@ -5978,7 +6421,7 @@
5978
  "version": "4.0.0",
5979
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
5980
  "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
5981
- "dev": true,
5982
  "engines": {
5983
  "node": ">=8"
5984
  }
@@ -6241,6 +6684,13 @@
6241
  "node": ">= 4"
6242
  }
6243
  },
 
 
 
 
 
 
 
6244
  "node_modules/import-fresh": {
6245
  "version": "3.3.0",
6246
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -6982,6 +7432,22 @@
6982
  "get-func-name": "^2.0.1"
6983
  }
6984
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6985
  "node_modules/magic-string": {
6986
  "version": "0.30.11",
6987
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
@@ -8604,17 +9070,36 @@
8604
  "node": "10.* || >= 12.*"
8605
  }
8606
  },
8607
- "node_modules/prosemirror-commands": {
8608
- "version": "1.6.0",
8609
- "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
8610
- "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
 
8611
  "dependencies": {
8612
- "prosemirror-model": "^1.0.0",
8613
- "prosemirror-state": "^1.0.0",
8614
  "prosemirror-transform": "^1.0.0"
8615
  }
8616
  },
8617
- "node_modules/prosemirror-dropcursor": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8618
  "version": "1.8.1",
8619
  "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
8620
  "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
@@ -8737,18 +9222,48 @@
8737
  "prosemirror-view": "^1.27.0"
8738
  }
8739
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8740
  "node_modules/prosemirror-transform": {
8741
- "version": "1.10.0",
8742
- "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
8743
- "integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
 
8744
  "dependencies": {
8745
  "prosemirror-model": "^1.21.0"
8746
  }
8747
  },
8748
  "node_modules/prosemirror-view": {
8749
- "version": "1.34.3",
8750
- "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.3.tgz",
8751
- "integrity": "sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==",
 
8752
  "dependencies": {
8753
  "prosemirror-model": "^1.20.0",
8754
  "prosemirror-state": "^1.0.0",
@@ -9236,7 +9751,7 @@
9236
  "version": "7.8.1",
9237
  "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
9238
  "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
9239
- "dev": true,
9240
  "dependencies": {
9241
  "tslib": "^2.1.0"
9242
  }
@@ -9329,6 +9844,387 @@
9329
  "rimraf": "bin.js"
9330
  }
9331
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9332
  "node_modules/semver": {
9333
  "version": "7.6.3",
9334
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@@ -10044,6 +10940,29 @@
10044
  "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==",
10045
  "dev": true
10046
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10047
  "node_modules/tabbable": {
10048
  "version": "6.2.0",
10049
  "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
@@ -10416,6 +11335,7 @@
10416
  "version": "7.2.0",
10417
  "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz",
10418
  "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==",
 
10419
  "dependencies": {
10420
  "@mixmark-io/domino": "^2.2.0"
10421
  }
@@ -10629,6 +11549,13 @@
10629
  "node": ">= 10.13.0"
10630
  }
10631
  },
 
 
 
 
 
 
 
10632
  "node_modules/verror": {
10633
  "version": "1.10.0",
10634
  "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
 
1
  {
2
  "name": "open-webui",
3
+ "version": "0.4.3",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "open-webui",
9
+ "version": "0.4.3",
10
  "dependencies": {
11
  "@codemirror/lang-javascript": "^6.2.2",
12
  "@codemirror/lang-python": "^6.1.6",
 
16
  "@mediapipe/tasks-vision": "^0.10.17",
17
  "@pyscript/core": "^0.4.32",
18
  "@sveltejs/adapter-node": "^2.0.0",
19
+ "@tiptap/core": "^2.10.0",
20
+ "@tiptap/extension-code-block-lowlight": "^2.10.0",
21
+ "@tiptap/extension-highlight": "^2.10.0",
22
+ "@tiptap/extension-placeholder": "^2.10.0",
23
+ "@tiptap/extension-typography": "^2.10.0",
24
+ "@tiptap/pm": "^2.10.0",
25
+ "@tiptap/starter-kit": "^2.10.0",
26
  "@xyflow/svelte": "^0.1.19",
27
  "async": "^3.2.5",
28
  "bits-ui": "^0.19.7",
 
80
  "postcss": "^8.4.31",
81
  "prettier": "^3.3.3",
82
  "prettier-plugin-svelte": "^3.2.6",
83
+ "sass-embedded": "^1.81.0",
84
  "svelte": "^4.2.18",
85
  "svelte-check": "^3.8.5",
86
  "svelte-confetti": "^1.3.2",
 
144
  "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
145
  "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
146
  },
147
+ "node_modules/@bufbuild/protobuf": {
148
+ "version": "2.2.2",
149
+ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz",
150
+ "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==",
151
+ "devOptional": true,
152
+ "license": "(Apache-2.0 AND BSD-3-Clause)"
153
+ },
154
  "node_modules/@codemirror/autocomplete": {
155
  "version": "6.16.2",
156
  "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz",
 
1916
  "type-checked-collections": "^0.1.7"
1917
  }
1918
  },
1919
+ "node_modules/@remirror/core-constants": {
1920
+ "version": "3.0.0",
1921
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
1922
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
1923
+ "license": "MIT"
1924
+ },
1925
  "node_modules/@rollup/plugin-commonjs": {
1926
  "version": "25.0.7",
1927
  "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
 
2347
  "tailwindcss": ">=3.0.0 || insiders"
2348
  }
2349
  },
2350
+ "node_modules/@tiptap/core": {
2351
+ "version": "2.10.0",
2352
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.0.tgz",
2353
+ "integrity": "sha512-58nAjPxLRFcXepdDqQRC1mhrw6E8Sanqr6bbO4Tz0+FWgDJMZvHG+dOK5wHaDVNSgK2iJDz08ETvQayfOOgDvg==",
2354
+ "license": "MIT",
2355
+ "funding": {
2356
+ "type": "github",
2357
+ "url": "https://github.com/sponsors/ueberdosis"
2358
+ },
2359
+ "peerDependencies": {
2360
+ "@tiptap/pm": "^2.7.0"
2361
+ }
2362
+ },
2363
+ "node_modules/@tiptap/extension-blockquote": {
2364
+ "version": "2.10.0",
2365
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.10.0.tgz",
2366
+ "integrity": "sha512-6Xmfo2lpfIRcbfkLD/NGX4YgQqfgAbu6XaZQZf5oGtHLPTrz4D7Mw20GgNBHzae2XwUCwLMt6zXOkBgU/LnlZg==",
2367
+ "license": "MIT",
2368
+ "funding": {
2369
+ "type": "github",
2370
+ "url": "https://github.com/sponsors/ueberdosis"
2371
+ },
2372
+ "peerDependencies": {
2373
+ "@tiptap/core": "^2.7.0"
2374
+ }
2375
+ },
2376
+ "node_modules/@tiptap/extension-bold": {
2377
+ "version": "2.10.0",
2378
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.10.0.tgz",
2379
+ "integrity": "sha512-1wL8UI1Aii0u2cbDEvwyqsZb2pgBt8HLJdsIax/ELoF2tKCD5821nElqTGLBBg4pUGPa0ru9ZemuL8GdXZp3Qg==",
2380
+ "license": "MIT",
2381
+ "funding": {
2382
+ "type": "github",
2383
+ "url": "https://github.com/sponsors/ueberdosis"
2384
+ },
2385
+ "peerDependencies": {
2386
+ "@tiptap/core": "^2.7.0"
2387
+ }
2388
+ },
2389
+ "node_modules/@tiptap/extension-bullet-list": {
2390
+ "version": "2.10.0",
2391
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.10.0.tgz",
2392
+ "integrity": "sha512-Cl+DGu6D3SgF/hlKUDNet3gaZFy6cPEonOOkHwzXoybDXXdddFbaTvt9MLkBRUR3ldksXuVRP2/LwZsK5WyxJQ==",
2393
+ "license": "MIT",
2394
+ "funding": {
2395
+ "type": "github",
2396
+ "url": "https://github.com/sponsors/ueberdosis"
2397
+ },
2398
+ "peerDependencies": {
2399
+ "@tiptap/core": "^2.7.0"
2400
+ }
2401
+ },
2402
+ "node_modules/@tiptap/extension-code": {
2403
+ "version": "2.10.0",
2404
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.10.0.tgz",
2405
+ "integrity": "sha512-8JznKG1Jmv8gJezZGPoka8oRmfrcAAnMEOeMpKXjwMrIbQ6QynTZpqMGGVL1kfkZlLV84PYm+CGjGgjSsT4iZw==",
2406
+ "license": "MIT",
2407
+ "funding": {
2408
+ "type": "github",
2409
+ "url": "https://github.com/sponsors/ueberdosis"
2410
+ },
2411
+ "peerDependencies": {
2412
+ "@tiptap/core": "^2.7.0"
2413
+ }
2414
+ },
2415
+ "node_modules/@tiptap/extension-code-block": {
2416
+ "version": "2.10.0",
2417
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.10.0.tgz",
2418
+ "integrity": "sha512-QH+LP7L1s1EJlrDFnfgOP0q+Siqt0Zbkx4ICMcUGvEsycl53Ti8P0DRW7fAjRISdTCItuWJYvtmiYY7O3rYb+Q==",
2419
+ "license": "MIT",
2420
+ "funding": {
2421
+ "type": "github",
2422
+ "url": "https://github.com/sponsors/ueberdosis"
2423
+ },
2424
+ "peerDependencies": {
2425
+ "@tiptap/core": "^2.7.0",
2426
+ "@tiptap/pm": "^2.7.0"
2427
+ }
2428
+ },
2429
+ "node_modules/@tiptap/extension-code-block-lowlight": {
2430
+ "version": "2.10.0",
2431
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.10.0.tgz",
2432
+ "integrity": "sha512-dAv03XIHT5h+sdFmJzvx2FfpfFOOK9SBKHflRUdqTa8eA+0VZNAcPRjvJWVEWqts1fKZDJj774mO28NlhFzk9Q==",
2433
+ "license": "MIT",
2434
+ "funding": {
2435
+ "type": "github",
2436
+ "url": "https://github.com/sponsors/ueberdosis"
2437
+ },
2438
+ "peerDependencies": {
2439
+ "@tiptap/core": "^2.7.0",
2440
+ "@tiptap/extension-code-block": "^2.7.0",
2441
+ "@tiptap/pm": "^2.7.0",
2442
+ "highlight.js": "^11",
2443
+ "lowlight": "^2 || ^3"
2444
+ }
2445
+ },
2446
+ "node_modules/@tiptap/extension-document": {
2447
+ "version": "2.10.0",
2448
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.10.0.tgz",
2449
+ "integrity": "sha512-vseMW3EKiQAPgdbN48Y8F0nRqWhhrAo9DLacAfP7tu0x3uv44uotNjDBtAgp5QmJmqQVyrEdkLSZaU5vFzduhQ==",
2450
+ "license": "MIT",
2451
+ "funding": {
2452
+ "type": "github",
2453
+ "url": "https://github.com/sponsors/ueberdosis"
2454
+ },
2455
+ "peerDependencies": {
2456
+ "@tiptap/core": "^2.7.0"
2457
+ }
2458
+ },
2459
+ "node_modules/@tiptap/extension-dropcursor": {
2460
+ "version": "2.10.0",
2461
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.10.0.tgz",
2462
+ "integrity": "sha512-tifxp/a3NxTjLAuYBx9XAwVo4MSDoY/mQ8E18QtuXj0vuieCFxd8Bkyre0otubIAAQePXLTVGQoxPrKmMAa+Jg==",
2463
+ "license": "MIT",
2464
+ "funding": {
2465
+ "type": "github",
2466
+ "url": "https://github.com/sponsors/ueberdosis"
2467
+ },
2468
+ "peerDependencies": {
2469
+ "@tiptap/core": "^2.7.0",
2470
+ "@tiptap/pm": "^2.7.0"
2471
+ }
2472
+ },
2473
+ "node_modules/@tiptap/extension-gapcursor": {
2474
+ "version": "2.10.0",
2475
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.10.0.tgz",
2476
+ "integrity": "sha512-GViEnSnEBE74k7SYdXrQ4aXlKmWkrd9awdj/TgDSORgpZ4Dfyqtn+ENIWWby4NhL+BPM9P5hGCjkQXZsi6JKOw==",
2477
+ "license": "MIT",
2478
+ "funding": {
2479
+ "type": "github",
2480
+ "url": "https://github.com/sponsors/ueberdosis"
2481
+ },
2482
+ "peerDependencies": {
2483
+ "@tiptap/core": "^2.7.0",
2484
+ "@tiptap/pm": "^2.7.0"
2485
+ }
2486
+ },
2487
+ "node_modules/@tiptap/extension-hard-break": {
2488
+ "version": "2.10.0",
2489
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.10.0.tgz",
2490
+ "integrity": "sha512-NL/xPYUhhvQyCnOO5Yn+BlBOMLC1ru32nw7ox12TShGmaeKBrnV0DhzBRkyJU0MqCS26oWjieNPxfu0lR3oMSA==",
2491
+ "license": "MIT",
2492
+ "funding": {
2493
+ "type": "github",
2494
+ "url": "https://github.com/sponsors/ueberdosis"
2495
+ },
2496
+ "peerDependencies": {
2497
+ "@tiptap/core": "^2.7.0"
2498
+ }
2499
+ },
2500
+ "node_modules/@tiptap/extension-heading": {
2501
+ "version": "2.10.0",
2502
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.10.0.tgz",
2503
+ "integrity": "sha512-x2Uj5wrAHFaUdlChwLoQVmWtzZCuNyJpBRA19kA4idWL5z+6cIrUWepvwVBxA8ou6ictbzWW15o+blKtW7DlqA==",
2504
+ "license": "MIT",
2505
+ "funding": {
2506
+ "type": "github",
2507
+ "url": "https://github.com/sponsors/ueberdosis"
2508
+ },
2509
+ "peerDependencies": {
2510
+ "@tiptap/core": "^2.7.0"
2511
+ }
2512
+ },
2513
+ "node_modules/@tiptap/extension-highlight": {
2514
+ "version": "2.10.0",
2515
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.10.0.tgz",
2516
+ "integrity": "sha512-HU8UuKU7ljlzNn7jg29pM8QtIX7QvePcBjcWAt6K3qVwF1cbBNguIjKRY2rmoonU2nu8I6GknQNgV847kZifCQ==",
2517
+ "license": "MIT",
2518
+ "funding": {
2519
+ "type": "github",
2520
+ "url": "https://github.com/sponsors/ueberdosis"
2521
+ },
2522
+ "peerDependencies": {
2523
+ "@tiptap/core": "^2.7.0"
2524
+ }
2525
+ },
2526
+ "node_modules/@tiptap/extension-history": {
2527
+ "version": "2.10.0",
2528
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.10.0.tgz",
2529
+ "integrity": "sha512-5aYOmxqaCnw7e7wmWqFZmkpYCxxDjEzFbgVI6WknqNwqeOizR4+YJf3aAt/lTbksLJe47XF+NBX51gOm/ZBCiw==",
2530
+ "license": "MIT",
2531
+ "funding": {
2532
+ "type": "github",
2533
+ "url": "https://github.com/sponsors/ueberdosis"
2534
+ },
2535
+ "peerDependencies": {
2536
+ "@tiptap/core": "^2.7.0",
2537
+ "@tiptap/pm": "^2.7.0"
2538
+ }
2539
+ },
2540
+ "node_modules/@tiptap/extension-horizontal-rule": {
2541
+ "version": "2.10.0",
2542
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.10.0.tgz",
2543
+ "integrity": "sha512-el1SzI/x/h4HW8UltxJlyMSrRsO55ypKPLQHJC9h7F6kTTR31fJUzQa3AeTFrZvXS0kNHIFRpAMstw+N0L5TYg==",
2544
+ "license": "MIT",
2545
+ "funding": {
2546
+ "type": "github",
2547
+ "url": "https://github.com/sponsors/ueberdosis"
2548
+ },
2549
+ "peerDependencies": {
2550
+ "@tiptap/core": "^2.7.0",
2551
+ "@tiptap/pm": "^2.7.0"
2552
+ }
2553
+ },
2554
+ "node_modules/@tiptap/extension-italic": {
2555
+ "version": "2.10.0",
2556
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.10.0.tgz",
2557
+ "integrity": "sha512-MqPYbHAEeO8QBvZRIkF4J2OTf/uiUPzUiXGLJ50w1ozfMBIw1txMvfR3g2cpwfvZlcOgYTgy7M0Oq00nQz5eXg==",
2558
+ "license": "MIT",
2559
+ "funding": {
2560
+ "type": "github",
2561
+ "url": "https://github.com/sponsors/ueberdosis"
2562
+ },
2563
+ "peerDependencies": {
2564
+ "@tiptap/core": "^2.7.0"
2565
+ }
2566
+ },
2567
+ "node_modules/@tiptap/extension-list-item": {
2568
+ "version": "2.10.0",
2569
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.10.0.tgz",
2570
+ "integrity": "sha512-BxC6NNHd2xcC+mk5hpYWURUdj/mRz6TGFwH5CsyrUXPxApx0+V+EPHaAgdpu8dr+jtTEzjXF62V6e2JmOAPimg==",
2571
+ "license": "MIT",
2572
+ "funding": {
2573
+ "type": "github",
2574
+ "url": "https://github.com/sponsors/ueberdosis"
2575
+ },
2576
+ "peerDependencies": {
2577
+ "@tiptap/core": "^2.7.0"
2578
+ }
2579
+ },
2580
+ "node_modules/@tiptap/extension-ordered-list": {
2581
+ "version": "2.10.0",
2582
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.10.0.tgz",
2583
+ "integrity": "sha512-jsK+mvzs7HmxQuQOU3HgIga+v7zUbQlmSP4/danusqUihJ+lc1n0frDCIkVvJrnSB3FChvNgT6ZEA14HOhdJzg==",
2584
+ "license": "MIT",
2585
+ "funding": {
2586
+ "type": "github",
2587
+ "url": "https://github.com/sponsors/ueberdosis"
2588
+ },
2589
+ "peerDependencies": {
2590
+ "@tiptap/core": "^2.7.0"
2591
+ }
2592
+ },
2593
+ "node_modules/@tiptap/extension-paragraph": {
2594
+ "version": "2.10.0",
2595
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.10.0.tgz",
2596
+ "integrity": "sha512-4LUkVaJYjNdNZ7QOX6TRcA+m7oCtyrLGk49G22wl7XcPBkQPILP1mCUCU4f41bhjfhCgK5PPWP63kMtD+cEACg==",
2597
+ "license": "MIT",
2598
+ "funding": {
2599
+ "type": "github",
2600
+ "url": "https://github.com/sponsors/ueberdosis"
2601
+ },
2602
+ "peerDependencies": {
2603
+ "@tiptap/core": "^2.7.0"
2604
+ }
2605
+ },
2606
+ "node_modules/@tiptap/extension-placeholder": {
2607
+ "version": "2.10.0",
2608
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.10.0.tgz",
2609
+ "integrity": "sha512-1o6azk2plgYAFgMrV3prnBb1NZjl2V1T3wwnH4n3/h9z9lJ0v5BBAk9r+TRYSrcdXknwwHAWFYnQe6dc9buG2g==",
2610
+ "license": "MIT",
2611
+ "funding": {
2612
+ "type": "github",
2613
+ "url": "https://github.com/sponsors/ueberdosis"
2614
+ },
2615
+ "peerDependencies": {
2616
+ "@tiptap/core": "^2.7.0",
2617
+ "@tiptap/pm": "^2.7.0"
2618
+ }
2619
+ },
2620
+ "node_modules/@tiptap/extension-strike": {
2621
+ "version": "2.10.0",
2622
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.10.0.tgz",
2623
+ "integrity": "sha512-SxApLJMQkxnmPGR3lwaskvLK61yI+Bu9hGZGdwMZqNh6o3LoDOxDaXjHD5joeMYQiqQrBE9zg46506MsXtrU7Q==",
2624
+ "license": "MIT",
2625
+ "funding": {
2626
+ "type": "github",
2627
+ "url": "https://github.com/sponsors/ueberdosis"
2628
+ },
2629
+ "peerDependencies": {
2630
+ "@tiptap/core": "^2.7.0"
2631
+ }
2632
+ },
2633
+ "node_modules/@tiptap/extension-text": {
2634
+ "version": "2.10.0",
2635
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.10.0.tgz",
2636
+ "integrity": "sha512-SSnNncADS1KucdEcJlF6WGCs5+1pAhPrD68vlw34oj3NDT3Zh05KiyXsCV3Nw4wpHOnbWahV+z3uT2SnR+xgoQ==",
2637
+ "license": "MIT",
2638
+ "funding": {
2639
+ "type": "github",
2640
+ "url": "https://github.com/sponsors/ueberdosis"
2641
+ },
2642
+ "peerDependencies": {
2643
+ "@tiptap/core": "^2.7.0"
2644
+ }
2645
+ },
2646
+ "node_modules/@tiptap/extension-text-style": {
2647
+ "version": "2.10.0",
2648
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.10.0.tgz",
2649
+ "integrity": "sha512-VZtH1dp64wg1UcFtUPpRQK+kOm4JHBIv+WXuKX7EnpIEKjHKnyfV94BBVmaqY5UE4n3kbkkmIRB2Cmix/10AMg==",
2650
+ "license": "MIT",
2651
+ "funding": {
2652
+ "type": "github",
2653
+ "url": "https://github.com/sponsors/ueberdosis"
2654
+ },
2655
+ "peerDependencies": {
2656
+ "@tiptap/core": "^2.7.0"
2657
+ }
2658
+ },
2659
+ "node_modules/@tiptap/extension-typography": {
2660
+ "version": "2.10.0",
2661
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.10.0.tgz",
2662
+ "integrity": "sha512-03IOfJm4bk2hZ4SsSfxgBOVzcDxMRBlFD7ZY12H2EGNf1TKxj/0ANWhAH54FtquuOMoY5aWg5LZf0lk++8UDAw==",
2663
+ "license": "MIT",
2664
+ "funding": {
2665
+ "type": "github",
2666
+ "url": "https://github.com/sponsors/ueberdosis"
2667
+ },
2668
+ "peerDependencies": {
2669
+ "@tiptap/core": "^2.7.0"
2670
+ }
2671
+ },
2672
+ "node_modules/@tiptap/pm": {
2673
+ "version": "2.10.0",
2674
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.10.0.tgz",
2675
+ "integrity": "sha512-ohshlWf4MlW6D3rQkNQnhmiQ2w4pwRoQcJmTPt8UJoIDGkeKmZh494fQp4Aeh80XuGd81SsCv//1HJeyaeHJYQ==",
2676
+ "license": "MIT",
2677
+ "dependencies": {
2678
+ "prosemirror-changeset": "^2.2.1",
2679
+ "prosemirror-collab": "^1.3.1",
2680
+ "prosemirror-commands": "^1.6.2",
2681
+ "prosemirror-dropcursor": "^1.8.1",
2682
+ "prosemirror-gapcursor": "^1.3.2",
2683
+ "prosemirror-history": "^1.4.1",
2684
+ "prosemirror-inputrules": "^1.4.0",
2685
+ "prosemirror-keymap": "^1.2.2",
2686
+ "prosemirror-markdown": "^1.13.1",
2687
+ "prosemirror-menu": "^1.2.4",
2688
+ "prosemirror-model": "^1.23.0",
2689
+ "prosemirror-schema-basic": "^1.2.3",
2690
+ "prosemirror-schema-list": "^1.4.1",
2691
+ "prosemirror-state": "^1.4.3",
2692
+ "prosemirror-tables": "^1.6.1",
2693
+ "prosemirror-trailing-node": "^3.0.0",
2694
+ "prosemirror-transform": "^1.10.2",
2695
+ "prosemirror-view": "^1.36.0"
2696
+ },
2697
+ "funding": {
2698
+ "type": "github",
2699
+ "url": "https://github.com/sponsors/ueberdosis"
2700
+ }
2701
+ },
2702
+ "node_modules/@tiptap/starter-kit": {
2703
+ "version": "2.10.0",
2704
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.10.0.tgz",
2705
+ "integrity": "sha512-hMIM9a6HjYZo25EzhZHlKEIR7CFi0grRSOltEyggiyBuQqKFkI7iwCpZVVtviDV1FwV0EPANpIAxPS7aBRgFdg==",
2706
+ "license": "MIT",
2707
+ "dependencies": {
2708
+ "@tiptap/core": "^2.10.0",
2709
+ "@tiptap/extension-blockquote": "^2.10.0",
2710
+ "@tiptap/extension-bold": "^2.10.0",
2711
+ "@tiptap/extension-bullet-list": "^2.10.0",
2712
+ "@tiptap/extension-code": "^2.10.0",
2713
+ "@tiptap/extension-code-block": "^2.10.0",
2714
+ "@tiptap/extension-document": "^2.10.0",
2715
+ "@tiptap/extension-dropcursor": "^2.10.0",
2716
+ "@tiptap/extension-gapcursor": "^2.10.0",
2717
+ "@tiptap/extension-hard-break": "^2.10.0",
2718
+ "@tiptap/extension-heading": "^2.10.0",
2719
+ "@tiptap/extension-history": "^2.10.0",
2720
+ "@tiptap/extension-horizontal-rule": "^2.10.0",
2721
+ "@tiptap/extension-italic": "^2.10.0",
2722
+ "@tiptap/extension-list-item": "^2.10.0",
2723
+ "@tiptap/extension-ordered-list": "^2.10.0",
2724
+ "@tiptap/extension-paragraph": "^2.10.0",
2725
+ "@tiptap/extension-strike": "^2.10.0",
2726
+ "@tiptap/extension-text": "^2.10.0",
2727
+ "@tiptap/extension-text-style": "^2.10.0",
2728
+ "@tiptap/pm": "^2.10.0"
2729
+ },
2730
+ "funding": {
2731
+ "type": "github",
2732
+ "url": "https://github.com/sponsors/ueberdosis"
2733
+ }
2734
+ },
2735
  "node_modules/@types/cookie": {
2736
  "version": "0.6.0",
2737
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
 
2811
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
2812
  "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
2813
  },
2814
+ "node_modules/@types/hast": {
2815
+ "version": "3.0.4",
2816
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
2817
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
2818
+ "license": "MIT",
2819
+ "peer": true,
2820
+ "dependencies": {
2821
+ "@types/unist": "*"
2822
+ }
2823
+ },
2824
  "node_modules/@types/json-schema": {
2825
  "version": "7.0.15",
2826
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
 
3841
  "ieee754": "^1.2.1"
3842
  }
3843
  },
3844
+ "node_modules/buffer-builder": {
3845
+ "version": "0.2.0",
3846
+ "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
3847
+ "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
3848
+ "devOptional": true,
3849
+ "license": "MIT/X11"
3850
+ },
3851
  "node_modules/buffer-crc32": {
3852
  "version": "0.2.13",
3853
  "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
 
4321
  "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
4322
  "dev": true
4323
  },
4324
+ "node_modules/colorjs.io": {
4325
+ "version": "0.5.2",
4326
+ "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
4327
+ "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
4328
+ "devOptional": true,
4329
+ "license": "MIT"
4330
+ },
4331
  "node_modules/colors": {
4332
  "version": "1.4.0",
4333
  "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
 
5190
  "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
5191
  "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="
5192
  },
5193
+ "node_modules/devlop": {
5194
+ "version": "1.1.0",
5195
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
5196
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
5197
+ "license": "MIT",
5198
+ "peer": true,
5199
+ "dependencies": {
5200
+ "dequal": "^2.0.0"
5201
+ },
5202
+ "funding": {
5203
+ "type": "github",
5204
+ "url": "https://github.com/sponsors/wooorm"
5205
+ }
5206
+ },
5207
  "node_modules/didyoumean": {
5208
  "version": "1.2.2",
5209
  "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
 
5491
  "version": "4.0.0",
5492
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
5493
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
 
5494
  "engines": {
5495
  "node": ">=10"
5496
  },
 
6421
  "version": "4.0.0",
6422
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
6423
  "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
6424
+ "devOptional": true,
6425
  "engines": {
6426
  "node": ">=8"
6427
  }
 
6684
  "node": ">= 4"
6685
  }
6686
  },
6687
+ "node_modules/immutable": {
6688
+ "version": "5.0.3",
6689
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
6690
+ "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
6691
+ "devOptional": true,
6692
+ "license": "MIT"
6693
+ },
6694
  "node_modules/import-fresh": {
6695
  "version": "3.3.0",
6696
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
 
7432
  "get-func-name": "^2.0.1"
7433
  }
7434
  },
7435
+ "node_modules/lowlight": {
7436
+ "version": "3.1.0",
7437
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.1.0.tgz",
7438
+ "integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==",
7439
+ "license": "MIT",
7440
+ "peer": true,
7441
+ "dependencies": {
7442
+ "@types/hast": "^3.0.0",
7443
+ "devlop": "^1.0.0",
7444
+ "highlight.js": "~11.9.0"
7445
+ },
7446
+ "funding": {
7447
+ "type": "github",
7448
+ "url": "https://github.com/sponsors/wooorm"
7449
+ }
7450
+ },
7451
  "node_modules/magic-string": {
7452
  "version": "0.30.11",
7453
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
 
9070
  "node": "10.* || >= 12.*"
9071
  }
9072
  },
9073
+ "node_modules/prosemirror-changeset": {
9074
+ "version": "2.2.1",
9075
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz",
9076
+ "integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==",
9077
+ "license": "MIT",
9078
  "dependencies": {
 
 
9079
  "prosemirror-transform": "^1.0.0"
9080
  }
9081
  },
9082
+ "node_modules/prosemirror-collab": {
9083
+ "version": "1.3.1",
9084
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
9085
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
9086
+ "license": "MIT",
9087
+ "dependencies": {
9088
+ "prosemirror-state": "^1.0.0"
9089
+ }
9090
+ },
9091
+ "node_modules/prosemirror-commands": {
9092
+ "version": "1.6.2",
9093
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz",
9094
+ "integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==",
9095
+ "license": "MIT",
9096
+ "dependencies": {
9097
+ "prosemirror-model": "^1.0.0",
9098
+ "prosemirror-state": "^1.0.0",
9099
+ "prosemirror-transform": "^1.10.2"
9100
+ }
9101
+ },
9102
+ "node_modules/prosemirror-dropcursor": {
9103
  "version": "1.8.1",
9104
  "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
9105
  "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
 
9222
  "prosemirror-view": "^1.27.0"
9223
  }
9224
  },
9225
+ "node_modules/prosemirror-tables": {
9226
+ "version": "1.6.1",
9227
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz",
9228
+ "integrity": "sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==",
9229
+ "license": "MIT",
9230
+ "dependencies": {
9231
+ "prosemirror-keymap": "^1.1.2",
9232
+ "prosemirror-model": "^1.8.1",
9233
+ "prosemirror-state": "^1.3.1",
9234
+ "prosemirror-transform": "^1.2.1",
9235
+ "prosemirror-view": "^1.13.3"
9236
+ }
9237
+ },
9238
+ "node_modules/prosemirror-trailing-node": {
9239
+ "version": "3.0.0",
9240
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
9241
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
9242
+ "license": "MIT",
9243
+ "dependencies": {
9244
+ "@remirror/core-constants": "3.0.0",
9245
+ "escape-string-regexp": "^4.0.0"
9246
+ },
9247
+ "peerDependencies": {
9248
+ "prosemirror-model": "^1.22.1",
9249
+ "prosemirror-state": "^1.4.2",
9250
+ "prosemirror-view": "^1.33.8"
9251
+ }
9252
+ },
9253
  "node_modules/prosemirror-transform": {
9254
+ "version": "1.10.2",
9255
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
9256
+ "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
9257
+ "license": "MIT",
9258
  "dependencies": {
9259
  "prosemirror-model": "^1.21.0"
9260
  }
9261
  },
9262
  "node_modules/prosemirror-view": {
9263
+ "version": "1.36.0",
9264
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.36.0.tgz",
9265
+ "integrity": "sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==",
9266
+ "license": "MIT",
9267
  "dependencies": {
9268
  "prosemirror-model": "^1.20.0",
9269
  "prosemirror-state": "^1.0.0",
 
9751
  "version": "7.8.1",
9752
  "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
9753
  "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
9754
+ "devOptional": true,
9755
  "dependencies": {
9756
  "tslib": "^2.1.0"
9757
  }
 
9844
  "rimraf": "bin.js"
9845
  }
9846
  },
9847
+ "node_modules/sass-embedded": {
9848
+ "version": "1.81.0",
9849
+ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.81.0.tgz",
9850
+ "integrity": "sha512-uZQ2Faxb1oWBHpeSSzjxnhClbMb3QadN0ql0ZFNuqWOLUxwaVhrMlMhPq6TDPbbfDUjihuwrMCuy695Bgna5RA==",
9851
+ "devOptional": true,
9852
+ "license": "MIT",
9853
+ "dependencies": {
9854
+ "@bufbuild/protobuf": "^2.0.0",
9855
+ "buffer-builder": "^0.2.0",
9856
+ "colorjs.io": "^0.5.0",
9857
+ "immutable": "^5.0.2",
9858
+ "rxjs": "^7.4.0",
9859
+ "supports-color": "^8.1.1",
9860
+ "sync-child-process": "^1.0.2",
9861
+ "varint": "^6.0.0"
9862
+ },
9863
+ "bin": {
9864
+ "sass": "dist/bin/sass.js"
9865
+ },
9866
+ "engines": {
9867
+ "node": ">=16.0.0"
9868
+ },
9869
+ "optionalDependencies": {
9870
+ "sass-embedded-android-arm": "1.81.0",
9871
+ "sass-embedded-android-arm64": "1.81.0",
9872
+ "sass-embedded-android-ia32": "1.81.0",
9873
+ "sass-embedded-android-riscv64": "1.81.0",
9874
+ "sass-embedded-android-x64": "1.81.0",
9875
+ "sass-embedded-darwin-arm64": "1.81.0",
9876
+ "sass-embedded-darwin-x64": "1.81.0",
9877
+ "sass-embedded-linux-arm": "1.81.0",
9878
+ "sass-embedded-linux-arm64": "1.81.0",
9879
+ "sass-embedded-linux-ia32": "1.81.0",
9880
+ "sass-embedded-linux-musl-arm": "1.81.0",
9881
+ "sass-embedded-linux-musl-arm64": "1.81.0",
9882
+ "sass-embedded-linux-musl-ia32": "1.81.0",
9883
+ "sass-embedded-linux-musl-riscv64": "1.81.0",
9884
+ "sass-embedded-linux-musl-x64": "1.81.0",
9885
+ "sass-embedded-linux-riscv64": "1.81.0",
9886
+ "sass-embedded-linux-x64": "1.81.0",
9887
+ "sass-embedded-win32-arm64": "1.81.0",
9888
+ "sass-embedded-win32-ia32": "1.81.0",
9889
+ "sass-embedded-win32-x64": "1.81.0"
9890
+ }
9891
+ },
9892
+ "node_modules/sass-embedded-android-arm": {
9893
+ "version": "1.81.0",
9894
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.81.0.tgz",
9895
+ "integrity": "sha512-NWEmIuaIEsGFNsIRa+5JpIpPJyZ32H15E85CNZqEIhhwWlk9UNw7vlOCmTH8MtabtnACwC/2NG8VyNa3nxKzUQ==",
9896
+ "cpu": [
9897
+ "arm"
9898
+ ],
9899
+ "license": "MIT",
9900
+ "optional": true,
9901
+ "os": [
9902
+ "android"
9903
+ ],
9904
+ "engines": {
9905
+ "node": ">=14.0.0"
9906
+ }
9907
+ },
9908
+ "node_modules/sass-embedded-android-arm64": {
9909
+ "version": "1.81.0",
9910
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.81.0.tgz",
9911
+ "integrity": "sha512-I36P77/PKAHx6sqOmexO2iEY5kpsmQ1VxcgITZSOxPMQhdB6m4t3bTabfDuWQQmCrqqiNFtLQHeytB65bUqwiw==",
9912
+ "cpu": [
9913
+ "arm64"
9914
+ ],
9915
+ "license": "MIT",
9916
+ "optional": true,
9917
+ "os": [
9918
+ "android"
9919
+ ],
9920
+ "engines": {
9921
+ "node": ">=14.0.0"
9922
+ }
9923
+ },
9924
+ "node_modules/sass-embedded-android-ia32": {
9925
+ "version": "1.81.0",
9926
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.81.0.tgz",
9927
+ "integrity": "sha512-k8V1usXw30w1GVxvrteG1RzgYJzYQ9PfL2aeOqGdroBN7zYTD9VGJXTGcxA4IeeRxmRd7szVW2mKXXS472fh8g==",
9928
+ "cpu": [
9929
+ "ia32"
9930
+ ],
9931
+ "license": "MIT",
9932
+ "optional": true,
9933
+ "os": [
9934
+ "android"
9935
+ ],
9936
+ "engines": {
9937
+ "node": ">=14.0.0"
9938
+ }
9939
+ },
9940
+ "node_modules/sass-embedded-android-riscv64": {
9941
+ "version": "1.81.0",
9942
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.81.0.tgz",
9943
+ "integrity": "sha512-RXlanyLXEpN/DEehXgLuKPsqT//GYlsGFxKXgRiCc8hIPAueFLQXKJmLWlL3BEtHgmFdbsStIu4aZCcb1hOFlQ==",
9944
+ "cpu": [
9945
+ "riscv64"
9946
+ ],
9947
+ "license": "MIT",
9948
+ "optional": true,
9949
+ "os": [
9950
+ "android"
9951
+ ],
9952
+ "engines": {
9953
+ "node": ">=14.0.0"
9954
+ }
9955
+ },
9956
+ "node_modules/sass-embedded-android-x64": {
9957
+ "version": "1.81.0",
9958
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.81.0.tgz",
9959
+ "integrity": "sha512-RQG0FxGQ1DERNyUDED8+BDVaLIjI+BNg8lVcyqlLZUrWY6NhzjwYEeiN/DNZmMmHtqDucAPNDcsdVUNQqsBy2A==",
9960
+ "cpu": [
9961
+ "x64"
9962
+ ],
9963
+ "license": "MIT",
9964
+ "optional": true,
9965
+ "os": [
9966
+ "android"
9967
+ ],
9968
+ "engines": {
9969
+ "node": ">=14.0.0"
9970
+ }
9971
+ },
9972
+ "node_modules/sass-embedded-darwin-arm64": {
9973
+ "version": "1.81.0",
9974
+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.81.0.tgz",
9975
+ "integrity": "sha512-gLKbsfII9Ppua76N41ODFnKGutla9qv0OGAas8gxe0jYBeAQFi/1iKQYdNtQtKi4mA9n5TQTqz+HHCKszZCoyA==",
9976
+ "cpu": [
9977
+ "arm64"
9978
+ ],
9979
+ "license": "MIT",
9980
+ "optional": true,
9981
+ "os": [
9982
+ "darwin"
9983
+ ],
9984
+ "engines": {
9985
+ "node": ">=14.0.0"
9986
+ }
9987
+ },
9988
+ "node_modules/sass-embedded-darwin-x64": {
9989
+ "version": "1.81.0",
9990
+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.81.0.tgz",
9991
+ "integrity": "sha512-7uMOlT9hD2KUJCbTN2XcfghDxt/rc50ujjfSjSHjX1SYj7mGplkINUXvVbbvvaV2wt6t9vkGkCo5qNbeBhfwBg==",
9992
+ "cpu": [
9993
+ "x64"
9994
+ ],
9995
+ "license": "MIT",
9996
+ "optional": true,
9997
+ "os": [
9998
+ "darwin"
9999
+ ],
10000
+ "engines": {
10001
+ "node": ">=14.0.0"
10002
+ }
10003
+ },
10004
+ "node_modules/sass-embedded-linux-arm": {
10005
+ "version": "1.81.0",
10006
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.81.0.tgz",
10007
+ "integrity": "sha512-REqR9qM4RchCE3cKqzRy9Q4zigIV82SbSpCi/O4O3oK3pg2I1z7vkb3TiJsivusG/li7aqKZGmYOtAXjruGQDA==",
10008
+ "cpu": [
10009
+ "arm"
10010
+ ],
10011
+ "license": "MIT",
10012
+ "optional": true,
10013
+ "os": [
10014
+ "linux"
10015
+ ],
10016
+ "engines": {
10017
+ "node": ">=14.0.0"
10018
+ }
10019
+ },
10020
+ "node_modules/sass-embedded-linux-arm64": {
10021
+ "version": "1.81.0",
10022
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.81.0.tgz",
10023
+ "integrity": "sha512-jy4bvhdUmqbyw1jv1f3Uxl+MF8EU/Y/GDx4w6XPJm4Ds+mwH/TwnyAwsxxoBhWfnBnW8q2ADy039DlS5p+9csQ==",
10024
+ "cpu": [
10025
+ "arm64"
10026
+ ],
10027
+ "license": "MIT",
10028
+ "optional": true,
10029
+ "os": [
10030
+ "linux"
10031
+ ],
10032
+ "engines": {
10033
+ "node": ">=14.0.0"
10034
+ }
10035
+ },
10036
+ "node_modules/sass-embedded-linux-ia32": {
10037
+ "version": "1.81.0",
10038
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.81.0.tgz",
10039
+ "integrity": "sha512-ga/Jk4q5Bn1aC+iHJteDZuLSKnmBUiS3dEg1fnl/Z7GaHIChceKDJOw0zNaILRXI0qT2E1at9MwzoRaRA5Nn/g==",
10040
+ "cpu": [
10041
+ "ia32"
10042
+ ],
10043
+ "license": "MIT",
10044
+ "optional": true,
10045
+ "os": [
10046
+ "linux"
10047
+ ],
10048
+ "engines": {
10049
+ "node": ">=14.0.0"
10050
+ }
10051
+ },
10052
+ "node_modules/sass-embedded-linux-musl-arm": {
10053
+ "version": "1.81.0",
10054
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.81.0.tgz",
10055
+ "integrity": "sha512-oWVUvQ4d5Kx1Md75YXZl5z1WBjc+uOhfRRqzkJ3nWc8tjszxJN+y/5EOJavhsNI3/2yoTt6eMXRTqDD9b0tWSQ==",
10056
+ "cpu": [
10057
+ "arm"
10058
+ ],
10059
+ "license": "MIT",
10060
+ "optional": true,
10061
+ "os": [
10062
+ "linux"
10063
+ ],
10064
+ "engines": {
10065
+ "node": ">=14.0.0"
10066
+ }
10067
+ },
10068
+ "node_modules/sass-embedded-linux-musl-arm64": {
10069
+ "version": "1.81.0",
10070
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.81.0.tgz",
10071
+ "integrity": "sha512-hpntWf5kjkoxncA1Vh8vhsUOquZ8AROZKx0rQh7ZjSRs4JrYZASz1cfevPKaEM3wIim/nYa6TJqm0VqWsrERlA==",
10072
+ "cpu": [
10073
+ "arm64"
10074
+ ],
10075
+ "license": "MIT",
10076
+ "optional": true,
10077
+ "os": [
10078
+ "linux"
10079
+ ],
10080
+ "engines": {
10081
+ "node": ">=14.0.0"
10082
+ }
10083
+ },
10084
+ "node_modules/sass-embedded-linux-musl-ia32": {
10085
+ "version": "1.81.0",
10086
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.81.0.tgz",
10087
+ "integrity": "sha512-UEXUYkBuqTSwg5JNWiNlfMZ1Jx6SJkaEdx+fsL3Tk099L8cKSoJWH2EPz4ZJjNbyIMymrSdVfymheTeZ8u24xA==",
10088
+ "cpu": [
10089
+ "ia32"
10090
+ ],
10091
+ "license": "MIT",
10092
+ "optional": true,
10093
+ "os": [
10094
+ "linux"
10095
+ ],
10096
+ "engines": {
10097
+ "node": ">=14.0.0"
10098
+ }
10099
+ },
10100
+ "node_modules/sass-embedded-linux-musl-riscv64": {
10101
+ "version": "1.81.0",
10102
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.81.0.tgz",
10103
+ "integrity": "sha512-1D7OznytbIhx2XDHWi1nuQ8d/uCVR7FGGzELgaU//T8A9DapVTUgPKvB70AF1k4GzChR9IXU/WvFZs2hDTbaJg==",
10104
+ "cpu": [
10105
+ "riscv64"
10106
+ ],
10107
+ "license": "MIT",
10108
+ "optional": true,
10109
+ "os": [
10110
+ "linux"
10111
+ ],
10112
+ "engines": {
10113
+ "node": ">=14.0.0"
10114
+ }
10115
+ },
10116
+ "node_modules/sass-embedded-linux-musl-x64": {
10117
+ "version": "1.81.0",
10118
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.81.0.tgz",
10119
+ "integrity": "sha512-ia6VCTeVDQtBSMktXRFza1AZCt8/6aUoujot6Ugf4KmdytQqPJIHxkHaGftm5xwi9WdrMGYS7zgolToPijR11A==",
10120
+ "cpu": [
10121
+ "x64"
10122
+ ],
10123
+ "license": "MIT",
10124
+ "optional": true,
10125
+ "os": [
10126
+ "linux"
10127
+ ],
10128
+ "engines": {
10129
+ "node": ">=14.0.0"
10130
+ }
10131
+ },
10132
+ "node_modules/sass-embedded-linux-riscv64": {
10133
+ "version": "1.81.0",
10134
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.81.0.tgz",
10135
+ "integrity": "sha512-KbxSsqu4tT1XbhZfJV/5NfW0VtJIGlD58RjqJqJBi8Rnjrx29/upBsuwoDWtsPV/LhoGwwU1XkSa9Q1ifCz4fQ==",
10136
+ "cpu": [
10137
+ "riscv64"
10138
+ ],
10139
+ "license": "MIT",
10140
+ "optional": true,
10141
+ "os": [
10142
+ "linux"
10143
+ ],
10144
+ "engines": {
10145
+ "node": ">=14.0.0"
10146
+ }
10147
+ },
10148
+ "node_modules/sass-embedded-linux-x64": {
10149
+ "version": "1.81.0",
10150
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.81.0.tgz",
10151
+ "integrity": "sha512-AMDeVY2T9WAnSFkuQcsOn5c29GRs/TuqnCiblKeXfxCSKym5uKdBl/N7GnTV6OjzoxiJBbkYKdVIaS5By7Gj4g==",
10152
+ "cpu": [
10153
+ "x64"
10154
+ ],
10155
+ "license": "MIT",
10156
+ "optional": true,
10157
+ "os": [
10158
+ "linux"
10159
+ ],
10160
+ "engines": {
10161
+ "node": ">=14.0.0"
10162
+ }
10163
+ },
10164
+ "node_modules/sass-embedded-win32-arm64": {
10165
+ "version": "1.81.0",
10166
+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.81.0.tgz",
10167
+ "integrity": "sha512-YOmBRYnygwWUmCoH14QbMRHjcvCJufeJBAp0m61tOJXIQh64ziwV4mjdqjS/Rx3zhTT4T+nulDUw4d3kLiMncA==",
10168
+ "cpu": [
10169
+ "arm64"
10170
+ ],
10171
+ "license": "MIT",
10172
+ "optional": true,
10173
+ "os": [
10174
+ "win32"
10175
+ ],
10176
+ "engines": {
10177
+ "node": ">=14.0.0"
10178
+ }
10179
+ },
10180
+ "node_modules/sass-embedded-win32-ia32": {
10181
+ "version": "1.81.0",
10182
+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.81.0.tgz",
10183
+ "integrity": "sha512-HFfr/C+uLJGGTENdnssuNTmXI/xnIasUuEHEKqI+2J0FHCWT5cpz3PGAOHymPyJcZVYGUG/7gIxIx/d7t0LFYw==",
10184
+ "cpu": [
10185
+ "ia32"
10186
+ ],
10187
+ "license": "MIT",
10188
+ "optional": true,
10189
+ "os": [
10190
+ "win32"
10191
+ ],
10192
+ "engines": {
10193
+ "node": ">=14.0.0"
10194
+ }
10195
+ },
10196
+ "node_modules/sass-embedded-win32-x64": {
10197
+ "version": "1.81.0",
10198
+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.81.0.tgz",
10199
+ "integrity": "sha512-wxj52jDcIAwWcXb7ShZ7vQYKcVUkJ+04YM9l46jDY+qwHzliGuorAUyujLyKTE9heGD3gShJ3wPPC1lXzq6v9A==",
10200
+ "cpu": [
10201
+ "x64"
10202
+ ],
10203
+ "license": "MIT",
10204
+ "optional": true,
10205
+ "os": [
10206
+ "win32"
10207
+ ],
10208
+ "engines": {
10209
+ "node": ">=14.0.0"
10210
+ }
10211
+ },
10212
+ "node_modules/sass-embedded/node_modules/supports-color": {
10213
+ "version": "8.1.1",
10214
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
10215
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
10216
+ "devOptional": true,
10217
+ "license": "MIT",
10218
+ "dependencies": {
10219
+ "has-flag": "^4.0.0"
10220
+ },
10221
+ "engines": {
10222
+ "node": ">=10"
10223
+ },
10224
+ "funding": {
10225
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
10226
+ }
10227
+ },
10228
  "node_modules/semver": {
10229
  "version": "7.6.3",
10230
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
 
10940
  "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==",
10941
  "dev": true
10942
  },
10943
+ "node_modules/sync-child-process": {
10944
+ "version": "1.0.2",
10945
+ "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
10946
+ "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
10947
+ "devOptional": true,
10948
+ "license": "MIT",
10949
+ "dependencies": {
10950
+ "sync-message-port": "^1.0.0"
10951
+ },
10952
+ "engines": {
10953
+ "node": ">=16.0.0"
10954
+ }
10955
+ },
10956
+ "node_modules/sync-message-port": {
10957
+ "version": "1.1.3",
10958
+ "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
10959
+ "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
10960
+ "devOptional": true,
10961
+ "license": "MIT",
10962
+ "engines": {
10963
+ "node": ">=16.0.0"
10964
+ }
10965
+ },
10966
  "node_modules/tabbable": {
10967
  "version": "6.2.0",
10968
  "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
 
11335
  "version": "7.2.0",
11336
  "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz",
11337
  "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==",
11338
+ "license": "MIT",
11339
  "dependencies": {
11340
  "@mixmark-io/domino": "^2.2.0"
11341
  }
 
11549
  "node": ">= 10.13.0"
11550
  }
11551
  },
11552
+ "node_modules/varint": {
11553
+ "version": "6.0.0",
11554
+ "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
11555
+ "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
11556
+ "devOptional": true,
11557
+ "license": "MIT"
11558
+ },
11559
  "node_modules/verror": {
11560
  "version": "1.10.0",
11561
  "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "open-webui",
3
- "version": "0.4.2",
4
  "private": true,
5
  "scripts": {
6
  "dev": "npm run pyodide:fetch && vite dev --host",
@@ -37,6 +37,7 @@
37
  "postcss": "^8.4.31",
38
  "prettier": "^3.3.3",
39
  "prettier-plugin-svelte": "^3.2.6",
 
40
  "svelte": "^4.2.18",
41
  "svelte-check": "^3.8.5",
42
  "svelte-confetti": "^1.3.2",
@@ -56,6 +57,13 @@
56
  "@mediapipe/tasks-vision": "^0.10.17",
57
  "@pyscript/core": "^0.4.32",
58
  "@sveltejs/adapter-node": "^2.0.0",
 
 
 
 
 
 
 
59
  "@xyflow/svelte": "^0.1.19",
60
  "async": "^3.2.5",
61
  "bits-ui": "^0.19.7",
 
1
  {
2
  "name": "open-webui",
3
+ "version": "0.4.3",
4
  "private": true,
5
  "scripts": {
6
  "dev": "npm run pyodide:fetch && vite dev --host",
 
37
  "postcss": "^8.4.31",
38
  "prettier": "^3.3.3",
39
  "prettier-plugin-svelte": "^3.2.6",
40
+ "sass-embedded": "^1.81.0",
41
  "svelte": "^4.2.18",
42
  "svelte-check": "^3.8.5",
43
  "svelte-confetti": "^1.3.2",
 
57
  "@mediapipe/tasks-vision": "^0.10.17",
58
  "@pyscript/core": "^0.4.32",
59
  "@sveltejs/adapter-node": "^2.0.0",
60
+ "@tiptap/core": "^2.10.0",
61
+ "@tiptap/extension-code-block-lowlight": "^2.10.0",
62
+ "@tiptap/extension-highlight": "^2.10.0",
63
+ "@tiptap/extension-placeholder": "^2.10.0",
64
+ "@tiptap/extension-typography": "^2.10.0",
65
+ "@tiptap/pm": "^2.10.0",
66
+ "@tiptap/starter-kit": "^2.10.0",
67
  "@xyflow/svelte": "^0.1.19",
68
  "async": "^3.2.5",
69
  "bits-ui": "^0.19.7",
src/app.css CHANGED
@@ -199,19 +199,86 @@ input[type='number'] {
199
  }
200
 
201
  .ProseMirror {
202
- @apply h-full min-h-fit max-h-full whitespace-pre-wrap;
203
  }
204
 
205
  .ProseMirror:focus {
206
  outline: none;
207
  }
208
 
209
- .placeholder::after {
210
  content: attr(data-placeholder);
211
- cursor: text;
 
212
  pointer-events: none;
 
 
213
 
214
- float: left;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- @apply absolute inset-0 z-0 text-gray-500;
 
217
  }
 
199
  }
200
 
201
  .ProseMirror {
202
+ @apply h-full min-h-fit max-h-full whitespace-pre-wrap;
203
  }
204
 
205
  .ProseMirror:focus {
206
  outline: none;
207
  }
208
 
209
+ .ProseMirror p.is-editor-empty:first-child::before {
210
  content: attr(data-placeholder);
211
+ float: left;
212
+ color: #adb5bd;
213
  pointer-events: none;
214
+ height: 0;
215
+ }
216
 
217
+ .tiptap > pre > code {
218
+ border-radius: 0.4rem;
219
+ font-size: 0.85rem;
220
+ padding: 0.25em 0.3em;
221
+
222
+ @apply dark:bg-gray-800 bg-gray-100;
223
+ }
224
+
225
+ .tiptap > pre {
226
+ border-radius: 0.5rem;
227
+ font-family: 'JetBrainsMono', monospace;
228
+ margin: 1.5rem 0;
229
+ padding: 0.75rem 1rem;
230
+
231
+ @apply dark:bg-gray-800 bg-gray-100;
232
+ }
233
+
234
+ /* Code styling */
235
+ .hljs-comment,
236
+ .hljs-quote {
237
+ color: #616161;
238
+ }
239
+
240
+ .hljs-variable,
241
+ .hljs-template-variable,
242
+ .hljs-attribute,
243
+ .hljs-tag,
244
+ .hljs-regexp,
245
+ .hljs-link,
246
+ .hljs-name,
247
+ .hljs-selector-id,
248
+ .hljs-selector-class {
249
+ color: #f98181;
250
+ }
251
+
252
+ .hljs-number,
253
+ .hljs-meta,
254
+ .hljs-built_in,
255
+ .hljs-builtin-name,
256
+ .hljs-literal,
257
+ .hljs-type,
258
+ .hljs-params {
259
+ color: #fbbc88;
260
+ }
261
+
262
+ .hljs-string,
263
+ .hljs-symbol,
264
+ .hljs-bullet {
265
+ color: #b9f18d;
266
+ }
267
+
268
+ .hljs-title,
269
+ .hljs-section {
270
+ color: #faf594;
271
+ }
272
+
273
+ .hljs-keyword,
274
+ .hljs-selector-tag {
275
+ color: #70cff8;
276
+ }
277
+
278
+ .hljs-emphasis {
279
+ font-style: italic;
280
+ }
281
 
282
+ .hljs-strong {
283
+ font-weight: 700;
284
  }
src/lib/apis/streaming/index.ts CHANGED
@@ -5,7 +5,7 @@ type TextStreamUpdate = {
5
  done: boolean;
6
  value: string;
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
- citations?: any;
9
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
  selectedModelId?: any;
11
  error?: any;
@@ -67,8 +67,8 @@ async function* openAIStreamToIterator(
67
  break;
68
  }
69
 
70
- if (parsedData.citations) {
71
- yield { done: false, value: '', citations: parsedData.citations };
72
  continue;
73
  }
74
 
@@ -98,7 +98,7 @@ async function* streamLargeDeltasAsRandomChunks(
98
  yield textStreamUpdate;
99
  return;
100
  }
101
- if (textStreamUpdate.citations) {
102
  yield textStreamUpdate;
103
  continue;
104
  }
 
5
  done: boolean;
6
  value: string;
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ sources?: any;
9
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
  selectedModelId?: any;
11
  error?: any;
 
67
  break;
68
  }
69
 
70
+ if (parsedData.sources) {
71
+ yield { done: false, value: '', sources: parsedData.sources };
72
  continue;
73
  }
74
 
 
98
  yield textStreamUpdate;
99
  return;
100
  }
101
+ if (textStreamUpdate.sources) {
102
  yield textStreamUpdate;
103
  continue;
104
  }
src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte CHANGED
@@ -60,7 +60,7 @@
60
  />
61
 
62
  {#if pipeline}
63
- <div class=" absolute top-2.5 right-2.5">
64
  <Tooltip content="Pipelines">
65
  <svg
66
  xmlns="http://www.w3.org/2000/svg"
 
60
  />
61
 
62
  {#if pipeline}
63
+ <div class=" absolute top-0.5 right-2.5">
64
  <Tooltip content="Pipelines">
65
  <svg
66
  xmlns="http://www.w3.org/2000/svg"
src/lib/components/admin/Settings/Models.svelte CHANGED
@@ -186,7 +186,7 @@
186
 
187
  <div class=" my-2 mb-5" id="model-list">
188
  {#if models.length > 0}
189
- {#each filteredModels as model (model.id)}
190
  <div
191
  class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
192
  id="model-item-{model.id}"
 
186
 
187
  <div class=" my-2 mb-5" id="model-list">
188
  {#if models.length > 0}
189
+ {#each filteredModels as model, modelIdx (`${model.id}-${modelIdx}`)}
190
  <div
191
  class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
192
  id="model-item-{model.id}"
src/lib/components/admin/Settings/WebSearch.svelte CHANGED
@@ -16,6 +16,7 @@
16
  'searxng',
17
  'google_pse',
18
  'brave',
 
19
  'serpstack',
20
  'serper',
21
  'serply',
@@ -151,6 +152,17 @@
151
  bind:value={webConfig.search.brave_search_api_key}
152
  />
153
  </div>
 
 
 
 
 
 
 
 
 
 
 
154
  {:else if webConfig.search.engine === 'serpstack'}
155
  <div>
156
  <div class=" self-center text-xs font-medium mb-1">
 
16
  'searxng',
17
  'google_pse',
18
  'brave',
19
+ 'mojeek',
20
  'serpstack',
21
  'serper',
22
  'serply',
 
152
  bind:value={webConfig.search.brave_search_api_key}
153
  />
154
  </div>
155
+ {:else if webConfig.search.engine === 'mojeek'}
156
+ <div>
157
+ <div class=" self-center text-xs font-medium mb-1">
158
+ {$i18n.t('Mojeek Search API Key')}
159
+ </div>
160
+
161
+ <SensitiveInput
162
+ placeholder={$i18n.t('Enter Mojeek Search API Key')}
163
+ bind:value={webConfig.search.mojeek_search_api_key}
164
+ />
165
+ </div>
166
  {:else if webConfig.search.engine === 'serpstack'}
167
  <div>
168
  <div class=" self-center text-xs font-medium mb-1">
src/lib/components/chat/Chat.svelte CHANGED
@@ -216,7 +216,7 @@
216
  } else {
217
  message.statusHistory = [data];
218
  }
219
- } else if (type === 'citation') {
220
  if (data?.type === 'code_execution') {
221
  // Code execution; update existing code execution by ID, or add new one.
222
  if (!message?.code_executions) {
@@ -235,11 +235,11 @@
235
 
236
  message.code_executions = message.code_executions;
237
  } else {
238
- // Regular citation.
239
- if (message?.citations) {
240
- message.citations.push(data);
241
  } else {
242
- message.citations = [data];
243
  }
244
  }
245
  } else if (type === 'message') {
@@ -664,7 +664,7 @@
664
  content: m.content,
665
  info: m.info ? m.info : undefined,
666
  timestamp: m.timestamp,
667
- ...(m.citations ? { citations: m.citations } : {})
668
  })),
669
  chat_id: chatId,
670
  session_id: $socket?.id,
@@ -718,7 +718,7 @@
718
  content: m.content,
719
  info: m.info ? m.info : undefined,
720
  timestamp: m.timestamp,
721
- ...(m.citations ? { citations: m.citations } : {})
722
  })),
723
  ...(event ? { event: event } : {}),
724
  chat_id: chatId,
@@ -1278,8 +1278,8 @@
1278
  console.log(line);
1279
  let data = JSON.parse(line);
1280
 
1281
- if ('citations' in data) {
1282
- responseMessage.citations = data.citations;
1283
  // Only remove status if it was initially set
1284
  if (model?.info?.meta?.knowledge ?? false) {
1285
  responseMessage.statusHistory = responseMessage.statusHistory.filter(
@@ -1632,7 +1632,7 @@
1632
  const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
1633
 
1634
  for await (const update of textStream) {
1635
- const { value, done, citations, selectedModelId, error, usage } = update;
1636
  if (error) {
1637
  await handleOpenAIError(error, null, model, responseMessage);
1638
  break;
@@ -1658,8 +1658,8 @@
1658
  continue;
1659
  }
1660
 
1661
- if (citations) {
1662
- responseMessage.citations = citations;
1663
  // Only remove status if it was initially set
1664
  if (model?.info?.meta?.knowledge ?? false) {
1665
  responseMessage.statusHistory = responseMessage.statusHistory.filter(
@@ -1938,7 +1938,7 @@
1938
  if (res && res.ok && res.body) {
1939
  const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
1940
  for await (const update of textStream) {
1941
- const { value, done, citations, error, usage } = update;
1942
  if (error || done) {
1943
  break;
1944
  }
 
216
  } else {
217
  message.statusHistory = [data];
218
  }
219
+ } else if (type === 'source') {
220
  if (data?.type === 'code_execution') {
221
  // Code execution; update existing code execution by ID, or add new one.
222
  if (!message?.code_executions) {
 
235
 
236
  message.code_executions = message.code_executions;
237
  } else {
238
+ // Regular source.
239
+ if (message?.sources) {
240
+ message.sources.push(data);
241
  } else {
242
+ message.sources = [data];
243
  }
244
  }
245
  } else if (type === 'message') {
 
664
  content: m.content,
665
  info: m.info ? m.info : undefined,
666
  timestamp: m.timestamp,
667
+ ...(m.sources ? { sources: m.sources } : {})
668
  })),
669
  chat_id: chatId,
670
  session_id: $socket?.id,
 
718
  content: m.content,
719
  info: m.info ? m.info : undefined,
720
  timestamp: m.timestamp,
721
+ ...(m.sources ? { sources: m.sources } : {})
722
  })),
723
  ...(event ? { event: event } : {}),
724
  chat_id: chatId,
 
1278
  console.log(line);
1279
  let data = JSON.parse(line);
1280
 
1281
+ if ('sources' in data) {
1282
+ responseMessage.sources = data.sources;
1283
  // Only remove status if it was initially set
1284
  if (model?.info?.meta?.knowledge ?? false) {
1285
  responseMessage.statusHistory = responseMessage.statusHistory.filter(
 
1632
  const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
1633
 
1634
  for await (const update of textStream) {
1635
+ const { value, done, sources, selectedModelId, error, usage } = update;
1636
  if (error) {
1637
  await handleOpenAIError(error, null, model, responseMessage);
1638
  break;
 
1658
  continue;
1659
  }
1660
 
1661
+ if (sources) {
1662
+ responseMessage.sources = sources;
1663
  // Only remove status if it was initially set
1664
  if (model?.info?.meta?.knowledge ?? false) {
1665
  responseMessage.statusHistory = responseMessage.statusHistory.filter(
 
1938
  if (res && res.ok && res.body) {
1939
  const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
1940
  for await (const update of textStream) {
1941
+ const { value, done, sources, error, usage } = update;
1942
  if (error || done) {
1943
  break;
1944
  }
src/lib/components/chat/MessageInput.svelte CHANGED
@@ -75,14 +75,6 @@
75
  (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
76
  );
77
 
78
- $: if (prompt) {
79
- if (chatInputContainerElement) {
80
- chatInputContainerElement.style.height = '';
81
- chatInputContainerElement.style.height =
82
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
83
- }
84
- }
85
-
86
  const scrollToBottom = () => {
87
  const element = document.getElementById('messages-container');
88
  element.scrollTo({
@@ -585,54 +577,47 @@
585
 
586
  {#if $settings?.richTextInput ?? true}
587
  <div
588
- bind:this={chatInputContainerElement}
589
- id="chat-input-container"
590
- 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"
591
  >
592
  <RichTextInput
593
  bind:this={chatInputElement}
594
  id="chat-input"
595
- trim={true}
596
- placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
597
- largeTextAsFile={$settings?.largeTextAsFile ?? false}
598
- bind:value={prompt}
599
  shiftEnter={!$mobile ||
600
  !(
601
  'ontouchstart' in window ||
602
  navigator.maxTouchPoints > 0 ||
603
  navigator.msMaxTouchPoints > 0
604
  )}
 
 
 
605
  on:enter={async (e) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  if (prompt !== '') {
607
  dispatch('submit', prompt);
608
  }
609
  }}
610
- on:input={async (e) => {
611
- if (chatInputContainerElement) {
612
- chatInputContainerElement.style.height = '';
613
- chatInputContainerElement.style.height =
614
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
615
- }
616
- }}
617
- on:focus={async (e) => {
618
- if (chatInputContainerElement) {
619
- chatInputContainerElement.style.height = '';
620
- chatInputContainerElement.style.height =
621
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
622
- }
623
- }}
624
  on:keypress={(e) => {
625
  e = e.detail.event;
626
  }}
627
  on:keydown={async (e) => {
628
  e = e.detail.event;
629
 
630
- if (chatInputContainerElement) {
631
- chatInputContainerElement.style.height = '';
632
- chatInputContainerElement.style.height =
633
- Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
634
- }
635
-
636
  const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
637
  const commandsContainerElement =
638
  document.getElementById('commands-container');
@@ -692,22 +677,6 @@
692
  commandOptionButton.scrollIntoView({ block: 'center' });
693
  }
694
 
695
- if (commandsContainerElement && e.key === 'Enter') {
696
- e.preventDefault();
697
-
698
- const commandOptionButton = [
699
- ...document.getElementsByClassName('selected-command-option-button')
700
- ]?.at(-1);
701
-
702
- if (e.shiftKey) {
703
- prompt = `${prompt}\n`;
704
- } else if (commandOptionButton) {
705
- commandOptionButton?.click();
706
- } else {
707
- document.getElementById('send-message-button')?.click();
708
- }
709
- }
710
-
711
  if (commandsContainerElement && e.key === 'Tab') {
712
  e.preventDefault();
713
 
 
75
  (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
76
  );
77
 
 
 
 
 
 
 
 
 
78
  const scrollToBottom = () => {
79
  const element = document.getElementById('messages-container');
80
  element.scrollTo({
 
577
 
578
  {#if $settings?.richTextInput ?? true}
579
  <div
580
+ class="scrollbar-hidden text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-60 overflow-auto"
 
 
581
  >
582
  <RichTextInput
583
  bind:this={chatInputElement}
584
  id="chat-input"
585
+ messageInput={true}
 
 
 
586
  shiftEnter={!$mobile ||
587
  !(
588
  'ontouchstart' in window ||
589
  navigator.maxTouchPoints > 0 ||
590
  navigator.msMaxTouchPoints > 0
591
  )}
592
+ placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
593
+ largeTextAsFile={$settings?.largeTextAsFile ?? false}
594
+ bind:value={prompt}
595
  on:enter={async (e) => {
596
+ const commandsContainerElement =
597
+ document.getElementById('commands-container');
598
+ if (commandsContainerElement) {
599
+ e.preventDefault();
600
+
601
+ const commandOptionButton = [
602
+ ...document.getElementsByClassName('selected-command-option-button')
603
+ ]?.at(-1);
604
+
605
+ if (commandOptionButton) {
606
+ commandOptionButton?.click();
607
+ return;
608
+ }
609
+ }
610
+
611
  if (prompt !== '') {
612
  dispatch('submit', prompt);
613
  }
614
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
  on:keypress={(e) => {
616
  e = e.detail.event;
617
  }}
618
  on:keydown={async (e) => {
619
  e = e.detail.event;
620
 
 
 
 
 
 
 
621
  const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
622
  const commandsContainerElement =
623
  document.getElementById('commands-container');
 
677
  commandOptionButton.scrollIntoView({ block: 'center' });
678
  }
679
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  if (commandsContainerElement && e.key === 'Tab') {
681
  e.preventDefault();
682
 
src/lib/components/chat/Messages/Citations.svelte CHANGED
@@ -7,9 +7,9 @@
7
 
8
  const i18n = getContext('i18n');
9
 
10
- export let citations = [];
11
 
12
- let _citations = [];
13
  let showPercentage = false;
14
  let showRelevance = true;
15
 
@@ -17,8 +17,8 @@
17
  let selectedCitation: any = null;
18
  let isCollapsibleOpen = false;
19
 
20
- function calculateShowRelevance(citations: any[]) {
21
- const distances = citations.flatMap((citation) => citation.distances ?? []);
22
  const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
23
  const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
24
 
@@ -36,25 +36,31 @@
36
  return true;
37
  }
38
 
39
- function shouldShowPercentage(citations: any[]) {
40
- const distances = citations.flatMap((citation) => citation.distances ?? []);
41
  return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
42
  }
43
 
44
  $: {
45
- _citations = citations.reduce((acc, citation) => {
46
- citation.document.forEach((document, index) => {
47
- const metadata = citation.metadata?.[index];
48
- const distance = citation.distances?.[index];
 
 
 
 
 
 
49
  const id = metadata?.source ?? 'N/A';
50
- let source = citation?.source;
51
 
52
  if (metadata?.name) {
53
- source = { ...source, name: metadata.name };
54
  }
55
 
56
  if (id.startsWith('http://') || id.startsWith('https://')) {
57
- source = { ...source, name: id, url: id };
58
  }
59
 
60
  const existingSource = acc.find((item) => item.id === id);
@@ -66,7 +72,7 @@
66
  } else {
67
  acc.push({
68
  id: id,
69
- source: source,
70
  document: [document],
71
  metadata: metadata ? [metadata] : [],
72
  distances: distance !== undefined ? [distance] : undefined
@@ -76,8 +82,8 @@
76
  return acc;
77
  }, []);
78
 
79
- showRelevance = calculateShowRelevance(_citations);
80
- showPercentage = shouldShowPercentage(_citations);
81
  }
82
  </script>
83
 
@@ -88,24 +94,27 @@
88
  {showRelevance}
89
  />
90
 
91
- {#if _citations.length > 0}
92
  <div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
93
- {#if _citations.length <= 3}
94
  <div class="flex text-xs font-medium">
95
- {#each _citations as citation, idx}
96
  <button
97
- class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
 
98
  on:click={() => {
99
  showCitationModal = true;
100
  selectedCitation = citation;
101
  }}
102
  >
103
- {#if _citations.every((c) => c.distances !== undefined)}
104
  <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
105
  {idx + 1}
106
  </div>
107
  {/if}
108
- <div class="flex-1 mx-1 line-clamp-1 truncate">
 
 
109
  {citation.source.name}
110
  </div>
111
  </button>
@@ -120,7 +129,7 @@
120
  <span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
121
  <div class="flex items-center">
122
  <div class="flex text-xs font-medium items-center">
123
- {#each _citations.slice(0, 2) as citation, idx}
124
  <button
125
  class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
126
  on:click={() => {
@@ -131,7 +140,7 @@
131
  e.stopPropagation();
132
  }}
133
  >
134
- {#if _citations.every((c) => c.distances !== undefined)}
135
  <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
136
  {idx + 1}
137
  </div>
@@ -145,7 +154,7 @@
145
  </div>
146
  <div class="flex items-center gap-1 whitespace-nowrap">
147
  <span class="hidden sm:inline">{$i18n.t('and')}</span>
148
- {_citations.length - 2}
149
  <span>{$i18n.t('more')}</span>
150
  </div>
151
  </div>
@@ -159,7 +168,7 @@
159
  </div>
160
  <div slot="content">
161
  <div class="flex text-xs font-medium">
162
- {#each _citations as citation, idx}
163
  <button
164
  class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
165
  on:click={() => {
@@ -167,7 +176,7 @@
167
  selectedCitation = citation;
168
  }}
169
  >
170
- {#if _citations.every((c) => c.distances !== undefined)}
171
  <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
172
  {idx + 1}
173
  </div>
 
7
 
8
  const i18n = getContext('i18n');
9
 
10
+ export let sources = [];
11
 
12
+ let citations = [];
13
  let showPercentage = false;
14
  let showRelevance = true;
15
 
 
17
  let selectedCitation: any = null;
18
  let isCollapsibleOpen = false;
19
 
20
+ function calculateShowRelevance(sources: any[]) {
21
+ const distances = sources.flatMap((citation) => citation.distances ?? []);
22
  const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
23
  const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
24
 
 
36
  return true;
37
  }
38
 
39
+ function shouldShowPercentage(sources: any[]) {
40
+ const distances = sources.flatMap((citation) => citation.distances ?? []);
41
  return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
42
  }
43
 
44
  $: {
45
+ citations = sources.reduce((acc, source) => {
46
+ if (Object.keys(source).length === 0) {
47
+ return acc;
48
+ }
49
+
50
+ source.document.forEach((document, index) => {
51
+ const metadata = source.metadata?.[index];
52
+ const distance = source.distances?.[index];
53
+
54
+ // Within the same citation there could be multiple documents
55
  const id = metadata?.source ?? 'N/A';
56
+ let _source = source?.source;
57
 
58
  if (metadata?.name) {
59
+ _source = { ..._source, name: metadata.name };
60
  }
61
 
62
  if (id.startsWith('http://') || id.startsWith('https://')) {
63
+ _source = { ..._source, name: id, url: id };
64
  }
65
 
66
  const existingSource = acc.find((item) => item.id === id);
 
72
  } else {
73
  acc.push({
74
  id: id,
75
+ source: _source,
76
  document: [document],
77
  metadata: metadata ? [metadata] : [],
78
  distances: distance !== undefined ? [distance] : undefined
 
82
  return acc;
83
  }, []);
84
 
85
+ showRelevance = calculateShowRelevance(citations);
86
+ showPercentage = shouldShowPercentage(citations);
87
  }
88
  </script>
89
 
 
94
  {showRelevance}
95
  />
96
 
97
+ {#if citations.length > 0}
98
  <div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
99
+ {#if citations.length <= 3}
100
  <div class="flex text-xs font-medium">
101
+ {#each citations as citation, idx}
102
  <button
103
+ id={`source-${citation.source.name}`}
104
+ class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
105
  on:click={() => {
106
  showCitationModal = true;
107
  selectedCitation = citation;
108
  }}
109
  >
110
+ {#if citations.every((c) => c.distances !== undefined)}
111
  <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
112
  {idx + 1}
113
  </div>
114
  {/if}
115
+ <div
116
+ class="flex-1 mx-1 line-clamp-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
117
+ >
118
  {citation.source.name}
119
  </div>
120
  </button>
 
129
  <span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
130
  <div class="flex items-center">
131
  <div class="flex text-xs font-medium items-center">
132
+ {#each citations.slice(0, 2) as citation, idx}
133
  <button
134
  class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
135
  on:click={() => {
 
140
  e.stopPropagation();
141
  }}
142
  >
143
+ {#if citations.every((c) => c.distances !== undefined)}
144
  <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
145
  {idx + 1}
146
  </div>
 
154
  </div>
155
  <div class="flex items-center gap-1 whitespace-nowrap">
156
  <span class="hidden sm:inline">{$i18n.t('and')}</span>
157
+ {citations.length - 2}
158
  <span>{$i18n.t('more')}</span>
159
  </div>
160
  </div>
 
168
  </div>
169
  <div slot="content">
170
  <div class="flex text-xs font-medium">
171
+ {#each citations as citation, idx}
172
  <button
173
  class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
174
  on:click={() => {
 
176
  selectedCitation = citation;
177
  }}
178
  >
179
+ {#if citations.every((c) => c.distances !== undefined)}
180
  <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
181
  {idx + 1}
182
  </div>
src/lib/components/chat/Messages/ContentRenderer.svelte CHANGED
@@ -7,13 +7,16 @@
7
  import LightBlub from '$lib/components/icons/LightBlub.svelte';
8
  import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
9
  import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
 
10
 
11
  export let id;
12
  export let content;
13
  export let model = null;
 
14
 
15
  export let save = false;
16
  export let floatingButtons = true;
 
17
 
18
  let contentContainerElement;
19
  let buttonsContainerElement;
@@ -129,6 +132,32 @@
129
  {content}
130
  {model}
131
  {save}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  on:update={(e) => {
133
  dispatch('update', e.detail);
134
  }}
 
7
  import LightBlub from '$lib/components/icons/LightBlub.svelte';
8
  import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
9
  import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
10
+ import { stringify } from 'postcss';
11
 
12
  export let id;
13
  export let content;
14
  export let model = null;
15
+ export let sources = null;
16
 
17
  export let save = false;
18
  export let floatingButtons = true;
19
+ export let onSourceClick = () => {};
20
 
21
  let contentContainerElement;
22
  let buttonsContainerElement;
 
132
  {content}
133
  {model}
134
  {save}
135
+ sourceIds={(sources ?? []).reduce((acc, s) => {
136
+ let ids = [];
137
+ s.document.forEach((document, index) => {
138
+ const metadata = s.metadata?.[index];
139
+ const id = metadata?.source ?? 'N/A';
140
+
141
+ if (metadata?.name) {
142
+ ids.push(metadata.name);
143
+ return ids;
144
+ }
145
+
146
+ if (id.startsWith('http://') || id.startsWith('https://')) {
147
+ ids.push(id);
148
+ } else {
149
+ ids.push(s?.source?.name ?? id);
150
+ }
151
+
152
+ return ids;
153
+ });
154
+
155
+ acc = [...acc, ...ids];
156
+
157
+ // remove duplicates
158
+ return acc.filter((item, index) => acc.indexOf(item) === index);
159
+ }, [])}
160
+ {onSourceClick}
161
  on:update={(e) => {
162
  dispatch('update', e.detail);
163
  }}
src/lib/components/chat/Messages/Markdown.svelte CHANGED
@@ -16,6 +16,9 @@
16
  export let model = null;
17
  export let save = false;
18
 
 
 
 
19
  let tokens = [];
20
 
21
  const options = {
@@ -28,7 +31,7 @@
28
  $: (async () => {
29
  if (content) {
30
  tokens = marked.lexer(
31
- replaceTokens(processResponseContent(content), model?.name, $user?.name)
32
  );
33
  }
34
  })();
@@ -39,6 +42,7 @@
39
  {tokens}
40
  {id}
41
  {save}
 
42
  on:update={(e) => {
43
  dispatch('update', e.detail);
44
  }}
 
16
  export let model = null;
17
  export let save = false;
18
 
19
+ export let sourceIds = [];
20
+ export let onSourceClick = () => {};
21
+
22
  let tokens = [];
23
 
24
  const options = {
 
31
  $: (async () => {
32
  if (content) {
33
  tokens = marked.lexer(
34
+ replaceTokens(processResponseContent(content), sourceIds, model?.name, $user?.name)
35
  );
36
  }
37
  })();
 
42
  {tokens}
43
  {id}
44
  {save}
45
+ {onSourceClick}
46
  on:update={(e) => {
47
  dispatch('update', e.detail);
48
  }}
src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte CHANGED
@@ -12,9 +12,11 @@
12
 
13
  import Image from '$lib/components/common/Image.svelte';
14
  import KatexRenderer from './KatexRenderer.svelte';
 
15
 
16
  export let id: string;
17
  export let tokens: Token[];
 
18
  </script>
19
 
20
  {#each tokens as token}
@@ -26,13 +28,15 @@
26
  {@html html}
27
  {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
28
  {@html `${token.text}`}
 
 
29
  {:else}
30
  {token.text}
31
  {/if}
32
  {:else if token.type === 'link'}
33
  {#if token.tokens}
34
  <a href={token.href} target="_blank" rel="nofollow" title={token.title}>
35
- <svelte:self id={`${id}-a`} tokens={token.tokens} />
36
  </a>
37
  {:else}
38
  <a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
@@ -41,11 +45,11 @@
41
  <Image src={token.href} alt={token.text} />
42
  {:else if token.type === 'strong'}
43
  <strong>
44
- <svelte:self id={`${id}-strong`} tokens={token.tokens} />
45
  </strong>
46
  {:else if token.type === 'em'}
47
  <em>
48
- <svelte:self id={`${id}-em`} tokens={token.tokens} />
49
  </em>
50
  {:else if token.type === 'codespan'}
51
  <!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -61,7 +65,7 @@
61
  <br />
62
  {:else if token.type === 'del'}
63
  <del>
64
- <svelte:self id={`${id}-del`} tokens={token.tokens} />
65
  </del>
66
  {:else if token.type === 'inlineKatex'}
67
  {#if token.text}
 
12
 
13
  import Image from '$lib/components/common/Image.svelte';
14
  import KatexRenderer from './KatexRenderer.svelte';
15
+ import Source from './Source.svelte';
16
 
17
  export let id: string;
18
  export let tokens: Token[];
19
+ export let onSourceClick: Function = () => {};
20
  </script>
21
 
22
  {#each tokens as token}
 
28
  {@html html}
29
  {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
30
  {@html `${token.text}`}
31
+ {:else if token.text.includes(`<source_id`)}
32
+ <Source {token} onClick={onSourceClick} />
33
  {:else}
34
  {token.text}
35
  {/if}
36
  {:else if token.type === 'link'}
37
  {#if token.tokens}
38
  <a href={token.href} target="_blank" rel="nofollow" title={token.title}>
39
+ <svelte:self id={`${id}-a`} tokens={token.tokens} {onSourceClick} />
40
  </a>
41
  {:else}
42
  <a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
 
45
  <Image src={token.href} alt={token.text} />
46
  {:else if token.type === 'strong'}
47
  <strong>
48
+ <svelte:self id={`${id}-strong`} tokens={token.tokens} {onSourceClick} />
49
  </strong>
50
  {:else if token.type === 'em'}
51
  <em>
52
+ <svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} />
53
  </em>
54
  {:else if token.type === 'codespan'}
55
  <!-- svelte-ignore a11y-click-events-have-key-events -->
 
65
  <br />
66
  {:else if token.type === 'del'}
67
  <del>
68
+ <svelte:self id={`${id}-del`} tokens={token.tokens} {onSourceClick} />
69
  </del>
70
  {:else if token.type === 'inlineKatex'}
71
  {#if token.text}
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte CHANGED
@@ -25,6 +25,7 @@
25
  export let top = true;
26
 
27
  export let save = false;
 
28
 
29
  const headerComponent = (depth: number) => {
30
  return 'h' + depth;
@@ -62,7 +63,7 @@
62
  <hr />
63
  {:else if token.type === 'heading'}
64
  <svelte:element this={headerComponent(token.depth)}>
65
- <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
66
  </svelte:element>
67
  {:else if token.type === 'code'}
68
  {#if token.raw.includes('```')}
@@ -108,6 +109,7 @@
108
  <MarkdownInlineTokens
109
  id={`${id}-${tokenIdx}-header-${headerIdx}`}
110
  tokens={header.tokens}
 
111
  />
112
  </div>
113
  </th>
@@ -126,6 +128,7 @@
126
  <MarkdownInlineTokens
127
  id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
128
  tokens={cell.tokens}
 
129
  />
130
  </div>
131
  </td>
@@ -205,19 +208,27 @@
205
  ></iframe>
206
  {:else if token.type === 'paragraph'}
207
  <p>
208
- <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
 
 
 
 
209
  </p>
210
  {:else if token.type === 'text'}
211
  {#if top}
212
  <p>
213
  {#if token.tokens}
214
- <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
215
  {:else}
216
  {unescapeHtml(token.text)}
217
  {/if}
218
  </p>
219
  {:else if token.tokens}
220
- <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
 
 
 
 
221
  {:else}
222
  {unescapeHtml(token.text)}
223
  {/if}
 
25
  export let top = true;
26
 
27
  export let save = false;
28
+ export let onSourceClick: Function = () => {};
29
 
30
  const headerComponent = (depth: number) => {
31
  return 'h' + depth;
 
63
  <hr />
64
  {:else if token.type === 'heading'}
65
  <svelte:element this={headerComponent(token.depth)}>
66
+ <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
67
  </svelte:element>
68
  {:else if token.type === 'code'}
69
  {#if token.raw.includes('```')}
 
109
  <MarkdownInlineTokens
110
  id={`${id}-${tokenIdx}-header-${headerIdx}`}
111
  tokens={header.tokens}
112
+ {onSourceClick}
113
  />
114
  </div>
115
  </th>
 
128
  <MarkdownInlineTokens
129
  id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
130
  tokens={cell.tokens}
131
+ {onSourceClick}
132
  />
133
  </div>
134
  </td>
 
208
  ></iframe>
209
  {:else if token.type === 'paragraph'}
210
  <p>
211
+ <MarkdownInlineTokens
212
+ id={`${id}-${tokenIdx}-p`}
213
+ tokens={token.tokens ?? []}
214
+ {onSourceClick}
215
+ />
216
  </p>
217
  {:else if token.type === 'text'}
218
  {#if top}
219
  <p>
220
  {#if token.tokens}
221
+ <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
222
  {:else}
223
  {unescapeHtml(token.text)}
224
  {/if}
225
  </p>
226
  {:else if token.tokens}
227
+ <MarkdownInlineTokens
228
+ id={`${id}-${tokenIdx}-p`}
229
+ tokens={token.tokens ?? []}
230
+ {onSourceClick}
231
+ />
232
  {:else}
233
  {unescapeHtml(token.text)}
234
  {/if}
src/lib/components/chat/Messages/Markdown/Source.svelte ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let token;
3
+ export let onClick: Function = () => {};
4
+
5
+ let id = '';
6
+ function extractDataAttribute(input) {
7
+ // Use a regular expression to extract the value of the `data` attribute
8
+ const match = input.match(/data="([^"]*)"/);
9
+ // Check if a match was found and return the first captured group
10
+ return match ? match[1] : null;
11
+ }
12
+
13
+ $: id = extractDataAttribute(token.text);
14
+ </script>
15
+
16
+ <button
17
+ class="text-xs font-medium w-fit translate-y-[2px] px-2 py-0.5 dark:bg-white/5 dark:text-white/60 dark:hover:text-white bg-gray-50 text-black/60 hover:text-black transition rounded-lg"
18
+ on:click={() => {
19
+ onClick(id);
20
+ }}
21
+ >
22
+ <span class="line-clamp-1">
23
+ {id}
24
+ </span>
25
+ </button>
src/lib/components/chat/Messages/RateComment.svelte CHANGED
@@ -136,7 +136,7 @@
136
  class="size-7 text-sm border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
137
  rating
138
  ? 'bg-gray-100 dark:bg-gray-800'
139
- : ''} transition rounded-full disabled:cursor-not-allowed disabled:bg-white disabled:dark:bg-gray-900"
140
  on:click={() => {
141
  detailedRating = rating;
142
  }}
 
136
  class="size-7 text-sm border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
137
  rating
138
  ? 'bg-gray-100 dark:bg-gray-800'
139
+ : ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white disabled:dark:bg-gray-900"
140
  on:click={() => {
141
  detailedRating = rating;
142
  }}
src/lib/components/chat/Messages/ResponseMessage.svelte CHANGED
@@ -64,7 +64,7 @@
64
  };
65
  done: boolean;
66
  error?: boolean | { content: string };
67
- citations?: string[];
68
  code_executions?: {
69
  uuid: string;
70
  name: string;
@@ -621,9 +621,18 @@
621
  <ContentRenderer
622
  id={message.id}
623
  content={message.content}
 
624
  floatingButtons={message?.done}
625
  save={!readOnly}
626
  {model}
 
 
 
 
 
 
 
 
627
  on:update={(e) => {
628
  const { raw, oldContent, newContent } = e.detail;
629
 
@@ -653,8 +662,8 @@
653
  <Error content={message?.error?.content ?? message.content} />
654
  {/if}
655
 
656
- {#if message.citations && (model?.info?.meta?.capabilities?.citations ?? true)}
657
- <Citations citations={message.citations} />
658
  {/if}
659
 
660
  {#if message.code_executions}
 
64
  };
65
  done: boolean;
66
  error?: boolean | { content: string };
67
+ sources?: string[];
68
  code_executions?: {
69
  uuid: string;
70
  name: string;
 
621
  <ContentRenderer
622
  id={message.id}
623
  content={message.content}
624
+ sources={message.sources}
625
  floatingButtons={message?.done}
626
  save={!readOnly}
627
  {model}
628
+ onSourceClick={(e) => {
629
+ console.log(e);
630
+ const sourceButton = document.getElementById(`source-${e}`);
631
+
632
+ if (sourceButton) {
633
+ sourceButton.click();
634
+ }
635
+ }}
636
  on:update={(e) => {
637
  const { raw, oldContent, newContent } = e.detail;
638
 
 
662
  <Error content={message?.error?.content ?? message.content} />
663
  {/if}
664
 
665
+ {#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)}
666
+ <Citations sources={message?.sources ?? message?.citations} />
667
  {/if}
668
 
669
  {#if message.code_executions}
src/lib/components/chat/Messages/UserMessage.svelte CHANGED
@@ -5,11 +5,7 @@
5
 
6
  import { models, settings } from '$lib/stores';
7
  import { user as _user } from '$lib/stores';
8
- import {
9
- copyToClipboard as _copyToClipboard,
10
- processResponseContent,
11
- replaceTokens
12
- } from '$lib/utils';
13
 
14
  import Name from './Name.svelte';
15
  import ProfileImage from './ProfileImage.svelte';
 
5
 
6
  import { models, settings } from '$lib/stores';
7
  import { user as _user } from '$lib/stores';
8
+ import { copyToClipboard as _copyToClipboard } from '$lib/utils';
 
 
 
 
9
 
10
  import Name from './Name.svelte';
11
  import ProfileImage from './ProfileImage.svelte';
src/lib/components/common/RichTextInput.svelte CHANGED
@@ -1,241 +1,46 @@
1
  <script lang="ts">
2
- import { onDestroy, onMount } from 'svelte';
 
 
 
 
3
  import { createEventDispatcher } from 'svelte';
4
  const eventDispatch = createEventDispatcher();
5
 
6
  import { EditorState, Plugin, TextSelection } from 'prosemirror-state';
7
- import { EditorView, Decoration, DecorationSet } from 'prosemirror-view';
8
- import { undo, redo, history } from 'prosemirror-history';
9
- import {
10
- schema,
11
- defaultMarkdownParser,
12
- MarkdownParser,
13
- defaultMarkdownSerializer
14
- } from 'prosemirror-markdown';
15
-
16
- import {
17
- inputRules,
18
- wrappingInputRule,
19
- textblockTypeInputRule,
20
- InputRule
21
- } from 'prosemirror-inputrules'; // Import input rules
22
- import { splitListItem, liftListItem, sinkListItem } from 'prosemirror-schema-list'; // Import from prosemirror-schema-list
23
- import { keymap } from 'prosemirror-keymap';
24
- import { baseKeymap, chainCommands } from 'prosemirror-commands';
25
- import { DOMParser, DOMSerializer, Schema, Fragment } from 'prosemirror-model';
26
- import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
27
-
28
- export let className = 'input-prose';
29
- export let shiftEnter = false;
30
- export let largeTextAsFile = false;
31
-
32
- export let id = '';
33
- export let value = '';
34
- export let placeholder = 'Type here...';
35
- export let trim = false;
36
-
37
- let element: HTMLElement; // Element where ProseMirror will attach
38
- let state;
39
- let view;
40
-
41
- // Plugin to add placeholder when the content is empty
42
- function placeholderPlugin(placeholder: string) {
43
- return new Plugin({
44
- props: {
45
- decorations(state) {
46
- const doc = state.doc;
47
- if (
48
- doc.childCount === 1 &&
49
- doc.firstChild.isTextblock &&
50
- doc.firstChild?.textContent === ''
51
- ) {
52
- // If there's nothing in the editor, show the placeholder decoration
53
- const decoration = Decoration.node(0, doc.content.size, {
54
- 'data-placeholder': placeholder,
55
- class: 'placeholder'
56
- });
57
- return DecorationSet.create(doc, [decoration]);
58
- }
59
- return DecorationSet.empty;
60
- }
61
- }
62
- });
63
- }
64
-
65
- function unescapeMarkdown(text: string): string {
66
- return text
67
- .replace(/\\([\\`*{}[\]()#+\-.!_>])/g, '$1') // unescape backslashed characters
68
- .replace(/&amp;/g, '&')
69
- .replace(/</g, '<')
70
- .replace(/>/g, '>')
71
- .replace(/&quot;/g, '"')
72
- .replace(/&#39;/g, "'");
73
- }
74
-
75
- // Custom parsing rule that creates proper paragraphs for newlines and empty lines
76
- function markdownToProseMirrorDoc(markdown: string) {
77
- // Split the markdown into lines
78
- const lines = markdown.split('\n\n');
79
-
80
- // Create an array to hold our paragraph nodes
81
- const paragraphs = [];
82
-
83
- // Process each line
84
- lines.forEach((line) => {
85
- if (line.trim() === '') {
86
- // For empty lines, create an empty paragraph
87
- paragraphs.push(schema.nodes.paragraph.create());
88
- } else {
89
- // For non-empty lines, parse as usual
90
- const doc = defaultMarkdownParser.parse(line);
91
- // Extract the content of the parsed document
92
- doc.content.forEach((node) => {
93
- paragraphs.push(node);
94
- });
95
- }
96
- });
97
-
98
- // Create a new document with these paragraphs
99
- return schema.node('doc', null, paragraphs);
100
- }
101
-
102
- // Create a custom serializer for paragraphs
103
- // Custom paragraph serializer to preserve newlines for empty paragraphs (empty block).
104
- function serializeParagraph(state, node: Node) {
105
- const content = node.textContent.trim();
106
-
107
- // If the paragraph is empty, just add an empty line.
108
- if (content === '') {
109
- state.write('\n\n');
110
- } else {
111
- state.renderInline(node);
112
- state.closeBlock(node);
113
- }
114
- }
115
-
116
- const customMarkdownSerializer = new defaultMarkdownSerializer.constructor(
117
- {
118
- ...defaultMarkdownSerializer.nodes,
119
-
120
- paragraph: (state, node) => {
121
- serializeParagraph(state, node); // Use custom paragraph serialization
122
- }
123
-
124
- // Customize other block formats if needed
125
- },
126
-
127
- // Copy marks directly from the original serializer (or customize them if necessary)
128
- defaultMarkdownSerializer.marks
129
- );
130
-
131
- // Utility function to convert ProseMirror content back to markdown text
132
- function serializeEditorContent(doc) {
133
- const markdown = customMarkdownSerializer.serialize(doc);
134
- if (trim) {
135
- return unescapeMarkdown(markdown).trim();
136
- } else {
137
- return unescapeMarkdown(markdown);
138
- }
139
- }
140
-
141
- // ---- Input Rules ----
142
- // Input rule for heading (e.g., # Headings)
143
- function headingRule(schema) {
144
- return textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, (match) => ({
145
- level: match[1].length
146
- }));
147
- }
148
-
149
- // Input rule for bullet list (e.g., `- item`)
150
- function bulletListRule(schema) {
151
- return wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list);
152
- }
153
-
154
- // Input rule for ordered list (e.g., `1. item`)
155
- function orderedListRule(schema) {
156
- return wrappingInputRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, (match) => ({
157
- order: +match[1]
158
- }));
159
- }
160
-
161
- // Custom input rules for Bold/Italic (using * or _)
162
- function markInputRule(regexp: RegExp, markType: any) {
163
- return new InputRule(regexp, (state, match, start, end) => {
164
- const { tr } = state;
165
- if (match) {
166
- tr.replaceWith(start, end, schema.text(match[1], [markType.create()]));
167
- }
168
- return tr;
169
- });
170
- }
171
-
172
- function boldRule(schema) {
173
- return markInputRule(/(?<=^|\s)\*([^*]+)\*(?=\s|$)/, schema.marks.strong);
174
- }
175
 
176
- function italicRule(schema) {
177
- // Using lookbehind and lookahead to prevent the space from being consumed
178
- return markInputRule(/(?<=^|\s)_([^*_]+)_(?=\s|$)/, schema.marks.em);
179
- }
180
-
181
- // Initialize Editor State and View
182
- function afterSpacePress(state, dispatch) {
183
- // Get the position right after the space was naturally inserted by the browser.
184
- let { from, to, empty } = state.selection;
185
-
186
- if (dispatch && empty) {
187
- let tr = state.tr;
188
 
189
- // Check for any active marks at `from - 1` (the space we just inserted)
190
- const storedMarks = state.storedMarks || state.selection.$from.marks();
 
 
 
191
 
192
- const hasBold = storedMarks.some((mark) => mark.type === state.schema.marks.strong);
193
- const hasItalic = storedMarks.some((mark) => mark.type === state.schema.marks.em);
194
 
195
- // Remove marks from the space character (marks applied to the space character will be marked as false)
196
- if (hasBold) {
197
- tr = tr.removeMark(from - 1, from, state.schema.marks.strong);
198
- }
199
- if (hasItalic) {
200
- tr = tr.removeMark(from - 1, from, state.schema.marks.em);
201
- }
202
-
203
- // Dispatch the resulting transaction to update the editor state
204
- dispatch(tr);
205
- }
206
 
207
- return true;
208
- }
209
 
210
- function toggleMark(markType) {
211
- return (state, dispatch) => {
212
- const { from, to } = state.selection;
213
- if (state.doc.rangeHasMark(from, to, markType)) {
214
- if (dispatch) dispatch(state.tr.removeMark(from, to, markType));
215
- return true;
216
- } else {
217
- if (dispatch) dispatch(state.tr.addMark(from, to, markType.create()));
218
- return true;
219
- }
220
- };
221
- }
222
 
223
- function isInList(state) {
224
- const { $from } = state.selection;
225
- return (
226
- $from.parent.type === schema.nodes.paragraph && $from.node(-1).type === schema.nodes.list_item
227
- );
228
- }
229
 
230
- function isEmptyListItem(state) {
231
- const { $from } = state.selection;
232
- return isInList(state) && $from.parent.content.size === 0 && $from.node(-1).childCount === 1;
233
- }
234
 
235
- function exitList(state, dispatch) {
236
- return liftListItem(schema.nodes.list_item)(state, dispatch);
237
- }
238
 
 
239
  function findNextTemplate(doc, from = 0) {
240
  const patterns = [
241
  { start: '[', end: ']' },
@@ -270,6 +75,7 @@
270
  return result;
271
  }
272
 
 
273
  function selectNextTemplate(state, dispatch) {
274
  const { doc, selection } = state;
275
  const from = selection.to;
@@ -290,220 +96,203 @@
290
  return false;
291
  }
292
 
293
- // Replace tabs with four spaces
294
- function handleTabIndentation(text: string): string {
295
- // Replace each tab character with four spaces
296
- return text.replace(/\t/g, ' ');
297
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
- onMount(() => {
300
- const initialDoc = markdownToProseMirrorDoc(value || ''); // Convert the initial content
301
-
302
- state = EditorState.create({
303
- doc: initialDoc,
304
- schema,
305
- plugins: [
306
- history(),
307
- placeholderPlugin(placeholder),
308
- inputRules({
309
- rules: [
310
- headingRule(schema), // Handle markdown-style headings (# H1, ## H2, etc.)
311
- bulletListRule(schema), // Handle `-` or `*` input to start bullet list
312
- orderedListRule(schema), // Handle `1.` input to start ordered list
313
- boldRule(schema), // Bold input rule
314
- italicRule(schema) // Italic input rule
315
- ]
316
  }),
317
- keymap({
318
- ...baseKeymap,
319
- 'Mod-z': undo,
320
- 'Mod-y': redo,
321
- Enter: (state, dispatch, view) => {
322
- if (shiftEnter) {
323
- eventDispatch('enter');
324
- return true;
325
- }
326
- return chainCommands(
327
- (state, dispatch, view) => {
328
- if (isEmptyListItem(state)) {
329
- return exitList(state, dispatch);
330
- }
331
- return false;
332
- },
333
- (state, dispatch, view) => {
334
- if (isInList(state)) {
335
- return splitListItem(schema.nodes.list_item)(state, dispatch);
336
- }
337
- return false;
338
- },
339
- baseKeymap.Enter
340
- )(state, dispatch, view);
341
  },
342
-
343
- 'Shift-Enter': (state, dispatch, view) => {
344
- if (shiftEnter) {
345
- return chainCommands(
346
- (state, dispatch, view) => {
347
- if (isEmptyListItem(state)) {
348
- return exitList(state, dispatch);
349
- }
350
- return false;
351
- },
352
- (state, dispatch, view) => {
353
- if (isInList(state)) {
354
- return splitListItem(schema.nodes.list_item)(state, dispatch);
355
- }
356
- return false;
357
- },
358
- baseKeymap.Enter
359
- )(state, dispatch, view);
360
- } else {
361
- return baseKeymap.Enter(state, dispatch, view);
362
- }
363
  return false;
364
  },
365
 
366
- // Prevent default tab navigation and provide indent/outdent behavior inside lists:
367
- Tab: chainCommands((state, dispatch, view) => {
368
- const { $from } = state.selection;
369
- if (isInList(state)) {
370
- return sinkListItem(schema.nodes.list_item)(state, dispatch);
371
- } else {
372
- return selectNextTemplate(state, dispatch);
373
- }
374
- return true; // Prevent Tab from moving the focus
375
- }),
376
- 'Shift-Tab': (state, dispatch, view) => {
377
- const { $from } = state.selection;
378
- if (isInList(state)) {
379
- return liftListItem(schema.nodes.list_item)(state, dispatch);
380
  }
381
- return true; // Prevent Shift-Tab from moving the focus
382
- },
383
- 'Mod-b': toggleMark(schema.marks.strong),
384
- 'Mod-i': toggleMark(schema.marks.em)
385
- })
386
- ]
387
- });
388
 
389
- view = new EditorView(element, {
390
- state,
391
- dispatchTransaction(transaction) {
392
- // Update editor state
393
- let newState = view.state.apply(transaction);
394
- view.updateState(newState);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
- value = serializeEditorContent(newState.doc); // Convert ProseMirror content to markdown text
397
- eventDispatch('input', { value });
398
- },
399
- handleDOMEvents: {
400
- focus: (view, event) => {
401
- eventDispatch('focus', { event });
402
- return false;
403
- },
404
- keypress: (view, event) => {
405
- eventDispatch('keypress', { event });
406
- return false;
407
- },
408
- keydown: (view, event) => {
409
- eventDispatch('keydown', { event });
410
- return false;
411
- },
412
- paste: (view, event) => {
413
- if (event.clipboardData) {
414
- // Extract plain text from clipboard and paste it without formatting
415
- const plainText = event.clipboardData.getData('text/plain');
416
- if (plainText) {
417
- if (largeTextAsFile) {
418
- if (plainText.length > PASTED_TEXT_CHARACTER_LIMIT) {
419
- // Dispatch paste event to parent component
420
- eventDispatch('paste', { event });
421
  event.preventDefault();
422
  return true;
423
  }
424
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- const modifiedText = handleTabIndentation(plainText);
427
- console.log(modifiedText);
428
-
429
- // Replace the current selection with the plain text content
430
- const tr = view.state.tr.replaceSelectionWith(
431
- view.state.schema.text(modifiedText),
432
- false
433
  );
434
- view.dispatch(tr.scrollIntoView());
435
- event.preventDefault(); // Prevent the default paste behavior
436
- return true;
437
- }
438
 
439
- // Check if the pasted content contains image files
440
- const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
441
- file.type.startsWith('image/')
442
- );
443
-
444
- // Check for image in dataTransfer items (for cases where files are not available)
445
- const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
446
- item.type.startsWith('image/')
447
- );
448
- if (hasImageFile) {
449
- // If there's an image, dispatch the event to the parent
450
- eventDispatch('paste', { event });
451
- event.preventDefault();
452
- return true;
453
- }
454
 
455
- if (hasImageItem) {
456
- // If there's an image item, dispatch the event to the parent
457
- eventDispatch('paste', { event });
458
- event.preventDefault();
459
- return true;
 
460
  }
461
- }
462
 
463
- // For all other cases (text, formatted text, etc.), let ProseMirror handle it
464
- return false;
465
- },
466
- // Handle space input after browser has completed it
467
- keyup: (view, event) => {
468
- if (event.key === ' ' && event.code === 'Space') {
469
- afterSpacePress(view.state, view.dispatch);
470
  }
471
- return false;
472
  }
473
- },
474
- attributes: { id }
475
- });
476
- });
477
-
478
- // Reinitialize the editor if the value is externally changed (i.e. when `value` is updated)
479
- $: if (view && value !== serializeEditorContent(view.state.doc)) {
480
- const newDoc = markdownToProseMirrorDoc(value || '');
481
-
482
- const newState = EditorState.create({
483
- doc: newDoc,
484
- schema,
485
- plugins: view.state.plugins,
486
- selection: TextSelection.atEnd(newDoc) // This sets the cursor at the end
487
  });
488
- view.updateState(newState);
489
 
490
- if (value !== '') {
491
- // After updating the state, try to find and select the next template
492
- setTimeout(() => {
493
- const templateFound = selectNextTemplate(view.state, view.dispatch);
494
- if (!templateFound) {
495
- // If no template found, set cursor at the end
496
- const endPos = view.state.doc.content.size;
497
- view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, endPos)));
498
- }
499
- }, 0);
500
- }
501
- }
502
 
503
- // Destroy ProseMirror instance on unmount
504
  onDestroy(() => {
505
- view?.destroy();
 
 
506
  });
 
 
 
 
 
 
507
  </script>
508
 
509
- <div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}"></div>
 
1
  <script lang="ts">
2
+ import { marked } from 'marked';
3
+ import TurndownService from 'turndown';
4
+ const turndownService = new TurndownService();
5
+
6
+ import { onMount, onDestroy } from 'svelte';
7
  import { createEventDispatcher } from 'svelte';
8
  const eventDispatch = createEventDispatcher();
9
 
10
  import { EditorState, Plugin, TextSelection } from 'prosemirror-state';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ import { Editor } from '@tiptap/core';
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
15
+ import Placeholder from '@tiptap/extension-placeholder';
16
+ import Highlight from '@tiptap/extension-highlight';
17
+ import Typography from '@tiptap/extension-typography';
18
+ import StarterKit from '@tiptap/starter-kit';
19
 
20
+ import { all, createLowlight } from 'lowlight';
 
21
 
22
+ import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
 
 
 
 
 
 
 
 
 
 
23
 
24
+ // create a lowlight instance with all languages loaded
25
+ const lowlight = createLowlight(all);
26
 
27
+ export let className = 'input-prose';
28
+ export let placeholder = 'Type here...';
29
+ export let value = '';
30
+ export let id = '';
 
 
 
 
 
 
 
 
31
 
32
+ export let messageInput = false;
33
+ export let shiftEnter = false;
34
+ export let largeTextAsFile = false;
 
 
 
35
 
36
+ let element;
37
+ let editor;
 
 
38
 
39
+ const options = {
40
+ throwOnError: false
41
+ };
42
 
43
+ // Function to find the next template in the document
44
  function findNextTemplate(doc, from = 0) {
45
  const patterns = [
46
  { start: '[', end: ']' },
 
75
  return result;
76
  }
77
 
78
+ // Function to select the next template in the document
79
  function selectNextTemplate(state, dispatch) {
80
  const { doc, selection } = state;
81
  const from = selection.to;
 
96
  return false;
97
  }
98
 
99
+ export const setContent = (content) => {
100
+ editor.commands.setContent(content);
101
+ };
102
+
103
+ const selectTemplate = () => {
104
+ if (value !== '') {
105
+ // After updating the state, try to find and select the next template
106
+ setTimeout(() => {
107
+ const templateFound = selectNextTemplate(editor.view.state, editor.view.dispatch);
108
+ if (!templateFound) {
109
+ // If no template found, set cursor at the end
110
+ const endPos = editor.view.state.doc.content.size;
111
+ editor.view.dispatch(
112
+ editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, endPos))
113
+ );
114
+ }
115
+ }, 0);
116
+ }
117
+ };
118
+
119
+ onMount(async () => {
120
+ async function tryParse(value, attempts = 3, interval = 100) {
121
+ try {
122
+ // Try parsing the value
123
+ return marked.parse(value);
124
+ } catch (error) {
125
+ // If no attempts remain, fallback to plain text
126
+ if (attempts <= 1) {
127
+ return value;
128
+ }
129
+ // Wait for the interval, then retry
130
+ await new Promise((resolve) => setTimeout(resolve, interval));
131
+ return tryParse(value, attempts - 1, interval); // Recursive call
132
+ }
133
+ }
134
+
135
+ // Usage example
136
+ let content = await tryParse(value);
137
 
138
+ editor = new Editor({
139
+ element: element,
140
+ extensions: [
141
+ StarterKit,
142
+ CodeBlockLowlight.configure({
143
+ lowlight
 
 
 
 
 
 
 
 
 
 
 
144
  }),
145
+ Highlight,
146
+ Typography,
147
+ Placeholder.configure({ placeholder })
148
+ ],
149
+ content: content,
150
+ autofocus: true,
151
+ onTransaction: () => {
152
+ // force re-render so `editor.isActive` works as expected
153
+ editor = editor;
154
+
155
+ const newValue = turndownService.turndown(editor.getHTML());
156
+ if (value !== newValue) {
157
+ value = newValue; // Trigger parent updates
158
+ }
159
+ },
160
+ editorProps: {
161
+ attributes: { id },
162
+ handleDOMEvents: {
163
+ focus: (view, event) => {
164
+ eventDispatch('focus', { event });
165
+ return false;
 
 
 
166
  },
167
+ keypress: (view, event) => {
168
+ eventDispatch('keypress', { event });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  return false;
170
  },
171
 
172
+ keydown: (view, event) => {
173
+ // Handle Tab Key
174
+ if (event.key === 'Tab') {
175
+ const handled = selectNextTemplate(view.state, view.dispatch);
176
+ if (handled) {
177
+ event.preventDefault();
178
+ return true;
179
+ }
 
 
 
 
 
 
180
  }
 
 
 
 
 
 
 
181
 
182
+ if (messageInput) {
183
+ if (event.key === 'Enter') {
184
+ // Check if the current selection is inside a structured block (like codeBlock or list)
185
+ const { state } = view;
186
+ const { $head } = state.selection;
187
+
188
+ // Recursive function to check ancestors for specific node types
189
+ function isInside(nodeTypes: string[]): boolean {
190
+ let currentNode = $head;
191
+ while (currentNode) {
192
+ if (nodeTypes.includes(currentNode.parent.type.name)) {
193
+ return true;
194
+ }
195
+ if (!currentNode.depth) break; // Stop if we reach the top
196
+ currentNode = state.doc.resolve(currentNode.before()); // Move to the parent node
197
+ }
198
+ return false;
199
+ }
200
+
201
+ const isInCodeBlock = isInside(['codeBlock']);
202
+ const isInList = isInside(['listItem', 'bulletList', 'orderedList']);
203
+ const isInHeading = isInside(['heading']);
204
 
205
+ if (isInCodeBlock || isInList || isInHeading) {
206
+ // Let ProseMirror handle the normal Enter behavior
207
+ return false;
208
+ }
209
+ }
210
+
211
+ // Handle shift + Enter for a line break
212
+ if (shiftEnter) {
213
+ if (event.key === 'Enter' && event.shiftKey) {
214
+ editor.commands.setHardBreak(); // Insert a hard break
215
+ view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor
216
+ event.preventDefault();
217
+ return true;
218
+ }
219
+ if (event.key === 'Enter') {
220
+ eventDispatch('enter', { event });
 
 
 
 
 
 
 
 
 
221
  event.preventDefault();
222
  return true;
223
  }
224
  }
225
+ if (event.key === 'Enter') {
226
+ eventDispatch('enter', { event });
227
+ event.preventDefault();
228
+ return true;
229
+ }
230
+ }
231
+ eventDispatch('keydown', { event });
232
+ return false;
233
+ },
234
+ paste: (view, event) => {
235
+ if (event.clipboardData) {
236
+ // Extract plain text from clipboard and paste it without formatting
237
+ const plainText = event.clipboardData.getData('text/plain');
238
+ if (plainText) {
239
+ if (largeTextAsFile) {
240
+ if (plainText.length > PASTED_TEXT_CHARACTER_LIMIT) {
241
+ // Dispatch paste event to parent component
242
+ eventDispatch('paste', { event });
243
+ event.preventDefault();
244
+ return true;
245
+ }
246
+ }
247
+ return false;
248
+ }
249
 
250
+ // Check if the pasted content contains image files
251
+ const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
252
+ file.type.startsWith('image/')
 
 
 
 
253
  );
 
 
 
 
254
 
255
+ // Check for image in dataTransfer items (for cases where files are not available)
256
+ const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
257
+ item.type.startsWith('image/')
258
+ );
259
+ if (hasImageFile) {
260
+ // If there's an image, dispatch the event to the parent
261
+ eventDispatch('paste', { event });
262
+ event.preventDefault();
263
+ return true;
264
+ }
 
 
 
 
 
265
 
266
+ if (hasImageItem) {
267
+ // If there's an image item, dispatch the event to the parent
268
+ eventDispatch('paste', { event });
269
+ event.preventDefault();
270
+ return true;
271
+ }
272
  }
 
273
 
274
+ // For all other cases (text, formatted text, etc.), let ProseMirror handle it
275
+ view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor after pasting
276
+ return false;
 
 
 
 
277
  }
 
278
  }
279
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  });
 
281
 
282
+ selectTemplate();
283
+ });
 
 
 
 
 
 
 
 
 
 
284
 
 
285
  onDestroy(() => {
286
+ if (editor) {
287
+ editor.destroy();
288
+ }
289
  });
290
+
291
+ // Update the editor content if the external `value` changes
292
+ $: if (editor && value !== turndownService.turndown(editor.getHTML())) {
293
+ editor.commands.setContent(marked.parse(value)); // Update editor content
294
+ selectTemplate();
295
+ }
296
  </script>
297
 
298
+ <div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}" />
src/lib/components/workspace/Knowledge/KnowledgeBase.svelte CHANGED
@@ -68,7 +68,7 @@
68
  let inputFiles = null;
69
 
70
  let filteredItems = [];
71
- $: if (knowledge) {
72
  fuse = new Fuse(knowledge.files, {
73
  keys: ['meta.name', 'meta.description']
74
  });
 
68
  let inputFiles = null;
69
 
70
  let filteredItems = [];
71
+ $: if (knowledge && knowledge.files) {
72
  fuse = new Fuse(knowledge.files, {
73
  keys: ['meta.name', 'meta.description']
74
  });
src/lib/i18n/locales/ar-BH/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "أدخل كود اللغة",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
 
332
  "Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "محتوى الملف النموذجي",
570
  "Models": "الموديلات",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "المزيد",
574
  "Name": "الأسم",
 
329
  "Enter language codes": "أدخل كود اللغة",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "محتوى الملف النموذجي",
571
  "Models": "الموديلات",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "المزيد",
576
  "Name": "الأسم",
src/lib/i18n/locales/bg-BG/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Въведете кодове на езика",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Съдържание на модфайл",
570
  "Models": "Модели",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "Повече",
574
  "Name": "Име",
 
329
  "Enter language codes": "Въведете кодове на езика",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "Съдържание на модфайл",
571
  "Models": "Модели",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "Повече",
576
  "Name": "Име",
src/lib/i18n/locales/bn-BD/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "ল্যাঙ্গুয়েজ কোড লিখুন",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "মডেলফাইল কনটেন্ট",
570
  "Models": "মডেলসমূহ",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "আরো",
574
  "Name": "নাম",
 
329
  "Enter language codes": "ল্যাঙ্গুয়েজ কোড লিখুন",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "মডেলফাইল কনটেন্ট",
571
  "Models": "মডেলসমূহ",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "আরো",
576
  "Name": "নাম",
src/lib/i18n/locales/ca-ES/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Introdueix els codis de llenguatge",
330
  "Enter Model ID": "Introdueix l'identificador del model",
331
  "Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Introdueix el nombre de passos (p. ex. 50)",
333
  "Enter Sampler (e.g. Euler a)": "Introdueix el mostrejador (p.ex. Euler a)",
334
  "Enter Scheduler (e.g. Karras)": "Entra el programador (p.ex. Karras)",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Contingut del Modelfile",
570
  "Models": "Models",
571
  "Models Access": "",
 
572
  "more": "més",
573
  "More": "Més",
574
  "Name": "Nom",
 
329
  "Enter language codes": "Introdueix els codis de llenguatge",
330
  "Enter Model ID": "Introdueix l'identificador del model",
331
  "Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Introdueix el nombre de passos (p. ex. 50)",
334
  "Enter Sampler (e.g. Euler a)": "Introdueix el mostrejador (p.ex. Euler a)",
335
  "Enter Scheduler (e.g. Karras)": "Entra el programador (p.ex. Karras)",
 
570
  "Modelfile Content": "Contingut del Modelfile",
571
  "Models": "Models",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "més",
575
  "More": "Més",
576
  "Name": "Nom",
src/lib/i18n/locales/ceb-PH/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Pagsulod sa template tag (e.g. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Pagsulod sa gidaghanon sa mga lakang (e.g. 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Mga sulod sa template file",
570
  "Models": "Mga modelo",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "",
574
  "Name": "Ngalan",
 
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Pagsulod sa template tag (e.g. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Pagsulod sa gidaghanon sa mga lakang (e.g. 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "Mga sulod sa template file",
571
  "Models": "Mga modelo",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "",
576
  "Name": "Ngalan",
src/lib/i18n/locales/cs-CZ/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Zadejte kódy jazyků",
330
  "Enter Model ID": "Zadejte ID modelu",
331
  "Enter model tag (e.g. {{modelTag}})": "Zadejte označení modelu (např. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Zadejte počet kroků (např. 50)",
333
  "Enter Sampler (e.g. Euler a)": "Zadejte vzorkovač (např. Euler a)",
334
  "Enter Scheduler (e.g. Karras)": "Zadejte plánovač (např. Karras)",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Obsah souboru modelfile",
570
  "Models": "Modely",
571
  "Models Access": "",
 
572
  "more": "více",
573
  "More": "Více",
574
  "Name": "Jméno",
 
329
  "Enter language codes": "Zadejte kódy jazyků",
330
  "Enter Model ID": "Zadejte ID modelu",
331
  "Enter model tag (e.g. {{modelTag}})": "Zadejte označení modelu (např. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Zadejte počet kroků (např. 50)",
334
  "Enter Sampler (e.g. Euler a)": "Zadejte vzorkovač (např. Euler a)",
335
  "Enter Scheduler (e.g. Karras)": "Zadejte plánovač (např. Karras)",
 
570
  "Modelfile Content": "Obsah souboru modelfile",
571
  "Models": "Modely",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "více",
575
  "More": "Více",
576
  "Name": "Jméno",
src/lib/i18n/locales/da-DK/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Indtast sprogkoder",
330
  "Enter Model ID": "Indtast model-ID",
331
  "Enter model tag (e.g. {{modelTag}})": "Indtast modelmærke (f.eks. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Indtast antal trin (f.eks. 50)",
333
  "Enter Sampler (e.g. Euler a)": "Indtast sampler (f.eks. Euler a)",
334
  "Enter Scheduler (e.g. Karras)": "Indtast scheduler (f.eks. Karras)",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Modelfilindhold",
570
  "Models": "Modeller",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "Mere",
574
  "Name": "Navn",
 
329
  "Enter language codes": "Indtast sprogkoder",
330
  "Enter Model ID": "Indtast model-ID",
331
  "Enter model tag (e.g. {{modelTag}})": "Indtast modelmærke (f.eks. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Indtast antal trin (f.eks. 50)",
334
  "Enter Sampler (e.g. Euler a)": "Indtast sampler (f.eks. Euler a)",
335
  "Enter Scheduler (e.g. Karras)": "Indtast scheduler (f.eks. Karras)",
 
570
  "Modelfile Content": "Modelfilindhold",
571
  "Models": "Modeller",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "Mere",
576
  "Name": "Navn",
src/lib/i18n/locales/de-DE/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Geben Sie die Sprachcodes ein",
330
  "Enter Model ID": "Geben Sie die Modell-ID ein",
331
  "Enter model tag (e.g. {{modelTag}})": "Gebn Sie den Model-Tag ein",
 
332
  "Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)",
333
  "Enter Sampler (e.g. Euler a)": "Geben Sie den Sampler ein (z. B. Euler a)",
334
  "Enter Scheduler (e.g. Karras)": "Geben Sie den Scheduler ein (z. B. Karras)",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Modelfile-Inhalt",
570
  "Models": "Modelle",
571
  "Models Access": "",
 
572
  "more": "mehr",
573
  "More": "Mehr",
574
  "Name": "Name",
 
329
  "Enter language codes": "Geben Sie die Sprachcodes ein",
330
  "Enter Model ID": "Geben Sie die Modell-ID ein",
331
  "Enter model tag (e.g. {{modelTag}})": "Gebn Sie den Model-Tag ein",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)",
334
  "Enter Sampler (e.g. Euler a)": "Geben Sie den Sampler ein (z. B. Euler a)",
335
  "Enter Scheduler (e.g. Karras)": "Geben Sie den Scheduler ein (z. B. Karras)",
 
570
  "Modelfile Content": "Modelfile-Inhalt",
571
  "Models": "Modelle",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "mehr",
575
  "More": "Mehr",
576
  "Name": "Name",
src/lib/i18n/locales/dg-DG/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Enter model doge tag (e.g. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Enter Number of Steps (e.g. 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Modelfile Content",
570
  "Models": "Wowdels",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "",
574
  "Name": "Name",
 
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Enter model doge tag (e.g. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Enter Number of Steps (e.g. 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "Modelfile Content",
571
  "Models": "Wowdels",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "",
576
  "Name": "Name",
src/lib/i18n/locales/en-GB/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "",
 
332
  "Enter Number of Steps (e.g. 50)": "",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "",
570
  "Models": "",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "",
574
  "Name": "",
 
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "",
571
  "Models": "",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "",
576
  "Name": "",
src/lib/i18n/locales/en-US/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "",
 
332
  "Enter Number of Steps (e.g. 50)": "",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "",
570
  "Models": "",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "",
574
  "Name": "",
 
329
  "Enter language codes": "",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "",
571
  "Models": "",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "",
576
  "Name": "",
src/lib/i18n/locales/es-ES/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Ingrese códigos de idioma",
330
  "Enter Model ID": "Ingresa el ID del modelo",
331
  "Enter model tag (e.g. {{modelTag}})": "Ingrese la etiqueta del modelo (p.ej. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Ingrese el número de pasos (p.ej., 50)",
333
  "Enter Sampler (e.g. Euler a)": "Ingrese el sampler (p.ej., Euler a)",
334
  "Enter Scheduler (e.g. Karras)": "Ingrese el planificador (p.ej., Karras)",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Contenido del Modelfile",
570
  "Models": "Modelos",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "Más",
574
  "Name": "Nombre",
 
329
  "Enter language codes": "Ingrese códigos de idioma",
330
  "Enter Model ID": "Ingresa el ID del modelo",
331
  "Enter model tag (e.g. {{modelTag}})": "Ingrese la etiqueta del modelo (p.ej. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Ingrese el número de pasos (p.ej., 50)",
334
  "Enter Sampler (e.g. Euler a)": "Ingrese el sampler (p.ej., Euler a)",
335
  "Enter Scheduler (e.g. Karras)": "Ingrese el planificador (p.ej., Karras)",
 
570
  "Modelfile Content": "Contenido del Modelfile",
571
  "Models": "Modelos",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "Más",
576
  "Name": "Nombre",
src/lib/i18n/locales/fa-IR/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "کد زبان را وارد کنید",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "تگ مدل را وارد کنید (مثلا {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "تعداد گام ها را وارد کنید (مثال: 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "محتویات فایل مدل",
570
  "Models": "مدل\u200cها",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "بیشتر",
574
  "Name": "نام",
 
329
  "Enter language codes": "کد زبان را وارد کنید",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "تگ مدل را وارد کنید (مثلا {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "تعداد گام ها را وارد کنید (مثال: 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "محتویات فایل مدل",
571
  "Models": "مدل\u200cها",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "بیشتر",
576
  "Name": "نام",
src/lib/i18n/locales/fi-FI/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Syötä kielikoodit",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Syötä mallitagi (esim. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Syötä askelien määrä (esim. 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Mallitiedoston sisältö",
570
  "Models": "Mallit",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "Lisää",
574
  "Name": "Nimi",
 
329
  "Enter language codes": "Syötä kielikoodit",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Syötä mallitagi (esim. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Syötä askelien määrä (esim. 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "Mallitiedoston sisältö",
571
  "Models": "Mallit",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "Lisää",
576
  "Name": "Nimi",
src/lib/i18n/locales/fr-CA/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Entrez les codes de langue",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Entrez l'étiquette du modèle (par ex. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Entrez le nombre de pas (par ex. 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Contenu du Fichier de Modèle",
570
  "Models": "Modèles",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "Plus de",
574
  "Name": "Nom",
 
329
  "Enter language codes": "Entrez les codes de langue",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Entrez l'étiquette du modèle (par ex. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Entrez le nombre de pas (par ex. 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "Contenu du Fichier de Modèle",
571
  "Models": "Modèles",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "Plus de",
576
  "Name": "Nom",
src/lib/i18n/locales/fr-FR/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "Entrez les codes de langue",
330
  "Enter Model ID": "Entrez l'ID du modèle",
331
  "Enter model tag (e.g. {{modelTag}})": "Entrez le tag du modèle (par ex. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "Entrez le nombre d'étapes (par ex. 50)",
333
  "Enter Sampler (e.g. Euler a)": "Entrez le sampler (par ex. Euler a)",
334
  "Enter Scheduler (e.g. Karras)": "Entrez le planificateur (par ex. Karras)",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "Contenu du Fichier de Modèle",
570
  "Models": "Modèles",
571
  "Models Access": "",
 
572
  "more": "plus",
573
  "More": "Plus",
574
  "Name": "Nom d'utilisateur",
 
329
  "Enter language codes": "Entrez les codes de langue",
330
  "Enter Model ID": "Entrez l'ID du modèle",
331
  "Enter model tag (e.g. {{modelTag}})": "Entrez le tag du modèle (par ex. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "Entrez le nombre d'étapes (par ex. 50)",
334
  "Enter Sampler (e.g. Euler a)": "Entrez le sampler (par ex. Euler a)",
335
  "Enter Scheduler (e.g. Karras)": "Entrez le planificateur (par ex. Karras)",
 
570
  "Modelfile Content": "Contenu du Fichier de Modèle",
571
  "Models": "Modèles",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "plus",
575
  "More": "Plus",
576
  "Name": "Nom d'utilisateur",
src/lib/i18n/locales/he-IL/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "הזן קודי שפה",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "הזן תג מודל (למשל {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "הזן מספר שלבים (למשל 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "תוכן קובץ מודל",
570
  "Models": "מודלים",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "עוד",
574
  "Name": "שם",
 
329
  "Enter language codes": "הזן קודי שפה",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "הזן תג מודל (למשל {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "הזן מספר שלבים (למשל 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "תוכן קובץ מודל",
571
  "Models": "מודלים",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "עוד",
576
  "Name": "שם",
src/lib/i18n/locales/hi-IN/translation.json CHANGED
@@ -329,6 +329,7 @@
329
  "Enter language codes": "भाषा कोड दर्ज करें",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Model tag दर्ज करें (उदा. {{modelTag}})",
 
332
  "Enter Number of Steps (e.g. 50)": "चरणों की संख्या दर्ज करें (उदा. 50)",
333
  "Enter Sampler (e.g. Euler a)": "",
334
  "Enter Scheduler (e.g. Karras)": "",
@@ -569,6 +570,7 @@
569
  "Modelfile Content": "मॉडल फ़ाइल सामग्री",
570
  "Models": "सभी मॉडल",
571
  "Models Access": "",
 
572
  "more": "",
573
  "More": "और..",
574
  "Name": "नाम",
 
329
  "Enter language codes": "भाषा कोड दर्ज करें",
330
  "Enter Model ID": "",
331
  "Enter model tag (e.g. {{modelTag}})": "Model tag दर्ज करें (उदा. {{modelTag}})",
332
+ "Enter Mojeek Search API Key": "",
333
  "Enter Number of Steps (e.g. 50)": "चरणों की संख्या दर्ज करें (उदा. 50)",
334
  "Enter Sampler (e.g. Euler a)": "",
335
  "Enter Scheduler (e.g. Karras)": "",
 
570
  "Modelfile Content": "मॉडल फ़ाइल सामग्री",
571
  "Models": "सभी मॉडल",
572
  "Models Access": "",
573
+ "Mojeek Search API Key": "",
574
  "more": "",
575
  "More": "और..",
576
  "Name": "नाम",