mirror of
https://github.com/open-webui/open-webui.git
synced 2025-03-27 02:02:31 +01:00
Merge branch 'dev' of https://github.com/open-webui/open-webui
This commit is contained in:
commit
8dafe3cba8
@ -1094,21 +1094,27 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
|
||||
)
|
||||
|
||||
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """Create a concise, 3-5 word title with an emoji as a title for the chat history, in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
|
||||
Examples of titles:
|
||||
📉 Stock Market Trends
|
||||
🍪 Perfect Chocolate Chip Recipe
|
||||
Evolution of Music Streaming
|
||||
Remote Work Productivity Tips
|
||||
Artificial Intelligence in Healthcare
|
||||
🎮 Video Game Development Insights
|
||||
|
||||
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate a concise, 3-5 word title with an emoji summarizing the chat history.
|
||||
### Guidelines:
|
||||
- The title should clearly represent the main theme or subject of the conversation.
|
||||
- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
|
||||
- Write the title in the chat's primary language; default to English if multilingual.
|
||||
- Prioritize accuracy over excessive creativity; keep it clear and simple.
|
||||
### Output:
|
||||
JSON format: { "title": "your concise title here" }
|
||||
### Examples:
|
||||
- { "title": "📉 Stock Market Trends" },
|
||||
- { "title": "🍪 Perfect Chocolate Chip Recipe" },
|
||||
- { "title": "Evolution of Music Streaming" },
|
||||
- { "title": "Remote Work Productivity Tips" },
|
||||
- { "title": "Artificial Intelligence in Healthcare" },
|
||||
- { "title": "🎮 Video Game Development Insights" }
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:2}}
|
||||
</chat_history>"""
|
||||
|
||||
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TAGS_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.tags.prompt_template",
|
||||
|
@ -356,15 +356,16 @@ WEBUI_SECRET_KEY = os.environ.get(
|
||||
), # DEPRECATED: remove at next major version
|
||||
)
|
||||
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
|
||||
"WEBUI_SESSION_COOKIE_SAME_SITE",
|
||||
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
|
||||
)
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax")
|
||||
|
||||
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
|
||||
"WEBUI_SESSION_COOKIE_SECURE",
|
||||
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
|
||||
)
|
||||
WEBUI_SESSION_COOKIE_SECURE = os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true"
|
||||
|
||||
WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get("WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE)
|
||||
|
||||
WEBUI_AUTH_COOKIE_SECURE = os.environ.get(
|
||||
"WEBUI_AUTH_COOKIE_SECURE",
|
||||
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false")
|
||||
).lower() == "true"
|
||||
|
||||
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
||||
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
||||
|
@ -877,6 +877,7 @@ async def chat_completion(
|
||||
"tool_ids": form_data.get("tool_ids", None),
|
||||
"files": form_data.get("files", None),
|
||||
"features": form_data.get("features", None),
|
||||
"variables": form_data.get("variables", None),
|
||||
}
|
||||
form_data["metadata"] = metadata
|
||||
|
||||
|
@ -25,8 +25,8 @@ from open_webui.env import (
|
||||
WEBUI_AUTH,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
WEBUI_AUTH_COOKIE_SECURE,
|
||||
SRC_LOG_LEVELS,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@ -95,8 +95,8 @@ async def get_session_user(
|
||||
value=token,
|
||||
expires=datetime_expires_at,
|
||||
httponly=True, # Ensures the cookie is not accessible via JavaScript
|
||||
samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
@ -164,7 +164,7 @@ async def update_password(
|
||||
############################
|
||||
# LDAP Authentication
|
||||
############################
|
||||
@router.post("/ldap", response_model=SigninResponse)
|
||||
@router.post("/ldap", response_model=SessionUserResponse)
|
||||
async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
ENABLE_LDAP = request.app.state.config.ENABLE_LDAP
|
||||
LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
|
||||
@ -288,6 +288,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
httponly=True, # Ensures the cookie is not accessible via JavaScript
|
||||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"token_type": "Bearer",
|
||||
@ -296,6 +300,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"profile_image_url": user.profile_image_url,
|
||||
"permissions": user_permissions,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
@ -378,8 +383,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||
value=token,
|
||||
expires=datetime_expires_at,
|
||||
httponly=True, # Ensures the cookie is not accessible via JavaScript
|
||||
samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
@ -473,8 +478,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||
value=token,
|
||||
expires=datetime_expires_at,
|
||||
httponly=True, # Ensures the cookie is not accessible via JavaScript
|
||||
samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
if request.app.state.config.WEBHOOK_URL:
|
||||
|
@ -444,15 +444,21 @@ async def pin_chat_by_id(id: str, user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
class CloneForm(BaseModel):
|
||||
title: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/{id}/clone", response_model=Optional[ChatResponse])
|
||||
async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
|
||||
async def clone_chat_by_id(
|
||||
form_data: CloneForm, id: str, user=Depends(get_verified_user)
|
||||
):
|
||||
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||
if chat:
|
||||
updated_chat = {
|
||||
**chat.chat,
|
||||
"originalChatId": chat.id,
|
||||
"branchPointMessageId": chat.chat["history"]["currentId"],
|
||||
"title": f"Clone of {chat.title}",
|
||||
"title": form_data.title if form_data.title else f"Clone of {chat.title}",
|
||||
}
|
||||
|
||||
chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))
|
||||
|
@ -264,7 +264,11 @@ def add_file_to_knowledge_by_id(
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if knowledge.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@ -342,7 +346,12 @@ def update_file_from_knowledge_by_id(
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if knowledge.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@ -406,7 +415,11 @@ def remove_file_from_knowledge_by_id(
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if knowledge.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@ -429,10 +442,6 @@ def remove_file_from_knowledge_by_id(
|
||||
if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
|
||||
|
||||
# Delete physical file
|
||||
if file.path:
|
||||
Storage.delete_file(file.path)
|
||||
|
||||
# Delete file from database
|
||||
Files.delete_file_by_id(form_data.file_id)
|
||||
|
||||
@ -484,7 +493,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if knowledge.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@ -543,7 +556,11 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if knowledge.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@ -582,7 +599,11 @@ def add_files_to_knowledge_batch(
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if knowledge.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
|
@ -183,7 +183,11 @@ async def delete_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if model.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
user.role != "admin"
|
||||
and model.user_id != user.id
|
||||
and not has_access(user.id, "write", model.access_control)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
|
@ -395,7 +395,7 @@ async def get_ollama_tags(
|
||||
)
|
||||
|
||||
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
models["models"] = get_filtered_models(models, user)
|
||||
models["models"] = await get_filtered_models(models, user)
|
||||
|
||||
return models
|
||||
|
||||
@ -977,6 +977,7 @@ async def generate_chat_completion(
|
||||
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||
bypass_filter = True
|
||||
|
||||
metadata = form_data.pop("metadata", None)
|
||||
try:
|
||||
form_data = GenerateChatCompletionForm(**form_data)
|
||||
except Exception as e:
|
||||
@ -987,8 +988,6 @@ async def generate_chat_completion(
|
||||
)
|
||||
|
||||
payload = {**form_data.model_dump(exclude_none=True)}
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
model_id = payload["model"]
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
@ -1006,7 +1005,7 @@ async def generate_chat_completion(
|
||||
payload["options"] = apply_model_params_to_body_ollama(
|
||||
params, payload["options"]
|
||||
)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, metadata)
|
||||
|
||||
# Check if user has access to the model
|
||||
if not bypass_filter and user.role == "user":
|
||||
|
@ -489,7 +489,7 @@ async def get_models(
|
||||
raise HTTPException(status_code=500, detail=error_detail)
|
||||
|
||||
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
models["data"] = get_filtered_models(models, user)
|
||||
models["data"] = await get_filtered_models(models, user)
|
||||
|
||||
return models
|
||||
|
||||
@ -551,9 +551,9 @@ async def generate_chat_completion(
|
||||
bypass_filter = True
|
||||
|
||||
idx = 0
|
||||
|
||||
payload = {**form_data}
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
metadata = payload.pop("metadata", None)
|
||||
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
@ -566,7 +566,7 @@ async def generate_chat_completion(
|
||||
|
||||
params = model_info.params.model_dump()
|
||||
payload = apply_model_params_to_body_openai(params, payload)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, metadata)
|
||||
|
||||
# Check if user has access to the model
|
||||
if not bypass_filter and user.role == "user":
|
||||
|
@ -147,7 +147,11 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if prompt.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
prompt.user_id != user.id
|
||||
and not has_access(user.id, "write", prompt.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
|
@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import logging
|
||||
import re
|
||||
|
||||
from open_webui.utils.chat import generate_chat_completion
|
||||
from open_webui.utils.task import (
|
||||
@ -161,9 +162,20 @@ async def generate_title(
|
||||
else:
|
||||
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
|
||||
messages = form_data["messages"]
|
||||
|
||||
# Remove reasoning details from the messages
|
||||
for message in messages:
|
||||
message["content"] = re.sub(
|
||||
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
|
||||
"",
|
||||
message["content"],
|
||||
flags=re.S,
|
||||
).strip()
|
||||
|
||||
content = title_generation_template(
|
||||
template,
|
||||
form_data["messages"],
|
||||
messages,
|
||||
{
|
||||
"name": user.name,
|
||||
"location": user.info.get("location") if user.info else None,
|
||||
@ -175,10 +187,10 @@ async def generate_title(
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
**(
|
||||
{"max_tokens": 50}
|
||||
{"max_tokens": 1000}
|
||||
if models[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 50,
|
||||
"max_completion_tokens": 1000,
|
||||
}
|
||||
),
|
||||
"metadata": {
|
||||
|
@ -227,7 +227,11 @@ async def delete_tools_by_id(
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if tools.user_id != user.id and user.role != "admin":
|
||||
if (
|
||||
tools.user_id != user.id
|
||||
and not has_access(user.id, "write", tools.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
|
@ -661,6 +661,9 @@ def apply_params_to_form_data(form_data, model):
|
||||
if "temperature" in params:
|
||||
form_data["temperature"] = params["temperature"]
|
||||
|
||||
if "max_tokens" in params:
|
||||
form_data["max_tokens"] = params["max_tokens"]
|
||||
|
||||
if "top_p" in params:
|
||||
form_data["top_p"] = params["top_p"]
|
||||
|
||||
@ -741,6 +744,8 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
||||
files.extend(knowledge_files)
|
||||
form_data["files"] = files
|
||||
|
||||
variables = form_data.pop("variables", None)
|
||||
|
||||
features = form_data.pop("features", None)
|
||||
if features:
|
||||
if "web_search" in features and features["web_search"]:
|
||||
@ -884,16 +889,24 @@ async def process_chat_response(
|
||||
|
||||
if res and isinstance(res, dict):
|
||||
if len(res.get("choices", [])) == 1:
|
||||
title = (
|
||||
title_string = (
|
||||
res.get("choices", [])[0]
|
||||
.get("message", {})
|
||||
.get(
|
||||
"content",
|
||||
message.get("content", "New Chat"),
|
||||
)
|
||||
).strip()
|
||||
.get("content", message.get("content", "New Chat"))
|
||||
)
|
||||
else:
|
||||
title = None
|
||||
title_string = ""
|
||||
|
||||
title_string = title_string[
|
||||
title_string.find("{") : title_string.rfind("}") + 1
|
||||
]
|
||||
|
||||
try:
|
||||
title = json.loads(title_string).get(
|
||||
"title", "New Chat"
|
||||
)
|
||||
except Exception as e:
|
||||
title = ""
|
||||
|
||||
if not title:
|
||||
title = messages[0].get("content", "New Chat")
|
||||
|
@ -149,6 +149,7 @@ def openai_chat_chunk_message_template(
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
else:
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
template["choices"][0]["delta"] = {}
|
||||
|
||||
if usage:
|
||||
template["usage"] = usage
|
||||
|
@ -35,7 +35,7 @@ from open_webui.config import (
|
||||
AppConfig,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||
from open_webui.env import WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE
|
||||
from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE
|
||||
from open_webui.utils.misc import parse_duration
|
||||
from open_webui.utils.auth import get_password_hash, create_token
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
@ -276,8 +276,13 @@ class OAuthManager:
|
||||
picture_url = ""
|
||||
if not picture_url:
|
||||
picture_url = "/user.png"
|
||||
|
||||
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
|
||||
|
||||
name = user_data.get(username_claim)
|
||||
if not isinstance(user, str):
|
||||
name = email
|
||||
|
||||
role = self.get_user_role(None, user_data)
|
||||
|
||||
user = Auths.insert_new_auth(
|
||||
@ -285,7 +290,7 @@ class OAuthManager:
|
||||
password=get_password_hash(
|
||||
str(uuid.uuid4())
|
||||
), # Random password, not used
|
||||
name=user_data.get(username_claim, "User"),
|
||||
name=name,
|
||||
profile_image_url=picture_url,
|
||||
role=role,
|
||||
oauth_sub=provider_sub,
|
||||
@ -323,8 +328,8 @@ class OAuthManager:
|
||||
key="token",
|
||||
value=jwt_token,
|
||||
httponly=True, # Ensures the cookie is not accessible via JavaScript
|
||||
samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
if ENABLE_OAUTH_SIGNUP.value:
|
||||
@ -333,8 +338,8 @@ class OAuthManager:
|
||||
key="oauth_id_token",
|
||||
value=oauth_id_token,
|
||||
httponly=True,
|
||||
samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_SESSION_COOKIE_SECURE,
|
||||
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||
)
|
||||
# Redirect back to the frontend with the JWT token
|
||||
redirect_url = f"{request.base_url}auth#token={jwt_token}"
|
||||
|
@ -1,4 +1,4 @@
|
||||
from open_webui.utils.task import prompt_template
|
||||
from open_webui.utils.task import prompt_variables_template
|
||||
from open_webui.utils.misc import (
|
||||
add_or_update_system_message,
|
||||
)
|
||||
@ -7,19 +7,18 @@ from typing import Callable, Optional
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||
def apply_model_system_prompt_to_body(
|
||||
params: dict, form_data: dict, metadata: Optional[dict] = None
|
||||
) -> dict:
|
||||
system = params.get("system", None)
|
||||
if not system:
|
||||
return form_data
|
||||
|
||||
if user:
|
||||
template_params = {
|
||||
"user_name": user.name,
|
||||
"user_location": user.info.get("location") if user.info else None,
|
||||
}
|
||||
else:
|
||||
template_params = {}
|
||||
system = prompt_template(system, **template_params)
|
||||
if metadata:
|
||||
print("apply_model_system_prompt_to_body: metadata", metadata)
|
||||
variables = metadata.get("variables", {})
|
||||
system = prompt_variables_template(system, variables)
|
||||
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
system, form_data.get("messages", [])
|
||||
)
|
||||
@ -188,4 +187,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
|
||||
if ollama_options:
|
||||
ollama_payload["options"] = ollama_options
|
||||
|
||||
if "metadata" in openai_payload:
|
||||
ollama_payload["metadata"] = openai_payload["metadata"]
|
||||
|
||||
return ollama_payload
|
||||
|
@ -167,9 +167,14 @@ def load_function_module_by_id(function_id, content=None):
|
||||
|
||||
def install_frontmatter_requirements(requirements):
|
||||
if requirements:
|
||||
req_list = [req.strip() for req in requirements.split(",")]
|
||||
for req in req_list:
|
||||
log.info(f"Installing requirement: {req}")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
|
||||
try:
|
||||
req_list = [req.strip() for req in requirements.split(",")]
|
||||
for req in req_list:
|
||||
log.info(f"Installing requirement: {req}")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
|
||||
except Exception as e:
|
||||
log.error(f"Error installing package: {req}")
|
||||
raise e
|
||||
|
||||
else:
|
||||
log.info("No requirements found in frontmatter.")
|
||||
|
@ -9,7 +9,48 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
|
||||
model = ollama_response.get("model", "ollama")
|
||||
message_content = ollama_response.get("message", {}).get("content", "")
|
||||
|
||||
response = openai_chat_completion_message_template(model, message_content)
|
||||
data = ollama_response
|
||||
usage = {
|
||||
"response_token/s": (
|
||||
round(
|
||||
(
|
||||
(
|
||||
data.get("eval_count", 0)
|
||||
/ ((data.get("eval_duration", 0) / 10_000_000))
|
||||
)
|
||||
* 100
|
||||
),
|
||||
2,
|
||||
)
|
||||
if data.get("eval_duration", 0) > 0
|
||||
else "N/A"
|
||||
),
|
||||
"prompt_token/s": (
|
||||
round(
|
||||
(
|
||||
(
|
||||
data.get("prompt_eval_count", 0)
|
||||
/ ((data.get("prompt_eval_duration", 0) / 10_000_000))
|
||||
)
|
||||
* 100
|
||||
),
|
||||
2,
|
||||
)
|
||||
if data.get("prompt_eval_duration", 0) > 0
|
||||
else "N/A"
|
||||
),
|
||||
"total_duration": data.get("total_duration", 0),
|
||||
"load_duration": data.get("load_duration", 0),
|
||||
"prompt_eval_count": data.get("prompt_eval_count", 0),
|
||||
"prompt_eval_duration": data.get("prompt_eval_duration", 0),
|
||||
"eval_count": data.get("eval_count", 0),
|
||||
"eval_duration": data.get("eval_duration", 0),
|
||||
"approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")(
|
||||
(data.get("total_duration", 0) or 0) // 1_000_000_000
|
||||
),
|
||||
}
|
||||
|
||||
response = openai_chat_completion_message_template(model, message_content, usage)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -32,6 +32,12 @@ def get_task_model_id(
|
||||
return task_model_id
|
||||
|
||||
|
||||
def prompt_variables_template(template: str, variables: dict[str, str]) -> str:
|
||||
for variable, value in variables.items():
|
||||
template = template.replace(variable, value)
|
||||
return template
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
|
||||
) -> str:
|
||||
|
@ -1,4 +1,4 @@
|
||||
fastapi==0.111.0
|
||||
fastapi==0.115.7
|
||||
uvicorn[standard]==0.30.6
|
||||
pydantic==2.9.2
|
||||
python-multipart==0.0.18
|
||||
|
@ -6,7 +6,7 @@ authors = [
|
||||
]
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"fastapi==0.115.7",
|
||||
"uvicorn[standard]==0.30.6",
|
||||
"pydantic==2.9.2",
|
||||
"python-multipart==0.0.18",
|
||||
|
@ -580,7 +580,7 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const cloneChatById = async (token: string, id: string) => {
|
||||
export const cloneChatById = async (token: string, id: string, title?: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, {
|
||||
@ -589,7 +589,10 @@ export const cloneChatById = async (token: string, id: string) => {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(title && { title: title })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
|
@ -219,7 +219,7 @@ export const deleteModelById = async (token: string, id: string) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
|
@ -51,7 +51,7 @@
|
||||
models = [];
|
||||
} else {
|
||||
const res = await _getModels(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@ -74,7 +74,7 @@
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
@ -6,7 +6,9 @@
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@ -364,7 +366,7 @@
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1">
|
||||
{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||
{dayjs(user.created_at * 1000).format('LL')}
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
|
||||
|
@ -7,9 +7,11 @@
|
||||
import { updateUserById } from '$lib/apis/users';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let show = false;
|
||||
export let selectedUser;
|
||||
@ -87,7 +89,7 @@
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Created at')}
|
||||
{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,8 +2,10 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
||||
|
||||
@ -130,7 +132,7 @@
|
||||
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem] justify-end">
|
||||
<div class="my-auto shrink-0">
|
||||
{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
|
||||
{dayjs(chat.updated_at * 1000).format('LLL')}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
@ -200,7 +200,7 @@
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
};
|
||||
|
@ -3,10 +3,12 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
@ -154,9 +156,9 @@
|
||||
class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
|
||||
>
|
||||
<Tooltip
|
||||
content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
|
||||
content={dayjs(message.created_at / 1000000).format('LLLL')}
|
||||
>
|
||||
{dayjs(message.created_at / 1000000).format('HH:mm')}
|
||||
{dayjs(message.created_at / 1000000).format('LT')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
@ -175,7 +177,7 @@
|
||||
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||
>
|
||||
<Tooltip
|
||||
content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
|
||||
content={dayjs(message.created_at / 1000000).format('LLLL')}
|
||||
>
|
||||
<span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
|
||||
</Tooltip>
|
||||
|
@ -45,7 +45,8 @@
|
||||
promptTemplate,
|
||||
splitStream,
|
||||
sleep,
|
||||
removeDetailsWithReasoning
|
||||
removeDetailsWithReasoning,
|
||||
getPromptVariables
|
||||
} from '$lib/utils';
|
||||
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
@ -82,10 +83,12 @@
|
||||
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
import Placeholder from './Placeholder.svelte';
|
||||
import NotificationToast from '../NotificationToast.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
export let chatIdProp = '';
|
||||
|
||||
let loaded = false;
|
||||
let loading = false;
|
||||
|
||||
const eventTarget = new EventTarget();
|
||||
let controlPane;
|
||||
let controlPaneComponent;
|
||||
@ -133,6 +136,7 @@
|
||||
|
||||
$: if (chatIdProp) {
|
||||
(async () => {
|
||||
loading = true;
|
||||
console.log(chatIdProp);
|
||||
|
||||
prompt = '';
|
||||
@ -141,11 +145,9 @@
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
|
||||
loaded = false;
|
||||
|
||||
if (chatIdProp && (await loadChat())) {
|
||||
await tick();
|
||||
loaded = true;
|
||||
loading = false;
|
||||
|
||||
if (localStorage.getItem(`chat-input-${chatIdProp}`)) {
|
||||
try {
|
||||
@ -627,7 +629,7 @@
|
||||
} catch (e) {
|
||||
// Remove the failed doc from the files array
|
||||
files = files.filter((f) => f.name !== url);
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1557,10 +1559,17 @@
|
||||
|
||||
files: (files?.length ?? 0) > 0 ? files : undefined,
|
||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||
|
||||
features: {
|
||||
image_generation: imageGenerationEnabled,
|
||||
web_search: webSearchEnabled
|
||||
},
|
||||
variables: {
|
||||
...getPromptVariables(
|
||||
$user.name,
|
||||
$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined
|
||||
)
|
||||
},
|
||||
|
||||
session_id: $socket?.id,
|
||||
chat_id: $chatId,
|
||||
@ -1861,7 +1870,7 @@
|
||||
: ' '} w-full max-w-full flex flex-col"
|
||||
id="chat-container"
|
||||
>
|
||||
{#if !chatIdProp || (loaded && chatIdProp)}
|
||||
{#if chatIdProp === '' || (!loading && chatIdProp)}
|
||||
{#if $settings?.backgroundImageUrl ?? null}
|
||||
<div
|
||||
class="absolute {$showSidebar
|
||||
@ -2065,5 +2074,11 @@
|
||||
{eventTarget}
|
||||
/>
|
||||
</PaneGroup>
|
||||
{:else if loading}
|
||||
<div class=" flex items-center justify-center h-full w-full">
|
||||
<div class="m-auto">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -211,7 +211,7 @@
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
};
|
||||
|
@ -48,6 +48,9 @@
|
||||
init();
|
||||
}
|
||||
|
||||
let fileUploadEnabled = true;
|
||||
$: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload;
|
||||
|
||||
const init = async () => {
|
||||
if ($_tools === null) {
|
||||
await _tools.set(await getTools(localStorage.token));
|
||||
@ -166,26 +169,44 @@
|
||||
{/if}
|
||||
|
||||
{#if !$mobile}
|
||||
<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-xl"
|
||||
on:click={() => {
|
||||
screenCaptureHandler();
|
||||
}}
|
||||
<Tooltip
|
||||
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
|
||||
className="w-full"
|
||||
>
|
||||
<CameraSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Capture')}</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-xl {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
screenCaptureHandler();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CameraSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<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-xl"
|
||||
on:click={() => {
|
||||
uploadFilesHandler();
|
||||
}}
|
||||
<Tooltip
|
||||
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
|
||||
className="w-full"
|
||||
>
|
||||
<DocumentArrowUpSolid />
|
||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</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-xl {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
uploadFilesHandler();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DocumentArrowUpSolid />
|
||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</Tooltip>
|
||||
|
||||
{#if $config?.features?.enable_google_drive_integration}
|
||||
<DropdownMenu.Item
|
||||
|
@ -16,7 +16,9 @@
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Name from './Name.svelte';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let chatId;
|
||||
export let history;
|
||||
@ -264,7 +266,7 @@
|
||||
<span
|
||||
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
{dayjs(message.timestamp * 1000).format('LT')}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
|
@ -500,7 +500,7 @@
|
||||
<div
|
||||
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||
>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -13,8 +13,10 @@
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let user;
|
||||
|
||||
@ -112,7 +114,7 @@
|
||||
<div
|
||||
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||
>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
@ -11,8 +11,10 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import EditMemoryModal from './EditMemoryModal.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let show = false;
|
||||
|
||||
@ -84,9 +86,7 @@
|
||||
</td>
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
|
||||
<div class="my-auto whitespace-nowrap">
|
||||
{dayjs(memory.updated_at * 1000).format(
|
||||
$i18n.t('MMMM DD, YYYY hh:mm:ss A')
|
||||
)}
|
||||
{dayjs(memory.updated_at * 1000).format('LLL')}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-1">
|
||||
|
@ -4,6 +4,9 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@ -159,7 +162,7 @@
|
||||
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
|
||||
<div class="my-auto">
|
||||
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
|
||||
{dayjs(chat.created_at * 1000).format('LLL')}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
@ -87,7 +87,13 @@
|
||||
};
|
||||
|
||||
const cloneChatHandler = async (id) => {
|
||||
const res = await cloneChatById(localStorage.token, id).catch((error) => {
|
||||
const res = await cloneChatById(
|
||||
localStorage.token,
|
||||
id,
|
||||
$i18n.t('Clone of {{TITLE}}', {
|
||||
TITLE: title
|
||||
})
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
@ -33,6 +33,7 @@
|
||||
let loading = false;
|
||||
let stopResponseFlag = false;
|
||||
|
||||
let systemTextareaElement: HTMLTextAreaElement;
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
|
||||
let showSystem = false;
|
||||
@ -58,8 +59,29 @@
|
||||
console.log('stopResponse');
|
||||
};
|
||||
|
||||
const resizeSystemTextarea = async () => {
|
||||
await tick();
|
||||
if (systemTextareaElement) {
|
||||
systemTextareaElement.style.height = '';
|
||||
systemTextareaElement.style.height = Math.min(systemTextareaElement.scrollHeight, 555) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
$: if (showSystem) {
|
||||
resizeSystemTextarea();
|
||||
}
|
||||
|
||||
const chatCompletionHandler = async () => {
|
||||
if (selectedModelId === '') {
|
||||
toast.error($i18n.t('Please select a model.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const model = $models.find((model) => model.id === selectedModelId);
|
||||
if (!model) {
|
||||
selectedModelId = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const [res, controller] = await chatCompletion(
|
||||
localStorage.token,
|
||||
@ -258,10 +280,13 @@
|
||||
<div slot="content">
|
||||
<div class="pt-1 px-1.5">
|
||||
<textarea
|
||||
id="system-textarea"
|
||||
bind:this={systemTextareaElement}
|
||||
class="w-full h-full bg-transparent resize-none outline-none text-sm"
|
||||
bind:value={system}
|
||||
placeholder={$i18n.t("You're a helpful assistant.")}
|
||||
on:input={() => {
|
||||
resizeSystemTextarea();
|
||||
}}
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
|
76
src/lib/components/playground/Chat/Message.svelte
Normal file
76
src/lib/components/playground/Chat/Message.svelte
Normal file
@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let message;
|
||||
export let idx;
|
||||
|
||||
export let onDelete;
|
||||
|
||||
let textAreaElement: HTMLTextAreaElement;
|
||||
|
||||
onMount(() => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 group">
|
||||
<div class="flex items-start pt-1">
|
||||
<div
|
||||
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
|
||||
>
|
||||
{$i18n.t(message.role)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<!-- $i18n.t('a user') -->
|
||||
<!-- $i18n.t('an assistant') -->
|
||||
<textarea
|
||||
id="{message.role}-{idx}-textarea"
|
||||
bind:this={textAreaElement}
|
||||
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
|
||||
placeholder={$i18n.t(`Enter {{role}} message here`, {
|
||||
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
|
||||
})}
|
||||
rows="1"
|
||||
on:input={(e) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
}}
|
||||
on:focus={(e) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
|
||||
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
}}
|
||||
bind:value={message.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" pt-1">
|
||||
<button
|
||||
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -1,77 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import Message from './Message.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let messages = [];
|
||||
let textAreaElement: HTMLTextAreaElement;
|
||||
onMount(() => {
|
||||
messages.forEach((message, idx) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="py-3 space-y-3">
|
||||
{#each messages as message, idx}
|
||||
<div class="flex gap-2 group">
|
||||
<div class="flex items-start pt-1">
|
||||
<div
|
||||
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
|
||||
>
|
||||
{$i18n.t(message.role)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<!-- $i18n.t('a user') -->
|
||||
<!-- $i18n.t('an assistant') -->
|
||||
<textarea
|
||||
id="{message.role}-{idx}-textarea"
|
||||
bind:this={textAreaElement}
|
||||
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
|
||||
placeholder={$i18n.t(`Enter {{role}} message here`, {
|
||||
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
|
||||
})}
|
||||
rows="1"
|
||||
on:input={(e) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
}}
|
||||
on:focus={(e) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
|
||||
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
}}
|
||||
bind:value={message.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" pt-1">
|
||||
<button
|
||||
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Message
|
||||
{message}
|
||||
{idx}
|
||||
onDelete={() => {
|
||||
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@
|
||||
|
||||
const deleteHandler = async (item) => {
|
||||
const res = await deleteKnowledgeById(localStorage.token, item.id).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
@ -31,7 +31,7 @@
|
||||
description,
|
||||
accessControl
|
||||
).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@ -112,7 +112,7 @@
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
|
||||
<AccessControl bind:accessControl />
|
||||
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -149,7 +149,7 @@
|
||||
|
||||
try {
|
||||
const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
@ -169,7 +169,7 @@
|
||||
toast.error($i18n.t('Failed to upload file.'));
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -339,7 +339,7 @@
|
||||
const syncDirectoryHandler = async () => {
|
||||
if ((knowledge?.files ?? []).length > 0) {
|
||||
const res = await resetKnowledgeById(localStorage.token, id).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@ -357,7 +357,7 @@
|
||||
const addFileHandler = async (fileId) => {
|
||||
const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
|
||||
(e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
@ -386,7 +386,7 @@
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in deleteFileHandler:', e);
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -395,7 +395,7 @@
|
||||
const content = selectedFile.data.content;
|
||||
|
||||
const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
const updatedKnowledge = await updateFileFromKnowledgeById(
|
||||
@ -403,7 +403,7 @@
|
||||
id,
|
||||
fileId
|
||||
).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res && updatedKnowledge) {
|
||||
@ -430,7 +430,7 @@
|
||||
description: knowledge.description,
|
||||
access_control: knowledge.access_control
|
||||
}).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@ -522,7 +522,7 @@
|
||||
id = $page.params.id;
|
||||
|
||||
const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
||||
|
||||
const deleteModelHandler = async (model) => {
|
||||
const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
|
@ -531,7 +531,10 @@
|
||||
|
||||
<div class="my-2">
|
||||
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
|
||||
<AccessControl bind:accessControl />
|
||||
<AccessControl
|
||||
bind:accessControl
|
||||
accessRoles={['read', 'write']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -136,7 +136,7 @@
|
||||
|
||||
<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 flex flex-row space-x-1 items-center {saving
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full {saving
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
|
@ -766,7 +766,7 @@
|
||||
"Reset": "",
|
||||
"Reset All Models": "",
|
||||
"Reset Upload Directory": "アップロードディレクトリをリセット",
|
||||
"Reset Vector Storage/Knowledge": "ベクターストレージとナレッジべーうをリセット",
|
||||
"Reset Vector Storage/Knowledge": "ベクターストレージとナレッジベースをリセット",
|
||||
"Reset view": "",
|
||||
"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
|
||||
"Response splitting": "応答の分割",
|
||||
|
@ -8,39 +8,39 @@
|
||||
"{{COUNT}} Replies": "",
|
||||
"{{user}}'s Chats": "{{user}}의 채팅",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.",
|
||||
"*Prompt node ID(s) are required for image generation": "사진 생성을 위해 프롬포트 노드 ID가 필요합니다",
|
||||
"*Prompt node ID(s) are required for image generation": "사진 생성을 위해 프롬프트 노드 ID가 필요합니다",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "최신 버전 (v{{LATEST_VERSION}})이 가능합니다",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "작업 모델은 채팅 및 웹 검색 쿼리에 대한 제목 생성 등의 작업 수행 시 사용됩니다.",
|
||||
"a user": "사용자",
|
||||
"About": "정보",
|
||||
"Access": "",
|
||||
"Access Control": "",
|
||||
"Access": "접근",
|
||||
"Access Control": "접근 제어",
|
||||
"Accessible to all users": "모든 사용자가 접근 가능",
|
||||
"Account": "계정",
|
||||
"Account Activation Pending": "계정 활성화 대기",
|
||||
"Accurate information": "정확한 정보",
|
||||
"Actions": "행동",
|
||||
"Activate this command by typing \"/{{COMMAND}}\" to chat input.": "",
|
||||
"Activate this command by typing \"/{{COMMAND}}\" to chat input.": "채팅에서 \"{{COMMAND}}\"을 입력하여 이 명령을 활성화할 수 있습니다.",
|
||||
"Active Users": "활성 사용자",
|
||||
"Add": "추가",
|
||||
"Add a model ID": "",
|
||||
"Add a model ID": "모델 ID 추가",
|
||||
"Add a short description about what this model does": "모델의 기능에 대한 간단한 설명 추가",
|
||||
"Add a tag": "태그 추가",
|
||||
"Add Arena Model": "아레나 모델 추가",
|
||||
"Add Connection": "",
|
||||
"Add Connection": "연결 추가",
|
||||
"Add Content": "내용 추가",
|
||||
"Add content here": "여기에 내용을 추가하세요",
|
||||
"Add custom prompt": "사용자 정의 프롬프트 추가",
|
||||
"Add Files": "파일 추가",
|
||||
"Add Group": "",
|
||||
"Add Group": "그룹 추가",
|
||||
"Add Memory": "메모리 추가",
|
||||
"Add Model": "모델 추가",
|
||||
"Add Reaction": "",
|
||||
"Add Reaction": "리액션 추가",
|
||||
"Add Tag": "태그 추가",
|
||||
"Add Tags": "태그 추가",
|
||||
"Add text content": "글 추가",
|
||||
"Add User": "사용자 추가",
|
||||
"Add User Group": "",
|
||||
"Add User Group": "사용자 그룹 추가",
|
||||
"Adjusting these settings will apply changes universally to all users.": "위와 같이 설정시 모든 사용자에게 적용됩니다.",
|
||||
"admin": "관리자",
|
||||
"Admin": "관리자",
|
||||
@ -50,12 +50,12 @@
|
||||
"Advanced Parameters": "고급 매개변수",
|
||||
"Advanced Params": "고급 매개변수",
|
||||
"All Documents": "모든 문서",
|
||||
"All models deleted successfully": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"All models deleted successfully": "성공적으로 모든 모델이 삭제되었습니다",
|
||||
"Allow Chat Controls": "채팅 제어 허용",
|
||||
"Allow Chat Delete": "채팅 삭제 허용",
|
||||
"Allow Chat Deletion": "채팅 삭제 허용",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow File Upload": "",
|
||||
"Allow Chat Edit": "채팅 수정 허용",
|
||||
"Allow File Upload": "파일 업로드 허용",
|
||||
"Allow non-local voices": "외부 음성 허용",
|
||||
"Allow Temporary Chat": "임시 채팅 허용",
|
||||
"Allow User Location": "사용자 위치 활용 허용",
|
||||
@ -75,15 +75,15 @@
|
||||
"API keys": "API 키",
|
||||
"Application DN": "",
|
||||
"Application DN Password": "",
|
||||
"applies to all users with the \"user\" role": "",
|
||||
"applies to all users with the \"user\" role": "\"사용자\" 권한의 모든 사용자에게 적용됩니다",
|
||||
"April": "4월",
|
||||
"Archive": "보관",
|
||||
"Archive All Chats": "모든 채팅 보관",
|
||||
"Archived Chats": "보관된 채팅",
|
||||
"archived-chat-export": "",
|
||||
"Are you sure you want to delete this channel?": "",
|
||||
"Are you sure you want to delete this message?": "",
|
||||
"Are you sure you want to unarchive all archived chats?": "",
|
||||
"Are you sure you want to delete this channel?": "정말 이 채널을 삭제하시겠습니까?",
|
||||
"Are you sure you want to delete this message?": "정말 이 메세지를 삭제하시겠습니까?",
|
||||
"Are you sure you want to unarchive all archived chats?": "정말 보관된 모든 채팅을 보관 해제하시겠습니까?",
|
||||
"Are you sure?": "확실합니까?",
|
||||
"Arena Models": "아레나 모델",
|
||||
"Artifacts": "아티팩트",
|
||||
@ -141,7 +141,7 @@
|
||||
"Chat Controls": "채팅 제어",
|
||||
"Chat direction": "채팅 방향",
|
||||
"Chat Overview": "채팅",
|
||||
"Chat Permissions": "",
|
||||
"Chat Permissions": "채팅 권한",
|
||||
"Chat Tags Auto-Generation": "채팅 태그 자동생성",
|
||||
"Chats": "채팅",
|
||||
"Check Again": "다시 확인",
|
||||
@ -210,16 +210,16 @@
|
||||
"Copy Link": "링크 복사",
|
||||
"Copy to clipboard": "클립보드에 복사",
|
||||
"Copying to clipboard was successful!": "성공적으로 클립보드에 복사되었습니다!",
|
||||
"Create": "",
|
||||
"Create a knowledge base": "",
|
||||
"Create a model": "모델 만들기",
|
||||
"Create Account": "계정 만들기",
|
||||
"Create Admin Account": "",
|
||||
"Create Channel": "",
|
||||
"Create Group": "",
|
||||
"Create Knowledge": "지식 만들기",
|
||||
"Create new key": "새 키 만들기",
|
||||
"Create new secret key": "새 비밀 키 만들기",
|
||||
"Create": "생성",
|
||||
"Create a knowledge base": "지식 기반 생성",
|
||||
"Create a model": "모델 생성",
|
||||
"Create Account": "계정 생성",
|
||||
"Create Admin Account": "관리자 계정 생성",
|
||||
"Create Channel": "채널 생성",
|
||||
"Create Group": "그룹 생성",
|
||||
"Create Knowledge": "지식 생성",
|
||||
"Create new key": "새로운 키 생성",
|
||||
"Create new secret key": "새로운 비밀 키 생성",
|
||||
"Created at": "생성일",
|
||||
"Created At": "생성일",
|
||||
"Created by": "생성자",
|
||||
@ -236,8 +236,8 @@
|
||||
"Default Model": "기본 모델",
|
||||
"Default model updated": "기본 모델이 업데이트되었습니다.",
|
||||
"Default Models": "기본 모델",
|
||||
"Default permissions": "",
|
||||
"Default permissions updated successfully": "",
|
||||
"Default permissions": "기본 권한",
|
||||
"Default permissions updated successfully": "성공적으로 기본 권한이 수정되었습니다",
|
||||
"Default Prompt Suggestions": "기본 프롬프트 제안",
|
||||
"Default to 389 or 636 if TLS is enabled": "",
|
||||
"Default to ALL": "",
|
||||
@ -245,21 +245,21 @@
|
||||
"Delete": "삭제",
|
||||
"Delete a model": "모델 삭제",
|
||||
"Delete All Chats": "모든 채팅 삭제",
|
||||
"Delete All Models": "",
|
||||
"Delete All Models": "모든 모델 삭제",
|
||||
"Delete chat": "채팅 삭제",
|
||||
"Delete Chat": "채팅 삭제",
|
||||
"Delete chat?": "채팅을 삭제하겠습니까?",
|
||||
"Delete folder?": "폴더를 삭제하시겠습니까?",
|
||||
"Delete function?": "함수를 삭제하시겠습니까?",
|
||||
"Delete Message": "",
|
||||
"Delete prompt?": "프롬포트를 삭제하시겠습니까?",
|
||||
"Delete prompt?": "프롬프트를 삭제하시겠습니까?",
|
||||
"delete this link": "이 링크를 삭제합니다.",
|
||||
"Delete tool?": "도구를 삭제하시겠습니까?",
|
||||
"Delete User": "사용자 삭제",
|
||||
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨",
|
||||
"Deleted {{name}}": "{{name}}을(를) 삭제했습니다.",
|
||||
"Deleted User": "",
|
||||
"Describe your knowledge base and objectives": "",
|
||||
"Deleted User": "삭제된 사용자",
|
||||
"Describe your knowledge base and objectives": "지식 기반에 대한 설명과 목적을 입력하세요",
|
||||
"Description": "설명",
|
||||
"Didn't fully follow instructions": "완전히 지침을 따르지 않음",
|
||||
"Disabled": "제한됨",
|
||||
@ -370,7 +370,7 @@
|
||||
"Enter server label": "",
|
||||
"Enter server port": "",
|
||||
"Enter stop sequence": "중지 시퀀스 입력",
|
||||
"Enter system prompt": "시스템 프롬포트 입력",
|
||||
"Enter system prompt": "시스템 프롬프트 입력",
|
||||
"Enter Tavily API Key": "Tavily API 키 입력",
|
||||
"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "WebUI의 공개 URL을 입력해 주세요. 이 URL은 알림에서 링크를 생성하는 데 사용합니다.",
|
||||
"Enter Tika Server URL": "Tika 서버 URL 입력",
|
||||
@ -419,10 +419,10 @@
|
||||
"Failed to save models configuration": "",
|
||||
"Failed to update settings": "설정 업데이트에 실패하였습니다.",
|
||||
"Failed to upload file.": "파일 업로드에 실패했습니다",
|
||||
"Features Permissions": "",
|
||||
"Features Permissions": "기능 권한",
|
||||
"February": "2월",
|
||||
"Feedback History": "피드백 기록",
|
||||
"Feedbacks": "",
|
||||
"Feedbacks": "피드백",
|
||||
"Feel free to add specific details": "자세한 내용을 자유롭게 추가하세요.",
|
||||
"File": "파일",
|
||||
"File added successfully.": "성공적으로 파일이 추가되었습니다",
|
||||
@ -472,12 +472,12 @@
|
||||
"Google Drive": "",
|
||||
"Google PSE API Key": "Google PSE API 키",
|
||||
"Google PSE Engine Id": "Google PSE 엔진 ID",
|
||||
"Group created successfully": "",
|
||||
"Group deleted successfully": "",
|
||||
"Group Description": "",
|
||||
"Group Name": "",
|
||||
"Group updated successfully": "",
|
||||
"Groups": "",
|
||||
"Group created successfully": "성공적으로 그룹을 생성했습니다",
|
||||
"Group deleted successfully": "성공적으로 그룹을 삭제했습니다",
|
||||
"Group Description": "그룹 설명",
|
||||
"Group Name": "그룹 명",
|
||||
"Group updated successfully": "성공적으로 그룹을 수정했습니다",
|
||||
"Groups": "그룹",
|
||||
"h:mm a": "h:mm a",
|
||||
"Haptic Feedback": "햅틱 피드백",
|
||||
"has no conversations.": "대화가 없습니다.",
|
||||
@ -494,13 +494,13 @@
|
||||
"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "",
|
||||
"ID": "ID",
|
||||
"Ignite curiosity": "",
|
||||
"Image": "",
|
||||
"Image": "이미지",
|
||||
"Image Compression": "이미지 압축",
|
||||
"Image generation": "",
|
||||
"Image Generation": "",
|
||||
"Image generation": "이미지 생성",
|
||||
"Image Generation": "이미지 생성",
|
||||
"Image Generation (Experimental)": "이미지 생성(실험적)",
|
||||
"Image Generation Engine": "이미지 생성 엔진",
|
||||
"Image Max Compression Size": "",
|
||||
"Image Max Compression Size": "이미지 최대 압축 크기",
|
||||
"Image Prompt Generation": "",
|
||||
"Image Prompt Generation Prompt": "",
|
||||
"Image Settings": "이미지 설정",
|
||||
@ -538,7 +538,7 @@
|
||||
"Key": "",
|
||||
"Keyboard shortcuts": "키보드 단축키",
|
||||
"Knowledge": "지식 기반",
|
||||
"Knowledge Access": "",
|
||||
"Knowledge Access": "지식 접근",
|
||||
"Knowledge created successfully.": "성공적으로 지식 기반이 생성되었습니다",
|
||||
"Knowledge deleted successfully.": "성공적으로 지식 기반이 삭제되었습니다",
|
||||
"Knowledge reset successfully.": "성공적으로 지식 기반이 초기화되었습니다",
|
||||
@ -556,7 +556,7 @@
|
||||
"Leave empty to include all models from \"{{URL}}/api/tags\" endpoint": "",
|
||||
"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
|
||||
"Leave empty to include all models or select specific models": "특정 모델을 선택하거나 모든 모델을 포함하고 싶으면 빈칸으로 남겨두세요",
|
||||
"Leave empty to use the default prompt, or enter a custom prompt": "기본 프롬포트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬포트를 입력하세요",
|
||||
"Leave empty to use the default prompt, or enter a custom prompt": "기본 프롬프트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬프트를 입력하세요",
|
||||
"Light": "라이트",
|
||||
"Listening...": "듣는 중...",
|
||||
"Llama.cpp": "",
|
||||
@ -617,13 +617,13 @@
|
||||
"Model updated successfully": "성공적으로 모델이 업데이트되었습니다",
|
||||
"Modelfile Content": "Modelfile 내용",
|
||||
"Models": "모델",
|
||||
"Models Access": "",
|
||||
"Models Access": "모델 접근",
|
||||
"Models configuration saved successfully": "",
|
||||
"Mojeek Search API Key": "Mojeek Search API 키",
|
||||
"more": "더보기",
|
||||
"More": "더보기",
|
||||
"Name": "이름",
|
||||
"Name your knowledge base": "",
|
||||
"Name your knowledge base": "지식 기반 이름을 지정하세요",
|
||||
"New Chat": "새 채팅",
|
||||
"New Folder": "",
|
||||
"New Password": "새 비밀번호",
|
||||
@ -667,8 +667,8 @@
|
||||
"Ollama API settings updated": "",
|
||||
"Ollama Version": "Ollama 버전",
|
||||
"On": "켜기",
|
||||
"Only alphanumeric characters and hyphens are allowed": "",
|
||||
"Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈만 허용됩니다.",
|
||||
"Only alphanumeric characters and hyphens are allowed": "영문자, 숫자 및 하이픈(-)만 허용됨",
|
||||
"Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈(-)만 허용됩니다.",
|
||||
"Only collections can be edited, create a new knowledge base to edit/add documents.": "가지고 있는 컬렉션만 수정 가능합니다, 새 지식 기반을 생성하여 문서를 수정 혹은 추가하십시오",
|
||||
"Only select users and groups with permission can access": "권한이 있는 사용자와 그룹만 접근 가능합니다",
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "이런! URL이 잘못된 것 같습니다. 다시 한번 확인하고 다시 시도해주세요.",
|
||||
@ -688,7 +688,7 @@
|
||||
"OpenAI API settings updated": "",
|
||||
"OpenAI URL/Key required.": "OpenAI URL/키가 필요합니다.",
|
||||
"or": "또는",
|
||||
"Organize your users": "",
|
||||
"Organize your users": "사용자를 ",
|
||||
"Other": "기타",
|
||||
"OUTPUT": "출력력",
|
||||
"Output format": "출력 형식",
|
||||
@ -702,7 +702,7 @@
|
||||
"Permission denied when accessing media devices": "미디어 장치 접근 권한이 거부되었습니다.",
|
||||
"Permission denied when accessing microphone": "마이크 접근 권한이 거부되었습니다.",
|
||||
"Permission denied when accessing microphone: {{error}}": "마이크 접근 권환이 거부되었습니다: {{error}}",
|
||||
"Permissions": "",
|
||||
"Permissions": "권한",
|
||||
"Personalization": "개인화",
|
||||
"Pin": "고정",
|
||||
"Pinned": "고정됨",
|
||||
@ -710,16 +710,16 @@
|
||||
"Pipeline deleted successfully": "성공적으로 파이프라인이 삭제되었습니다",
|
||||
"Pipeline downloaded successfully": "성공적으로 파이프라인이 설치되었습니다",
|
||||
"Pipelines": "파이프라인",
|
||||
"Pipelines Not Detected": "파이프라인 발견되지않음",
|
||||
"Pipelines Not Detected": "파이프라인이 발견되지 않음",
|
||||
"Pipelines Valves": "파이프라인 밸브",
|
||||
"Plain text (.txt)": "일반 텍스트(.txt)",
|
||||
"Playground": "놀이터",
|
||||
"Please carefully review the following warnings:": "다음 주의를 조심히 확인해주십시오",
|
||||
"Please enter a prompt": "프롬포트를 입력해주세요",
|
||||
"Please enter a prompt": "프롬프트를 입력해주세요",
|
||||
"Please fill in all fields.": "모두 빈칸없이 채워주세요",
|
||||
"Please select a model first.": "",
|
||||
"Please select a reason": "이유를 선택하주세요",
|
||||
"Port": "",
|
||||
"Please select a reason": "이유를 선택해주세요",
|
||||
"Port": "포트",
|
||||
"Positive attitude": "긍정적인 자세",
|
||||
"Prefix ID": "",
|
||||
"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "",
|
||||
@ -728,11 +728,11 @@
|
||||
"Profile Image": "프로필 이미지",
|
||||
"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "프롬프트 (예: 로마 황제에 대해 재미있는 사실을 알려주세요)",
|
||||
"Prompt Content": "프롬프트 내용",
|
||||
"Prompt created successfully": "",
|
||||
"Prompt created successfully": "성공적으로 프롬프트를 생성했습니다",
|
||||
"Prompt suggestions": "프롬프트 제안",
|
||||
"Prompt updated successfully": "",
|
||||
"Prompt updated successfully": "성공적으로 프롬프트를 수정했습니다",
|
||||
"Prompts": "프롬프트",
|
||||
"Prompts Access": "",
|
||||
"Prompts Access": "프롬프트 접근",
|
||||
"Proxy URL": "프록시 URL",
|
||||
"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com에서 \"{{searchValue}}\" 가져오기",
|
||||
"Pull a model from Ollama.com": "Ollama.com에서 모델 가져오기(pull)",
|
||||
@ -792,13 +792,13 @@
|
||||
"Search a model": "모델 검색",
|
||||
"Search Base": "",
|
||||
"Search Chats": "채팅 검색",
|
||||
"Search Collection": "컬렉션검색",
|
||||
"Search Filters": "",
|
||||
"Search Collection": "컬렉션 검색",
|
||||
"Search Filters": "필터 검색",
|
||||
"search for tags": "태그 검색",
|
||||
"Search Functions": "함수 검색",
|
||||
"Search Knowledge": "지식 기반 검색",
|
||||
"Search Models": "모델 검색",
|
||||
"Search options": "",
|
||||
"Search options": "옵션 검색",
|
||||
"Search Prompts": "프롬프트 검색",
|
||||
"Search Result Count": "검색 결과 수",
|
||||
"Search the web": "",
|
||||
@ -895,7 +895,7 @@
|
||||
"System Instructions": "시스템 설명서",
|
||||
"System Prompt": "시스템 프롬프트",
|
||||
"Tags Generation": "태그 생성",
|
||||
"Tags Generation Prompt": "태그 생성 프롬포트트",
|
||||
"Tags Generation Prompt": "태그 생성 프롬프트",
|
||||
"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "",
|
||||
"Tap to interrupt": "탭하여 중단",
|
||||
"Tavily API Key": "Tavily API 키",
|
||||
@ -963,16 +963,16 @@
|
||||
"Too verbose": "말이 너무 많은",
|
||||
"Tool created successfully": "성공적으로 도구가 생성되었습니다",
|
||||
"Tool deleted successfully": "성공적으로 도구가 삭제되었습니다",
|
||||
"Tool Description": "",
|
||||
"Tool ID": "",
|
||||
"Tool Description": "도구 설명",
|
||||
"Tool ID": "도구 ID",
|
||||
"Tool imported successfully": "성공적으로 도구를 가져왔습니다",
|
||||
"Tool Name": "",
|
||||
"Tool Name": "도구 이름",
|
||||
"Tool updated successfully": "성공적으로 도구가 업데이트되었습니다",
|
||||
"Tools": "도구",
|
||||
"Tools Access": "",
|
||||
"Tools are a function calling system with arbitrary code execution": "도구는 임이의 코드를 실행시키는 함수를 불러오는 시스템입니다",
|
||||
"Tools have a function calling system that allows arbitrary code execution": "도구가 임이의 코드를 실행시키는 함수를 가지기",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "도구가 임이의 코드를 실행시키는 함수를 가지고 있습니다.",
|
||||
"Tools Access": "도구 접근",
|
||||
"Tools are a function calling system with arbitrary code execution": "도구는 임의 코드를 실행시키는 함수를 불러오는 시스템입니다",
|
||||
"Tools have a function calling system that allows arbitrary code execution": "도구에 임의 코드 실행을 허용하는 함수가 포함되어 있습니다",
|
||||
"Tools have a function calling system that allows arbitrary code execution.": "도구에 임의 코드 실행을 허용하는 함수가 포함되어 있습니다.",
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Transformers": "",
|
||||
@ -984,9 +984,9 @@
|
||||
"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (다운로드) URL 입력",
|
||||
"Uh-oh! There was an issue with the response.": "",
|
||||
"UI": "UI",
|
||||
"Unarchive All": "",
|
||||
"Unarchive All Archived Chats": "",
|
||||
"Unarchive Chat": "",
|
||||
"Unarchive All": "모두 보관 해제",
|
||||
"Unarchive All Archived Chats": "보관된 모든 채팅을 보관 해제",
|
||||
"Unarchive Chat": "채팅 보관 해제",
|
||||
"Unlock mysteries": "",
|
||||
"Unpin": "고정 해제",
|
||||
"Unravel secrets": "",
|
||||
@ -1009,7 +1009,7 @@
|
||||
"URL Mode": "URL 모드",
|
||||
"Use '#' in the prompt input to load and include your knowledge.": "프롬프트 입력에서 '#'를 사용하여 지식 기반을 불러오고 포함하세요.",
|
||||
"Use Gravatar": "Gravatar 사용",
|
||||
"Use groups to group your users and assign permissions.": "",
|
||||
"Use groups to group your users and assign permissions.": "그룹을 사용하여 사용자를 그룹화하고 권한을 할당하세요.",
|
||||
"Use Initials": "초성 사용",
|
||||
"use_mlock (Ollama)": "use_mlock (올라마)",
|
||||
"use_mmap (Ollama)": "use_mmap (올라마)",
|
||||
@ -1034,7 +1034,7 @@
|
||||
"Voice Input": "음성 입력",
|
||||
"Warning": "경고",
|
||||
"Warning:": "주의:",
|
||||
"Warning: Enabling this will allow users to upload arbitrary code on the server.": "",
|
||||
"Warning: Enabling this will allow users to upload arbitrary code on the server.": "주의: 이 기능을 활성화하면 사용자가 서버에 임의 코드를 업로드할 수 있습니다.",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "주의: 기존 임베딩 모델을 변경 또는 업데이트하는 경우, 모든 문서를 다시 가져와야 합니다.",
|
||||
"Web": "웹",
|
||||
"Web API": "웹 API",
|
||||
@ -1047,8 +1047,8 @@
|
||||
"WebUI URL": "",
|
||||
"WebUI will make requests to \"{{url}}/api/chat\"": "WebUI가 \"{{url}}/api/chat\"로 요청을 보냅니다",
|
||||
"WebUI will make requests to \"{{url}}/chat/completions\"": "WebUI가 \"{{url}}/chat/completions\"로 요청을 보냅니다",
|
||||
"What are you trying to achieve?": "",
|
||||
"What are you working on?": "",
|
||||
"What are you trying to achieve?": "무엇을 성취하고 싶으신가요?",
|
||||
"What are you working on?": "어떤 작업을 하고 계신가요?",
|
||||
"What’s New in": "새로운 기능:",
|
||||
"When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "활성화하면 모델이 각 채팅 메시지에 실시간으로 응답하여 사용자가 메시지를 보내는 즉시 응답을 생성합니다. 이 모드는 실시간 채팅 애플리케이션에 유용하지만, 느린 하드웨어에서는 성능에 영향을 미칠 수 있습니다.",
|
||||
"wherever you are": "",
|
||||
@ -1058,12 +1058,12 @@
|
||||
"Won": "승리",
|
||||
"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)": "",
|
||||
"Workspace": "워크스페이스",
|
||||
"Workspace Permissions": "",
|
||||
"Workspace Permissions": "워크스페이스 권한",
|
||||
"Write": "",
|
||||
"Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)",
|
||||
"Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.",
|
||||
"Write something...": "아무거나 쓰세요...",
|
||||
"Write your model template content here": "",
|
||||
"Write your model template content here": "여기에 모델 템플릿 내용을 입력하세요",
|
||||
"Yesterday": "어제",
|
||||
"You": "당신",
|
||||
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "동시에 최대 {{maxCount}} 파일과만 대화할 수 있습니다 ",
|
||||
|
@ -494,7 +494,7 @@
|
||||
"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "我已阅读并理解我的行为所带来的影响,明白执行任意代码所涉及的风险。且我已验证代码来源可信度。",
|
||||
"ID": "ID",
|
||||
"Ignite curiosity": "点燃好奇心",
|
||||
"Image": "图像",
|
||||
"Image": "图像生成",
|
||||
"Image Compression": "图像压缩",
|
||||
"Image generation": "图像生成",
|
||||
"Image Generation": "图像生成",
|
||||
@ -854,7 +854,7 @@
|
||||
"Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)": "设置模型回溯多远以防止重复。(默认值:64,0 = 禁用,-1 = num_ctx)",
|
||||
"Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1)": "设置对重复的惩罚力度。数值越大(如 1.5),对重复的惩罚力度越大,而数值越小(如 0.9),惩罚力度越轻。(默认值:1.1)",
|
||||
"Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt. (Default: random)": "设置生成文本时使用的随机数种子。将其设置为一个特定的数字将使模型在同一提示下生成相同的文本。 默认值:随机",
|
||||
"Sets the size of the context window used to generate the next token. (Default: 2048)": "设置用于生成下一个 Toekn 的上下文大小。(默认值:2048)",
|
||||
"Sets the size of the context window used to generate the next token. (Default: 2048)": "设置用于生成下一个 Token 的上下文大小。(默认值:2048)",
|
||||
"Sets the stop sequences to use. When this pattern is encountered, the LLM will stop generating text and return. Multiple stop patterns may be set by specifying multiple separate stop parameters in a modelfile.": "设置要使用的停止序列。遇到这种模式时,大语言模型将停止生成文本并返回。可以通过在模型文件中指定多个单独的停止参数来设置多个停止模式。",
|
||||
"Settings": "设置",
|
||||
"Settings saved successfully!": "设置已保存",
|
||||
@ -934,7 +934,7 @@
|
||||
"This will delete all models including custom models and cannot be undone.": "这将删除所有模型,包括自定义模型,且无法撤销。",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "这将重置知识库并替换所有文件为目录下文件。确认继续?",
|
||||
"Thorough explanation": "解释较为详细",
|
||||
"Thought for {{DURATION}}": "",
|
||||
"Thought for {{DURATION}}": "思考时间 {{DURATION}}",
|
||||
"Tika": "Tika",
|
||||
"Tika Server URL required.": "请输入 Tika 服务器地址。",
|
||||
"Tiktoken": "Tiktoken",
|
||||
|
@ -934,7 +934,7 @@
|
||||
"This will delete all models including custom models and cannot be undone.": "這將刪除所有模型,包括自訂模型,且無法復原。",
|
||||
"This will reset the knowledge base and sync all files. Do you wish to continue?": "這將重設知識庫並同步所有檔案。您確定要繼續嗎?",
|
||||
"Thorough explanation": "詳細解釋",
|
||||
"Thought for {{DURATION}}": "",
|
||||
"Thought for {{DURATION}}": "思考時間 {{DURATION}}",
|
||||
"Tika": "Tika",
|
||||
"Tika Server URL required.": "需要 Tika 伺服器 URL。",
|
||||
"Tiktoken": "Tiktoken",
|
||||
|
@ -5,10 +5,12 @@ import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { TTS_RESPONSE_SPLIT } from '$lib/types';
|
||||
@ -295,11 +297,11 @@ export const formatDate = (inputDate) => {
|
||||
const now = dayjs();
|
||||
|
||||
if (date.isToday()) {
|
||||
return `Today at ${date.format('HH:mm')}`;
|
||||
return `Today at ${date.format('LT')}`;
|
||||
} else if (date.isYesterday()) {
|
||||
return `Yesterday at ${date.format('HH:mm')}`;
|
||||
return `Yesterday at ${date.format('LT')}`;
|
||||
} else {
|
||||
return `${date.format('DD/MM/YYYY')} at ${date.format('HH:mm')}`;
|
||||
return `${date.format('L')} at ${date.format('LT')}`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -764,6 +766,19 @@ export const blobToFile = (blob, fileName) => {
|
||||
return file;
|
||||
};
|
||||
|
||||
export const getPromptVariables = (user_name, user_location) => {
|
||||
return {
|
||||
'{{USER_NAME}}': user_name,
|
||||
'{{USER_LOCATION}}': user_location || 'Unknown',
|
||||
'{{CURRENT_DATETIME}}': getCurrentDateTime(),
|
||||
'{{CURRENT_DATE}}': getFormattedDate(),
|
||||
'{{CURRENT_TIME}}': getFormattedTime(),
|
||||
'{{CURRENT_WEEKDAY}}': getWeekday(),
|
||||
'{{CURRENT_TIMEZONE}}': getUserTimezone(),
|
||||
'{{USER_LANGUAGE}}': localStorage.getItem('locale') || 'en-US'
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} template - The template string containing placeholders.
|
||||
* @returns {string} The template string with the placeholders replaced by the prompt.
|
||||
|
@ -16,8 +16,10 @@
|
||||
import { getUserById } from '$lib/apis/users';
|
||||
import { getModels } from '$lib/apis';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
let loaded = false;
|
||||
|
||||
@ -138,7 +140,7 @@
|
||||
|
||||
<div class="flex text-sm justify-between items-center mt-1">
|
||||
<div class="text-gray-400">
|
||||
{dayjs(chat.chat.timestamp).format($i18n.t('MMMM DD, YYYY'))}
|
||||
{dayjs(chat.chat.timestamp).format('LLL')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user