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