mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 12:55:19 +00:00
Merge branch 'dev' into dev
This commit is contained in:
commit
768b7e139c
134 changed files with 5490 additions and 1407 deletions
|
|
@ -709,8 +709,10 @@ def save_docs_to_vector_db(
|
|||
if overwrite:
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name)
|
||||
log.info(f"deleting existing collection {collection_name}")
|
||||
|
||||
if add is False:
|
||||
elif add is False:
|
||||
log.info(
|
||||
f"collection {collection_name} already exists, overwrite is False and add is False"
|
||||
)
|
||||
return True
|
||||
|
||||
log.info(f"adding to collection {collection_name}")
|
||||
|
|
@ -823,7 +825,7 @@ def process_file(
|
|||
# Process the file and save the content
|
||||
# Usage: /files/
|
||||
|
||||
file_path = file.meta.get("path", None)
|
||||
file_path = file.path
|
||||
if file_path:
|
||||
loader = Loader(
|
||||
engine=app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||
|
|
|
|||
|
|
@ -385,6 +385,8 @@ def get_rag_context(
|
|||
extracted_collections.extend(collection_names)
|
||||
|
||||
if context:
|
||||
if "data" in file:
|
||||
del file["data"]
|
||||
relevant_contexts.append({**context, "file": file})
|
||||
|
||||
contexts = []
|
||||
|
|
@ -401,7 +403,6 @@ def get_rag_context(
|
|||
]
|
||||
)
|
||||
)
|
||||
|
||||
contexts.append(
|
||||
((", ".join(file_names) + ":\n\n") if file_names else "")
|
||||
+ "\n\n".join(
|
||||
|
|
@ -410,13 +411,14 @@ def get_rag_context(
|
|||
)
|
||||
|
||||
if "metadatas" in context:
|
||||
citations.append(
|
||||
{
|
||||
"source": context["file"],
|
||||
"document": context["documents"][0],
|
||||
"metadata": context["metadatas"][0],
|
||||
}
|
||||
)
|
||||
citation = {
|
||||
"source": context["file"],
|
||||
"document": context["documents"][0],
|
||||
"metadata": context["metadatas"][0],
|
||||
}
|
||||
if "distances" in context and context["distances"]:
|
||||
citation["distances"] = context["distances"][0]
|
||||
citations.append(citation)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,10 @@ class ChromaClient:
|
|||
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Insert the items into the collection, if the collection does not exist, it will be created.
|
||||
collection = self.client.get_or_create_collection(name=collection_name)
|
||||
collection = self.client.get_or_create_collection(
|
||||
name=collection_name,
|
||||
metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
|
||||
ids = [item["id"] for item in items]
|
||||
documents = [item["text"] for item in items]
|
||||
|
|
@ -127,7 +130,10 @@ class ChromaClient:
|
|||
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
|
||||
collection = self.client.get_or_create_collection(name=collection_name)
|
||||
collection = self.client.get_or_create_collection(
|
||||
name=collection_name,
|
||||
metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
|
||||
ids = [item["id"] for item in items]
|
||||
documents = [item["text"] for item in items]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from open_webui.apps.webui.models.models import Models
|
|||
from open_webui.apps.webui.routers import (
|
||||
auths,
|
||||
chats,
|
||||
folders,
|
||||
configs,
|
||||
files,
|
||||
functions,
|
||||
|
|
@ -119,6 +120,7 @@ app.include_router(configs.router, prefix="/configs", tags=["configs"])
|
|||
app.include_router(auths.router, prefix="/auths", tags=["auths"])
|
||||
app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||
app.include_router(folders.router, prefix="/folders", tags=["folders"])
|
||||
|
||||
app.include_router(models.router, prefix="/models", tags=["models"])
|
||||
app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
|
||||
|
|
@ -344,7 +346,7 @@ async def generate_function_chat_completion(form_data, user):
|
|||
pipe = function_module.pipe
|
||||
params = get_function_params(function_module, form_data, user, extra_params)
|
||||
|
||||
if form_data["stream"]:
|
||||
if form_data.get("stream", False):
|
||||
|
||||
async def stream_content():
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class Chat(Base):
|
|||
pinned = Column(Boolean, default=False, nullable=True)
|
||||
|
||||
meta = Column(JSON, server_default="{}")
|
||||
folder_id = Column(Text, nullable=True)
|
||||
|
||||
|
||||
class ChatModel(BaseModel):
|
||||
|
|
@ -51,6 +52,7 @@ class ChatModel(BaseModel):
|
|||
pinned: Optional[bool] = False
|
||||
|
||||
meta: dict = {}
|
||||
folder_id: Optional[str] = None
|
||||
|
||||
|
||||
####################
|
||||
|
|
@ -62,6 +64,12 @@ class ChatForm(BaseModel):
|
|||
chat: dict
|
||||
|
||||
|
||||
class ChatImportForm(ChatForm):
|
||||
meta: Optional[dict] = {}
|
||||
pinned: Optional[bool] = False
|
||||
folder_id: Optional[str] = None
|
||||
|
||||
|
||||
class ChatTitleMessagesForm(BaseModel):
|
||||
title: str
|
||||
messages: list[dict]
|
||||
|
|
@ -82,6 +90,7 @@ class ChatResponse(BaseModel):
|
|||
archived: bool
|
||||
pinned: Optional[bool] = False
|
||||
meta: dict = {}
|
||||
folder_id: Optional[str] = None
|
||||
|
||||
|
||||
class ChatTitleIdResponse(BaseModel):
|
||||
|
|
@ -116,6 +125,35 @@ class ChatTable:
|
|||
db.refresh(result)
|
||||
return ChatModel.model_validate(result) if result else None
|
||||
|
||||
def import_chat(
|
||||
self, user_id: str, form_data: ChatImportForm
|
||||
) -> Optional[ChatModel]:
|
||||
with get_db() as db:
|
||||
id = str(uuid.uuid4())
|
||||
chat = ChatModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"title": (
|
||||
form_data.chat["title"]
|
||||
if "title" in form_data.chat
|
||||
else "New Chat"
|
||||
),
|
||||
"chat": form_data.chat,
|
||||
"meta": form_data.meta,
|
||||
"pinned": form_data.pinned,
|
||||
"folder_id": form_data.folder_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
result = Chat(**chat.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
return ChatModel.model_validate(result) if result else None
|
||||
|
||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
|
@ -254,7 +292,7 @@ class ChatTable:
|
|||
limit: int = 50,
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
|
||||
if not include_archived:
|
||||
query = query.filter_by(archived=False)
|
||||
|
||||
|
|
@ -276,7 +314,7 @@ class ChatTable:
|
|||
limit: Optional[int] = None,
|
||||
) -> list[ChatTitleIdResponse]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
|
||||
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||
|
||||
if not include_archived:
|
||||
|
|
@ -444,7 +482,18 @@ class ChatTable:
|
|||
)
|
||||
|
||||
# Check if there are any tags to filter, it should have all the tags
|
||||
if tag_ids:
|
||||
if "none" in tag_ids:
|
||||
query = query.filter(
|
||||
text(
|
||||
"""
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(Chat.meta, '$.tags') AS tag
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
elif tag_ids:
|
||||
query = query.filter(
|
||||
and_(
|
||||
*[
|
||||
|
|
@ -482,7 +531,18 @@ class ChatTable:
|
|||
)
|
||||
|
||||
# Check if there are any tags to filter, it should have all the tags
|
||||
if tag_ids:
|
||||
if "none" in tag_ids:
|
||||
query = query.filter(
|
||||
text(
|
||||
"""
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM json_array_elements_text(Chat.meta->'tags') AS tag
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
elif tag_ids:
|
||||
query = query.filter(
|
||||
and_(
|
||||
*[
|
||||
|
|
@ -512,6 +572,49 @@ class ChatTable:
|
|||
# Validate and return chats
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chats_by_folder_id_and_user_id(
|
||||
self, folder_id: str, user_id: str
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id)
|
||||
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||
query = query.filter_by(archived=False)
|
||||
|
||||
query = query.order_by(Chat.updated_at.desc())
|
||||
|
||||
all_chats = query.all()
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chats_by_folder_ids_and_user_id(
|
||||
self, folder_ids: list[str], user_id: str
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter(
|
||||
Chat.folder_id.in_(folder_ids), Chat.user_id == user_id
|
||||
)
|
||||
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||
query = query.filter_by(archived=False)
|
||||
|
||||
query = query.order_by(Chat.updated_at.desc())
|
||||
|
||||
all_chats = query.all()
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def update_chat_folder_id_by_id_and_user_id(
|
||||
self, id: str, user_id: str, folder_id: str
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
chat = db.get(Chat, id)
|
||||
chat.folder_id = folder_id
|
||||
chat.updated_at = int(time.time())
|
||||
chat.pinned = False
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
return ChatModel.model_validate(chat)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]:
|
||||
with get_db() as db:
|
||||
chat = db.get(Chat, id)
|
||||
|
|
@ -673,6 +776,18 @@ class ChatTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_chats_by_user_id_and_folder_id(
|
||||
self, user_id: str, folder_id: str
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
|
|
|||
|
|
@ -17,14 +17,15 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
|||
|
||||
class File(Base):
|
||||
__tablename__ = "file"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String)
|
||||
hash = Column(Text, nullable=True)
|
||||
|
||||
filename = Column(Text)
|
||||
path = Column(Text, nullable=True)
|
||||
|
||||
data = Column(JSON, nullable=True)
|
||||
meta = Column(JSONField)
|
||||
meta = Column(JSON, nullable=True)
|
||||
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
|
@ -38,8 +39,10 @@ class FileModel(BaseModel):
|
|||
hash: Optional[str] = None
|
||||
|
||||
filename: str
|
||||
path: Optional[str] = None
|
||||
|
||||
data: Optional[dict] = None
|
||||
meta: dict
|
||||
meta: Optional[dict] = None
|
||||
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
|
@ -82,6 +85,7 @@ class FileForm(BaseModel):
|
|||
id: str
|
||||
hash: Optional[str] = None
|
||||
filename: str
|
||||
path: str
|
||||
data: dict = {}
|
||||
meta: dict = {}
|
||||
|
||||
|
|
|
|||
271
backend/open_webui/apps/webui/models/folders.py
Normal file
271
backend/open_webui/apps/webui/models/folders.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.webui.internal.db import Base, get_db
|
||||
from open_webui.apps.webui.models.chats import Chats
|
||||
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
|
||||
####################
|
||||
# Folder DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = "folder"
|
||||
id = Column(Text, primary_key=True)
|
||||
parent_id = Column(Text, nullable=True)
|
||||
user_id = Column(Text)
|
||||
name = Column(Text)
|
||||
items = Column(JSON, nullable=True)
|
||||
meta = Column(JSON, nullable=True)
|
||||
is_expanded = Column(Boolean, default=False)
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
|
||||
class FolderModel(BaseModel):
|
||||
id: str
|
||||
parent_id: Optional[str] = None
|
||||
user_id: str
|
||||
name: str
|
||||
items: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
is_expanded: bool = False
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class FolderForm(BaseModel):
|
||||
name: str
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class FolderTable:
|
||||
def insert_new_folder(
|
||||
self, user_id: str, name: str, parent_id: Optional[str] = None
|
||||
) -> Optional[FolderModel]:
|
||||
with get_db() as db:
|
||||
id = str(uuid.uuid4())
|
||||
folder = FolderModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"name": name,
|
||||
"parent_id": parent_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
try:
|
||||
result = Folder(**folder.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
if result:
|
||||
return FolderModel.model_validate(result)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def get_folder_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_children_folders_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folders = []
|
||||
|
||||
def get_children(folder):
|
||||
children = self.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user_id
|
||||
)
|
||||
for child in children:
|
||||
get_children(child)
|
||||
folders.append(child)
|
||||
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
get_children(folder)
|
||||
return folders
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_folders_by_user_id(self, user_id: str) -> list[FolderModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
FolderModel.model_validate(folder)
|
||||
for folder in db.query(Folder).filter_by(user_id=user_id).all()
|
||||
]
|
||||
|
||||
def get_folder_by_parent_id_and_user_id_and_name(
|
||||
self, parent_id: Optional[str], user_id: str, name: str
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
# Check if folder exists
|
||||
folder = (
|
||||
db.query(Folder)
|
||||
.filter_by(parent_id=parent_id, user_id=user_id)
|
||||
.filter(Folder.name.ilike(name))
|
||||
.first()
|
||||
)
|
||||
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"get_folder_by_parent_id_and_user_id_and_name: {e}")
|
||||
return None
|
||||
|
||||
def get_folders_by_parent_id_and_user_id(
|
||||
self, parent_id: Optional[str], user_id: str
|
||||
) -> list[FolderModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
FolderModel.model_validate(folder)
|
||||
for folder in db.query(Folder)
|
||||
.filter_by(parent_id=parent_id, user_id=user_id)
|
||||
.all()
|
||||
]
|
||||
|
||||
def update_folder_parent_id_by_id_and_user_id(
|
||||
self,
|
||||
id: str,
|
||||
user_id: str,
|
||||
parent_id: str,
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
folder.parent_id = parent_id
|
||||
folder.updated_at = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"update_folder: {e}")
|
||||
return
|
||||
|
||||
def update_folder_name_by_id_and_user_id(
|
||||
self, id: str, user_id: str, name: str
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
existing_folder = (
|
||||
db.query(Folder)
|
||||
.filter_by(name=name, parent_id=folder.parent_id, user_id=user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_folder:
|
||||
return None
|
||||
|
||||
folder.name = name
|
||||
folder.updated_at = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"update_folder: {e}")
|
||||
return
|
||||
|
||||
def update_folder_is_expanded_by_id_and_user_id(
|
||||
self, id: str, user_id: str, is_expanded: bool
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
folder.is_expanded = is_expanded
|
||||
folder.updated_at = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"update_folder: {e}")
|
||||
return
|
||||
|
||||
def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
if not folder:
|
||||
return False
|
||||
|
||||
# Delete all chats in the folder
|
||||
Chats.delete_chats_by_user_id_and_folder_id(user_id, folder.id)
|
||||
|
||||
# Delete all children folders
|
||||
def delete_children(folder):
|
||||
folder_children = self.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user_id
|
||||
)
|
||||
for folder_child in folder_children:
|
||||
Chats.delete_chats_by_user_id_and_folder_id(
|
||||
user_id, folder_child.id
|
||||
)
|
||||
delete_children(folder_child)
|
||||
|
||||
folder = db.query(Folder).filter_by(id=folder_child.id).first()
|
||||
db.delete(folder)
|
||||
db.commit()
|
||||
|
||||
delete_children(folder)
|
||||
db.delete(folder)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"delete_folder: {e}")
|
||||
return False
|
||||
|
||||
|
||||
Folders = FolderTable()
|
||||
|
|
@ -4,11 +4,13 @@ from typing import Optional
|
|||
|
||||
from open_webui.apps.webui.models.chats import (
|
||||
ChatForm,
|
||||
ChatImportForm,
|
||||
ChatResponse,
|
||||
Chats,
|
||||
ChatTitleIdResponse,
|
||||
)
|
||||
from open_webui.apps.webui.models.tags import TagModel, Tags
|
||||
from open_webui.apps.webui.models.folders import Folders
|
||||
|
||||
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
|
@ -99,6 +101,34 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
|
|||
)
|
||||
|
||||
|
||||
############################
|
||||
# ImportChat
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/import", response_model=Optional[ChatResponse])
|
||||
async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user)):
|
||||
try:
|
||||
chat = Chats.import_chat(user.id, form_data)
|
||||
if chat:
|
||||
tags = chat.meta.get("tags", [])
|
||||
for tag_id in tags:
|
||||
tag_id = tag_id.replace(" ", "_").lower()
|
||||
tag_name = " ".join([word.capitalize() for word in tag_id.split("_")])
|
||||
if (
|
||||
tag_id != "none"
|
||||
and Tags.get_tag_by_name_and_user_id(tag_name, user.id) is None
|
||||
):
|
||||
Tags.insert_new_tag(tag_name, user.id)
|
||||
|
||||
return ChatResponse(**chat.model_dump())
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChats
|
||||
############################
|
||||
|
|
@ -133,6 +163,26 @@ async def search_user_chats(
|
|||
return chat_list
|
||||
|
||||
|
||||
############################
|
||||
# GetChatsByFolderId
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/folder/{folder_id}", response_model=list[ChatResponse])
|
||||
async def get_chats_by_folder_id(folder_id: str, user=Depends(get_verified_user)):
|
||||
folder_ids = [folder_id]
|
||||
children_folders = Folders.get_children_folders_by_id_and_user_id(
|
||||
folder_id, user.id
|
||||
)
|
||||
if children_folders:
|
||||
folder_ids.extend([folder.id for folder in children_folders])
|
||||
|
||||
return [
|
||||
ChatResponse(**chat.model_dump())
|
||||
for chat in Chats.get_chats_by_folder_ids_and_user_id(folder_ids, user.id)
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# GetPinnedChats
|
||||
############################
|
||||
|
|
@ -491,6 +541,31 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
|
|||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateChatFolderIdById
|
||||
############################
|
||||
|
||||
|
||||
class ChatFolderIdForm(BaseModel):
|
||||
folder_id: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/{id}/folder", response_model=Optional[ChatResponse])
|
||||
async def update_chat_folder_id_by_id(
|
||||
id: str, form_data: ChatFolderIdForm, user=Depends(get_verified_user)
|
||||
):
|
||||
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||
if chat:
|
||||
chat = Chats.update_chat_folder_id_by_id_and_user_id(
|
||||
id, user.id, form_data.folder_id
|
||||
)
|
||||
return ChatResponse(**chat.model_dump())
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChatTagsById
|
||||
############################
|
||||
|
|
@ -522,6 +597,12 @@ async def add_tag_by_id_and_tag_name(
|
|||
tags = chat.meta.get("tags", [])
|
||||
tag_id = form_data.name.replace(" ", "_").lower()
|
||||
|
||||
if tag_id == "none":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Tag name cannot be 'None'"),
|
||||
)
|
||||
|
||||
print(tags, tag_id)
|
||||
if tag_id not in tags:
|
||||
Chats.add_chat_tag_by_id_and_user_id_and_tag_name(
|
||||
|
|
|
|||
|
|
@ -57,11 +57,11 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
|
|||
**{
|
||||
"id": id,
|
||||
"filename": filename,
|
||||
"path": file_path,
|
||||
"meta": {
|
||||
"name": name,
|
||||
"content_type": file.content_type,
|
||||
"size": len(contents),
|
||||
"path": file_path,
|
||||
},
|
||||
}
|
||||
),
|
||||
|
|
@ -218,7 +218,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
file = Files.get_file_by_id(id)
|
||||
|
||||
if file and (file.user_id == user.id or user.role == "admin"):
|
||||
file_path = Path(file.meta["path"])
|
||||
file_path = Path(file.path)
|
||||
|
||||
# Check if the file already exists in the cache
|
||||
if file_path.is_file():
|
||||
|
|
@ -244,7 +244,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
file = Files.get_file_by_id(id)
|
||||
|
||||
if file and (file.user_id == user.id or user.role == "admin"):
|
||||
file_path = file.meta.get("path")
|
||||
file_path = file.path
|
||||
if file_path:
|
||||
file_path = Path(file_path)
|
||||
|
||||
|
|
|
|||
251
backend/open_webui/apps/webui/routers/folders.py
Normal file
251
backend/open_webui/apps/webui/routers/folders.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
import mimetypes
|
||||
|
||||
|
||||
from open_webui.apps.webui.models.folders import (
|
||||
FolderForm,
|
||||
FolderModel,
|
||||
Folders,
|
||||
)
|
||||
from open_webui.apps.webui.models.chats import Chats
|
||||
|
||||
from open_webui.config import UPLOAD_DIR
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
|
||||
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
############################
|
||||
# Get Folders
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=list[FolderModel])
|
||||
async def get_folders(user=Depends(get_verified_user)):
|
||||
folders = Folders.get_folders_by_user_id(user.id)
|
||||
|
||||
return [
|
||||
{
|
||||
**folder.model_dump(),
|
||||
"items": {
|
||||
"chats": [
|
||||
{"title": chat.title, "id": chat.id}
|
||||
for chat in Chats.get_chats_by_folder_id_and_user_id(
|
||||
folder.id, user.id
|
||||
)
|
||||
]
|
||||
},
|
||||
}
|
||||
for folder in folders
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# Create Folder
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/")
|
||||
def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
|
||||
folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
None, user.id, form_data.name
|
||||
)
|
||||
|
||||
if folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
|
||||
)
|
||||
|
||||
try:
|
||||
folder = Folders.insert_new_folder(user.id, form_data.name)
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error("Error creating folder")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error creating folder"),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Get Folders By Id
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[FolderModel])
|
||||
async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
if folder:
|
||||
return folder
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Update Folder Name By Id
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/update")
|
||||
async def update_folder_name_by_id(
|
||||
id: str, form_data: FolderForm, user=Depends(get_verified_user)
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
if folder:
|
||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
folder.parent_id, user.id, form_data.name
|
||||
)
|
||||
if existing_folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
|
||||
)
|
||||
|
||||
try:
|
||||
folder = Folders.update_folder_name_by_id_and_user_id(
|
||||
id, user.id, form_data.name
|
||||
)
|
||||
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error updating folder: {id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Update Folder Parent Id By Id
|
||||
############################
|
||||
|
||||
|
||||
class FolderParentIdForm(BaseModel):
|
||||
parent_id: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/{id}/update/parent")
|
||||
async def update_folder_parent_id_by_id(
|
||||
id: str, form_data: FolderParentIdForm, user=Depends(get_verified_user)
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
if folder:
|
||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
form_data.parent_id, user.id, folder.name
|
||||
)
|
||||
|
||||
if existing_folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
|
||||
)
|
||||
|
||||
try:
|
||||
folder = Folders.update_folder_parent_id_by_id_and_user_id(
|
||||
id, user.id, form_data.parent_id
|
||||
)
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error updating folder: {id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Update Folder Is Expanded By Id
|
||||
############################
|
||||
|
||||
|
||||
class FolderIsExpandedForm(BaseModel):
|
||||
is_expanded: bool
|
||||
|
||||
|
||||
@router.post("/{id}/update/expanded")
|
||||
async def update_folder_is_expanded_by_id(
|
||||
id: str, form_data: FolderIsExpandedForm, user=Depends(get_verified_user)
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
if folder:
|
||||
try:
|
||||
folder = Folders.update_folder_is_expanded_by_id_and_user_id(
|
||||
id, user.id, form_data.is_expanded
|
||||
)
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error updating folder: {id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Delete Folder By Id
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_folder_by_id(id: str, user=Depends(get_verified_user)):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
if folder:
|
||||
try:
|
||||
result = Folders.delete_folder_by_id_and_user_id(id, user.id)
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
raise Exception("Error deleting folder")
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error deleting folder: {id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
|
@ -876,6 +876,12 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
|||
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
|
||||
)
|
||||
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TAGS_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.tags.prompt_template",
|
||||
os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""),
|
||||
)
|
||||
|
||||
ENABLE_SEARCH_QUERY = PersistentConfig(
|
||||
"ENABLE_SEARCH_QUERY",
|
||||
"task.search.enable",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ class ERROR_MESSAGES(str, Enum):
|
|||
def __str__(self) -> str:
|
||||
return super().__str__()
|
||||
|
||||
DEFAULT = lambda err="": f"Something went wrong :/\n[ERROR: {err if err else ''}]"
|
||||
DEFAULT = (
|
||||
lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
|
||||
)
|
||||
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
||||
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
||||
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
||||
|
|
@ -106,6 +108,7 @@ class TASKS(str, Enum):
|
|||
|
||||
DEFAULT = lambda task="": f"{task if task else 'generation'}"
|
||||
TITLE_GENERATION = "title_generation"
|
||||
TAGS_GENERATION = "tags_generation"
|
||||
EMOJI_GENERATION = "emoji_generation"
|
||||
QUERY_GENERATION = "query_generation"
|
||||
FUNCTION_CALLING = "function_calling"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ from open_webui.config import (
|
|||
TASK_MODEL,
|
||||
TASK_MODEL_EXTERNAL,
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
WEBHOOK_URL,
|
||||
WEBUI_AUTH,
|
||||
|
|
@ -118,6 +119,7 @@ from open_webui.utils.response import (
|
|||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||
from open_webui.utils.task import (
|
||||
moa_response_generation_template,
|
||||
tags_generation_template,
|
||||
search_query_generation_template,
|
||||
title_generation_template,
|
||||
tools_function_calling_generation_template,
|
||||
|
|
@ -194,6 +196,7 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
|||
app.state.config.TASK_MODEL = TASK_MODEL
|
||||
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
|
|
@ -1403,6 +1406,7 @@ async def get_task_config(user=Depends(get_verified_user)):
|
|||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||
"ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
|
|
@ -1413,6 +1417,7 @@ class TaskConfigForm(BaseModel):
|
|||
TASK_MODEL: Optional[str]
|
||||
TASK_MODEL_EXTERNAL: Optional[str]
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
||||
ENABLE_SEARCH_QUERY: bool
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
|
||||
|
|
@ -1425,6 +1430,10 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
|||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
||||
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = (
|
||||
form_data.TAGS_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
|
||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
|
|
@ -1437,6 +1446,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
|||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
|
|
@ -1521,6 +1531,75 @@ Prompt: {{prompt:middletruncate:8000}}"""
|
|||
return await generate_chat_completions(form_data=payload, user=user)
|
||||
|
||||
|
||||
@app.post("/api/task/tags/completions")
|
||||
async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_chat_tags")
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
|
||||
template = app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE
|
||||
else:
|
||||
template = """### Task:
|
||||
Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
|
||||
|
||||
### Guidelines:
|
||||
- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
|
||||
- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
|
||||
- If content is too short (less than 3 messages) or too diverse, use only ["General"]
|
||||
- Use the chat's primary language; default to English if multilingual
|
||||
- Prioritize accuracy over specificity
|
||||
|
||||
### Output:
|
||||
JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
content = tags_generation_template(
|
||||
template, form_data["messages"], {"name": user.name}
|
||||
)
|
||||
|
||||
print("content", content)
|
||||
payload = {
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"metadata": {"task": str(TASKS.TAGS_GENERATION), "task_body": form_data},
|
||||
}
|
||||
log.debug(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
if len(e.args) > 1:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
return await generate_chat_completions(form_data=payload, user=user)
|
||||
|
||||
|
||||
@app.post("/api/task/query/completions")
|
||||
async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_search_query")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
"""Update file table path
|
||||
|
||||
Revision ID: c29facfe716b
|
||||
Revises: c69f45358db4
|
||||
Create Date: 2024-10-20 17:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import json
|
||||
from sqlalchemy.sql import table, column
|
||||
from sqlalchemy import String, Text, JSON, and_
|
||||
|
||||
|
||||
revision = "c29facfe716b"
|
||||
down_revision = "c69f45358db4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# 1. Add the `path` column to the "file" table.
|
||||
op.add_column("file", sa.Column("path", sa.Text(), nullable=True))
|
||||
|
||||
# 2. Convert the `meta` column from Text/JSONField to `JSON()`
|
||||
# Use Alembic's default batch_op for dialect compatibility.
|
||||
with op.batch_alter_table("file", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"meta",
|
||||
type_=sa.JSON(),
|
||||
existing_type=sa.Text(),
|
||||
existing_nullable=True,
|
||||
nullable=True,
|
||||
postgresql_using="meta::json",
|
||||
)
|
||||
|
||||
# 3. Migrate legacy data from `meta` JSONField
|
||||
# Fetch and process `meta` data from the table, add values to the new `path` column as necessary.
|
||||
# We will use SQLAlchemy core bindings to ensure safety across different databases.
|
||||
|
||||
file_table = table(
|
||||
"file", column("id", String), column("meta", JSON), column("path", Text)
|
||||
)
|
||||
|
||||
# Create connection to the database
|
||||
connection = op.get_bind()
|
||||
|
||||
# Get the rows where `meta` has a path and `path` column is null (new column)
|
||||
# Loop through each row in the result set to update the path
|
||||
results = connection.execute(
|
||||
sa.select(file_table.c.id, file_table.c.meta).where(
|
||||
and_(file_table.c.path.is_(None), file_table.c.meta.isnot(None))
|
||||
)
|
||||
).fetchall()
|
||||
|
||||
# Iterate over each row to extract and update the `path` from `meta` column
|
||||
for row in results:
|
||||
if "path" in row.meta:
|
||||
# Extract the `path` field from the `meta` JSON
|
||||
path = row.meta.get("path")
|
||||
|
||||
# Update the `file` table with the new `path` value
|
||||
connection.execute(
|
||||
file_table.update()
|
||||
.where(file_table.c.id == row.id)
|
||||
.values({"path": path})
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# 1. Remove the `path` column
|
||||
op.drop_column("file", "path")
|
||||
|
||||
# 2. Revert the `meta` column back to Text/JSONField
|
||||
with op.batch_alter_table("file", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"meta", type_=sa.Text(), existing_type=sa.JSON(), existing_nullable=True
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
"""Add folder table
|
||||
|
||||
Revision ID: c69f45358db4
|
||||
Revises: 3ab32c4b8f59
|
||||
Create Date: 2024-10-16 02:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "c69f45358db4"
|
||||
down_revision = "3ab32c4b8f59"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"folder",
|
||||
sa.Column("id", sa.Text(), nullable=False),
|
||||
sa.Column("parent_id", sa.Text(), nullable=True),
|
||||
sa.Column("user_id", sa.Text(), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column("items", sa.JSON(), nullable=True),
|
||||
sa.Column("meta", sa.JSON(), nullable=True),
|
||||
sa.Column("is_expanded", sa.Boolean(), default=False, nullable=False),
|
||||
sa.Column(
|
||||
"created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", "user_id"),
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"chat",
|
||||
sa.Column("folder_id", sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("chat", "folder_id")
|
||||
|
||||
op.drop_table("folder")
|
||||
|
|
@ -123,6 +123,24 @@ def replace_messages_variable(template: str, messages: list[str]) -> str:
|
|||
return template
|
||||
|
||||
|
||||
def tags_generation_template(
|
||||
template: str, messages: list[dict], user: Optional[dict] = None
|
||||
) -> str:
|
||||
prompt = get_last_user_message(messages)
|
||||
template = replace_prompt_variable(template, prompt)
|
||||
template = replace_messages_variable(template, messages)
|
||||
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "user_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
return template
|
||||
|
||||
|
||||
def search_query_generation_template(
|
||||
template: str, messages: list[dict], user: Optional[dict] = None
|
||||
) -> str:
|
||||
|
|
|
|||
|
|
@ -91,4 +91,4 @@ docker~=7.1.0
|
|||
pytest~=8.3.2
|
||||
pytest-docker~=3.1.1
|
||||
|
||||
googleapis-common-protos=1.63.2
|
||||
googleapis-common-protos==1.63.2
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ describe('Settings', () => {
|
|||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
|
|
@ -50,7 +50,7 @@ describe('Settings', () => {
|
|||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
|
|
@ -85,7 +85,7 @@ describe('Settings', () => {
|
|||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
|
|
|
|||
236
package-lock.json
generated
236
package-lock.json
generated
|
|
@ -35,6 +35,16 @@
|
|||
"mermaid": "^10.9.1",
|
||||
"paneforge": "^0.0.6",
|
||||
"panzoom": "^9.4.3",
|
||||
"prosemirror-commands": "^1.6.0",
|
||||
"prosemirror-example-setup": "^1.2.3",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-model": "^1.23.0",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.4.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-view": "^1.34.3",
|
||||
"pyodide": "^0.26.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
|
|
@ -1963,6 +1973,20 @@
|
|||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "3.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
||||
|
|
@ -1971,6 +1995,11 @@
|
|||
"@types/unist": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
|
|
@ -2552,8 +2581,7 @@
|
|||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.0",
|
||||
|
|
@ -4460,7 +4488,6 @@
|
|||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
|
|
@ -6233,6 +6260,14 @@
|
|||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/listr2": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
||||
|
|
@ -6470,6 +6505,22 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
|
||||
|
|
@ -6556,6 +6607,11 @@
|
|||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
|
@ -7332,6 +7388,11 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
|
||||
},
|
||||
"node_modules/ospath": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
|
||||
|
|
@ -7941,6 +8002,157 @@
|
|||
"node": "10.* || >= 12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
|
||||
"integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
|
||||
"integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-example-setup": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.2.3.tgz",
|
||||
"integrity": "sha512-+hXZi8+xbFvYM465zZH3rdZ9w7EguVKmUYwYLZjIJIjPK+I0nPTwn8j0ByW2avchVczRwZmOJGNvehblyIerSQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-dropcursor": "^1.0.0",
|
||||
"prosemirror-gapcursor": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-inputrules": "^1.0.0",
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-menu": "^1.0.0",
|
||||
"prosemirror-schema-list": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
|
||||
"integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
|
||||
"integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
|
||||
"integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz",
|
||||
"integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
|
||||
"integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"prosemirror-model": "^1.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz",
|
||||
"integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==",
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
|
||||
"integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-basic": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz",
|
||||
"integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz",
|
||||
"integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
|
||||
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
|
||||
"integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.34.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.3.tgz",
|
||||
"integrity": "sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
|
||||
|
|
@ -7977,6 +8189,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pyodide": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz",
|
||||
|
|
@ -8345,6 +8565,11 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
|
||||
},
|
||||
"node_modules/rsvp": {
|
||||
"version": "4.8.5",
|
||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||
|
|
@ -9562,6 +9787,11 @@
|
|||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -75,6 +75,16 @@
|
|||
"mermaid": "^10.9.1",
|
||||
"paneforge": "^0.0.6",
|
||||
"panzoom": "^9.4.3",
|
||||
"prosemirror-commands": "^1.6.0",
|
||||
"prosemirror-example-setup": "^1.2.3",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-model": "^1.23.0",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.4.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-view": "^1.34.3",
|
||||
"pyodide": "^0.26.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ dependencies = [
|
|||
"pytest~=8.3.2",
|
||||
"pytest-docker~=3.1.1",
|
||||
|
||||
"googleapis-common-protos=1.63.2"
|
||||
"googleapis-common-protos==1.63.2"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11, < 3.12.0a1"
|
||||
|
|
|
|||
30
src/app.css
30
src/app.css
|
|
@ -34,6 +34,14 @@ math {
|
|||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
.input-prose {
|
||||
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
}
|
||||
|
||||
.input-prose-sm {
|
||||
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line text-sm;
|
||||
}
|
||||
|
||||
.markdown-prose {
|
||||
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
}
|
||||
|
|
@ -56,7 +64,7 @@ li p {
|
|||
|
||||
::-webkit-scrollbar-thumb {
|
||||
--tw-border-opacity: 1;
|
||||
background-color: rgba(217, 217, 227, 0.8);
|
||||
background-color: rgba(236, 236, 236, 0.8);
|
||||
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
|
|
@ -64,7 +72,7 @@ li p {
|
|||
|
||||
/* Dark theme scrollbar styles */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(69, 69, 74, 0.8); /* Darker color for dark theme */
|
||||
background-color: rgba(33, 33, 33, 0.8); /* Darker color for dark theme */
|
||||
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
|
|
@ -179,3 +187,21 @@ input[type='number'] {
|
|||
.bg-gray-950-90 {
|
||||
background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9);
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
@apply h-full min-h-fit max-h-full;
|
||||
}
|
||||
|
||||
.ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.placeholder::after {
|
||||
content: attr(data-placeholder);
|
||||
cursor: text;
|
||||
pointer-events: none;
|
||||
|
||||
float: left;
|
||||
|
||||
@apply absolute inset-0 z-0 text-gray-500;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,46 @@ export const createNewChat = async (token: string, chat: object) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const importChat = async (
|
||||
token: string,
|
||||
chat: object,
|
||||
meta: object | null,
|
||||
pinned?: boolean,
|
||||
folderId?: string | null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/import`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat: chat,
|
||||
meta: meta ?? {},
|
||||
pinned: pinned,
|
||||
folder_id: folderId
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChatList = async (token: string = '', page: number | null = null) => {
|
||||
let error = null;
|
||||
const searchParams = new URLSearchParams();
|
||||
|
|
@ -205,6 +245,37 @@ export const getChatListBySearchText = async (token: string, text: string, page:
|
|||
}));
|
||||
};
|
||||
|
||||
export const getChatsByFolderId = async (token: string, folderId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/folder/${folderId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAllArchivedChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
|
@ -579,6 +650,41 @@ export const shareChatById = async (token: string, id: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const updateChatFolderIdById = async (token: string, id: string, folderId?: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/folder`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder_id: folderId
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const archiveChatById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
|
|
@ -764,8 +870,7 @@ export const addTagById = async (token: string, id: string, tagName: string) =>
|
|||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
|
|
|||
269
src/lib/apis/folders/index.ts
Normal file
269
src/lib/apis/folders/index.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const createNewFolder = async (token: string, name: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getFolders = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getFolderById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateFolderNameById = async (token: string, id: string, name: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateFolderIsExpandedById = async (
|
||||
token: string,
|
||||
id: string,
|
||||
isExpanded: boolean
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/expanded`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
is_expanded: isExpanded
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateFolderParentIdById = async (token: string, id: string, parentId?: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/parent`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parent_id: parentId
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
type FolderItems = {
|
||||
chat_ids: string[];
|
||||
file_ids: string[];
|
||||
};
|
||||
|
||||
export const updateFolderItemsById = async (token: string, id: string, items: FolderItems) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/items`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: items
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteFolderById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
|
@ -245,6 +245,78 @@ export const generateTitle = async (
|
|||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generateTags = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
messages: string,
|
||||
chat_id?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/tags/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: messages,
|
||||
...(chat_id && { chat_id: chat_id })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Safely extract the response string
|
||||
const response = res?.choices[0]?.message?.content ?? '';
|
||||
|
||||
// Step 2: Attempt to fix common JSON format issues like single quotes
|
||||
const sanitizedResponse = response.replace(/['‘’`]/g, '"'); // Convert single quotes to double quotes for valid JSON
|
||||
|
||||
// Step 3: Find the relevant JSON block within the response
|
||||
const jsonStartIndex = sanitizedResponse.indexOf('{');
|
||||
const jsonEndIndex = sanitizedResponse.lastIndexOf('}');
|
||||
|
||||
// Step 4: Check if we found a valid JSON block (with both `{` and `}`)
|
||||
if (jsonStartIndex !== -1 && jsonEndIndex !== -1) {
|
||||
const jsonResponse = sanitizedResponse.substring(jsonStartIndex, jsonEndIndex + 1);
|
||||
|
||||
// Step 5: Parse the JSON block
|
||||
const parsed = JSON.parse(jsonResponse);
|
||||
|
||||
// Step 6: If there's a "tags" key, return the tags array; otherwise, return an empty array
|
||||
if (parsed && parsed.tags) {
|
||||
return Array.isArray(parsed.tags) ? parsed.tags : [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid JSON block found, return an empty array
|
||||
return [];
|
||||
} catch (e) {
|
||||
// Catch and safely return empty array on any parsing errors
|
||||
console.error('Failed to parse response: ', e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const generateEmoji = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { text } from '@sveltejs/kit';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -629,11 +630,9 @@
|
|||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
<Textarea
|
||||
bind:value={querySettings.template}
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
TASK_MODEL: '',
|
||||
TASK_MODEL_EXTERNAL: '',
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||
TAG_GENERATION_PROMPT_TEMPLATE: '',
|
||||
ENABLE_SEARCH_QUERY: true,
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
|
||||
};
|
||||
|
|
@ -124,10 +126,22 @@
|
|||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
<Textarea
|
||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="3"
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={taskConfig.TAG_GENERATION_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -151,10 +165,8 @@
|
|||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
<Textarea
|
||||
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="3"
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -763,7 +763,7 @@
|
|||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
||||
<option value={model.name} class="bg-gray-50 dark:bg-gray-700"
|
||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
||||
>{model.name +
|
||||
' (' +
|
||||
(model.ollama.size / 1024 ** 3).toFixed(1) +
|
||||
|
|
|
|||
|
|
@ -546,12 +546,14 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
{#if PIPELINES_LIST !== null && PIPELINES_LIST.length > 0}
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import type { Unsubscriber, Writable } from 'svelte/store';
|
||||
import { get, type Unsubscriber, type Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
config,
|
||||
type Model,
|
||||
models,
|
||||
tags as allTags,
|
||||
settings,
|
||||
showSidebar,
|
||||
WEBUI_NAME,
|
||||
|
|
@ -46,7 +47,9 @@
|
|||
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
import {
|
||||
addTagById,
|
||||
createNewChat,
|
||||
getAllTags,
|
||||
getChatById,
|
||||
getChatList,
|
||||
getTagsById,
|
||||
|
|
@ -62,7 +65,8 @@
|
|||
generateTitle,
|
||||
generateSearchQuery,
|
||||
chatAction,
|
||||
generateMoACompletion
|
||||
generateMoACompletion,
|
||||
generateTags
|
||||
} from '$lib/apis';
|
||||
|
||||
import Banner from '../common/Banner.svelte';
|
||||
|
|
@ -85,6 +89,8 @@
|
|||
let processing = '';
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
|
||||
let navbarElement;
|
||||
|
||||
let showEventConfirmation = false;
|
||||
let eventConfirmationTitle = '';
|
||||
let eventConfirmationMessage = '';
|
||||
|
|
@ -125,7 +131,7 @@
|
|||
loaded = true;
|
||||
|
||||
window.setTimeout(() => scrollToBottom(), 0);
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
} else {
|
||||
await goto('/');
|
||||
|
|
@ -264,7 +270,7 @@
|
|||
if (event.data.type === 'input:prompt') {
|
||||
console.debug(event.data.text);
|
||||
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
const inputElement = document.getElementById('chat-input');
|
||||
|
||||
if (inputElement) {
|
||||
prompt = event.data.text;
|
||||
|
|
@ -327,7 +333,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
|
||||
chats.subscribe(() => {});
|
||||
|
|
@ -437,7 +443,29 @@
|
|||
if ($page.url.searchParams.get('models')) {
|
||||
selectedModels = $page.url.searchParams.get('models')?.split(',');
|
||||
} else if ($page.url.searchParams.get('model')) {
|
||||
selectedModels = $page.url.searchParams.get('model')?.split(',');
|
||||
const urlModels = $page.url.searchParams.get('model')?.split(',');
|
||||
|
||||
if (urlModels.length === 1) {
|
||||
const m = $models.find((m) => m.id === urlModels[0]);
|
||||
if (!m) {
|
||||
const modelSelectorButton = document.getElementById('model-selector-0-button');
|
||||
if (modelSelectorButton) {
|
||||
modelSelectorButton.click();
|
||||
await tick();
|
||||
|
||||
const modelSelectorInput = document.getElementById('model-search-input');
|
||||
if (modelSelectorInput) {
|
||||
modelSelectorInput.focus();
|
||||
modelSelectorInput.value = urlModels[0];
|
||||
modelSelectorInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedModels = urlModels;
|
||||
}
|
||||
} else {
|
||||
selectedModels = urlModels;
|
||||
}
|
||||
} else if ($settings?.models) {
|
||||
selectedModels = $settings?.models;
|
||||
} else if ($config?.default_models) {
|
||||
|
|
@ -501,7 +529,7 @@
|
|||
settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||
}
|
||||
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
setTimeout(() => chatInput?.focus(), 0);
|
||||
};
|
||||
|
||||
|
|
@ -513,7 +541,10 @@
|
|||
});
|
||||
|
||||
if (chat) {
|
||||
tags = await getTags();
|
||||
tags = await getTagsById(localStorage.token, $chatId).catch(async (error) => {
|
||||
return [];
|
||||
});
|
||||
|
||||
const chatContent = chat.chat;
|
||||
|
||||
if (chatContent) {
|
||||
|
|
@ -798,12 +829,14 @@
|
|||
})
|
||||
);
|
||||
} else {
|
||||
// Reset chat input textarea
|
||||
const chatTextAreaElement = document.getElementById('chat-textarea');
|
||||
prompt = '';
|
||||
|
||||
if (chatTextAreaElement) {
|
||||
chatTextAreaElement.value = '';
|
||||
chatTextAreaElement.style.height = '';
|
||||
// Reset chat input textarea
|
||||
const chatInputContainer = document.getElementById('chat-input-container');
|
||||
|
||||
if (chatInputContainer) {
|
||||
chatInputContainer.value = '';
|
||||
chatInputContainer.style.height = '';
|
||||
}
|
||||
|
||||
const _files = JSON.parse(JSON.stringify(files));
|
||||
|
|
@ -841,6 +874,11 @@
|
|||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// focus on chat input
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
|
||||
_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
|
||||
}
|
||||
|
||||
|
|
@ -1364,6 +1402,10 @@
|
|||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||
const title = await generateChatTitle(userPrompt);
|
||||
await setChatTitle(_chatId, title);
|
||||
|
||||
if ($settings?.autoTags ?? true) {
|
||||
await setChatTags(messages);
|
||||
}
|
||||
}
|
||||
|
||||
return _response;
|
||||
|
|
@ -1678,6 +1720,10 @@
|
|||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||
const title = await generateChatTitle(userPrompt);
|
||||
await setChatTitle(_chatId, title);
|
||||
|
||||
if ($settings?.autoTags ?? true) {
|
||||
await setChatTags(messages);
|
||||
}
|
||||
}
|
||||
|
||||
return _response;
|
||||
|
|
@ -1864,6 +1910,33 @@
|
|||
}
|
||||
};
|
||||
|
||||
const setChatTags = async (messages) => {
|
||||
if (!$temporaryChatEnabled) {
|
||||
let generatedTags = await generateTags(
|
||||
localStorage.token,
|
||||
selectedModels[0],
|
||||
messages,
|
||||
$chatId
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
const currentTags = await getTagsById(localStorage.token, $chatId);
|
||||
generatedTags = generatedTags.filter(
|
||||
(tag) => !currentTags.find((t) => t.id === tag.replaceAll(' ', '_').toLowerCase())
|
||||
);
|
||||
console.log(generatedTags);
|
||||
|
||||
for (const tag of generatedTags) {
|
||||
await addTagById(localStorage.token, $chatId, tag);
|
||||
}
|
||||
|
||||
chat = await getChatById(localStorage.token, $chatId);
|
||||
allTags.set(await getAllTags(localStorage.token));
|
||||
}
|
||||
};
|
||||
|
||||
const getWebSearchResults = async (
|
||||
model: string,
|
||||
parentId: string,
|
||||
|
|
@ -1949,12 +2022,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
return await getTagsById(localStorage.token, $chatId).catch(async (error) => {
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
const initChatHandler = async () => {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
|
|
@ -2046,6 +2113,7 @@
|
|||
{/if}
|
||||
|
||||
<Navbar
|
||||
bind:this={navbarElement}
|
||||
chat={{
|
||||
id: $chatId,
|
||||
chat: {
|
||||
|
|
@ -2182,9 +2250,8 @@
|
|||
}}
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
prompt = '';
|
||||
await tick();
|
||||
submitPrompt(e.detail);
|
||||
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -2227,9 +2294,8 @@
|
|||
}}
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
prompt = '';
|
||||
await tick();
|
||||
submitPrompt(e.detail);
|
||||
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||
import Commands from './MessageInput/Commands.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -52,9 +53,10 @@
|
|||
|
||||
let recording = false;
|
||||
|
||||
let chatTextAreaElement: HTMLTextAreaElement;
|
||||
let filesInputElement;
|
||||
let chatInputContainerElement;
|
||||
let chatInputElement;
|
||||
|
||||
let filesInputElement;
|
||||
let commandsElement;
|
||||
|
||||
let inputFiles;
|
||||
|
|
@ -69,9 +71,10 @@
|
|||
);
|
||||
|
||||
$: if (prompt) {
|
||||
if (chatTextAreaElement) {
|
||||
chatTextAreaElement.style.height = '';
|
||||
chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +216,10 @@
|
|||
};
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||
window.setTimeout(() => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
}, 0);
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
|
|
@ -316,7 +322,8 @@
|
|||
atSelectedModel = data.data;
|
||||
}
|
||||
|
||||
chatTextAreaElement?.focus();
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
chatInputElement?.focus();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -351,7 +358,7 @@
|
|||
recording = false;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
}}
|
||||
on:confirm={async (e) => {
|
||||
const response = e.detail;
|
||||
|
|
@ -360,7 +367,7 @@
|
|||
recording = false;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
|
||||
if ($settings?.speechAutoSend ?? false) {
|
||||
dispatch('submit', prompt);
|
||||
|
|
@ -478,7 +485,9 @@
|
|||
}}
|
||||
onClose={async () => {
|
||||
await tick();
|
||||
chatTextAreaElement?.focus();
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
|
|
@ -500,177 +509,172 @@
|
|||
</InputMenu>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="chat-textarea"
|
||||
bind:this={chatTextAreaElement}
|
||||
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]"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {
|
||||
if (
|
||||
!$mobile ||
|
||||
<div
|
||||
bind:this={chatInputContainerElement}
|
||||
id="chat-input-container"
|
||||
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"
|
||||
>
|
||||
<RichTextInput
|
||||
bind:this={chatInputElement}
|
||||
id="chat-input"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
shiftEnter={!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||
)}
|
||||
on:enter={async (e) => {
|
||||
if (prompt !== '') {
|
||||
dispatch('submit', prompt);
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
const commandsContainerElement = document.getElementById('commands-container');
|
||||
|
||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
createMessagePair(prompt);
|
||||
}
|
||||
|
||||
// Check if Ctrl + R is pressed
|
||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
||||
e.preventDefault();
|
||||
console.log('regenerate');
|
||||
|
||||
const regenerateButton = [
|
||||
...document.getElementsByClassName('regenerate-response-button')
|
||||
]?.at(-1);
|
||||
|
||||
regenerateButton?.click();
|
||||
}
|
||||
|
||||
if (prompt === '' && e.key == 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
|
||||
const userMessageElement = [
|
||||
...document.getElementsByClassName('user-message')
|
||||
]?.at(-1);
|
||||
|
||||
const editButton = [
|
||||
...document.getElementsByClassName('edit-user-message-button')
|
||||
]?.at(-1);
|
||||
|
||||
console.log(userMessageElement);
|
||||
|
||||
userMessageElement.scrollIntoView({ block: 'center' });
|
||||
editButton?.click();
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectUp();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectDown();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (e.shiftKey) {
|
||||
prompt = `${prompt}\n`;
|
||||
} else if (commandOptionButton) {
|
||||
commandOptionButton?.click();
|
||||
} else {
|
||||
document.getElementById('send-message-button')?.click();
|
||||
}}
|
||||
on:input={async (e) => {
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:focus={async (e) => {
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}}
|
||||
on:keypress={(e) => {
|
||||
e = e.detail.event;
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
e = e.detail.event;
|
||||
|
||||
if (commandsContainerElement && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
commandOptionButton?.click();
|
||||
} else if (e.key === 'Tab') {
|
||||
const words = findWordIndices(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
const fullPrompt = prompt;
|
||||
|
||||
prompt = prompt.substring(0, word?.endIndex + 1);
|
||||
await tick();
|
||||
|
||||
e.target.scrollTop = e.target.scrollHeight;
|
||||
prompt = fullPrompt;
|
||||
await tick();
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
const commandsContainerElement =
|
||||
document.getElementById('commands-container');
|
||||
|
||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
createMessagePair(prompt);
|
||||
}
|
||||
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
}
|
||||
// Check if Ctrl + R is pressed
|
||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
||||
e.preventDefault();
|
||||
console.log('regenerate');
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
console.log('Escape');
|
||||
atSelectedModel = undefined;
|
||||
}
|
||||
}}
|
||||
rows="1"
|
||||
on:input={async (e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
user = null;
|
||||
}}
|
||||
on:focus={async (e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
}}
|
||||
on:paste={async (e) => {
|
||||
const clipboardData = e.clipboardData || window.clipboardData;
|
||||
const regenerateButton = [
|
||||
...document.getElementsByClassName('regenerate-response-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (clipboardData && clipboardData.items) {
|
||||
for (const item of clipboardData.items) {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const blob = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
regenerateButton?.click();
|
||||
}
|
||||
|
||||
reader.onload = function (e) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: `${e.target.result}`
|
||||
}
|
||||
];
|
||||
};
|
||||
if (prompt === '' && e.key == 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
const userMessageElement = [
|
||||
...document.getElementsByClassName('user-message')
|
||||
]?.at(-1);
|
||||
|
||||
const editButton = [
|
||||
...document.getElementsByClassName('edit-user-message-button')
|
||||
]?.at(-1);
|
||||
|
||||
console.log(userMessageElement);
|
||||
|
||||
userMessageElement.scrollIntoView({ block: 'center' });
|
||||
editButton?.click();
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectUp();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectDown();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (e.shiftKey) {
|
||||
prompt = `${prompt}\n`;
|
||||
} else if (commandOptionButton) {
|
||||
commandOptionButton?.click();
|
||||
} else {
|
||||
document.getElementById('send-message-button')?.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
if (commandsContainerElement && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
commandOptionButton?.click();
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
console.log('Escape');
|
||||
atSelectedModel = undefined;
|
||||
}
|
||||
}}
|
||||
on:paste={async (e) => {
|
||||
e = e.detail.event;
|
||||
console.log(e);
|
||||
|
||||
const clipboardData = e.clipboardData || window.clipboardData;
|
||||
|
||||
if (clipboardData && clipboardData.items) {
|
||||
for (const item of clipboardData.items) {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const blob = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: `${e.target.result}`
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,17 @@
|
|||
};
|
||||
|
||||
let command = '';
|
||||
$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
|
||||
$: command = prompt?.split('\n').pop()?.split(' ')?.pop() ?? '';
|
||||
</script>
|
||||
|
||||
{#if ['/', '#', '@'].includes(command?.charAt(0))}
|
||||
{#if ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2)}
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
{:else if command?.charAt(0) === '#'}
|
||||
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
{command}
|
||||
command={command.includes('\\#') ? command.slice(2) : command}
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
dispatch('select', item);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
dispatch('url', url);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
dispatch('youtube', url);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
|
|
|
|||
|
|
@ -110,21 +110,17 @@
|
|||
|
||||
prompt = text;
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
|
||||
chatInputElement?.focus();
|
||||
|
||||
await tick();
|
||||
|
||||
const words = findWordIndices(prompt);
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
chatInputElement?.focus();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -61,9 +61,14 @@
|
|||
<div
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<WrenchSolid />
|
||||
<Tooltip content={tools[toolId]?.description ?? ''} className="flex-1">
|
||||
<div class="flex-1">
|
||||
<Tooltip
|
||||
content={tools[toolId]?.description ?? ''}
|
||||
placement="top-start"
|
||||
className="flex flex-1 gap-2 items-center"
|
||||
>
|
||||
<WrenchSolid />
|
||||
|
||||
<div class=" line-clamp-1">{tools[toolId].name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let recording = false;
|
||||
export let className = ' p-2.5 w-full max-w-full';
|
||||
|
||||
let loading = false;
|
||||
let confirmed = false;
|
||||
|
|
@ -213,7 +214,7 @@
|
|||
transcription = `${transcription}${transcript}`;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
|
||||
// Restart the inactivity timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
|
|
@ -282,7 +283,7 @@
|
|||
<div
|
||||
class="{loading
|
||||
? ' bg-gray-100/50 dark:bg-gray-850/50'
|
||||
: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex p-2.5"
|
||||
: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex {className}"
|
||||
>
|
||||
<div class="flex items-center mr-1">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -330,20 +330,14 @@
|
|||
|
||||
await tick();
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
if (chatInputElement) {
|
||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
||||
if (chatInputContainerElement) {
|
||||
prompt = p;
|
||||
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||
chatInputElement.focus();
|
||||
|
||||
const words = findWordIndices(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
}
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
chatInputContainerElement.focus();
|
||||
}
|
||||
|
||||
await tick();
|
||||
|
|
|
|||
|
|
@ -1,67 +1,219 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import CitationsModal from './CitationsModal.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let citations = [];
|
||||
|
||||
let _citations = [];
|
||||
|
||||
$: _citations = citations.reduce((acc, citation) => {
|
||||
citation.document.forEach((document, index) => {
|
||||
const metadata = citation.metadata?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
let source = citation?.source;
|
||||
|
||||
if (metadata?.name) {
|
||||
source = { ...source, name: metadata.name };
|
||||
}
|
||||
|
||||
// Check if ID looks like a URL
|
||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
||||
source = { name: id };
|
||||
}
|
||||
|
||||
const existingSource = acc.find((item) => item.id === id);
|
||||
|
||||
if (existingSource) {
|
||||
existingSource.document.push(document);
|
||||
existingSource.metadata.push(metadata);
|
||||
} else {
|
||||
acc.push({
|
||||
id: id,
|
||||
source: source,
|
||||
document: [document],
|
||||
metadata: metadata ? [metadata] : []
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
let showPercentage = false;
|
||||
let showRelevance = true;
|
||||
|
||||
let showCitationModal = false;
|
||||
let selectedCitation = null;
|
||||
let selectedCitation: any = null;
|
||||
let isCollapsibleOpen = false;
|
||||
|
||||
function calculateShowRelevance(citations: any[]) {
|
||||
const distances = citations.flatMap((citation) => citation.distances ?? []);
|
||||
const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
|
||||
const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
|
||||
|
||||
if (distances.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(inRange === distances.length - 1 && outOfRange === 1) ||
|
||||
(outOfRange === distances.length - 1 && inRange === 1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function shouldShowPercentage(citations: any[]) {
|
||||
const distances = citations.flatMap((citation) => citation.distances ?? []);
|
||||
return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
|
||||
}
|
||||
|
||||
$: {
|
||||
_citations = citations.reduce((acc, citation) => {
|
||||
citation.document.forEach((document, index) => {
|
||||
const metadata = citation.metadata?.[index];
|
||||
const distance = citation.distances?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
let source = citation?.source;
|
||||
|
||||
if (metadata?.name) {
|
||||
source = { ...source, name: metadata.name };
|
||||
}
|
||||
|
||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
||||
source = { name: id };
|
||||
}
|
||||
|
||||
const existingSource = acc.find((item) => item.id === id);
|
||||
|
||||
if (existingSource) {
|
||||
existingSource.document.push(document);
|
||||
existingSource.metadata.push(metadata);
|
||||
if (distance !== undefined) existingSource.distances.push(distance);
|
||||
} else {
|
||||
acc.push({
|
||||
id: id,
|
||||
source: source,
|
||||
document: [document],
|
||||
metadata: metadata ? [metadata] : [],
|
||||
distances: distance !== undefined ? [distance] : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
showRelevance = calculateShowRelevance(_citations);
|
||||
showPercentage = shouldShowPercentage(_citations);
|
||||
}
|
||||
</script>
|
||||
|
||||
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
|
||||
<CitationsModal
|
||||
bind:show={showCitationModal}
|
||||
citation={selectedCitation}
|
||||
{showPercentage}
|
||||
{showRelevance}
|
||||
/>
|
||||
|
||||
{#if _citations.length > 0}
|
||||
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
|
||||
{#each _citations as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
{#if _citations.length <= 3}
|
||||
{#each _citations as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1 truncate">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<Collapsible bind:open={isCollapsibleOpen} className="w-full">
|
||||
<div
|
||||
class="flex items-center gap-1 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
<div class="flex-grow flex items-center gap-1 overflow-hidden">
|
||||
<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
|
||||
<div class="flex items-center">
|
||||
{#if _citations.length > 1 && _citations
|
||||
.slice(0, 2)
|
||||
.reduce((acc, citation) => acc + citation.source.name.length, 0) <= 50}
|
||||
{#each _citations.slice(0, 2) as citation, idx}
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96 text-xs font-semibold"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
{#if idx === 0}<span class="mr-1">,</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each _citations.slice(0, 1) as citation, idx}
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96 text-xs font-semibold"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 whitespace-nowrap">
|
||||
<span class="hidden sm:inline">{$i18n.t('and')}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
{_citations.length -
|
||||
(_citations.length > 1 &&
|
||||
_citations
|
||||
.slice(0, 2)
|
||||
.reduce((acc, citation) => acc + citation.source.name.length, 0) <= 50
|
||||
? 2
|
||||
: 1)}
|
||||
</span>
|
||||
<span>{$i18n.t('more')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
<div class="flex-shrink-0">
|
||||
{#if isCollapsibleOpen}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div slot="content" class="mt-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each _citations as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,44 @@
|
|||
import { getContext, onMount, tick } from 'svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let citation;
|
||||
export let showPercentage = false;
|
||||
export let showRelevance = true;
|
||||
|
||||
let mergedDocuments = [];
|
||||
|
||||
function calculatePercentage(distance: number) {
|
||||
if (distance < 0) return 100;
|
||||
if (distance > 1) return 0;
|
||||
return Math.round((1 - distance) * 10000) / 100;
|
||||
}
|
||||
|
||||
function getRelevanceColor(percentage: number) {
|
||||
if (percentage >= 80)
|
||||
return 'bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200';
|
||||
if (percentage >= 60)
|
||||
return 'bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200';
|
||||
if (percentage >= 40)
|
||||
return 'bg-orange-200 dark:bg-orange-800 text-orange-800 dark:text-orange-200';
|
||||
return 'bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200';
|
||||
}
|
||||
|
||||
$: if (citation) {
|
||||
mergedDocuments = citation.document?.map((c, i) => {
|
||||
return {
|
||||
source: citation.source,
|
||||
document: c,
|
||||
metadata: citation.metadata?.[i]
|
||||
metadata: citation.metadata?.[i],
|
||||
distance: citation.distances?.[i]
|
||||
};
|
||||
});
|
||||
if (mergedDocuments.every((doc) => doc.distance !== undefined)) {
|
||||
mergedDocuments.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -57,13 +80,14 @@
|
|||
|
||||
{#if document.source?.name}
|
||||
<Tooltip
|
||||
className="w-fit"
|
||||
content={$i18n.t('Open file')}
|
||||
placement="left"
|
||||
tippyOptions={{ duration: [500, 0], animation: 'perspective' }}
|
||||
placement="top-start"
|
||||
tippyOptions={{ duration: [500, 0] }}
|
||||
>
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
<div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit">
|
||||
<a
|
||||
class="hover:text-gray-500 hover:dark:text-gray-100 underline"
|
||||
class="hover:text-gray-500 hover:dark:text-gray-100 underline flex-grow"
|
||||
href={document?.metadata?.file_id
|
||||
? `/api/v1/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||
: document.source.name.includes('http')
|
||||
|
|
@ -73,11 +97,47 @@
|
|||
>
|
||||
{document?.metadata?.name ?? document.source.name}
|
||||
</a>
|
||||
{document?.metadata?.page
|
||||
? `(${$i18n.t('page')} ${document.metadata.page + 1})`
|
||||
: ''}
|
||||
{#if document?.metadata?.page}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
({$i18n.t('page')}
|
||||
{document.metadata.page + 1})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{#if showRelevance}
|
||||
<div class="text-sm font-medium dark:text-gray-300 mt-2">
|
||||
{$i18n.t('Relevance')}
|
||||
</div>
|
||||
{#if document.distance !== undefined}
|
||||
<Tooltip
|
||||
className="w-fit"
|
||||
content={$i18n.t('Semantic distance to query')}
|
||||
placement="top-start"
|
||||
tippyOptions={{ duration: [500, 0] }}
|
||||
>
|
||||
<div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit">
|
||||
{#if showPercentage}
|
||||
{@const percentage = calculatePercentage(document.distance)}
|
||||
<span class={`px-1 rounded font-medium ${getRelevanceColor(percentage)}`}>
|
||||
{percentage.toFixed(2)}%
|
||||
</span>
|
||||
<span class="text-gray-500 dark:text-gray-500">
|
||||
({document.distance.toFixed(4)})
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-gray-500 dark:text-gray-500">
|
||||
{document.distance.toFixed(4)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
{$i18n.t('No distance available')}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
{$i18n.t('No source available')}
|
||||
|
|
@ -85,7 +145,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" text-sm font-medium dark:text-gray-300">
|
||||
<div class=" text-sm font-medium dark:text-gray-300 mt-2">
|
||||
{$i18n.t('Content')}
|
||||
</div>
|
||||
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { config } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -69,7 +70,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-50 dark:border-gray-850"
|
||||
id="message-feedback-{message.id}"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
|
|
@ -97,7 +98,7 @@
|
|||
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
|
||||
{#each reasons as reason}
|
||||
<button
|
||||
class="px-3.5 py-1 border dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
|
||||
class="px-3.5 py-1 border border-gray-50 dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
|
||||
reason
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ''} transition rounded-lg"
|
||||
|
|
@ -120,9 +121,21 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end">
|
||||
<div class="mt-2 gap-1.5 flex justify-end">
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<button
|
||||
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5"
|
||||
class=" bg-emerald-700 hover:bg-emerald-800 transition text-white text-sm font-medium rounded-xl px-3.5 py-1.5"
|
||||
on:click={() => {
|
||||
saveHandler();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -142,28 +142,30 @@
|
|||
|
||||
{#if edit === true}
|
||||
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('close-edit-message-button')?.click();
|
||||
}
|
||||
<div class="max-h-[25dvh] overflow-auto">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('close-edit-message-button')?.click();
|
||||
}
|
||||
|
||||
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
|
||||
const isEnterPressed = e.key === 'Enter';
|
||||
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
|
||||
const isEnterPressed = e.key === 'Enter';
|
||||
|
||||
if (isCmdOrCtrlPressed && isEnterPressed) {
|
||||
document.getElementById('confirm-edit-message-button')?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
if (isCmdOrCtrlPressed && isEnterPressed) {
|
||||
document.getElementById('confirm-edit-message-button')?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
<div class="overflow-hidden w-full">
|
||||
<div class="mr-1 max-w-full">
|
||||
<Selector
|
||||
id={`${selectedModelIdx}`}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
items={$models.map((model) => ({
|
||||
value: model.id,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id = '';
|
||||
export let value = '';
|
||||
export let placeholder = 'Select a model';
|
||||
export let searchEnabled = true;
|
||||
|
|
@ -229,7 +230,11 @@
|
|||
}}
|
||||
closeFocus={false}
|
||||
>
|
||||
<DropdownMenu.Trigger class="relative w-full font-primary" aria-label={placeholder}>
|
||||
<DropdownMenu.Trigger
|
||||
class="relative w-full font-primary"
|
||||
aria-label={placeholder}
|
||||
id="model-selector-{id}-button"
|
||||
>
|
||||
<div
|
||||
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-medium placeholder-gray-400 focus:outline-none"
|
||||
>
|
||||
|
|
@ -245,10 +250,10 @@
|
|||
<DropdownMenu.Content
|
||||
class=" z-40 {$mobile
|
||||
? `w-full`
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-none"
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-none"
|
||||
transition={flyAndScale}
|
||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||
sideOffset={4}
|
||||
sideOffset={3}
|
||||
>
|
||||
<slot>
|
||||
{#if searchEnabled}
|
||||
|
|
@ -281,7 +286,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800" />
|
||||
<hr class="border-gray-50 dark:border-gray-800" />
|
||||
{/if}
|
||||
|
||||
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
|
||||
|
|
@ -474,10 +479,16 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-col self-start">
|
||||
<div class="line-clamp-1">
|
||||
Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
|
||||
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
|
||||
: ''}
|
||||
<div class="flex gap-1">
|
||||
<div class="line-clamp-1">
|
||||
Downloading "{model}"
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
|
||||
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
|
||||
|
|
@ -488,7 +499,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mr-2 translate-y-0.5">
|
||||
<div class="mr-2 ml-1 translate-y-0.5">
|
||||
<Tooltip content={$i18n.t('Cancel')}>
|
||||
<button
|
||||
class="text-gray-800 dark:text-gray-100"
|
||||
|
|
@ -521,7 +532,7 @@
|
|||
</div>
|
||||
|
||||
{#if showTemporaryChatControl}
|
||||
<hr class="border-gray-100 dark:border-gray-800" />
|
||||
<hr class="border-gray-50 dark:border-gray-800" />
|
||||
|
||||
<div class="flex items-center mx-2 my-2">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -57,18 +57,14 @@
|
|||
console.log(prompt);
|
||||
await tick();
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
if (chatInputElement) {
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||
chatInputElement.focus();
|
||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
|
||||
const words = findWordIndices(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
}
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
chatInputElement?.focus();
|
||||
}
|
||||
|
||||
await tick();
|
||||
|
|
@ -106,7 +102,7 @@
|
|||
class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
|
||||
>
|
||||
<div class="w-full flex flex-col justify-center items-center">
|
||||
<div class="flex flex-col md:flex-row justify-center gap-2 md:gap-3.5 w-fit">
|
||||
<div class="flex flex-row justify-center gap-3 sm:gap-3.5 w-fit px-5">
|
||||
<div class="flex flex-shrink-0 justify-center">
|
||||
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
|
||||
{#each models as model, modelIdx}
|
||||
|
|
@ -127,7 +123,7 @@
|
|||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-[2.5rem] rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
class=" size-9 sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
|
|
@ -137,7 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" capitalize line-clamp-1 text-3xl md:text-4xl" in:fade={{ duration: 100 }}>
|
||||
<div class=" capitalize text-3xl sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
|
||||
{#if models[selectedModelIdx]?.info}
|
||||
{models[selectedModelIdx]?.info?.name}
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
// Addons
|
||||
let titleAutoGenerate = true;
|
||||
let autoTags = true;
|
||||
|
||||
let responseAutoCopy = false;
|
||||
let widescreenMode = false;
|
||||
let splitLargeChunks = false;
|
||||
|
|
@ -112,6 +114,11 @@
|
|||
});
|
||||
};
|
||||
|
||||
const toggleAutoTags = async () => {
|
||||
autoTags = !autoTags;
|
||||
saveSettings({ autoTags });
|
||||
};
|
||||
|
||||
const toggleResponseAutoCopy = async () => {
|
||||
const permission = await navigator.clipboard
|
||||
.readText()
|
||||
|
|
@ -149,6 +156,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
titleAutoGenerate = $settings?.title?.auto ?? true;
|
||||
autoTags = $settings.autoTags ?? true;
|
||||
|
||||
responseAutoCopy = $settings.responseAutoCopy ?? false;
|
||||
showUsername = $settings.showUsername ?? false;
|
||||
|
|
@ -431,6 +439,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleAutoTags();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if autoTags === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@
|
|||
<div
|
||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
||||
>
|
||||
⌫
|
||||
⌫/Delete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Tags from '../common/Tags.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let chatId = '';
|
||||
let tags = [];
|
||||
|
|
@ -31,7 +32,14 @@
|
|||
};
|
||||
|
||||
const addTag = async (tagName) => {
|
||||
const res = await addTagById(localStorage.token, chatId, tagName);
|
||||
const res = await addTagById(localStorage.token, chatId, tagName).catch(async (error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
|
||||
tags = await getTags();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
tags: tags
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
|
||||
success: 'bg-green-500/20 text-green-700 dark:text-green-200',
|
||||
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
|
||||
error: 'bg-red-500/20 text-red-700 dark:text-red-200'
|
||||
error: 'bg-red-500/20 text-red-700 dark:text-red-200',
|
||||
muted: 'bg-gray-500/20 text-gray-700 dark:text-gray-200'
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,23 @@
|
|||
export let className = '';
|
||||
export let buttonClassName = 'w-fit';
|
||||
export let title = null;
|
||||
|
||||
export let disabled = false;
|
||||
export let hide = false;
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
{#if title !== null}
|
||||
<button class={buttonClassName} on:click={() => (open = !open)}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{buttonClassName} cursor-pointer"
|
||||
on:pointerup={() => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" w-fit font-medium transition flex items-center justify-between gap-2">
|
||||
<div>
|
||||
{title}
|
||||
|
|
@ -26,24 +38,33 @@
|
|||
|
||||
<div>
|
||||
{#if open}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button class={buttonClassName} on:click={() => (open = !open)}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{buttonClassName} cursor-pointer"
|
||||
on:pointerup={() => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if open}
|
||||
{#if open && !hide}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { onMount, getContext, createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
export let title = '';
|
||||
export let message = '';
|
||||
|
||||
|
|
@ -27,6 +26,12 @@
|
|||
console.log('Escape');
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
console.log('Enter');
|
||||
show = false;
|
||||
dispatch('confirm', inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -56,7 +61,7 @@
|
|||
}}
|
||||
>
|
||||
<div
|
||||
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl border border-gray-850"
|
||||
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl"
|
||||
in:flyAndScale
|
||||
on:mousedown={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
bind:this={popupElement}
|
||||
class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none"
|
||||
>
|
||||
<div class=" absolute text-white z-[99999]" style="top: {y}px; left: {x}px;">
|
||||
<div class=" absolute text-white z-[99999]" style="top: {y + 10}px; left: {x + 10}px;">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,23 +6,11 @@
|
|||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show = false;
|
||||
export let size = 'md';
|
||||
export let className = '';
|
||||
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
|
||||
const sizeToWidth = (size) => {
|
||||
if (size === 'xs') {
|
||||
return 'w-[16rem]';
|
||||
} else if (size === 'sm') {
|
||||
return 'w-[30rem]';
|
||||
} else if (size === 'md') {
|
||||
return 'w-[48rem]';
|
||||
} else {
|
||||
return 'w-[56rem]';
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isTopModal()) {
|
||||
console.log('Escape');
|
||||
|
|
@ -76,7 +64,7 @@
|
|||
}}
|
||||
>
|
||||
<div
|
||||
class=" mt-auto max-w-full w-full bg-gray-50 dark:bg-gray-900 max-h-[100dvh] overflow-y-auto scrollbar-hidden"
|
||||
class=" mt-auto max-w-full w-full bg-gray-50 dark:bg-gray-900 dark:text-gray-100 {className} max-h-[100dvh] overflow-y-auto scrollbar-hidden"
|
||||
on:mousedown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<slot name="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
|
||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="start"
|
||||
|
|
|
|||
|
|
@ -14,34 +14,73 @@
|
|||
export let name = '';
|
||||
export let collapsible = true;
|
||||
|
||||
export let className = '';
|
||||
|
||||
let folderElement;
|
||||
|
||||
let dragged = false;
|
||||
let draggedOver = false;
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
dragged = true;
|
||||
e.stopPropagation();
|
||||
draggedOver = true;
|
||||
};
|
||||
|
||||
const onDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (folderElement.contains(e.target)) {
|
||||
console.log('Dropped on the Button');
|
||||
|
||||
// get data from the drag event
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
// Iterate over all items in the DataTransferItemList use functional programming
|
||||
for (const item of Array.from(e.dataTransfer.items)) {
|
||||
// If dropped items aren't files, reject them
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file && file.type === 'application/json') {
|
||||
console.log('Dropped file is a JSON file!');
|
||||
|
||||
dragged = false;
|
||||
// Read the JSON file with FileReader
|
||||
const reader = new FileReader();
|
||||
reader.onload = async function (event) {
|
||||
try {
|
||||
const fileContent = JSON.parse(event.target.result);
|
||||
console.log('Parsed JSON Content: ', fileContent);
|
||||
open = true;
|
||||
dispatch('import', fileContent);
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start reading the file
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
console.error('Only JSON file types are supported.');
|
||||
}
|
||||
} else {
|
||||
open = true;
|
||||
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draggedOver = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
dragged = false;
|
||||
e.stopPropagation();
|
||||
|
||||
draggedOver = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -57,10 +96,10 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={folderElement} class="relative">
|
||||
{#if dragged}
|
||||
<div bind:this={folderElement} class="relative {className}">
|
||||
{#if draggedOver}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-sm bg-gray-200 bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
|
||||
class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(260,85%,65%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -74,7 +113,7 @@
|
|||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="mx-2 w-full">
|
||||
<div class="w-full">
|
||||
<button
|
||||
class="w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
>
|
||||
|
|
@ -92,7 +131,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div slot="content" class=" pl-2">
|
||||
<div slot="content" class="w-full">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@
|
|||
|
||||
export let show = true;
|
||||
export let size = 'md';
|
||||
export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl';
|
||||
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
|
||||
const sizeToWidth = (size) => {
|
||||
if (size === 'full') {
|
||||
return 'w-full';
|
||||
}
|
||||
if (size === 'xs') {
|
||||
return 'w-[16rem]';
|
||||
} else if (size === 'sm') {
|
||||
|
|
@ -68,9 +72,9 @@
|
|||
}}
|
||||
>
|
||||
<div
|
||||
class=" m-auto rounded-2xl max-w-full {sizeToWidth(
|
||||
size
|
||||
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden"
|
||||
class=" m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
|
||||
? 'mx-2'
|
||||
: ''} shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden {className}"
|
||||
in:flyAndScale
|
||||
on:mousedown={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
470
src/lib/components/common/RichTextInput.svelte
Normal file
470
src/lib/components/common/RichTextInput.svelte
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const eventDispatch = createEventDispatcher();
|
||||
|
||||
import { EditorState, Plugin, TextSelection } from 'prosemirror-state';
|
||||
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { undo, redo, history } from 'prosemirror-history';
|
||||
import {
|
||||
schema,
|
||||
defaultMarkdownParser,
|
||||
MarkdownParser,
|
||||
defaultMarkdownSerializer
|
||||
} from 'prosemirror-markdown';
|
||||
|
||||
import {
|
||||
inputRules,
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
InputRule
|
||||
} from 'prosemirror-inputrules'; // Import input rules
|
||||
import { splitListItem, liftListItem, sinkListItem } from 'prosemirror-schema-list'; // Import from prosemirror-schema-list
|
||||
import { keymap } from 'prosemirror-keymap';
|
||||
import { baseKeymap, chainCommands } from 'prosemirror-commands';
|
||||
import { DOMParser, DOMSerializer, Schema, Fragment } from 'prosemirror-model';
|
||||
|
||||
export let className = 'input-prose';
|
||||
export let shiftEnter = false;
|
||||
|
||||
export let id = '';
|
||||
export let value = '';
|
||||
export let placeholder = 'Type here...';
|
||||
|
||||
let element: HTMLElement; // Element where ProseMirror will attach
|
||||
let state;
|
||||
let view;
|
||||
|
||||
// Plugin to add placeholder when the content is empty
|
||||
function placeholderPlugin(placeholder: string) {
|
||||
return new Plugin({
|
||||
props: {
|
||||
decorations(state) {
|
||||
const doc = state.doc;
|
||||
if (
|
||||
doc.childCount === 1 &&
|
||||
doc.firstChild.isTextblock &&
|
||||
doc.firstChild?.textContent === ''
|
||||
) {
|
||||
// If there's nothing in the editor, show the placeholder decoration
|
||||
const decoration = Decoration.node(0, doc.content.size, {
|
||||
'data-placeholder': placeholder,
|
||||
class: 'placeholder'
|
||||
});
|
||||
return DecorationSet.create(doc, [decoration]);
|
||||
}
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function unescapeMarkdown(text: string): string {
|
||||
return text
|
||||
.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, '$1') // unescape backslashed characters
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Custom parsing rule that creates proper paragraphs for newlines and empty lines
|
||||
function markdownToProseMirrorDoc(markdown: string) {
|
||||
// Split the markdown into lines
|
||||
const lines = markdown.split('\n\n');
|
||||
|
||||
// Create an array to hold our paragraph nodes
|
||||
const paragraphs = [];
|
||||
|
||||
// Process each line
|
||||
lines.forEach((line) => {
|
||||
if (line.trim() === '') {
|
||||
// For empty lines, create an empty paragraph
|
||||
paragraphs.push(schema.nodes.paragraph.create());
|
||||
} else {
|
||||
// For non-empty lines, parse as usual
|
||||
const doc = defaultMarkdownParser.parse(line);
|
||||
// Extract the content of the parsed document
|
||||
doc.content.forEach((node) => {
|
||||
paragraphs.push(node);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new document with these paragraphs
|
||||
return schema.node('doc', null, paragraphs);
|
||||
}
|
||||
|
||||
// Create a custom serializer for paragraphs
|
||||
// Custom paragraph serializer to preserve newlines for empty paragraphs (empty block).
|
||||
function serializeParagraph(state, node: Node) {
|
||||
const content = node.textContent.trim();
|
||||
|
||||
// If the paragraph is empty, just add an empty line.
|
||||
if (content === '') {
|
||||
state.write('\n\n');
|
||||
} else {
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
||||
|
||||
const customMarkdownSerializer = new defaultMarkdownSerializer.constructor(
|
||||
{
|
||||
...defaultMarkdownSerializer.nodes,
|
||||
|
||||
paragraph: (state, node) => {
|
||||
serializeParagraph(state, node); // Use custom paragraph serialization
|
||||
}
|
||||
|
||||
// Customize other block formats if needed
|
||||
},
|
||||
|
||||
// Copy marks directly from the original serializer (or customize them if necessary)
|
||||
defaultMarkdownSerializer.marks
|
||||
);
|
||||
|
||||
// Utility function to convert ProseMirror content back to markdown text
|
||||
function serializeEditorContent(doc) {
|
||||
const markdown = customMarkdownSerializer.serialize(doc);
|
||||
return unescapeMarkdown(markdown);
|
||||
}
|
||||
|
||||
// ---- Input Rules ----
|
||||
// Input rule for heading (e.g., # Headings)
|
||||
function headingRule(schema) {
|
||||
return textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, (match) => ({
|
||||
level: match[1].length
|
||||
}));
|
||||
}
|
||||
|
||||
// Input rule for bullet list (e.g., `- item`)
|
||||
function bulletListRule(schema) {
|
||||
return wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list);
|
||||
}
|
||||
|
||||
// Input rule for ordered list (e.g., `1. item`)
|
||||
function orderedListRule(schema) {
|
||||
return wrappingInputRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, (match) => ({
|
||||
order: +match[1]
|
||||
}));
|
||||
}
|
||||
|
||||
// Custom input rules for Bold/Italic (using * or _)
|
||||
function markInputRule(regexp: RegExp, markType: any) {
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const { tr } = state;
|
||||
if (match) {
|
||||
tr.replaceWith(start, end, schema.text(match[1], [markType.create()]));
|
||||
}
|
||||
return tr;
|
||||
});
|
||||
}
|
||||
|
||||
function boldRule(schema) {
|
||||
return markInputRule(/\*([^*]+)\*/, schema.marks.strong);
|
||||
}
|
||||
|
||||
function italicRule(schema) {
|
||||
return markInputRule(/\_([^*]+)\_/, schema.marks.em);
|
||||
}
|
||||
|
||||
// Initialize Editor State and View
|
||||
function afterSpacePress(state, dispatch) {
|
||||
// Get the position right after the space was naturally inserted by the browser.
|
||||
let { from, to, empty } = state.selection;
|
||||
|
||||
if (dispatch && empty) {
|
||||
let tr = state.tr;
|
||||
|
||||
// Check for any active marks at `from - 1` (the space we just inserted)
|
||||
const storedMarks = state.storedMarks || state.selection.$from.marks();
|
||||
|
||||
const hasBold = storedMarks.some((mark) => mark.type === state.schema.marks.strong);
|
||||
const hasItalic = storedMarks.some((mark) => mark.type === state.schema.marks.em);
|
||||
|
||||
// Remove marks from the space character (marks applied to the space character will be marked as false)
|
||||
if (hasBold) {
|
||||
tr = tr.removeMark(from - 1, from, state.schema.marks.strong);
|
||||
}
|
||||
if (hasItalic) {
|
||||
tr = tr.removeMark(from - 1, from, state.schema.marks.em);
|
||||
}
|
||||
|
||||
// Dispatch the resulting transaction to update the editor state
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleMark(markType) {
|
||||
return (state, dispatch) => {
|
||||
const { from, to } = state.selection;
|
||||
if (state.doc.rangeHasMark(from, to, markType)) {
|
||||
if (dispatch) dispatch(state.tr.removeMark(from, to, markType));
|
||||
return true;
|
||||
} else {
|
||||
if (dispatch) dispatch(state.tr.addMark(from, to, markType.create()));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function isInList(state) {
|
||||
const { $from } = state.selection;
|
||||
return (
|
||||
$from.parent.type === schema.nodes.paragraph && $from.node(-1).type === schema.nodes.list_item
|
||||
);
|
||||
}
|
||||
|
||||
function isEmptyListItem(state) {
|
||||
const { $from } = state.selection;
|
||||
return isInList(state) && $from.parent.content.size === 0 && $from.node(-1).childCount === 1;
|
||||
}
|
||||
|
||||
function exitList(state, dispatch) {
|
||||
return liftListItem(schema.nodes.list_item)(state, dispatch);
|
||||
}
|
||||
|
||||
function findNextTemplate(doc, from = 0) {
|
||||
const patterns = [
|
||||
{ start: '[', end: ']' },
|
||||
{ start: '{{', end: '}}' }
|
||||
];
|
||||
|
||||
let result = null;
|
||||
|
||||
doc.nodesBetween(from, doc.content.size, (node, pos) => {
|
||||
if (result) return false; // Stop if we've found a match
|
||||
if (node.isText) {
|
||||
const text = node.text;
|
||||
let index = Math.max(0, from - pos);
|
||||
while (index < text.length) {
|
||||
for (const pattern of patterns) {
|
||||
if (text.startsWith(pattern.start, index)) {
|
||||
const endIndex = text.indexOf(pattern.end, index + pattern.start.length);
|
||||
if (endIndex !== -1) {
|
||||
result = {
|
||||
from: pos + index,
|
||||
to: pos + endIndex + pattern.end.length
|
||||
};
|
||||
return false; // Stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function selectNextTemplate(state, dispatch) {
|
||||
const { doc, selection } = state;
|
||||
const from = selection.to;
|
||||
let template = findNextTemplate(doc, from);
|
||||
|
||||
if (!template) {
|
||||
// If not found, search from the beginning
|
||||
template = findNextTemplate(doc, 0);
|
||||
}
|
||||
|
||||
if (template) {
|
||||
if (dispatch) {
|
||||
const tr = state.tr.setSelection(TextSelection.create(doc, template.from, template.to));
|
||||
dispatch(tr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const initialDoc = markdownToProseMirrorDoc(value || ''); // Convert the initial content
|
||||
|
||||
state = EditorState.create({
|
||||
doc: initialDoc,
|
||||
schema,
|
||||
plugins: [
|
||||
history(),
|
||||
placeholderPlugin(placeholder),
|
||||
inputRules({
|
||||
rules: [
|
||||
headingRule(schema), // Handle markdown-style headings (# H1, ## H2, etc.)
|
||||
bulletListRule(schema), // Handle `-` or `*` input to start bullet list
|
||||
orderedListRule(schema), // Handle `1.` input to start ordered list
|
||||
boldRule(schema), // Bold input rule
|
||||
italicRule(schema) // Italic input rule
|
||||
]
|
||||
}),
|
||||
keymap({
|
||||
...baseKeymap,
|
||||
'Mod-z': undo,
|
||||
'Mod-y': redo,
|
||||
Enter: (state, dispatch, view) => {
|
||||
if (shiftEnter) {
|
||||
eventDispatch('enter');
|
||||
return true;
|
||||
}
|
||||
return chainCommands(
|
||||
(state, dispatch, view) => {
|
||||
if (isEmptyListItem(state)) {
|
||||
return exitList(state, dispatch);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
(state, dispatch, view) => {
|
||||
if (isInList(state)) {
|
||||
return splitListItem(schema.nodes.list_item)(state, dispatch);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
baseKeymap.Enter
|
||||
)(state, dispatch, view);
|
||||
},
|
||||
|
||||
'Shift-Enter': (state, dispatch, view) => {
|
||||
if (shiftEnter) {
|
||||
return chainCommands(
|
||||
(state, dispatch, view) => {
|
||||
if (isEmptyListItem(state)) {
|
||||
return exitList(state, dispatch);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
(state, dispatch, view) => {
|
||||
if (isInList(state)) {
|
||||
return splitListItem(schema.nodes.list_item)(state, dispatch);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
baseKeymap.Enter
|
||||
)(state, dispatch, view);
|
||||
} else {
|
||||
return baseKeymap.Enter(state, dispatch, view);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// Prevent default tab navigation and provide indent/outdent behavior inside lists:
|
||||
Tab: chainCommands((state, dispatch, view) => {
|
||||
const { $from } = state.selection;
|
||||
if (isInList(state)) {
|
||||
return sinkListItem(schema.nodes.list_item)(state, dispatch);
|
||||
} else {
|
||||
return selectNextTemplate(state, dispatch);
|
||||
}
|
||||
return true; // Prevent Tab from moving the focus
|
||||
}),
|
||||
'Shift-Tab': (state, dispatch, view) => {
|
||||
const { $from } = state.selection;
|
||||
if (isInList(state)) {
|
||||
return liftListItem(schema.nodes.list_item)(state, dispatch);
|
||||
}
|
||||
return true; // Prevent Shift-Tab from moving the focus
|
||||
},
|
||||
'Mod-b': toggleMark(schema.marks.strong),
|
||||
'Mod-i': toggleMark(schema.marks.em)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
view = new EditorView(element, {
|
||||
state,
|
||||
dispatchTransaction(transaction) {
|
||||
// Update editor state
|
||||
let newState = view.state.apply(transaction);
|
||||
view.updateState(newState);
|
||||
|
||||
value = serializeEditorContent(newState.doc); // Convert ProseMirror content to markdown text
|
||||
eventDispatch('input', { value });
|
||||
},
|
||||
handleDOMEvents: {
|
||||
focus: (view, event) => {
|
||||
eventDispatch('focus', { event });
|
||||
return false;
|
||||
},
|
||||
keypress: (view, event) => {
|
||||
eventDispatch('keypress', { event });
|
||||
return false;
|
||||
},
|
||||
keydown: (view, event) => {
|
||||
eventDispatch('keydown', { event });
|
||||
return false;
|
||||
},
|
||||
paste: (view, event) => {
|
||||
if (event.clipboardData) {
|
||||
// Check if the pasted content contains image files
|
||||
const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
|
||||
file.type.startsWith('image/')
|
||||
);
|
||||
|
||||
// Check for image in dataTransfer items (for cases where files are not available)
|
||||
const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
|
||||
item.type.startsWith('image/')
|
||||
);
|
||||
if (hasImageFile) {
|
||||
// If there's an image, dispatch the event to the parent
|
||||
eventDispatch('paste', { event });
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasImageItem) {
|
||||
// If there's an image item, dispatch the event to the parent
|
||||
eventDispatch('paste', { event });
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For all other cases (text, formatted text, etc.), let ProseMirror handle it
|
||||
return false;
|
||||
},
|
||||
// Handle space input after browser has completed it
|
||||
keyup: (view, event) => {
|
||||
if (event.key === ' ' && event.code === 'Space') {
|
||||
afterSpacePress(view.state, view.dispatch);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
attributes: { id }
|
||||
});
|
||||
});
|
||||
|
||||
// Reinitialize the editor if the value is externally changed (i.e. when `value` is updated)
|
||||
$: if (view && value !== serializeEditorContent(view.state.doc)) {
|
||||
const newDoc = markdownToProseMirrorDoc(value || '');
|
||||
|
||||
const newState = EditorState.create({
|
||||
doc: newDoc,
|
||||
schema,
|
||||
plugins: view.state.plugins,
|
||||
selection: TextSelection.atEnd(newDoc) // This sets the cursor at the end
|
||||
});
|
||||
view.updateState(newState);
|
||||
|
||||
if (value !== '') {
|
||||
// After updating the state, try to find and select the next template
|
||||
setTimeout(() => {
|
||||
const templateFound = selectNextTemplate(view.state, view.dispatch);
|
||||
if (!templateFound) {
|
||||
// If no template found, set cursor at the end
|
||||
const endPos = view.state.doc.content.size;
|
||||
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, endPos)));
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy ProseMirror instance on unmount
|
||||
onDestroy(() => {
|
||||
view?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}"></div>
|
||||
|
|
@ -1,34 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Tooltip from '../Tooltip.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Badge from '../Badge.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let tags = [];
|
||||
</script>
|
||||
|
||||
{#each tags as tag}
|
||||
<div
|
||||
class="px-2 py-[0.5px] gap-0.5 flex justify-between h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white"
|
||||
>
|
||||
<div class=" text-[0.7rem] font-medium self-center line-clamp-1">
|
||||
{tag.name}
|
||||
</div>
|
||||
<button
|
||||
class="h-full flex self-center cursor-pointer"
|
||||
on:click={() => {
|
||||
dispatch('delete', tag.name);
|
||||
}}
|
||||
type="button"
|
||||
<Tooltip content={tag.name}>
|
||||
<div
|
||||
class="relative group px-1.5 py-[0.2px] gap-0.5 flex justify-between h-fit max-h-fit w-fit items-center rounded-full bg-gray-500/20 text-gray-700 dark:text-gray-200 transition cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-3 m-auto self-center translate-y-[0.3px] translate-x-[3px]"
|
||||
>
|
||||
<path
|
||||
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class=" text-[0.7rem] font-medium self-center line-clamp-1 w-fit">
|
||||
{tag.name}
|
||||
</div>
|
||||
<div class="absolute invisible right-0.5 group-hover:visible transition">
|
||||
<button
|
||||
class="rounded-full border bg-white dark:bg-gray-700 h-full flex self-center cursor-pointer"
|
||||
on:click={() => {
|
||||
dispatch('delete', tag.name);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<XMark className="size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
|
|
|
|||
37
src/lib/components/common/Textarea.svelte
Normal file
37
src/lib/components/common/Textarea.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
export let value = '';
|
||||
export let placeholder = '';
|
||||
|
||||
export let className =
|
||||
'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none h-full';
|
||||
|
||||
let textareaElement;
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
if (textareaElement) {
|
||||
setInterval(adjustHeight, 0);
|
||||
}
|
||||
});
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (textareaElement) {
|
||||
textareaElement.style.height = '';
|
||||
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={textareaElement}
|
||||
bind:value
|
||||
{placeholder}
|
||||
class={className}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
rows="1"
|
||||
/>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Tags from '$lib/components/chat/Tags.svelte';
|
||||
import Share from '$lib/components/icons/Share.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import Star from '$lib/components/icons/Star.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let pinHandler: Function;
|
||||
export let shareHandler: Function;
|
||||
export let cloneChatHandler: Function;
|
||||
export let archiveChatHandler: Function;
|
||||
export let renameHandler: Function;
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
|
||||
export let chatId = '';
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('More')}>
|
||||
<slot />
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
pinHandler();
|
||||
}}
|
||||
>
|
||||
<Star strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Pin')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
renameHandler();
|
||||
}}
|
||||
>
|
||||
<Pencil strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
cloneChatHandler();
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicate strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Clone')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
>
|
||||
<ArchiveBox strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Archive')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
>
|
||||
<Share />
|
||||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags
|
||||
{chatId}
|
||||
on:close={() => {
|
||||
show = false;
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
19
src/lib/components/icons/Document.svelte
Normal file
19
src/lib/components/icons/Document.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
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.25m2.25 0H5.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"
|
||||
/>
|
||||
</svg>
|
||||
19
src/lib/components/icons/Download.svelte
Normal file
19
src/lib/components/icons/Download.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
10
src/lib/components/icons/Mic.svelte
Normal file
10
src/lib/components/icons/Mic.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
|
||||
<path d="M7 4a3 3 0 0 1 6 0v6a3 3 0 1 1-6 0V4Z" />
|
||||
<path
|
||||
d="M5.5 9.643a.75.75 0 0 0-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-1.5v-1.546A6.001 6.001 0 0 0 16 10v-.357a.75.75 0 0 0-1.5 0V10a4.5 4.5 0 0 1-9 0v-.357Z"
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
||||
></div>
|
||||
|
||||
<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem] bg-transparen">
|
||||
<div class=" flex max-w-full w-full mx-auto px-2 pt-0.5 md:px-[1rem] bg-transparent">
|
||||
<div class="flex items-center w-full max-w-full">
|
||||
<div
|
||||
class="{$showSidebar
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="end"
|
||||
|
|
@ -152,6 +152,30 @@
|
|||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
{#if !$temporaryChatEnabled}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-share-button"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-overview-button"
|
||||
|
|
@ -178,47 +202,6 @@
|
|||
<div class="flex items-center">{$i18n.t('Artifacts')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-copy-button"
|
||||
on:click={async () => {
|
||||
const res = await copyToClipboard(await getChatAsText()).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Copied to clipboard'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clipboard className=" size-4" strokeWidth="1.5" />
|
||||
<div class="flex items-center">{$i18n.t('Copy')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
{#if !$temporaryChatEnabled}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-share-button"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
|
|
@ -241,7 +224,7 @@
|
|||
<div class="flex items-center">{$i18n.t('Download')}</div>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
transition={flyAndScale}
|
||||
sideOffset={8}
|
||||
>
|
||||
|
|
@ -273,8 +256,25 @@
|
|||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-copy-button"
|
||||
on:click={async () => {
|
||||
const res = await copyToClipboard(await getChatAsText()).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Copied to clipboard'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clipboard className=" size-4" strokeWidth="1.5" />
|
||||
<div class="flex items-center">{$i18n.t('Copy')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
{#if !$temporaryChatEnabled}
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
<hr class="border-gray-50 dark:border-gray-850 my-0.5" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags chatId={chat.id} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
user,
|
||||
|
|
@ -28,25 +30,25 @@
|
|||
createNewChat,
|
||||
getPinnedChatList,
|
||||
toggleChatPinnedStatusById,
|
||||
getChatPinnedStatusById
|
||||
getChatPinnedStatusById,
|
||||
getChatById,
|
||||
updateChatFolderIdById,
|
||||
importChat
|
||||
} from '$lib/apis/chats';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
|
||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||
import ChatItem from './Sidebar/ChatItem.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
||||
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
||||
import { select } from 'd3-selection';
|
||||
import SearchInput from './Sidebar/SearchInput.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||
import Collapsible from '../common/Collapsible.svelte';
|
||||
import Folder from '../common/Folder.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
|
||||
import Folders from './Sidebar/Folders.svelte';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
|
|
@ -56,26 +58,105 @@
|
|||
let shiftKey = false;
|
||||
|
||||
let selectedChatId = null;
|
||||
let deleteChat = null;
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
let showDropdown = false;
|
||||
|
||||
let selectedTagName = null;
|
||||
|
||||
let showPinnedChat = true;
|
||||
|
||||
// Pagination variables
|
||||
let chatListLoading = false;
|
||||
let allChatsLoaded = false;
|
||||
|
||||
let folders = {};
|
||||
|
||||
const initFolders = async () => {
|
||||
const folderList = await getFolders(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
folders = {};
|
||||
|
||||
// First pass: Initialize all folder entries
|
||||
for (const folder of folderList) {
|
||||
// Ensure folder is added to folders with its data
|
||||
folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
|
||||
}
|
||||
|
||||
// Second pass: Tie child folders to their parents
|
||||
for (const folder of folderList) {
|
||||
if (folder.parent_id) {
|
||||
// Ensure the parent folder is initialized if it doesn't exist
|
||||
if (!folders[folder.parent_id]) {
|
||||
folders[folder.parent_id] = {}; // Create a placeholder if not already present
|
||||
}
|
||||
|
||||
// Initialize childrenIds array if it doesn't exist and add the current folder id
|
||||
folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
|
||||
? [...folders[folder.parent_id].childrenIds, folder.id]
|
||||
: [folder.id];
|
||||
|
||||
// Sort the children by updated_at field
|
||||
folders[folder.parent_id].childrenIds.sort((a, b) => {
|
||||
return folders[b].updated_at - folders[a].updated_at;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = async (name = 'Untitled') => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
|
||||
if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
|
||||
// If a folder with the same name already exists, append a number to the name
|
||||
let i = 1;
|
||||
while (
|
||||
rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase())
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
|
||||
name = `${name} ${i}`;
|
||||
}
|
||||
|
||||
// Add a dummy folder to the list to show the user that the folder is being created
|
||||
const tempId = uuidv4();
|
||||
folders = {
|
||||
...folders,
|
||||
tempId: {
|
||||
id: tempId,
|
||||
name: name,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
const res = await createNewFolder(localStorage.token, name).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await initFolders();
|
||||
}
|
||||
};
|
||||
|
||||
const initChatList = async () => {
|
||||
// Reset pagination variables
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initFolders();
|
||||
|
||||
currentChatPage.set(1);
|
||||
allChatsLoaded = false;
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
if (search) {
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
|
||||
} else {
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
|
||||
// Enable pagination
|
||||
scrollPaginationEnabled.set(true);
|
||||
|
|
@ -106,7 +187,6 @@
|
|||
const searchDebounceHandler = async () => {
|
||||
console.log('search', search);
|
||||
chats.set(null);
|
||||
selectedTagName = null;
|
||||
|
||||
if (searchDebounceTimeout) {
|
||||
clearTimeout(searchDebounceTimeout);
|
||||
|
|
@ -117,6 +197,7 @@
|
|||
return;
|
||||
} else {
|
||||
searchDebounceTimeout = setTimeout(async () => {
|
||||
allChatsLoaded = false;
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search));
|
||||
|
||||
|
|
@ -127,26 +208,16 @@
|
|||
}
|
||||
};
|
||||
|
||||
const deleteChatHandler = async (id) => {
|
||||
const res = await deleteChatById(localStorage.token, id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
|
||||
if ($chatId === id) {
|
||||
await chatId.set('');
|
||||
await tick();
|
||||
goto('/');
|
||||
const importChatHandler = async (items, pinned = false, folderId = null) => {
|
||||
console.log('importChatHandler', items, pinned, folderId);
|
||||
for (const item of items) {
|
||||
console.log(item);
|
||||
if (item.chat) {
|
||||
await importChat(localStorage.token, item.chat, item?.meta ?? {}, pinned, folderId);
|
||||
}
|
||||
|
||||
allChatsLoaded = false;
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
}
|
||||
|
||||
initChatList();
|
||||
};
|
||||
|
||||
const inputFilesHandler = async (files) => {
|
||||
|
|
@ -158,18 +229,11 @@
|
|||
const content = e.target.result;
|
||||
|
||||
try {
|
||||
const items = JSON.parse(content);
|
||||
|
||||
for (const item of items) {
|
||||
if (item.chat) {
|
||||
await createNewChat(localStorage.token, item.chat);
|
||||
}
|
||||
}
|
||||
const chatItems = JSON.parse(content);
|
||||
importChatHandler(chatItems);
|
||||
} catch {
|
||||
toast.error($i18n.t(`Invalid file format.`));
|
||||
}
|
||||
|
||||
initChatList();
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
|
|
@ -179,29 +243,27 @@
|
|||
const tagEventHandler = async (type, tagName, chatId) => {
|
||||
console.log(type, tagName, chatId);
|
||||
if (type === 'delete') {
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
|
||||
initChatList();
|
||||
} else if (type === 'add') {
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
|
||||
initChatList();
|
||||
}
|
||||
};
|
||||
|
||||
let dragged = false;
|
||||
let draggedOver = false;
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Check if a file is being dragged.
|
||||
// Check if a file is being draggedOver.
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
dragged = true;
|
||||
draggedOver = true;
|
||||
} else {
|
||||
dragged = false;
|
||||
draggedOver = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
dragged = false;
|
||||
draggedOver = false;
|
||||
};
|
||||
|
||||
const onDrop = async (e) => {
|
||||
|
|
@ -218,7 +280,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
dragged = false; // Reset dragged status after drop
|
||||
draggedOver = false; // Reset draggedOver status after drop
|
||||
};
|
||||
|
||||
let touchstart;
|
||||
|
|
@ -284,7 +346,6 @@
|
|||
localStorage.sidebar = value;
|
||||
});
|
||||
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
await initChatList();
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
|
|
@ -324,23 +385,10 @@
|
|||
<ArchivedChatsModal
|
||||
bind:show={$showArchivedChats}
|
||||
on:change={async () => {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
await initChatList();
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete chat?')}
|
||||
on:confirm={() => {
|
||||
deleteChatHandler(deleteChat.id);
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500 flex-1 line-clamp-3">
|
||||
{$i18n.t('This will delete')} <span class=" font-semibold">{deleteChat.title}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
{#if $showSidebar}
|
||||
|
|
@ -361,18 +409,6 @@
|
|||
"
|
||||
data-state={$showSidebar}
|
||||
>
|
||||
{#if dragged}
|
||||
<div
|
||||
class="absolute w-full h-full max-h-full backdrop-blur bg-gray-800/40 flex justify-center z-[999] touch-none pointer-events-none"
|
||||
>
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<AddFilesPlaceholder
|
||||
title={$i18n.t('Drop Chat Export')}
|
||||
content={$i18n.t('Drop a chat export file here to import it.')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden z-50 {$showSidebar
|
||||
? ''
|
||||
|
|
@ -381,7 +417,7 @@
|
|||
<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
id="sidebar-new-chat-button"
|
||||
class="flex flex-1 justify-between rounded-xl px-2 h-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class="flex flex-1 justify-between rounded-lg px-2 h-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
href="/"
|
||||
draggable="false"
|
||||
on:click={async () => {
|
||||
|
|
@ -425,7 +461,7 @@
|
|||
</a>
|
||||
|
||||
<button
|
||||
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class=" cursor-pointer px-2 py-2 flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showSidebar.set(!$showSidebar);
|
||||
}}
|
||||
|
|
@ -452,7 +488,7 @@
|
|||
{#if $user?.role === 'admin'}
|
||||
<div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200">
|
||||
<a
|
||||
class="flex-grow flex space-x-3 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class="flex-grow flex space-x-3 rounded-lg px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
href="/workspace"
|
||||
on:click={() => {
|
||||
selectedChatId = null;
|
||||
|
|
@ -493,6 +529,19 @@
|
|||
<div class="absolute z-40 w-full h-full flex justify-center"></div>
|
||||
{/if}
|
||||
|
||||
<div class="absolute z-40 right-4 top-1">
|
||||
<Tooltip content={$i18n.t('New folder')}>
|
||||
<button
|
||||
class="p-1 rounded-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
createFolder();
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<SearchInput
|
||||
bind:value={search}
|
||||
on:input={searchDebounceHandler}
|
||||
|
|
@ -510,33 +559,60 @@
|
|||
{/if}
|
||||
|
||||
{#if !search && $pinnedChats.length > 0}
|
||||
<div class=" flex flex-col space-y-1">
|
||||
<div class="flex flex-col space-y-1 rounded-xl">
|
||||
<Folder
|
||||
className="px-2"
|
||||
bind:open={showPinnedChat}
|
||||
on:change={(e) => {
|
||||
localStorage.setItem('showPinnedChat', e.detail);
|
||||
console.log(e.detail);
|
||||
}}
|
||||
on:import={(e) => {
|
||||
importChatHandler(e.detail, true);
|
||||
}}
|
||||
on:drop={async (e) => {
|
||||
const { id } = e.detail;
|
||||
const { type, id } = e.detail;
|
||||
|
||||
const status = await getChatPinnedStatusById(localStorage.token, id);
|
||||
if (type === 'chat') {
|
||||
const chat = await getChatById(localStorage.token, id);
|
||||
|
||||
if (!status) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
if (chat) {
|
||||
console.log(chat);
|
||||
if (chat.folder_id) {
|
||||
const res = await updateChatFolderIdById(
|
||||
localStorage.token,
|
||||
chat.id,
|
||||
null
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initChatList();
|
||||
if (res) {
|
||||
initChatList();
|
||||
}
|
||||
}
|
||||
|
||||
if (!chat.pinned) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
|
||||
if (res) {
|
||||
initChatList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
name={$i18n.t('Pinned')}
|
||||
>
|
||||
<div class="pl-2 mt-0.5 flex flex-col overflow-y-auto scrollbar-hidden">
|
||||
<div
|
||||
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
||||
>
|
||||
{#each $pinnedChats as chat, idx}
|
||||
<ChatItem
|
||||
{chat}
|
||||
className=""
|
||||
id={chat.id}
|
||||
title={chat.title}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
|
|
@ -545,13 +621,8 @@
|
|||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
on:change={async () => {
|
||||
initChatList();
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
const { type, name } = e.detail;
|
||||
|
|
@ -564,25 +635,78 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
|
||||
<div class=" flex-1 flex flex-col overflow-y-auto scrollbar-hidden">
|
||||
{#if !search && folders}
|
||||
<Folders
|
||||
{folders}
|
||||
on:import={(e) => {
|
||||
const { folderId, items } = e.detail;
|
||||
importChatHandler(items, false, folderId);
|
||||
}}
|
||||
on:update={async (e) => {
|
||||
initChatList();
|
||||
}}
|
||||
on:change={async () => {
|
||||
initChatList();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Folder
|
||||
collapsible={false}
|
||||
collapsible={!search}
|
||||
className="px-2 mt-0.5"
|
||||
name={$i18n.t('All chats')}
|
||||
on:import={(e) => {
|
||||
importChatHandler(e.detail);
|
||||
}}
|
||||
on:drop={async (e) => {
|
||||
const { id } = e.detail;
|
||||
const { type, id } = e.detail;
|
||||
|
||||
const status = await getChatPinnedStatusById(localStorage.token, id);
|
||||
if (type === 'chat') {
|
||||
const chat = await getChatById(localStorage.token, id);
|
||||
|
||||
if (status) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
if (chat) {
|
||||
console.log(chat);
|
||||
if (chat.folder_id) {
|
||||
const res = await updateChatFolderIdById(localStorage.token, chat.id, null).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
initChatList();
|
||||
}
|
||||
}
|
||||
|
||||
if (chat.pinned) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
|
||||
if (res) {
|
||||
initChatList();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type === 'folder') {
|
||||
if (folders[id].parent_id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await updateFolderParentIdById(localStorage.token, id, null).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initChatList();
|
||||
await initFolders();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="pt-2 pl-2">
|
||||
<div class="pt-1.5">
|
||||
{#if $chats}
|
||||
{#each $chats as chat, idx}
|
||||
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
|
||||
|
|
@ -615,7 +739,9 @@
|
|||
{/if}
|
||||
|
||||
<ChatItem
|
||||
{chat}
|
||||
className=""
|
||||
id={chat.id}
|
||||
title={chat.title}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
|
|
@ -624,13 +750,8 @@
|
|||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
on:change={async () => {
|
||||
initChatList();
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
const { type, name } = e.detail;
|
||||
|
|
|
|||
|
|
@ -28,13 +28,21 @@
|
|||
} from '$lib/stores';
|
||||
|
||||
import ChatMenu from './ChatMenu.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Document from '$lib/components/icons/Document.svelte';
|
||||
|
||||
export let className = '';
|
||||
|
||||
export let id;
|
||||
export let title;
|
||||
|
||||
export let chat;
|
||||
export let selected = false;
|
||||
export let shiftKey = false;
|
||||
|
||||
|
|
@ -43,7 +51,7 @@
|
|||
let showShareChatModal = false;
|
||||
let confirmEdit = false;
|
||||
|
||||
let chatTitle = chat.title;
|
||||
let chatTitle = title;
|
||||
|
||||
const editChatTitle = async (id, title) => {
|
||||
if (title === '') {
|
||||
|
|
@ -78,13 +86,27 @@
|
|||
}
|
||||
};
|
||||
|
||||
const deleteChatHandler = async (id) => {
|
||||
const res = await deleteChatById(localStorage.token, id).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
if ($chatId === id) {
|
||||
await chatId.set('');
|
||||
await tick();
|
||||
goto('/');
|
||||
}
|
||||
|
||||
dispatch('change');
|
||||
}
|
||||
};
|
||||
|
||||
const archiveChatHandler = async (id) => {
|
||||
await archiveChatById(localStorage.token, id);
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
|
|
@ -93,7 +115,7 @@
|
|||
|
||||
let itemElement;
|
||||
|
||||
let drag = false;
|
||||
let dragged = false;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
|
|
@ -102,28 +124,35 @@
|
|||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
|
||||
const onDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
|
||||
// Set the data to be transferred
|
||||
event.dataTransfer.setData(
|
||||
'text/plain',
|
||||
JSON.stringify({
|
||||
id: chat.id
|
||||
type: 'chat',
|
||||
id: id
|
||||
})
|
||||
);
|
||||
|
||||
drag = true;
|
||||
dragged = true;
|
||||
itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
|
||||
};
|
||||
|
||||
const onDrag = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
};
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
itemElement.style.opacity = '1'; // Reset visual cue after drag
|
||||
drag = false;
|
||||
dragged = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -144,26 +173,42 @@
|
|||
itemElement.removeEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={chat.id} />
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={id} />
|
||||
|
||||
{#if drag && x && y}
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete chat?')}
|
||||
on:confirm={() => {
|
||||
deleteChatHandler(id);
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500 flex-1 line-clamp-3">
|
||||
{$i18n.t('This will delete')} <span class=" font-semibold">{title}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
{#if dragged && x && y}
|
||||
<DragGhost {x} {y}>
|
||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-44">
|
||||
<div>
|
||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
||||
<div class="flex items-center gap-1">
|
||||
<Document className=" size-[18px]" strokeWidth="2" />
|
||||
<div class=" text-xs text-white line-clamp-1">
|
||||
{chat.title}
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragGhost>
|
||||
{/if}
|
||||
|
||||
<div bind:this={itemElement} class=" w-full pr-2 relative group" draggable="true">
|
||||
<div bind:this={itemElement} class=" w-full {className} relative group" draggable="true">
|
||||
{#if confirmEdit}
|
||||
<div
|
||||
class=" w-full flex justify-between rounded-xl px-2.5 py-2 {chat.id === $chatId || confirmEdit
|
||||
class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
|
||||
confirmEdit
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: selected
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
|
|
@ -177,12 +222,13 @@
|
|||
</div>
|
||||
{:else}
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-lg px-2.5 py-2 {chat.id === $chatId || confirmEdit
|
||||
class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
|
||||
confirmEdit
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: selected
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
href="/c/{chat.id}"
|
||||
href="/c/{id}"
|
||||
on:click={() => {
|
||||
dispatch('select');
|
||||
|
||||
|
|
@ -191,7 +237,7 @@
|
|||
}
|
||||
}}
|
||||
on:dblclick={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitle = title;
|
||||
confirmEdit = true;
|
||||
}}
|
||||
on:mouseenter={(e) => {
|
||||
|
|
@ -205,7 +251,7 @@
|
|||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
<div class=" text-left self-center overflow-hidden w-full h-[20px]">
|
||||
{chat.title}
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
|
@ -214,12 +260,14 @@
|
|||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="
|
||||
{chat.id === $chatId || confirmEdit
|
||||
{id === $chatId || confirmEdit
|
||||
? 'from-gray-200 dark:from-gray-900'
|
||||
: selected
|
||||
? 'from-gray-100 dark:from-gray-950'
|
||||
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
|
||||
absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80%
|
||||
absolute {className === 'pr-2'
|
||||
? 'right-[8px]'
|
||||
: 'right-0'} top-[4px] py-1 pr-0.5 mr-1.5 pl-5 bg-gradient-to-l from-80%
|
||||
|
||||
to-transparent"
|
||||
on:mouseenter={(e) => {
|
||||
|
|
@ -230,28 +278,19 @@
|
|||
}}
|
||||
>
|
||||
{#if confirmEdit}
|
||||
<div class="flex self-center space-x-1.5 z-10">
|
||||
<div
|
||||
class="flex self-center items-center space-x-1.5 z-10 translate-y-[0.5px] -translate-x-[0.5px]"
|
||||
>
|
||||
<Tooltip content={$i18n.t('Confirm')}>
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
editChatTitle(chat.id, chatTitle);
|
||||
editChatTitle(id, chatTitle);
|
||||
confirmEdit = false;
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Check className=" size-3.5" strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -263,16 +302,7 @@
|
|||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -282,7 +312,7 @@
|
|||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
archiveChatHandler(id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -294,7 +324,7 @@
|
|||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
dispatch('delete', 'shift');
|
||||
deleteChatHandler(id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -305,29 +335,29 @@
|
|||
{:else}
|
||||
<div class="flex self-center space-x-1 z-10">
|
||||
<ChatMenu
|
||||
chatId={chat.id}
|
||||
chatId={id}
|
||||
cloneChatHandler={() => {
|
||||
cloneChatHandler(chat.id);
|
||||
cloneChatHandler(id);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
showShareChatModal = true;
|
||||
}}
|
||||
archiveChatHandler={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
archiveChatHandler(id);
|
||||
}}
|
||||
renameHandler={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitle = title;
|
||||
|
||||
confirmEdit = true;
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
dispatch('delete');
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {
|
||||
dispatch('unselect');
|
||||
}}
|
||||
on:change={async () => {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
dispatch('change');
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
dispatch('tag', e.detail);
|
||||
|
|
@ -353,13 +383,13 @@
|
|||
</button>
|
||||
</ChatMenu>
|
||||
|
||||
{#if chat.id === $chatId}
|
||||
{#if id === $chatId}
|
||||
<!-- Shortcut support using "delete-chat-button" id -->
|
||||
<button
|
||||
id="delete-chat-button"
|
||||
class="hidden"
|
||||
on:click={() => {
|
||||
dispatch('delete');
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
|
|
@ -15,8 +18,15 @@
|
|||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import Bookmark from '$lib/components/icons/Bookmark.svelte';
|
||||
import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
|
||||
import { getChatPinnedStatusById, toggleChatPinnedStatusById } from '$lib/apis/chats';
|
||||
import {
|
||||
getChatById,
|
||||
getChatPinnedStatusById,
|
||||
toggleChatPinnedStatusById
|
||||
} from '$lib/apis/chats';
|
||||
import { chats } from '$lib/stores';
|
||||
import { createMessagesList } from '$lib/utils';
|
||||
import { downloadChatAsPDF } from '$lib/apis/utils';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -41,6 +51,70 @@
|
|||
pinned = await getChatPinnedStatusById(localStorage.token, chatId);
|
||||
};
|
||||
|
||||
const getChatAsText = async (chat) => {
|
||||
const history = chat.chat.history;
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const chatText = messages.reduce((a, message, i, arr) => {
|
||||
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
|
||||
}, '');
|
||||
|
||||
return chatText.trim();
|
||||
};
|
||||
|
||||
const downloadTxt = async () => {
|
||||
const chat = await getChatById(localStorage.token, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatText = await getChatAsText(chat);
|
||||
let blob = new Blob([chatText], {
|
||||
type: 'text/plain'
|
||||
});
|
||||
|
||||
saveAs(blob, `chat-${chat.chat.title}.txt`);
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
const chat = await getChatById(localStorage.token, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const history = chat.chat.history;
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const blob = await downloadChatAsPDF(chat.chat.title, messages);
|
||||
|
||||
// Create a URL for the blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a link element to trigger the download
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `chat-${chat.chat.title}.pdf`;
|
||||
|
||||
// Append the link to the body and click it programmatically
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Remove the link from the body
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the URL to release memory
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadJSONExport = async () => {
|
||||
const chat = await getChatById(localStorage.token, chatId);
|
||||
|
||||
if (chat) {
|
||||
let blob = new Blob([JSON.stringify([chat])], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
checkPinned();
|
||||
}
|
||||
|
|
@ -60,14 +134,14 @@
|
|||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
pinHandler();
|
||||
}}
|
||||
|
|
@ -82,7 +156,7 @@
|
|||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
renameHandler();
|
||||
}}
|
||||
|
|
@ -92,7 +166,7 @@
|
|||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
cloneChatHandler();
|
||||
}}
|
||||
|
|
@ -102,7 +176,7 @@
|
|||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
|
|
@ -112,7 +186,7 @@
|
|||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
|
|
@ -121,8 +195,48 @@
|
|||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
>
|
||||
<Download strokeWidth="2" />
|
||||
|
||||
<div class="flex items-center">{$i18n.t('Download')}</div>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
transition={flyAndScale}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
downloadJSONExport();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
downloadTxt();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
downloadPdf();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
|
|
@ -131,7 +245,7 @@
|
|||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
<hr class="border-gray-50 dark:border-gray-850 my-0.5" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags
|
||||
|
|
|
|||
35
src/lib/components/layout/Sidebar/Folders.svelte
Normal file
35
src/lib/components/layout/Sidebar/Folders.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||
export let folders = {};
|
||||
|
||||
let folderList = [];
|
||||
// Get the list of folders that have no parent, sorted by name alphabetically
|
||||
$: folderList = Object.keys(folders)
|
||||
.filter((key) => folders[key].parent_id === null)
|
||||
.sort((a, b) =>
|
||||
folders[a].name.localeCompare(folders[b].name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#each folderList as folderId (folderId)}
|
||||
<RecursiveFolder
|
||||
className="px-2"
|
||||
{folders}
|
||||
{folderId}
|
||||
on:import={(e) => {
|
||||
dispatch('import', e.detail);
|
||||
}}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
70
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte
Normal file
70
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
dispatch('close');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('More')}>
|
||||
<slot />
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[160px] rounded-lg px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('rename');
|
||||
}}
|
||||
>
|
||||
<Pencil strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('export');
|
||||
}}
|
||||
>
|
||||
<Download strokeWidth="2" />
|
||||
|
||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('delete');
|
||||
}}
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
482
src/lib/components/layout/Sidebar/RecursiveFolder.svelte
Normal file
482
src/lib/components/layout/Sidebar/RecursiveFolder.svelte
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import ChevronDown from '../../icons/ChevronDown.svelte';
|
||||
import ChevronRight from '../../icons/ChevronRight.svelte';
|
||||
import Collapsible from '../../common/Collapsible.svelte';
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import {
|
||||
deleteFolderById,
|
||||
updateFolderIsExpandedById,
|
||||
updateFolderNameById,
|
||||
updateFolderParentIdById
|
||||
} from '$lib/apis/folders';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getChatsByFolderId, updateChatFolderIdById } from '$lib/apis/chats';
|
||||
import ChatItem from './ChatItem.svelte';
|
||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
export let open = false;
|
||||
|
||||
export let folders;
|
||||
export let folderId;
|
||||
|
||||
export let className = '';
|
||||
|
||||
export let parentDragged = false;
|
||||
|
||||
let folderElement;
|
||||
|
||||
let edit = false;
|
||||
|
||||
let draggedOver = false;
|
||||
let dragged = false;
|
||||
|
||||
let name = '';
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (dragged || parentDragged) {
|
||||
return;
|
||||
}
|
||||
draggedOver = true;
|
||||
};
|
||||
|
||||
const onDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (dragged || parentDragged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (folderElement.contains(e.target)) {
|
||||
console.log('Dropped on the Button');
|
||||
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
// Iterate over all items in the DataTransferItemList use functional programming
|
||||
for (const item of Array.from(e.dataTransfer.items)) {
|
||||
// If dropped items aren't files, reject them
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file && file.type === 'application/json') {
|
||||
console.log('Dropped file is a JSON file!');
|
||||
|
||||
// Read the JSON file with FileReader
|
||||
const reader = new FileReader();
|
||||
reader.onload = async function (event) {
|
||||
try {
|
||||
const fileContent = JSON.parse(event.target.result);
|
||||
open = true;
|
||||
dispatch('import', {
|
||||
folderId: folderId,
|
||||
items: fileContent
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start reading the file
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
console.error('Only JSON file types are supported.');
|
||||
}
|
||||
|
||||
console.log(file);
|
||||
} else {
|
||||
// Handle the drag-and-drop data for folders or chats (same as before)
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
console.log(data);
|
||||
|
||||
const { type, id } = data;
|
||||
|
||||
if (type === 'folder') {
|
||||
open = true;
|
||||
if (id === folderId) {
|
||||
return;
|
||||
}
|
||||
// Move the folder
|
||||
const res = await updateFolderParentIdById(localStorage.token, id, folderId).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
dispatch('update');
|
||||
}
|
||||
} else if (type === 'chat') {
|
||||
open = true;
|
||||
|
||||
// Move the chat
|
||||
const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
dispatch('update');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draggedOver = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
if (dragged || parentDragged) {
|
||||
return;
|
||||
}
|
||||
|
||||
draggedOver = false;
|
||||
};
|
||||
|
||||
const dragImage = new Image();
|
||||
dragImage.src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
|
||||
let x;
|
||||
let y;
|
||||
|
||||
const onDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
|
||||
// Set the data to be transferred
|
||||
event.dataTransfer.setData(
|
||||
'text/plain',
|
||||
JSON.stringify({
|
||||
type: 'folder',
|
||||
id: folderId
|
||||
})
|
||||
);
|
||||
|
||||
dragged = true;
|
||||
folderElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
|
||||
};
|
||||
|
||||
const onDrag = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
};
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
folderElement.style.opacity = '1'; // Reset visual cue after drag
|
||||
dragged = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
open = folders[folderId].is_expanded;
|
||||
if (folderElement) {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.addEventListener('drop', onDrop);
|
||||
folderElement.addEventListener('dragleave', onDragLeave);
|
||||
|
||||
// Event listener for when dragging starts
|
||||
folderElement.addEventListener('dragstart', onDragStart);
|
||||
// Event listener for when dragging occurs (optional)
|
||||
folderElement.addEventListener('drag', onDrag);
|
||||
// Event listener for when dragging ends
|
||||
folderElement.addEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (folderElement) {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
||||
|
||||
folderElement.removeEventListener('dragstart', onDragStart);
|
||||
folderElement.removeEventListener('drag', onDrag);
|
||||
folderElement.removeEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
const deleteHandler = async () => {
|
||||
const res = await deleteFolderById(localStorage.token, folderId).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Folder deleted successfully'));
|
||||
dispatch('update');
|
||||
}
|
||||
};
|
||||
|
||||
const nameUpdateHandler = async () => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === folders[folderId].name) {
|
||||
edit = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentName = folders[folderId].name;
|
||||
|
||||
name = name.trim();
|
||||
folders[folderId].name = name;
|
||||
|
||||
const res = await updateFolderNameById(localStorage.token, folderId, name).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
folders[folderId].name = currentName;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
folders[folderId].name = name;
|
||||
toast.success($i18n.t('Folder name updated successfully'));
|
||||
dispatch('update');
|
||||
}
|
||||
};
|
||||
|
||||
const isExpandedUpdateHandler = async () => {
|
||||
const res = await updateFolderIsExpandedById(localStorage.token, folderId, open).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let isExpandedUpdateTimeout;
|
||||
|
||||
const isExpandedUpdateDebounceHandler = (open) => {
|
||||
clearTimeout(isExpandedUpdateTimeout);
|
||||
isExpandedUpdateTimeout = setTimeout(() => {
|
||||
isExpandedUpdateHandler();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$: isExpandedUpdateDebounceHandler(open);
|
||||
|
||||
const editHandler = async () => {
|
||||
console.log('Edit');
|
||||
await tick();
|
||||
name = folders[folderId].name;
|
||||
edit = true;
|
||||
|
||||
await tick();
|
||||
|
||||
// focus on the input
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
input.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const exportHandler = async () => {
|
||||
const chats = await getChatsByFolderId(localStorage.token, folderId).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
if (!chats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(chats)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
saveAs(blob, `folder-${folders[folderId].name}-export-${Date.now()}.json`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete folder?')}
|
||||
on:confirm={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-700 dark:text-gray-300 flex-1 line-clamp-3">
|
||||
{@html DOMPurify.sanitize(
|
||||
$i18n.t('This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.', {
|
||||
NAME: folders[folderId].name
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
{#if dragged && x && y}
|
||||
<DragGhost {x} {y}>
|
||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
||||
<div class="flex items-center gap-1">
|
||||
<FolderOpen className="size-3.5" strokeWidth="2" />
|
||||
<div class=" text-xs text-white line-clamp-1">
|
||||
{folders[folderId].name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragGhost>
|
||||
{/if}
|
||||
|
||||
<div bind:this={folderElement} class="relative {className}" draggable="true">
|
||||
{#if draggedOver}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(260,85%,65%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<Collapsible
|
||||
bind:open
|
||||
className="w-full"
|
||||
buttonClassName="w-full"
|
||||
hide={(folders[folderId]?.childrenIds ?? []).length === 0 &&
|
||||
(folders[folderId].items?.chats ?? []).length === 0}
|
||||
on:change={(e) => {
|
||||
dispatch('open', e.detail);
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-full group">
|
||||
<button
|
||||
id="folder-{folderId}-button"
|
||||
class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:dblclick={() => {
|
||||
editHandler();
|
||||
}}
|
||||
>
|
||||
<div class="text-gray-300 dark:text-gray-600">
|
||||
{#if open}
|
||||
<ChevronDown className=" size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<ChevronRight className=" size-3" strokeWidth="2.5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="translate-y-[0.5px] flex-1 justify-start text-start line-clamp-1">
|
||||
{#if edit}
|
||||
<input
|
||||
id="folder-{folderId}-input"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
on:blur={() => {
|
||||
nameUpdateHandler();
|
||||
edit = false;
|
||||
}}
|
||||
on:click={(e) => {
|
||||
// Prevent accidental collapse toggling when clicking inside input
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:mousedown={(e) => {
|
||||
// Prevent accidental collapse toggling when clicking inside input
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
edit = false;
|
||||
}
|
||||
}}
|
||||
class="w-full h-full bg-transparent text-gray-500 dark:text-gray-500 outline-none"
|
||||
/>
|
||||
{:else}
|
||||
{folders[folderId].name}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||
on:pointerup={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FolderMenu
|
||||
on:rename={() => {
|
||||
editHandler();
|
||||
}}
|
||||
on:delete={() => {
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
on:export={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
>
|
||||
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</button>
|
||||
</FolderMenu>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div slot="content" class="w-full">
|
||||
{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0}
|
||||
<div
|
||||
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
||||
>
|
||||
{#if folders[folderId]?.childrenIds}
|
||||
{@const children = folders[folderId]?.childrenIds
|
||||
.map((id) => folders[id])
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
)}
|
||||
|
||||
{#each children as childFolder (`${folderId}-${childFolder.id}`)}
|
||||
<svelte:self
|
||||
{folders}
|
||||
folderId={childFolder.id}
|
||||
parentDragged={dragged}
|
||||
on:import={(e) => {
|
||||
dispatch('import', e.detail);
|
||||
}}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if folders[folderId].items?.chats}
|
||||
{#each folders[folderId].items.chats as chat (chat.id)}
|
||||
<ChatItem
|
||||
id={chat.id}
|
||||
title={chat.title}
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
|
@ -30,7 +30,13 @@
|
|||
|
||||
let filteredTags = [];
|
||||
$: filteredTags = lastWord.startsWith('tag:')
|
||||
? $tags.filter((tag) => {
|
||||
? [
|
||||
...$tags,
|
||||
{
|
||||
id: 'none',
|
||||
name: $i18n.t('Untagged')
|
||||
}
|
||||
].filter((tag) => {
|
||||
const tagName = lastWord.slice(4);
|
||||
if (tagName) {
|
||||
const tagId = tagName.replace(' ', '_').toLowerCase();
|
||||
|
|
@ -144,7 +150,7 @@
|
|||
{#if filteredTags.length > 0}
|
||||
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div>
|
||||
|
||||
<div class="">
|
||||
<div class="max-h-60 overflow-auto">
|
||||
{#each filteredTags as tag, tagIdx}
|
||||
<button
|
||||
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
|
||||
|
|
@ -163,7 +169,11 @@
|
|||
dispatch('input');
|
||||
}}
|
||||
>
|
||||
<div class="dark:text-gray-300 text-gray-700 font-medium">{tag.name}</div>
|
||||
<div
|
||||
class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 flex-shrink-0"
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 line-clamp-1">
|
||||
{tag.id}
|
||||
|
|
@ -174,7 +184,7 @@
|
|||
{:else if filteredOptions.length > 0}
|
||||
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Search options</div>
|
||||
|
||||
<div class="">
|
||||
<div class=" max-h-60 overflow-auto">
|
||||
{#each filteredOptions as option, optionIdx}
|
||||
<button
|
||||
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<slot name="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow font-primary"
|
||||
class="w-full {className} text-sm rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl font-primary"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="start"
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
|
||||
<div class=" self-center">{$i18n.t('Settings')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
<div class=" self-center mr-3">
|
||||
<ArchiveBox className="size-5" strokeWidth="1.5" />
|
||||
</div>
|
||||
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
|
||||
<div class=" self-center">{$i18n.t('Archived Chats')}</div>
|
||||
</button>
|
||||
|
||||
{#if role === 'admin'}
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
|
||||
<div class=" self-center">{$i18n.t('Playground')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
|
||||
<div class=" self-center">{$i18n.t('Admin Panel')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
|
|
@ -179,7 +179,7 @@
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
|
||||
<div class=" self-center">{$i18n.t('Sign Out')}</div>
|
||||
</button>
|
||||
|
||||
{#if $activeUserCount}
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
</div>
|
||||
|
||||
<div class=" ">
|
||||
<span class=" font-medium">
|
||||
<span class="">
|
||||
{$i18n.t('Active Users')}:
|
||||
</span>
|
||||
<span class=" font-semibold">
|
||||
|
|
@ -212,7 +212,7 @@
|
|||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||
<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm ">
|
||||
<div class="flex items-center">Profile</div>
|
||||
</DropdownMenu.Item> -->
|
||||
</DropdownMenu.Content>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { WEBUI_NAME, functions, models } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, functions, models } from '$lib/stores';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||
|
||||
|
|
@ -468,38 +468,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom functions')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom functions')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
|
||||
let formElement = null;
|
||||
let loading = false;
|
||||
|
|
@ -294,61 +297,64 @@ class Pipe:
|
|||
}
|
||||
}}
|
||||
>
|
||||
<div class="mb-2.5">
|
||||
<button
|
||||
class="flex space-x-1"
|
||||
on:click={() => {
|
||||
goto('/workspace/functions');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
||||
<div class="w-full mb-2 flex flex-col gap-1.5">
|
||||
<div class="flex gap-2 w-full">
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function Name (e.g. My Filter)')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
||||
<div class="flex w-full items-center">
|
||||
<div class=" flex-shrink-0 mr-2">
|
||||
<Tooltip content={$i18n.t('Back')}>
|
||||
<button
|
||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||
on:click={() => {
|
||||
goto('/workspace/functions');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-2xl font-medium bg-transparent outline-none font-primary"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function Name (e.g. My Filter)')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge type="muted" content={$i18n.t('Function')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex gap-2 px-1">
|
||||
{#if edit}
|
||||
<div class="text-sm text-gray-500 flex-shrink-0">
|
||||
{id}
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function ID (e.g. my_filter)')}
|
||||
bind:value={id}
|
||||
required
|
||||
disabled={edit}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function ID (e.g. my_filter)')}
|
||||
bind:value={id}
|
||||
placeholder={$i18n.t(
|
||||
'Function Description (e.g. A filter to remove profanity from text)'
|
||||
)}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
disabled={edit}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t(
|
||||
'Function Description (e.g. A filter to remove profanity from text)'
|
||||
)}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||
|
|
@ -380,7 +386,7 @@ class Pipe:
|
|||
</div>
|
||||
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
|
||||
class="px-3 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
import Pencil from '../icons/Pencil.svelte';
|
||||
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
||||
import Badge from '../common/Badge.svelte';
|
||||
|
||||
let query = '';
|
||||
let selectedItem = null;
|
||||
|
|
@ -167,20 +168,12 @@
|
|||
<div class="mt-5 flex justify-between">
|
||||
<div>
|
||||
{#if item?.meta?.document}
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
|
||||
>
|
||||
{$i18n.t('Document')}
|
||||
</div>
|
||||
<Badge type="muted" content={$i18n.t('Document')} />
|
||||
{:else}
|
||||
<div
|
||||
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1"
|
||||
>
|
||||
{$i18n.t('Collection')}
|
||||
</div>
|
||||
<Badge type="success" content={$i18n.t('Collection')} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class=" text-xs text-gray-500">
|
||||
<div class=" text-xs text-gray-500 line-clamp-1">
|
||||
{$i18n.t('Updated')}
|
||||
{dayjs(item.updated_at * 1000).fromNow()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import Fuse from 'fuse.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||
|
||||
import { onMount, getContext, onDestroy, tick } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -21,22 +22,30 @@
|
|||
updateKnowledgeById
|
||||
} from '$lib/apis/knowledge';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import Files from './Collection/Files.svelte';
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
import AddContentModal from './Collection/AddTextContentModal.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import { blobToFile } from '$lib/utils';
|
||||
import { processFile } from '$lib/apis/retrieval';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Files from './Collection/Files.svelte';
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
|
||||
import AddContentMenu from './Collection/AddContentMenu.svelte';
|
||||
import AddTextContentModal from './Collection/AddTextContentModal.svelte';
|
||||
|
||||
import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
|
||||
import RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
import Drawer from '$lib/components/common/Drawer.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
||||
|
||||
let largeScreen = true;
|
||||
|
||||
let pane;
|
||||
let showSidepanel = true;
|
||||
let minSize = 0;
|
||||
|
||||
type Knowledge = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -93,7 +102,7 @@
|
|||
|
||||
const createFileFromText = (name, content) => {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const file = blobToFile(blob, `${name}.md`);
|
||||
const file = blobToFile(blob, `${name}.txt`);
|
||||
|
||||
console.log(file);
|
||||
return file;
|
||||
|
|
@ -459,6 +468,36 @@
|
|||
mediaQuery.addEventListener('change', handleMediaQuery);
|
||||
handleMediaQuery(mediaQuery);
|
||||
|
||||
// Select the container element you want to observe
|
||||
const container = document.getElementById('collection-container');
|
||||
|
||||
// initialize the minSize based on the container width
|
||||
minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100);
|
||||
|
||||
// Create a new ResizeObserver instance
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
const width = entry.contentRect.width;
|
||||
// calculate the percentage of 300
|
||||
const percentage = (300 / width) * 100;
|
||||
// set the minSize to the percentage, must be an integer
|
||||
minSize = !largeScreen ? 100 : Math.floor(percentage);
|
||||
|
||||
if (showSidepanel) {
|
||||
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
|
||||
pane.resize(minSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing the container's size changes
|
||||
resizeObserver.observe(container);
|
||||
|
||||
if (pane) {
|
||||
pane.expand();
|
||||
}
|
||||
|
||||
id = $page.params.id;
|
||||
|
||||
const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
|
||||
|
|
@ -552,91 +591,222 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full max-h-[100dvh] h-full">
|
||||
<div class="flex flex-col mb-2 flex-1 overflow-auto h-0">
|
||||
{#if id && knowledge}
|
||||
<div class="flex flex-row h-0 flex-1 overflow-auto">
|
||||
<div
|
||||
class=" {largeScreen
|
||||
? 'flex-shrink-0'
|
||||
: 'flex-1'} flex py-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850"
|
||||
<div class="flex flex-col w-full h-full max-h-[100dvh]" id="collection-container">
|
||||
{#if id && knowledge}
|
||||
<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
|
||||
<PaneGroup direction="horizontal">
|
||||
<Pane
|
||||
bind:pane
|
||||
defaultSize={minSize}
|
||||
collapsible={true}
|
||||
maxSize={50}
|
||||
{minSize}
|
||||
class="h-full"
|
||||
onExpand={() => {
|
||||
showSidepanel = true;
|
||||
}}
|
||||
onCollapse={() => {
|
||||
showSidepanel = false;
|
||||
}}
|
||||
>
|
||||
<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class=" px-3">
|
||||
<div class="flex">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Collection')}
|
||||
on:focus={() => {
|
||||
selectedFileId = null;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<AddContentMenu
|
||||
on:upload={(e) => {
|
||||
if (e.detail.type === 'directory') {
|
||||
uploadDirectoryHandler();
|
||||
} else if (e.detail.type === 'text') {
|
||||
showAddTextContentModal = true;
|
||||
} else {
|
||||
document.getElementById('files-input').click();
|
||||
}
|
||||
<div
|
||||
class="{largeScreen ? 'flex-shrink-0' : 'flex-1'}
|
||||
flex
|
||||
py-2
|
||||
rounded-2xl
|
||||
border
|
||||
border-gray-50
|
||||
h-full
|
||||
dark:border-gray-850"
|
||||
>
|
||||
<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class=" px-3">
|
||||
<div class="flex py-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Collection')}
|
||||
on:focus={() => {
|
||||
selectedFileId = null;
|
||||
}}
|
||||
on:sync={(e) => {
|
||||
showSyncConfirmModal = true;
|
||||
/>
|
||||
|
||||
<div>
|
||||
<AddContentMenu
|
||||
on:upload={(e) => {
|
||||
if (e.detail.type === 'directory') {
|
||||
uploadDirectoryHandler();
|
||||
} else if (e.detail.type === 'text') {
|
||||
showAddTextContentModal = true;
|
||||
} else {
|
||||
document.getElementById('files-input').click();
|
||||
}
|
||||
}}
|
||||
on:sync={(e) => {
|
||||
showSyncConfirmModal = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if filteredItems.length > 0}
|
||||
<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
|
||||
<Files
|
||||
files={filteredItems}
|
||||
{selectedFileId}
|
||||
on:click={(e) => {
|
||||
selectedFileId = selectedFileId === e.detail ? null : e.detail;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
console.log(e.detail);
|
||||
|
||||
selectedFileId = null;
|
||||
deleteFileHandler(e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" mt-2 mb-1 border-gray-50 dark:border-gray-850" />
|
||||
{:else}
|
||||
<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if filteredItems.length > 0}
|
||||
<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
|
||||
<Files
|
||||
files={filteredItems}
|
||||
{selectedFileId}
|
||||
on:click={(e) => {
|
||||
selectedFileId = selectedFileId === e.detail ? null : e.detail;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
console.log(e.detail);
|
||||
|
||||
selectedFileId = null;
|
||||
deleteFileHandler(e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
{#if largeScreen}
|
||||
<div class="flex-1 flex justify-start max-h-full overflow-hidden pl-3">
|
||||
{#if selectedFile}
|
||||
<div class=" flex flex-col w-full h-full">
|
||||
<div class=" flex-shrink-0 mb-2 flex items-center">
|
||||
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||
</div>
|
||||
</PaneResizer>
|
||||
<Pane>
|
||||
<div class="flex-1 flex justify-start h-full max-h-full">
|
||||
{#if selectedFile}
|
||||
<div class=" flex flex-col w-full h-full max-h-full ml-2.5">
|
||||
<div class="flex-shrink-0 mb-2 flex items-center">
|
||||
{#if !showSidepanel}
|
||||
<div class="-translate-x-2">
|
||||
<button
|
||||
class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||
on:click={() => {
|
||||
pane.expand();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft strokeWidth="2.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" flex-1 text-2xl font-medium">
|
||||
<a
|
||||
class="hover:text-gray-500 hover:dark:text-gray-100 hover:underline flex-grow line-clamp-1"
|
||||
href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
|
||||
target="_blank"
|
||||
>
|
||||
{selectedFile?.meta?.name}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
|
||||
on:click={() => {
|
||||
updateFileContentHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-none overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#key selectedFile.id}
|
||||
<RichTextInput
|
||||
className="input-prose-sm"
|
||||
bind:value={selectedFile.data.content}
|
||||
placeholder={$i18n.t('Add content here')}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="m-auto pb-32">
|
||||
<div>
|
||||
<div class=" flex w-full mt-1 mb-3.5">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between w-full px-0.5 mb-1">
|
||||
<div class="w-full">
|
||||
<input
|
||||
type="text"
|
||||
class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
|
||||
bind:value={knowledge.name}
|
||||
on:input={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full px-1">
|
||||
<input
|
||||
type="text"
|
||||
class="text-center w-full text-gray-500 bg-transparent outline-none"
|
||||
bind:value={knowledge.description}
|
||||
on:input={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
|
||||
{$i18n.t('Select a file to view or drag and drop a file to upload')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
{:else if !largeScreen && selectedFileId !== null}
|
||||
<Drawer
|
||||
className="h-full"
|
||||
show={selectedFileId !== null}
|
||||
on:close={() => {
|
||||
selectedFileId = null;
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col justify-start h-full max-h-full p-2">
|
||||
<div class=" flex flex-col w-full h-full max-h-full">
|
||||
<div class="flex-shrink-0 mt-1 mb-2 flex items-center">
|
||||
<div class="mr-2">
|
||||
<button
|
||||
class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||
on:click={() => {
|
||||
selectedFileId = null;
|
||||
}}
|
||||
>
|
||||
<ChevronLeft strokeWidth="2.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class=" flex-1 text-xl line-clamp-1">
|
||||
{selectedFile?.meta?.name}
|
||||
</div>
|
||||
|
|
@ -653,56 +823,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex-grow">
|
||||
<textarea
|
||||
class=" w-full h-full resize-none rounded-xl py-4 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={selectedFile.data.content}
|
||||
placeholder={$i18n.t('Add content here')}
|
||||
/>
|
||||
<div
|
||||
class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-lg text-sm bg-transparent overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#key selectedFile.id}
|
||||
<RichTextInput
|
||||
className="input-prose-sm"
|
||||
bind:value={selectedFile.data.content}
|
||||
placeholder={$i18n.t('Add content here')}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="m-auto pb-32">
|
||||
<div>
|
||||
<div class=" flex w-full mt-1 mb-3.5">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between w-full px-0.5 mb-1">
|
||||
<div class="w-full">
|
||||
<input
|
||||
type="text"
|
||||
class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
|
||||
bind:value={knowledge.name}
|
||||
on:input={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full px-1">
|
||||
<input
|
||||
type="text"
|
||||
class="text-center w-full text-gray-500 bg-transparent outline-none"
|
||||
bind:value={knowledge.description}
|
||||
on:input={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
|
||||
{$i18n.t('Select a file to view or drag and drop a file to upload')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/if}
|
||||
</div>
|
||||
</PaneGroup>
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
>
|
||||
<Tooltip content={$i18n.t('Add Content')}>
|
||||
<button
|
||||
class=" px-2 py-2 rounded-xl border border-gray-50 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
|
||||
class=" p-1.5 rounded-xl hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition font-medium text-sm flex items-center space-x-1"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
show = true;
|
||||
|
|
|
|||
|
|
@ -7,98 +7,138 @@
|
|||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Mic from '$lib/components/icons/Mic.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import VoiceRecording from '$lib/components/chat/MessageInput/VoiceRecording.svelte';
|
||||
export let show = false;
|
||||
|
||||
let name = '';
|
||||
let name = 'Untitled';
|
||||
let content = '';
|
||||
|
||||
let voiceInput = false;
|
||||
</script>
|
||||
|
||||
<Modal size="md" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Add Content')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit|preventDefault={() => {
|
||||
if (name.trim() === '' || content.trim() === '') {
|
||||
toast.error($i18n.t('Please fill in all fields.'));
|
||||
name = '';
|
||||
content = '';
|
||||
return;
|
||||
}
|
||||
<Modal size="full" className="h-full bg-white dark:bg-gray-900" bind:show>
|
||||
<div class="absolute top-0 right-0 p-5">
|
||||
<button
|
||||
class="self-center dark:text-white"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row w-full h-full md:space-x-4 dark:text-gray-200">
|
||||
<form
|
||||
class="flex flex-col w-full h-full"
|
||||
on:submit|preventDefault={() => {
|
||||
if (name.trim() === '' || content.trim() === '') {
|
||||
toast.error($i18n.t('Please fill in all fields.'));
|
||||
name = name.trim();
|
||||
content = content.trim();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('submit', {
|
||||
name,
|
||||
content
|
||||
});
|
||||
show = false;
|
||||
name = '';
|
||||
content = '';
|
||||
}}
|
||||
>
|
||||
<div class="mb-3 w-full">
|
||||
<div class="w-full flex flex-col gap-2.5">
|
||||
<div class="w-full">
|
||||
<div class=" text-sm mb-2">{$i18n.t('Title')}</div>
|
||||
|
||||
<div class="w-full mt-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={`Name your content`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-sm mb-2">{$i18n.t('Content')}</div>
|
||||
|
||||
<div class=" w-full mt-1">
|
||||
<textarea
|
||||
class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-whites dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
rows="10"
|
||||
bind:value={content}
|
||||
placeholder={`Write your content here`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
dispatch('submit', {
|
||||
name,
|
||||
content
|
||||
});
|
||||
show = false;
|
||||
name = '';
|
||||
content = '';
|
||||
}}
|
||||
>
|
||||
<div class=" flex-1 w-full h-full flex justify-center overflow-auto px-5 py-4">
|
||||
<div class=" max-w-3xl py-2 md:py-10 w-full flex flex-col gap-2">
|
||||
<div class="flex-shrink-0 w-full flex justify-between items-center">
|
||||
<div class="w-full">
|
||||
<input
|
||||
class="w-full text-3xl font-semibold bg-transparent outline-none"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Title')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
<div class=" flex-1 w-full h-full">
|
||||
<RichTextInput bind:value={content} placeholder={$i18n.t('Write something...')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-row items-center justify-end text-sm font-medium flex-shrink-0 mt-1 p-4 gap-1.5"
|
||||
>
|
||||
<div class="">
|
||||
{#if voiceInput}
|
||||
<div class=" max-w-full w-64">
|
||||
<VoiceRecording
|
||||
bind:recording={voiceInput}
|
||||
className="p-1"
|
||||
on:cancel={() => {
|
||||
voiceInput = false;
|
||||
}}
|
||||
on:confirm={(e) => {
|
||||
const response = e.detail;
|
||||
content = `${content}${response} `;
|
||||
|
||||
voiceInput = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Tooltip content={$i18n.t('Voice Input')}>
|
||||
<button
|
||||
class=" p-2 bg-gray-50 text-gray-700 dark:bg-gray-700 dark:text-white transition rounded-full"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
try {
|
||||
let stream = await navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.catch(function (err) {
|
||||
toast.error(
|
||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
||||
error: err
|
||||
})
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
voiceInput = true;
|
||||
const tracks = stream.getTracks();
|
||||
tracks.forEach((track) => track.stop());
|
||||
}
|
||||
stream = null;
|
||||
} catch {
|
||||
toast.error($i18n.t('Permission denied when accessing microphone'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Mic className="size-5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" flex-shrink-0">
|
||||
<Tooltip content={$i18n.t('Save')}>
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
class=" px-3.5 py-2 bg-black text-white dark:bg-white dark:text-black transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Add Content')}
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<div class=" max-h-full flex flex-col w-full">
|
||||
{#each files as file}
|
||||
<div class="mt-2 px-2">
|
||||
<div class="mt-1 px-2">
|
||||
<FileItem
|
||||
className="w-full"
|
||||
colorClassName="{selectedFileId === file.id
|
||||
|
|
@ -23,9 +23,17 @@
|
|||
loading={file.status === 'uploading'}
|
||||
dismissible
|
||||
on:click={() => {
|
||||
if (file.status === 'uploading') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('click', file.id);
|
||||
}}
|
||||
on:dismiss={() => {
|
||||
if (file.status === 'uploading') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('delete', file.id);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
|
||||
import { WEBUI_NAME, mobile, models, settings, user } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, mobile, models, settings, user } from '$lib/stores';
|
||||
import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
|
||||
|
||||
import { deleteModel } from '$lib/apis/ollama';
|
||||
|
|
@ -674,35 +674,42 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore model presets')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore model presets')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { WEBUI_NAME, prompts } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, prompts } from '$lib/stores';
|
||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -67,6 +67,18 @@
|
|||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete prompt?')}
|
||||
on:confirm={() => {
|
||||
deleteHandler(deletePrompt);
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500">
|
||||
{$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<div class=" flex w-full space-x-2 mb-2.5">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
|
|
@ -128,7 +140,7 @@
|
|||
>
|
||||
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
||||
<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
|
||||
<div class=" flex-1 self-center pl-5">
|
||||
<div class=" flex-1 self-center pl-1.5">
|
||||
<div class=" font-semibold line-clamp-1">{prompt.command}</div>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||
{prompt.title}
|
||||
|
|
@ -284,47 +296,42 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom prompts')}
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete prompt?')}
|
||||
on:confirm={() => {
|
||||
deleteHandler(deletePrompt);
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500">
|
||||
{$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>.
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom prompts')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { WEBUI_NAME, prompts, tools } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, prompts, tools } from '$lib/stores';
|
||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -422,38 +422,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-lg font-semibold mb-3 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom tools')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
|
||||
href="https://openwebui.com/#open-webui-community"
|
||||
target="_blank"
|
||||
>
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
|
||||
<div class=" text-sm line-clamp-1">
|
||||
{$i18n.t('Discover, download, and explore custom tools')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -182,61 +185,64 @@ class Tools:
|
|||
}
|
||||
}}
|
||||
>
|
||||
<div class="mb-2.5">
|
||||
<button
|
||||
class="flex space-x-1"
|
||||
on:click={() => {
|
||||
goto('/workspace/tools');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
|
||||
clip-rule="evenodd"
|
||||
<div class="flex flex-col flex-1 overflow-auto h-0">
|
||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
||||
<div class="flex w-full items-center">
|
||||
<div class=" flex-shrink-0 mr-2">
|
||||
<Tooltip content={$i18n.t('Back')}>
|
||||
<button
|
||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||
on:click={() => {
|
||||
goto('/workspace/tools');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-2xl font-medium bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit Name (e.g. My ToolKit)')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
||||
<div class="w-full mb-2 flex flex-col gap-1.5">
|
||||
<div class="flex gap-2 w-full">
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit Name (e.g. My ToolKit)')}
|
||||
bind:value={name}
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<Badge type="muted" content={$i18n.t('Tool')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex gap-2 px-1">
|
||||
{#if edit}
|
||||
<div class="text-sm text-gray-500 flex-shrink-0">
|
||||
{id}
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit ID (e.g. my_toolkit)')}
|
||||
bind:value={id}
|
||||
required
|
||||
disabled={edit}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Toolkit ID (e.g. my_toolkit)')}
|
||||
bind:value={id}
|
||||
placeholder={$i18n.t(
|
||||
'Toolkit Description (e.g. A toolkit for performing various operations)'
|
||||
)}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
disabled={edit}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t(
|
||||
'Toolkit Description (e.g. A toolkit for performing various operations)'
|
||||
)}
|
||||
bind:value={meta.description}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||
|
|
@ -268,7 +274,7 @@ class Tools:
|
|||
</div>
|
||||
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
|
||||
class="px-3 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "التعليمات المتقدمة",
|
||||
"Advanced Params": "المعلمات المتقدمة",
|
||||
"All chats": "",
|
||||
"All Documents": "جميع الملفات",
|
||||
"All Users": "جميع المستخدمين",
|
||||
"Allow Chat Deletion": "يستطيع حذف المحادثات",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "حذف المحادثه",
|
||||
"Delete Chat": "حذف المحادثه.",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "أحذف هذا الرابط",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "تحميل",
|
||||
"Download canceled": "تم اللغاء التحميل",
|
||||
"Download Database": "تحميل قاعدة البيانات",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.",
|
||||
"Edit": "تعديل",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "تم اكتشاف انتحال بصمة الإصبع: غير قادر على استخدام الأحرف الأولى كصورة رمزية. الافتراضي لصورة الملف الشخصي الافتراضية.",
|
||||
"Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة",
|
||||
"Focus chat input": "التركيز على إدخال الدردشة",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "القائمة البيضاء الموديل",
|
||||
"Modelfile Content": "محتوى الملف النموذجي",
|
||||
"Models": "الموديلات",
|
||||
"more": "",
|
||||
"More": "المزيد",
|
||||
"Move to Top": "",
|
||||
"Name": "الأسم",
|
||||
"Name your model": "قم بتسمية النموذج الخاص بك",
|
||||
"New Chat": "دردشة جديدة",
|
||||
"New folder": "",
|
||||
"New Password": "كلمة المرور الجديدة",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "سجل صوت",
|
||||
"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
|
||||
"Regenerate": "تجديد",
|
||||
"Release Notes": "ملاحظات الإصدار",
|
||||
"Relevance": "",
|
||||
"Remove": "إزالة",
|
||||
"Remove Model": "حذف الموديل",
|
||||
"Rename": "إعادة تسمية",
|
||||
|
|
@ -604,6 +613,7 @@
|
|||
"Select model": " أختار موديل",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "تم",
|
||||
"Send a Message": "يُرجى إدخال طلبك هنا",
|
||||
"Send message": "يُرجى إدخال طلبك هنا.",
|
||||
|
|
@ -684,6 +694,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "شرح شامل",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Разширени Параметри",
|
||||
"Advanced Params": "Разширени параметри",
|
||||
"All chats": "",
|
||||
"All Documents": "Всички Документи",
|
||||
"All Users": "Всички Потребители",
|
||||
"Allow Chat Deletion": "Позволи Изтриване на Чат",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Изтриване на чат",
|
||||
"Delete Chat": "Изтриване на Чат",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "Изтриване на този линк",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Изтегляне отменено",
|
||||
"Download canceled": "Изтегляне отменено",
|
||||
"Download Database": "Сваляне на база данни",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Пускане на файлове тук, за да ги добавите в чата",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.",
|
||||
"Edit": "Редактиране",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Потвърждаване на отпечатък: Не може да се използва инициализационна буква като аватар. Потребителят се връща към стандартна аватарка.",
|
||||
"Fluidly stream large external response chunks": "Плавно предаване на големи части от външен отговор",
|
||||
"Focus chat input": "Фокусиране на чат вход",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "Следвайте инструкциите перфектно",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Модели Whitelisted",
|
||||
"Modelfile Content": "Съдържание на модфайл",
|
||||
"Models": "Модели",
|
||||
"more": "",
|
||||
"More": "Повече",
|
||||
"Move to Top": "",
|
||||
"Name": "Име",
|
||||
"Name your model": "Дайте име на вашия модел",
|
||||
"New Chat": "Нов чат",
|
||||
"New folder": "",
|
||||
"New Password": "Нова парола",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Записване на глас",
|
||||
"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "Отказано, когато не трябва да бъде",
|
||||
"Regenerate": "Регенериране",
|
||||
"Release Notes": "Бележки по изданието",
|
||||
"Relevance": "",
|
||||
"Remove": "Изтриване",
|
||||
"Remove Model": "Изтриване на модела",
|
||||
"Rename": "Преименуване",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "Изберете модел",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Изпрати",
|
||||
"Send a Message": "Изпращане на Съобщение",
|
||||
"Send message": "Изпращане на съобщение",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "Това е подробно описание.",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "এডভান্সড প্যারামিটার্স",
|
||||
"Advanced Params": "অ্যাডভান্সড প্যারাম",
|
||||
"All chats": "",
|
||||
"All Documents": "সব ডকুমেন্ট",
|
||||
"All Users": "সব ইউজার",
|
||||
"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "চ্যাট মুছে ফেলুন",
|
||||
"Delete Chat": "চ্যাট মুছে ফেলুন",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "এই লিংক মুছে ফেলুন",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "ডাউনলোড",
|
||||
"Download canceled": "ডাউনলোড বাতিল করা হয়েছে",
|
||||
"Download Database": "ডেটাবেজ ডাউনলোড করুন",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "আলোচনায় যুক্ত করার জন্য যে কোন ফাইল এখানে ড্রপ করুন",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "যেমন '30s','10m'. সময়ের অনুমোদিত অনুমোদিত এককগুলি হচ্ছে 's', 'm', 'h'.",
|
||||
"Edit": "এডিট করুন",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ফিঙ্গারপ্রিন্ট স্পুফিং ধরা পড়েছে: অ্যাভাটার হিসেবে নামের আদ্যক্ষর ব্যবহার করা যাচ্ছে না। ডিফল্ট প্রোফাইল পিকচারে ফিরিয়ে নেয়া হচ্ছে।",
|
||||
"Fluidly stream large external response chunks": "বড় এক্সটার্নাল রেসপন্স চাঙ্কগুলো মসৃণভাবে প্রবাহিত করুন",
|
||||
"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "আপনার ভেরিয়বলগুলো এভাবে স্কয়ার ব্রাকেটের মাধ্যমে সাজান",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "হোয়াইটলিস্টেড মডেল(সমূহ)",
|
||||
"Modelfile Content": "মডেলফাইল কনটেন্ট",
|
||||
"Models": "মডেলসমূহ",
|
||||
"more": "",
|
||||
"More": "আরো",
|
||||
"Move to Top": "",
|
||||
"Name": "নাম",
|
||||
"Name your model": "আপনার মডেলের নাম দিন",
|
||||
"New Chat": "নতুন চ্যাট",
|
||||
"New folder": "",
|
||||
"New Password": "নতুন পাসওয়ার্ড",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "ভয়েস রেকর্ড করুন",
|
||||
"Redirecting you to OpenWebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "যদি উপযুক্ত নয়, তবে রেজিগেনেট করা হচ্ছে",
|
||||
"Regenerate": "রেজিগেনেট করুন",
|
||||
"Release Notes": "রিলিজ নোটসমূহ",
|
||||
"Relevance": "",
|
||||
"Remove": "রিমুভ করুন",
|
||||
"Remove Model": "মডেল রিমুভ করুন",
|
||||
"Rename": "রেনেম",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "মডেল নির্বাচন করুন",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "পাঠান",
|
||||
"Send a Message": "একটি মেসেজ পাঠান",
|
||||
"Send message": "মেসেজ পাঠান",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "পুঙ্খানুপুঙ্খ ব্যাখ্যা",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Els administradors tenen accés a totes les eines en tot moment; els usuaris necessiten eines assignades per model a l'espai de treball.",
|
||||
"Advanced Parameters": "Paràmetres avançats",
|
||||
"Advanced Params": "Paràmetres avançats",
|
||||
"All chats": "Tots els xats",
|
||||
"All Documents": "Tots els documents",
|
||||
"All Users": "Tots els usuaris",
|
||||
"Allow Chat Deletion": "Permetre la supressió del xat",
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
"Cancel": "Cancel·lar",
|
||||
"Capabilities": "Capacitats",
|
||||
"Change Password": "Canviar la contrasenya",
|
||||
"Character": "",
|
||||
"Character": "Personatge",
|
||||
"Chat": "Xat",
|
||||
"Chat Background Image": "Imatge de fons del xat",
|
||||
"Chat Bubble UI": "Chat Bubble UI",
|
||||
|
|
@ -124,7 +125,7 @@
|
|||
"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permís d'escriptura al porta-retalls denegat. Comprova els ajustos de navegador per donar l'accés necessari.",
|
||||
"Clone": "Clonar",
|
||||
"Close": "Tancar",
|
||||
"Code execution": "",
|
||||
"Code execution": "Execució de codi",
|
||||
"Code formatted successfully": "Codi formatat correctament",
|
||||
"Collection": "Col·lecció",
|
||||
"ComfyUI": "ComfyUI",
|
||||
|
|
@ -153,7 +154,7 @@
|
|||
"Copy last code block": "Copiar l'últim bloc de codi",
|
||||
"Copy last response": "Copiar l'última resposta",
|
||||
"Copy Link": "Copiar l'enllaç",
|
||||
"Copy to clipboard": "",
|
||||
"Copy to clipboard": "Copiar al porta-retalls",
|
||||
"Copying to clipboard was successful!": "La còpia al porta-retalls s'ha realitzat correctament",
|
||||
"Create a model": "Crear un model",
|
||||
"Create Account": "Crear un compte",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Eliminar xat",
|
||||
"Delete Chat": "Eliminar xat",
|
||||
"Delete chat?": "Eliminar el xat?",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "Eliminar funció?",
|
||||
"Delete prompt?": "Eliminar indicació?",
|
||||
"delete this link": "Eliminar aquest enllaç",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Descarregar",
|
||||
"Download canceled": "Descàrrega cancel·lada",
|
||||
"Download Database": "Descarregar la base de dades",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Deixa qualsevol arxiu aquí per afegir-lo a la conversa",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
|
||||
"Edit": "Editar",
|
||||
"Edit Memory": "Editar la memòria",
|
||||
|
|
@ -278,7 +278,7 @@
|
|||
"Enter Your Password": "Introdueix la teva contrasenya",
|
||||
"Enter Your Role": "Introdueix el teu rol",
|
||||
"Error": "Error",
|
||||
"ERROR": "",
|
||||
"ERROR": "ERROR",
|
||||
"Experimental": "Experimental",
|
||||
"Export": "Exportar",
|
||||
"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "S'ha detectat la suplantació d'identitat de l'empremta digital: no es poden utilitzar les inicials com a avatar. S'estableix la imatge de perfil predeterminada.",
|
||||
"Fluidly stream large external response chunks": "Transmetre amb fluïdesa grans trossos de resposta externa",
|
||||
"Focus chat input": "Estableix el focus a l'entrada del xat",
|
||||
"Folder deleted successfully": "Carpeta eliminada correctament",
|
||||
"Folder name cannot be empty": "El nom de la carpeta no pot ser buit",
|
||||
"Folder name cannot be empty.": "El nom de la carpeta no pot ser buit.",
|
||||
"Folder name updated successfully": "Nom de la carpeta actualitzat correctament",
|
||||
"Followed instructions perfectly": "S'han seguit les instruccions perfectament",
|
||||
"Form": "Formulari",
|
||||
"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
|
||||
|
|
@ -365,7 +369,7 @@
|
|||
"Install from Github URL": "Instal·lar des de l'URL de Github",
|
||||
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
|
||||
"Interface": "Interfície",
|
||||
"Invalid file format.": "",
|
||||
"Invalid file format.": "Format d'arxiu no vàlid.",
|
||||
"Invalid Tag": "Etiqueta no vàlida",
|
||||
"January": "Gener",
|
||||
"join our Discord for help.": "uneix-te al nostre Discord per obtenir ajuda.",
|
||||
|
|
@ -440,16 +444,19 @@
|
|||
"Model(s) Whitelisted": "Model(s) a la llista blanca",
|
||||
"Modelfile Content": "Contingut del Modelfile",
|
||||
"Models": "Models",
|
||||
"more": "més",
|
||||
"More": "Més",
|
||||
"Move to Top": "Moure a dalt de tot",
|
||||
"Name": "Nom",
|
||||
"Name your model": "Posa un nom al teu model",
|
||||
"New Chat": "Nou xat",
|
||||
"New folder": "Nova carpeta",
|
||||
"New Password": "Nova contrasenya",
|
||||
"No content found": "No s'ha trobat contingut",
|
||||
"No content to speak": "No hi ha contingut per parlar",
|
||||
"No distance available": "No hi ha distància disponible",
|
||||
"No file selected": "No s'ha escollit cap fitxer",
|
||||
"No files found.": "",
|
||||
"No files found.": "No s'han trobat arxius.",
|
||||
"No HTML, CSS, or JavaScript content found.": "No s'ha trobat contingut HTML, CSS o JavaScript.",
|
||||
"No knowledge found": "No s'ha trobat Coneixement",
|
||||
"No results found": "No s'han trobat resultats",
|
||||
|
|
@ -492,7 +499,7 @@
|
|||
"OpenAI URL/Key required.": "URL/Clau d'OpenAI requerides.",
|
||||
"or": "o",
|
||||
"Other": "Altres",
|
||||
"OUTPUT": "",
|
||||
"OUTPUT": "SORTIDA",
|
||||
"Output format": "Format de sortida",
|
||||
"Overview": "Vista general",
|
||||
"page": "pàgina",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Enregistrar la veu",
|
||||
"Redirecting you to OpenWebUI Community": "Redirigint-te a la comunitat OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
|
||||
"References from": "Referències de",
|
||||
"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
|
||||
"Regenerate": "Regenerar",
|
||||
"Release Notes": "Notes de la versió",
|
||||
"Relevance": "Rellevància",
|
||||
"Remove": "Eliminar",
|
||||
"Remove Model": "Eliminar el model",
|
||||
"Rename": "Canviar el nom",
|
||||
|
|
@ -545,7 +554,7 @@
|
|||
"Reranking model set to \"{{reranking_model}}\"": "Model de reavaluació establert a \"{{reranking_model}}\"",
|
||||
"Reset": "Restableix",
|
||||
"Reset Upload Directory": "Restableix el directori de pujades",
|
||||
"Reset Vector Storage/Knowledge": "",
|
||||
"Reset Vector Storage/Knowledge": "Restableix el Repositori de vectors/Coneixement",
|
||||
"Response AutoCopy to Clipboard": "Copiar la resposta automàticament al porta-retalls",
|
||||
"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.",
|
||||
"Response splitting": "Divisió de la resposta",
|
||||
|
|
@ -568,7 +577,7 @@
|
|||
"Search a model": "Cercar un model",
|
||||
"Search Chats": "Cercar xats",
|
||||
"Search Collection": "Cercar col·leccions",
|
||||
"search for tags": "",
|
||||
"search for tags": "cercar etiquetes",
|
||||
"Search Functions": "Cercar funcions",
|
||||
"Search Knowledge": "Cercar coneixement",
|
||||
"Search Models": "Cercar models",
|
||||
|
|
@ -601,6 +610,7 @@
|
|||
"Select model": "Seleccionar un model",
|
||||
"Select only one model to call": "Seleccionar només un model per trucar",
|
||||
"Selected model(s) do not support image inputs": "El(s) model(s) seleccionats no admeten l'entrada d'imatges",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Enviar",
|
||||
"Send a Message": "Enviar un missatge",
|
||||
"Send message": "Enviar missatge",
|
||||
|
|
@ -643,7 +653,7 @@
|
|||
"Speech Playback Speed": "Velocitat de la parla",
|
||||
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
|
||||
"Speech-to-Text Engine": "Motor de veu a text",
|
||||
"Stop": "",
|
||||
"Stop": "Atura",
|
||||
"Stop Sequence": "Atura la seqüència",
|
||||
"Stream Chat Response": "Fer streaming de la resposta del xat",
|
||||
"STT Model": "Model SST",
|
||||
|
|
@ -666,7 +676,7 @@
|
|||
"Template": "Plantilla",
|
||||
"Temporary Chat": "Xat temporal",
|
||||
"Text Completion": "Completament de text",
|
||||
"Text Splitter": "",
|
||||
"Text Splitter": "Separador de text",
|
||||
"Text-to-Speech Engine": "Motor de text a veu",
|
||||
"Tfs Z": "Tfs Z",
|
||||
"Thanks for your feedback!": "Gràcies pel teu comentari!",
|
||||
|
|
@ -681,11 +691,12 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Aquesta és una funció experimental, és possible que no funcioni com s'espera i està subjecta a canvis en qualsevol moment.",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "Aquesta opció eliminarà tots els fitxers existents de la col·lecció i els substituirà per fitxers recentment penjats.",
|
||||
"This will delete": "Això eliminarà",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "Això restablirà la base de coneixement i sincronitzarà tots els fitxers. Vols continuar?",
|
||||
"Thorough explanation": "Explicació en detall",
|
||||
"Tika": "Tika",
|
||||
"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
|
||||
"Tiktoken": "",
|
||||
"Tiktoken": "Tiktoken",
|
||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza les diverses variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
|
||||
"Title": "Títol",
|
||||
"Title (e.g. Tell me a fun fact)": "Títol (p. ex. Digues-me quelcom divertit)",
|
||||
|
|
@ -703,7 +714,7 @@
|
|||
"Today": "Avui",
|
||||
"Toggle settings": "Alterna preferències",
|
||||
"Toggle sidebar": "Alterna la barra lateral",
|
||||
"Token": "",
|
||||
"Token": "Token",
|
||||
"Tokens To Keep On Context Refresh (num_keep)": "Tokens a mantenir en l'actualització del context (num_keep)",
|
||||
"Tool created successfully": "Eina creada correctament",
|
||||
"Tool deleted successfully": "Eina eliminada correctament",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "advanced settings",
|
||||
"Advanced Params": "",
|
||||
"All chats": "",
|
||||
"All Documents": "",
|
||||
"All Users": "Ang tanan nga mga tiggamit",
|
||||
"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Pagtangtang sa panaghisgot",
|
||||
"Delete Chat": "",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "",
|
||||
"Download canceled": "",
|
||||
"Download Database": "I-download ang database",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Ihulog ang bisan unsang file dinhi aron idugang kini sa panag-istoryahanay",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ",
|
||||
"Edit": "",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
|
||||
"Fluidly stream large external response chunks": "Hapsay nga paghatud sa daghang mga tipik sa eksternal nga mga tubag",
|
||||
"Focus chat input": "Pag-focus sa entry sa diskusyon",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "I-format ang imong mga variable gamit ang square brackets sama niini:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Gi-whitelist nga (mga) modelo",
|
||||
"Modelfile Content": "Mga sulod sa template file",
|
||||
"Models": "Mga modelo",
|
||||
"more": "",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "Ngalan",
|
||||
"Name your model": "",
|
||||
"New Chat": "Bag-ong diskusyon",
|
||||
"New folder": "",
|
||||
"New Password": "Bag-ong Password",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Irekord ang tingog",
|
||||
"Redirecting you to OpenWebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "",
|
||||
"Regenerate": "",
|
||||
"Release Notes": "Release Notes",
|
||||
"Relevance": "",
|
||||
"Remove": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "Pagpili og modelo",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "",
|
||||
"Send a Message": "Magpadala ug mensahe",
|
||||
"Send message": "Magpadala ug mensahe",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Administratoren haben jederzeit Zugriff auf alle Werkzeuge. Benutzer können im Arbeitsbereich zugewiesen.",
|
||||
"Advanced Parameters": "Erweiterte Parameter",
|
||||
"Advanced Params": "Erweiterte Parameter",
|
||||
"All chats": "",
|
||||
"All Documents": "Alle Dokumente",
|
||||
"All Users": "Alle Benutzer",
|
||||
"Allow Chat Deletion": "Unterhaltungen löschen erlauben",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Unterhaltung löschen",
|
||||
"Delete Chat": "Unterhaltung löschen",
|
||||
"Delete chat?": "Unterhaltung löschen?",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "Funktion löschen?",
|
||||
"Delete prompt?": "Prompt löschen?",
|
||||
"delete this link": "diesen Link löschen",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Exportieren",
|
||||
"Download canceled": "Exportierung abgebrochen",
|
||||
"Download Database": "Datenbank exportieren",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Ziehen Sie beliebige Dateien hierher, um sie der Unterhaltung hinzuzufügen",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z. B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit Memory": "Erinnerungen bearbeiten",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerabdruck-Spoofing erkannt: Initialen können nicht als Avatar verwendet werden. Standard-Avatar wird verwendet.",
|
||||
"Fluidly stream large external response chunks": "Nahtlose Übertragung großer externer Antwortabschnitte",
|
||||
"Focus chat input": "Chat-Eingabe fokussieren",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "Anweisungen perfekt befolgt",
|
||||
"Form": "Formular",
|
||||
"Format your variables using square brackets like this:": "Formatieren Sie Ihre Variablen mit eckigen Klammern wie folgt:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Modell(e) auf der Whitelist",
|
||||
"Modelfile Content": "Modelfile-Inhalt",
|
||||
"Models": "Modelle",
|
||||
"more": "mehr",
|
||||
"More": "Mehr",
|
||||
"Move to Top": "",
|
||||
"Name": "Name",
|
||||
"Name your model": "Benennen Sie Ihr Modell",
|
||||
"New Chat": "Neue Unterhaltung",
|
||||
"New folder": "",
|
||||
"New Password": "Neues Passwort",
|
||||
"No content found": "",
|
||||
"No content to speak": "Kein Inhalt zum Vorlesen",
|
||||
"No distance available": "",
|
||||
"No file selected": "Keine Datei ausgewählt",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Stimme aufnehmen",
|
||||
"Redirecting you to OpenWebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "Referenzen aus",
|
||||
"Refused when it shouldn't have": "Abgelehnt, obwohl es nicht hätte abgelehnt werden sollen",
|
||||
"Regenerate": "Neu generieren",
|
||||
"Release Notes": "Veröffentlichungshinweise",
|
||||
"Relevance": "",
|
||||
"Remove": "Entfernen",
|
||||
"Remove Model": "Modell entfernen",
|
||||
"Rename": "Umbenennen",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "Modell auswählen",
|
||||
"Select only one model to call": "Wählen Sie nur ein Modell zum Anrufen aus",
|
||||
"Selected model(s) do not support image inputs": "Ihre ausgewählten Modelle unterstützen keine Bildeingaben",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Senden",
|
||||
"Send a Message": "Eine Nachricht senden",
|
||||
"Send message": "Nachricht senden",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Dies ist eine experimentelle Funktion, sie funktioniert möglicherweise nicht wie erwartet und kann jederzeit geändert werden.",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "Dies löscht",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "Ausführliche Erklärung",
|
||||
"Tika": "Tika",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Advanced Parameters",
|
||||
"Advanced Params": "",
|
||||
"All chats": "",
|
||||
"All Documents": "",
|
||||
"All Users": "All Users",
|
||||
"Allow Chat Deletion": "Allow Delete Chats",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Delete chat",
|
||||
"Delete Chat": "",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "",
|
||||
"Download canceled": "",
|
||||
"Download Database": "Download Database",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Drop files here to add to conversation",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Much time units are 's', 'm', 'h'.",
|
||||
"Edit": "",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint dogeing: Unable to use initials as avatar. Defaulting to default doge image.",
|
||||
"Fluidly stream large external response chunks": "Fluidly wow big chunks",
|
||||
"Focus chat input": "Focus chat bork",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "Format variables using square brackets like wow:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Wowdel(s) Whitelisted",
|
||||
"Modelfile Content": "Modelfile Content",
|
||||
"Models": "Wowdels",
|
||||
"more": "",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "Name",
|
||||
"Name your model": "",
|
||||
"New Chat": "New Bark",
|
||||
"New folder": "",
|
||||
"New Password": "New Barkword",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Record Bark",
|
||||
"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "",
|
||||
"Regenerate": "",
|
||||
"Release Notes": "Release Borks",
|
||||
"Relevance": "",
|
||||
"Remove": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
|
|
@ -602,6 +611,7 @@
|
|||
"Select model": "Select model much choice",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "",
|
||||
"Send a Message": "Send a Message much message",
|
||||
"Send message": "Send message very send",
|
||||
|
|
@ -682,6 +692,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "",
|
||||
"Advanced Params": "",
|
||||
"All chats": "",
|
||||
"All Documents": "",
|
||||
"All Users": "",
|
||||
"Allow Chat Deletion": "",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "",
|
||||
"Delete Chat": "",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "",
|
||||
"Download canceled": "",
|
||||
"Download Database": "",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
|
||||
"Edit": "",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
|
||||
"Fluidly stream large external response chunks": "",
|
||||
"Focus chat input": "",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "",
|
||||
"Modelfile Content": "",
|
||||
"Models": "",
|
||||
"more": "",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "",
|
||||
"Name your model": "",
|
||||
"New Chat": "",
|
||||
"New folder": "",
|
||||
"New Password": "",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "",
|
||||
"Redirecting you to OpenWebUI Community": "",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "",
|
||||
"Regenerate": "",
|
||||
"Release Notes": "",
|
||||
"Relevance": "",
|
||||
"Remove": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "",
|
||||
"Send a Message": "",
|
||||
"Send message": "",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "",
|
||||
"Advanced Params": "",
|
||||
"All chats": "",
|
||||
"All Documents": "",
|
||||
"All Users": "",
|
||||
"Allow Chat Deletion": "",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "",
|
||||
"Delete Chat": "",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "",
|
||||
"Download canceled": "",
|
||||
"Download Database": "",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
|
||||
"Edit": "",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
|
||||
"Fluidly stream large external response chunks": "",
|
||||
"Focus chat input": "",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "",
|
||||
"Modelfile Content": "",
|
||||
"Models": "",
|
||||
"more": "",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "",
|
||||
"Name your model": "",
|
||||
"New Chat": "",
|
||||
"New folder": "",
|
||||
"New Password": "",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "",
|
||||
"Redirecting you to OpenWebUI Community": "",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "",
|
||||
"Regenerate": "",
|
||||
"Release Notes": "",
|
||||
"Relevance": "",
|
||||
"Remove": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "",
|
||||
"Send a Message": "",
|
||||
"Send message": "",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Admins tienen acceso a todas las herramientas en todo momento; los usuarios necesitan herramientas asignadas por modelo en el espacio de trabajo.",
|
||||
"Advanced Parameters": "Parámetros Avanzados",
|
||||
"Advanced Params": "Parámetros avanzados",
|
||||
"All chats": "",
|
||||
"All Documents": "Todos los Documentos",
|
||||
"All Users": "Todos los Usuarios",
|
||||
"Allow Chat Deletion": "Permitir Borrar Chats",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Borrar chat",
|
||||
"Delete Chat": "Borrar Chat",
|
||||
"Delete chat?": "Borrar el chat?",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "Borrar la función?",
|
||||
"Delete prompt?": "Borrar el prompt?",
|
||||
"delete this link": "Borrar este enlace",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Descargar",
|
||||
"Download canceled": "Descarga cancelada",
|
||||
"Download Database": "Descarga la Base de Datos",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Suelta cualquier archivo aquí para agregarlo a la conversación",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p.ej. '30s','10m'. Unidades válidas de tiempo son 's', 'm', 'h'.",
|
||||
"Edit": "Editar",
|
||||
"Edit Memory": "Editar Memoria",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Se detectó suplantación de huellas: No se pueden usar las iniciales como avatar. Por defecto se utiliza la imagen de perfil predeterminada.",
|
||||
"Fluidly stream large external response chunks": "Transmita con fluidez grandes fragmentos de respuesta externa",
|
||||
"Focus chat input": "Enfoca la entrada del chat",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "Siguió las instrucciones perfectamente",
|
||||
"Form": "De",
|
||||
"Format your variables using square brackets like this:": "Formatea tus variables usando corchetes de la siguiente manera:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Modelo(s) habilitados",
|
||||
"Modelfile Content": "Contenido del Modelfile",
|
||||
"Models": "Modelos",
|
||||
"more": "",
|
||||
"More": "Más",
|
||||
"Move to Top": "Mueve al tope",
|
||||
"Name": "Nombre",
|
||||
"Name your model": "Asigne un nombre a su modelo",
|
||||
"New Chat": "Nuevo Chat",
|
||||
"New folder": "",
|
||||
"New Password": "Nueva Contraseña",
|
||||
"No content found": "",
|
||||
"No content to speak": "No hay contenido para hablar",
|
||||
"No distance available": "",
|
||||
"No file selected": "Ningún archivo fué seleccionado",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "No se encontró contenido HTML, CSS, o JavaScript.",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Grabar voz",
|
||||
"Redirecting you to OpenWebUI Community": "Redireccionándote a la comunidad OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Referirse a usted mismo como \"Usuario\" (por ejemplo, \"El usuario está aprendiendo Español\")",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "Rechazado cuando no debería",
|
||||
"Regenerate": "Regenerar",
|
||||
"Release Notes": "Notas de la versión",
|
||||
"Relevance": "",
|
||||
"Remove": "Eliminar",
|
||||
"Remove Model": "Eliminar modelo",
|
||||
"Rename": "Renombrar",
|
||||
|
|
@ -601,6 +610,7 @@
|
|||
"Select model": "Selecciona un modelo",
|
||||
"Select only one model to call": "Selecciona sólo un modelo para llamar",
|
||||
"Selected model(s) do not support image inputs": "Los modelos seleccionados no admiten entradas de imagen",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Enviar",
|
||||
"Send a Message": "Enviar un Mensaje",
|
||||
"Send message": "Enviar Mensaje",
|
||||
|
|
@ -681,6 +691,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Esta es una característica experimental que puede no funcionar como se esperaba y está sujeto a cambios en cualquier momento.",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": " Esta opción eliminará todos los archivos existentes en la colección y los reemplazará con nuevos archivos subidos.",
|
||||
"This will delete": "Esto eliminará",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "Esto reseteará la base de conocimientos y sincronizará todos los archivos. ¿Desea continuar?",
|
||||
"Thorough explanation": "Explicación exhaustiva",
|
||||
"Tika": "Tika",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "پارامترهای پیشرفته",
|
||||
"Advanced Params": "پارام های پیشرفته",
|
||||
"All chats": "",
|
||||
"All Documents": "تمام سند ها",
|
||||
"All Users": "همه کاربران",
|
||||
"Allow Chat Deletion": "اجازه حذف گپ",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "حذف گپ",
|
||||
"Delete Chat": "حذف گپ",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "حذف این لینک",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "دانلود کن",
|
||||
"Download canceled": "دانلود لغو شد",
|
||||
"Download Database": "دانلود پایگاه داده",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "هر فایلی را اینجا رها کنید تا به مکالمه اضافه شود",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "به طور مثال '30s','10m'. واحد\u200cهای زمانی معتبر 's', 'm', 'h' هستند.",
|
||||
"Edit": "ویرایش",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "فانگ سرفیس شناسایی شد: نمی توان از نمایه شما به عنوان آواتار استفاده کرد. پیش فرض به عکس پروفایل پیش فرض برگشت داده شد.",
|
||||
"Fluidly stream large external response chunks": "تکه های پاسخ خارجی بزرگ را به صورت سیال پخش کنید",
|
||||
"Focus chat input": "فوکوس کردن ورودی گپ",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "دستورالعمل ها را کاملا دنبال کرد",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "متغیرهای خود را با استفاده از براکت مربع به شکل زیر قالب بندی کنید:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "مدل در لیست سفید ثبت شد",
|
||||
"Modelfile Content": "محتویات فایل مدل",
|
||||
"Models": "مدل\u200cها",
|
||||
"more": "",
|
||||
"More": "بیشتر",
|
||||
"Move to Top": "",
|
||||
"Name": "نام",
|
||||
"Name your model": "نام مدل خود را",
|
||||
"New Chat": "گپ جدید",
|
||||
"New folder": "",
|
||||
"New Password": "رمز عبور جدید",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "ضبط صدا",
|
||||
"Redirecting you to OpenWebUI Community": "در حال هدایت به OpenWebUI Community",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "رد شده زمانی که باید نباشد",
|
||||
"Regenerate": "ری\u200cسازی",
|
||||
"Release Notes": "یادداشت\u200cهای انتشار",
|
||||
"Relevance": "",
|
||||
"Remove": "حذف",
|
||||
"Remove Model": "حذف مدل",
|
||||
"Rename": "تغییر نام",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "انتخاب یک مدل",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "مدل) های (انتخاب شده ورودیهای تصویر را پشتیبانی نمیکند",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "ارسال",
|
||||
"Send a Message": "ارسال یک پیام",
|
||||
"Send message": "ارسال پیام",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "توضیح کامل",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "Edistyneet parametrit",
|
||||
"Advanced Params": "Edistyneet parametrit",
|
||||
"All chats": "",
|
||||
"All Documents": "Kaikki asiakirjat",
|
||||
"All Users": "Kaikki käyttäjät",
|
||||
"Allow Chat Deletion": "Salli keskustelujen poisto",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Poista keskustelu",
|
||||
"Delete Chat": "Poista keskustelu",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "poista tämä linkki",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Lataa",
|
||||
"Download canceled": "Lataus peruutettu",
|
||||
"Download Database": "Lataa tietokanta",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Pudota tiedostoja tähän lisätäksesi ne keskusteluun",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "esim. '30s', '10m'. Kelpoiset aikayksiköt ovat 's', 'm', 'h'.",
|
||||
"Edit": "Muokkaa",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Sormenjäljen väärentäminen havaittu: Ei voi käyttää alkukirjaimia avatarina. Käytetään oletusprofiilikuvaa.",
|
||||
"Fluidly stream large external response chunks": "Virtaa suuria ulkoisia vastausosia joustavasti",
|
||||
"Focus chat input": "Fokusoi syöttökenttään",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "Noudatti ohjeita täydellisesti",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "Muotoile muuttujat hakasulkeilla näin:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Malli(t) sallittu",
|
||||
"Modelfile Content": "Mallitiedoston sisältö",
|
||||
"Models": "Mallit",
|
||||
"more": "",
|
||||
"More": "Lisää",
|
||||
"Move to Top": "",
|
||||
"Name": "Nimi",
|
||||
"Name your model": "Mallin nimeäminen",
|
||||
"New Chat": "Uusi keskustelu",
|
||||
"New folder": "",
|
||||
"New Password": "Uusi salasana",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Nauhoita ääni",
|
||||
"Redirecting you to OpenWebUI Community": "Ohjataan sinut OpenWebUI-yhteisöön",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "Kieltäytyi, vaikka ei olisi pitänyt",
|
||||
"Regenerate": "Uudelleenluo",
|
||||
"Release Notes": "Julkaisutiedot",
|
||||
"Relevance": "",
|
||||
"Remove": "Poista",
|
||||
"Remove Model": "Poista malli",
|
||||
"Rename": "Nimeä uudelleen",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "Valitse malli",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Valitut mallit eivät tue kuvasyötteitä",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Lähetä",
|
||||
"Send a Message": "Lähetä viesti",
|
||||
"Send message": "Lähetä viesti",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "Perusteellinen selitys",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; les utilisateurs ont besoin d'outils affectés par modèle dans l'espace de travail.",
|
||||
"Advanced Parameters": "Paramètres avancés",
|
||||
"Advanced Params": "Paramètres avancés",
|
||||
"All chats": "",
|
||||
"All Documents": "Tous les documents",
|
||||
"All Users": "Tous les Utilisateurs",
|
||||
"Allow Chat Deletion": "Autoriser la suppression de l'historique de chat",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Supprimer la conversation",
|
||||
"Delete Chat": "Supprimer la Conversation",
|
||||
"Delete chat?": "Supprimer la conversation ?",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "Supprimer la fonction ?",
|
||||
"Delete prompt?": "Supprimer la prompt ?",
|
||||
"delete this link": "supprimer ce lien",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Télécharger",
|
||||
"Download canceled": "Téléchargement annulé",
|
||||
"Download Database": "Télécharger la base de données",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.",
|
||||
"Edit": "Modifier",
|
||||
"Edit Memory": "Modifier la mémoire",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.",
|
||||
"Fluidly stream large external response chunks": "Diffuser de manière fluide de larges portions de réponses externes",
|
||||
"Focus chat input": "Se concentrer sur le chat en entrée",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "A parfaitement suivi les instructions",
|
||||
"Form": "Formulaire",
|
||||
"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Modèle(s) Autorisé(s)",
|
||||
"Modelfile Content": "Contenu du Fichier de Modèle",
|
||||
"Models": "Modèles",
|
||||
"more": "",
|
||||
"More": "Plus de",
|
||||
"Move to Top": "",
|
||||
"Name": "Nom",
|
||||
"Name your model": "Nommez votre modèle",
|
||||
"New Chat": "Nouvelle conversation",
|
||||
"New folder": "",
|
||||
"New Password": "Nouveau mot de passe",
|
||||
"No content found": "",
|
||||
"No content to speak": "Rien à signaler",
|
||||
"No distance available": "",
|
||||
"No file selected": "Aucun fichier sélectionné",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Enregistrer la voix",
|
||||
"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être",
|
||||
"Regenerate": "Regénérer",
|
||||
"Release Notes": "Notes de publication",
|
||||
"Relevance": "",
|
||||
"Remove": "Retirer",
|
||||
"Remove Model": "Retirer le modèle",
|
||||
"Rename": "Renommer",
|
||||
|
|
@ -601,6 +610,7 @@
|
|||
"Select model": "Sélectionnez un modèle",
|
||||
"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
|
||||
"Selected model(s) do not support image inputs": "Les modèle(s) sélectionné(s) ne prennent pas en charge les entrées d'images",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Envoyer",
|
||||
"Send a Message": "Envoyer un message",
|
||||
"Send message": "Envoyer un message",
|
||||
|
|
@ -681,6 +691,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Il s'agit d'une fonctionnalité expérimentale, elle peut ne pas fonctionner comme prévu et est sujette à modification à tout moment.",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "Cela supprimera",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "Explication approfondie",
|
||||
"Tika": "Tika",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; il faut attribuer des outils aux utilisateurs par modèle dans l'espace de travail.",
|
||||
"Advanced Parameters": "Paramètres avancés",
|
||||
"Advanced Params": "Paramètres avancés",
|
||||
"All chats": "",
|
||||
"All Documents": "Tous les documents",
|
||||
"All Users": "Tous les Utilisateurs",
|
||||
"Allow Chat Deletion": "Autoriser la suppression de l'historique de chat",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "Supprimer la conversation",
|
||||
"Delete Chat": "Supprimer la Conversation",
|
||||
"Delete chat?": "Supprimer la conversation ?",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "Supprimer la fonction ?",
|
||||
"Delete prompt?": "Supprimer la prompt ?",
|
||||
"delete this link": "supprimer ce lien",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "Télécharger",
|
||||
"Download canceled": "Téléchargement annulé",
|
||||
"Download Database": "Télécharger la base de données",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.",
|
||||
"Edit": "Modifier",
|
||||
"Edit Memory": "Modifier la mémoire",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.",
|
||||
"Fluidly stream large external response chunks": "Streaming fluide de gros morceaux de réponses externes",
|
||||
"Focus chat input": "Se concentrer sur le chat en entrée",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "A parfaitement suivi les instructions",
|
||||
"Form": "Formulaire",
|
||||
"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "Modèle(s) Autorisé(s)",
|
||||
"Modelfile Content": "Contenu du Fichier de Modèle",
|
||||
"Models": "Modèles",
|
||||
"more": "",
|
||||
"More": "Plus de",
|
||||
"Move to Top": "Déplacer en haut",
|
||||
"Name": "Nom d'utilisateur",
|
||||
"Name your model": "Nommez votre modèle",
|
||||
"New Chat": "Nouvelle conversation",
|
||||
"New folder": "",
|
||||
"New Password": "Nouveau mot de passe",
|
||||
"No content found": "",
|
||||
"No content to speak": "Rien à signaler",
|
||||
"No distance available": "",
|
||||
"No file selected": "Aucun fichier sélectionné",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "Aucun contenu HTML, CSS ou JavaScript trouvé.",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "Enregistrer la voix",
|
||||
"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être",
|
||||
"Regenerate": "Regénérer",
|
||||
"Release Notes": "Notes de publication",
|
||||
"Relevance": "",
|
||||
"Remove": "Retirer",
|
||||
"Remove Model": "Retirer le modèle",
|
||||
"Rename": "Renommer",
|
||||
|
|
@ -601,6 +610,7 @@
|
|||
"Select model": "Sélectionnez un modèle",
|
||||
"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
|
||||
"Selected model(s) do not support image inputs": "Les modèle(s) sélectionné(s) ne prennent pas en charge les entrées d'images",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "Envoyer",
|
||||
"Send a Message": "Envoyer un message",
|
||||
"Send message": "Envoyer un message",
|
||||
|
|
@ -681,6 +691,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Il s'agit d'une fonctionnalité expérimentale, elle peut ne pas fonctionner comme prévu et est sujette à modification à tout moment.",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "Cette option supprimera tous les fichiers existants dans la collection et les remplacera par les fichiers nouvellement téléchargés.",
|
||||
"This will delete": "Cela supprimera",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "Cela réinitialisera la base de connaissances et synchronisera tous les fichiers. Souhaitez-vous continuer ?",
|
||||
"Thorough explanation": "Explication approfondie",
|
||||
"Tika": "Tika",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "פרמטרים מתקדמים",
|
||||
"Advanced Params": "פרמטרים מתקדמים",
|
||||
"All chats": "",
|
||||
"All Documents": "כל המסמכים",
|
||||
"All Users": "כל המשתמשים",
|
||||
"Allow Chat Deletion": "אפשר מחיקת צ'אט",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "מחק צ'אט",
|
||||
"Delete Chat": "מחק צ'אט",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "מחק את הקישור הזה",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "הורד",
|
||||
"Download canceled": "ההורדה בוטלה",
|
||||
"Download Database": "הורד מסד נתונים",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "גרור כל קובץ לכאן כדי להוסיף לשיחה",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "למשל '30s', '10m'. יחידות זמן חוקיות הן 's', 'm', 'h'.",
|
||||
"Edit": "ערוך",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "התגלתה הזיית טביעת אצבע: לא ניתן להשתמש בראשי תיבות כאווטאר. משתמש בתמונת פרופיל ברירת מחדל.",
|
||||
"Fluidly stream large external response chunks": "שידור נתונים חיצוניים בקצב רציף",
|
||||
"Focus chat input": "מיקוד הקלט לצ'אט",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "עקב אחר ההוראות במושלמות",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "עצב את המשתנים שלך באמצעות סוגריים מרובעים כך:",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "מודלים שנכללו ברשימה הלבנה",
|
||||
"Modelfile Content": "תוכן קובץ מודל",
|
||||
"Models": "מודלים",
|
||||
"more": "",
|
||||
"More": "עוד",
|
||||
"Move to Top": "",
|
||||
"Name": "שם",
|
||||
"Name your model": "תן שם לדגם שלך",
|
||||
"New Chat": "צ'אט חדש",
|
||||
"New folder": "",
|
||||
"New Password": "סיסמה חדשה",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "הקלט קול",
|
||||
"Redirecting you to OpenWebUI Community": "מפנה אותך לקהילת OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "נדחה כאשר לא היה צריך",
|
||||
"Regenerate": "הפק מחדש",
|
||||
"Release Notes": "הערות שחרור",
|
||||
"Relevance": "",
|
||||
"Remove": "הסר",
|
||||
"Remove Model": "הסר מודל",
|
||||
"Rename": "שנה שם",
|
||||
|
|
@ -601,6 +610,7 @@
|
|||
"Select model": "בחר מודל",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "דגמים נבחרים אינם תומכים בקלט תמונה",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "שלח",
|
||||
"Send a Message": "שלח הודעה",
|
||||
"Send message": "שלח הודעה",
|
||||
|
|
@ -681,6 +691,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "תיאור מפורט",
|
||||
"Tika": "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
|
||||
"Advanced Parameters": "उन्नत पैरामीटर",
|
||||
"Advanced Params": "उन्नत परम",
|
||||
"All chats": "",
|
||||
"All Documents": "सभी डॉक्यूमेंट्स",
|
||||
"All Users": "सभी उपयोगकर्ता",
|
||||
"Allow Chat Deletion": "चैट हटाने की अनुमति दें",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"Delete chat": "चैट हटाएं",
|
||||
"Delete Chat": "चैट हटाएं",
|
||||
"Delete chat?": "",
|
||||
"Delete folder?": "",
|
||||
"Delete function?": "",
|
||||
"Delete prompt?": "",
|
||||
"delete this link": "इस लिंक को हटाएं",
|
||||
|
|
@ -220,9 +222,7 @@
|
|||
"Download": "डाउनलोड",
|
||||
"Download canceled": "डाउनलोड रद्द किया गया",
|
||||
"Download Database": "डेटाबेस डाउनलोड करें",
|
||||
"Drop a chat export file here to import it.": "",
|
||||
"Drop any files here to add to the conversation": "बातचीत में जोड़ने के लिए कोई भी फ़ाइल यहां छोड़ें",
|
||||
"Drop Chat Export": "",
|
||||
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "जैसे '30s', '10m', मान्य समय इकाइयाँ 's', 'm', 'h' हैं।",
|
||||
"Edit": "संपादित करें",
|
||||
"Edit Memory": "",
|
||||
|
|
@ -312,6 +312,10 @@
|
|||
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "फ़िंगरप्रिंट स्पूफ़िंग का पता चला: प्रारंभिक अक्षरों को अवतार के रूप में उपयोग करने में असमर्थ। प्रोफ़ाइल छवि को डिफ़ॉल्ट पर डिफ़ॉल्ट किया जा रहा है.",
|
||||
"Fluidly stream large external response chunks": "बड़े बाह्य प्रतिक्रिया खंडों को तरल रूप से प्रवाहित करें",
|
||||
"Focus chat input": "चैट इनपुट पर फ़ोकस करें",
|
||||
"Folder deleted successfully": "",
|
||||
"Folder name cannot be empty": "",
|
||||
"Folder name cannot be empty.": "",
|
||||
"Folder name updated successfully": "",
|
||||
"Followed instructions perfectly": "निर्देशों का पूर्णतः पालन किया",
|
||||
"Form": "",
|
||||
"Format your variables using square brackets like this:": "वर्गाकार कोष्ठकों का उपयोग करके अपने चरों को इस प्रकार प्रारूपित करें :",
|
||||
|
|
@ -440,14 +444,17 @@
|
|||
"Model(s) Whitelisted": "मॉडल श्वेतसूची में है",
|
||||
"Modelfile Content": "मॉडल फ़ाइल सामग्री",
|
||||
"Models": "सभी मॉडल",
|
||||
"more": "",
|
||||
"More": "और..",
|
||||
"Move to Top": "",
|
||||
"Name": "नाम",
|
||||
"Name your model": "अपने मॉडल को नाम दें",
|
||||
"New Chat": "नई चैट",
|
||||
"New folder": "",
|
||||
"New Password": "नया पासवर्ड",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
"No distance available": "",
|
||||
"No file selected": "",
|
||||
"No files found.": "",
|
||||
"No HTML, CSS, or JavaScript content found.": "",
|
||||
|
|
@ -532,9 +539,11 @@
|
|||
"Record voice": "आवाज रिकॉर्ड करना",
|
||||
"Redirecting you to OpenWebUI Community": "आपको OpenWebUI समुदाय पर पुनर्निर्देशित किया जा रहा है",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"References from": "",
|
||||
"Refused when it shouldn't have": "जब ऐसा नहीं होना चाहिए था तो मना कर दिया",
|
||||
"Regenerate": "पुनः जेनरेट",
|
||||
"Release Notes": "रिलीज नोट्स",
|
||||
"Relevance": "",
|
||||
"Remove": "हटा दें",
|
||||
"Remove Model": "मोडेल हटाएँ",
|
||||
"Rename": "नाम बदलें",
|
||||
|
|
@ -600,6 +609,7 @@
|
|||
"Select model": "मॉडल चुनें",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "चयनित मॉडल छवि इनपुट का समर्थन नहीं करते हैं",
|
||||
"Semantic distance to query": "",
|
||||
"Send": "भेज",
|
||||
"Send a Message": "एक संदेश भेजो",
|
||||
"Send message": "मेसेज भेजें",
|
||||
|
|
@ -680,6 +690,7 @@
|
|||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
|
||||
"This will delete": "",
|
||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
|
||||
"Thorough explanation": "विस्तृत व्याख्या",
|
||||
"Tika": "",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue