Merge branch 'dockerfile-optimisation' of https://github.com/jannikstdl/open-webui into dockerfile-optimisation

This commit is contained in:
Jannik Streidl 2024-04-03 11:44:18 +02:00
commit ca8fd8af1d
34 changed files with 1644 additions and 380 deletions

View file

@ -81,6 +81,12 @@ async def check_url(request: Request, call_next):
return response return response
@app.head("/")
@app.get("/")
async def get_status():
return {"status": True}
@app.get("/urls") @app.get("/urls")
async def get_ollama_api_urls(user=Depends(get_admin_user)): async def get_ollama_api_urls(user=Depends(get_admin_user)):
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}

View file

@ -1,4 +1,5 @@
from peewee import * from peewee import *
from peewee_migrate import Router
from config import SRC_LOG_LEVELS, DATA_DIR from config import SRC_LOG_LEVELS, DATA_DIR
import os import os
import logging import logging
@ -16,4 +17,6 @@ else:
DB = SqliteDatabase(f"{DATA_DIR}/webui.db") DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
DB.connect() router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
router.run()
DB.connect(reuse_if_open=True)

View file

@ -0,0 +1,149 @@
"""Peewee migrations -- 001_initial_schema.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
email = pw.CharField(max_length=255)
password = pw.CharField(max_length=255)
active = pw.BooleanField()
class Meta:
table_name = "auth"
@migrator.create_model
class Chat(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.CharField()
chat = pw.TextField()
timestamp = pw.DateField()
class Meta:
table_name = "chat"
@migrator.create_model
class ChatIdTag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
class Meta:
table_name = "chatidtag"
@migrator.create_model
class Document(pw.Model):
id = pw.AutoField()
collection_name = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255, unique=True)
title = pw.CharField()
filename = pw.CharField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
class Meta:
table_name = "document"
@migrator.create_model
class Modelfile(pw.Model):
id = pw.AutoField()
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.DateField()
class Meta:
table_name = "modelfile"
@migrator.create_model
class Prompt(pw.Model):
id = pw.AutoField()
command = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.CharField()
content = pw.TextField()
timestamp = pw.DateField()
class Meta:
table_name = "prompt"
@migrator.create_model
class Tag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
data = pw.TextField(null=True)
class Meta:
table_name = "tag"
@migrator.create_model
class User(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.CharField(max_length=255)
timestamp = pw.DateField()
class Meta:
table_name = "user"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("user")
migrator.remove_model("tag")
migrator.remove_model("prompt")
migrator.remove_model("modelfile")
migrator.remove_model("document")
migrator.remove_model("chatidtag")
migrator.remove_model("chat")
migrator.remove_model("auth")

View file

@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("chat", "share_id")

View file

@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"user", api_key=pw.CharField(max_length=255, null=True, unique=True)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "api_key")

View file

@ -0,0 +1,21 @@
# Database Migrations
This directory contains all the database migrations for the web app.
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
Migrations are automatically ran at app startup.
## Creating a migration
Have you made a change to the schema of an existing model?
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
2. Make your changes to the models.
3. From the `backend` directory, run the following command:
```bash
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
```
- `$SQLITE_DB` should be the path to the database file.
- `$MIGRATION_NAME` should be a descriptive name for the migration.
4. The migration file will be created in the `apps/web/internal/migrations` directory.

View file

@ -20,6 +20,7 @@ from config import (
ENABLE_SIGNUP, ENABLE_SIGNUP,
USER_PERMISSIONS, USER_PERMISSIONS,
WEBHOOK_URL, WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
) )
app = FastAPI() app = FastAPI()
@ -34,7 +35,7 @@ app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.USER_PERMISSIONS = USER_PERMISSIONS app.state.USER_PERMISSIONS = USER_PERMISSIONS
app.state.WEBHOOK_URL = WEBHOOK_URL app.state.WEBHOOK_URL = WEBHOOK_URL
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View file

@ -47,6 +47,10 @@ class Token(BaseModel):
token_type: str token_type: str
class ApiKey(BaseModel):
api_key: Optional[str] = None
class UserResponse(BaseModel): class UserResponse(BaseModel):
id: str id: str
email: str email: str
@ -123,6 +127,28 @@ class AuthsTable:
except: except:
return None return None
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_api_key: {api_key}")
# if no api_key, return None
if not api_key:
return None
try:
user = Users.get_user_by_api_key(api_key)
return user if user else None
except:
return False
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_trusted_header: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
user = Users.get_user_by_id(auth.id)
return user
except:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool: def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try: try:
query = Auth.update(password=new_password).where(Auth.id == id) query = Auth.update(password=new_password).where(Auth.id == id)

View file

@ -20,6 +20,7 @@ class Chat(Model):
title = CharField() title = CharField()
chat = TextField() # Save Chat JSON as Text chat = TextField() # Save Chat JSON as Text
timestamp = DateField() timestamp = DateField()
share_id = CharField(null=True, unique=True)
class Meta: class Meta:
database = DB database = DB
@ -31,6 +32,7 @@ class ChatModel(BaseModel):
title: str title: str
chat: str chat: str
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
share_id: Optional[str] = None
#################### ####################
@ -52,6 +54,7 @@ class ChatResponse(BaseModel):
title: str title: str
chat: dict chat: dict
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
share_id: Optional[str] = None # id of the chat to be shared
class ChatTitleIdResponse(BaseModel): class ChatTitleIdResponse(BaseModel):
@ -95,6 +98,71 @@ class ChatTable:
except: except:
return None return None
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
# Get the existing chat to share
chat = Chat.get(Chat.id == chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"timestamp": int(time.time()),
}
)
shared_result = Chat.create(**shared_chat.model_dump())
# Update the original chat with the share_id
result = (
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
)
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
print("update_shared_chat_by_id")
chat = Chat.get(Chat.id == chat_id)
print(chat)
query = Chat.update(
title=chat.title,
chat=chat.chat,
).where(Chat.id == chat.share_id)
query.execute()
chat = Chat.get(Chat.id == chat.share_id)
return ChatModel(**model_to_dict(chat))
except:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try:
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def update_chat_share_id_by_id(
self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]:
try:
query = Chat.update(
share_id=share_id,
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_chat_lists_by_user_id( def get_chat_lists_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50 self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]: ) -> List[ChatModel]:
@ -131,6 +199,13 @@ class ChatTable:
.order_by(Chat.timestamp.desc()) .order_by(Chat.timestamp.desc())
] ]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id) chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
@ -149,12 +224,15 @@ class ChatTable:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
query.execute() # Remove the rows, return number of rows removed. query.execute() # Remove the rows, return number of rows removed.
return True return True and self.delete_shared_chat_by_chat_id(id)
except: except:
return False return False
def delete_chats_by_user_id(self, user_id: str) -> bool: def delete_chats_by_user_id(self, user_id: str) -> bool:
try: try:
self.delete_shared_chats_by_user_id(user_id)
query = Chat.delete().where(Chat.user_id == user_id) query = Chat.delete().where(Chat.user_id == user_id)
query.execute() # Remove the rows, return number of rows removed. query.execute() # Remove the rows, return number of rows removed.
@ -162,5 +240,19 @@ class ChatTable:
except: except:
return False return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
shared_chat_ids = [
f"shared-{chat.id}"
for chat in Chat.select().where(Chat.user_id == user_id)
]
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Chats = ChatTable(DB) Chats = ChatTable(DB)

View file

@ -20,6 +20,7 @@ class User(Model):
role = CharField() role = CharField()
profile_image_url = CharField() profile_image_url = CharField()
timestamp = DateField() timestamp = DateField()
api_key = CharField(null=True, unique=True)
class Meta: class Meta:
database = DB database = DB
@ -32,6 +33,7 @@ class UserModel(BaseModel):
role: str = "pending" role: str = "pending"
profile_image_url: str = "/user.png" profile_image_url: str = "/user.png"
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
api_key: Optional[str] = None
#################### ####################
@ -82,6 +84,13 @@ class UsersTable:
except: except:
return None return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
user = User.get(User.api_key == api_key)
return UserModel(**model_to_dict(user))
except:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]: def get_user_by_email(self, email: str) -> Optional[UserModel]:
try: try:
user = User.get(User.email == email) user = User.get(User.email == email)
@ -149,5 +158,21 @@ class UsersTable:
except: except:
return False return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try:
query = User.update(api_key=api_key).where(User.id == id)
result = query.execute()
return True if result == 1 else False
except:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
user = User.get(User.id == id)
return user.api_key
except:
return None
Users = UsersTable(DB) Users = UsersTable(DB)

View file

@ -1,13 +1,10 @@
from fastapi import Response, Request from fastapi import Request
from fastapi import Depends, FastAPI, HTTPException, status from fastapi import Depends, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union
from fastapi import APIRouter, status from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import time
import uuid
import re import re
import uuid
from apps.web.models.auths import ( from apps.web.models.auths import (
SigninForm, SigninForm,
@ -17,6 +14,7 @@ from apps.web.models.auths import (
UserResponse, UserResponse,
SigninResponse, SigninResponse,
Auths, Auths,
ApiKey,
) )
from apps.web.models.users import Users from apps.web.models.users import Users
@ -25,10 +23,12 @@ from utils.utils import (
get_current_user, get_current_user,
get_admin_user, get_admin_user,
create_token, create_token,
create_api_key,
) )
from utils.misc import parse_duration, validate_email_format from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
router = APIRouter() router = APIRouter()
@ -79,6 +79,8 @@ async def update_profile(
async def update_password( async def update_password(
form_data: UpdatePasswordForm, session_user=Depends(get_current_user) form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
): ):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
if session_user: if session_user:
user = Auths.authenticate_user(session_user.email, form_data.password) user = Auths.authenticate_user(session_user.email, form_data.password)
@ -98,7 +100,22 @@ async def update_password(
@router.post("/signin", response_model=SigninResponse) @router.post("/signin", response_model=SigninResponse)
async def signin(request: Request, form_data: SigninForm): async def signin(request: Request, form_data: SigninForm):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
if not Users.get_user_by_email(trusted_email.lower()):
await signup(
request,
SignupForm(
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
else:
user = Auths.authenticate_user(form_data.email.lower(), form_data.password) user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if user: if user:
token = create_token( token = create_token(
data={"id": user.id}, data={"id": user.id},
@ -249,3 +266,40 @@ async def update_token_expires_duration(
return request.app.state.JWT_EXPIRES_IN return request.app.state.JWT_EXPIRES_IN
else: else:
return request.app.state.JWT_EXPIRES_IN return request.app.state.JWT_EXPIRES_IN
############################
# API Key
############################
# create api key
@router.post("/api_key", response_model=ApiKey)
async def create_api_key_(user=Depends(get_current_user)):
api_key = create_api_key()
success = Users.update_user_api_key_by_id(user.id, api_key)
if success:
return {
"api_key": api_key,
}
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR)
# delete api key
@router.delete("/api_key", response_model=bool)
async def delete_api_key(user=Depends(get_current_user)):
success = Users.update_user_api_key_by_id(user.id, None)
return success
# get api key
@router.get("/api_key", response_model=ApiKey)
async def get_api_key(user=Depends(get_current_user)):
api_key = Users.get_user_api_key_by_id(user.id)
if api_key:
return {
"api_key": api_key,
}
else:
raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)

View file

@ -189,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
return result return result
############################
# ShareChatById
############################
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
if not shared_chat:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(),
)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# DeletedSharedChatById
############################
@router.delete("/{id}/share", response_model=Optional[bool])
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if not chat.share_id:
return False
result = Chats.delete_shared_chat_by_chat_id(id)
update_result = Chats.update_chat_share_id_by_id(id, None)
return result and update_result != None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################ ############################
# GetChatTagsById # GetChatTagsById
############################ ############################

View file

@ -367,6 +367,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
#################################### ####################################
WEBUI_AUTH = True WEBUI_AUTH = True
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
#################################### ####################################
# WEBUI_SECRET_KEY # WEBUI_SECRET_KEY

View file

@ -20,6 +20,7 @@ class ERROR_MESSAGES(str, Enum):
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." 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." 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." DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
USERNAME_TAKEN = ( USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username." "Uh-oh! This username is already registered. Please choose another username."
@ -36,6 +37,7 @@ class ERROR_MESSAGES(str, Enum):
INVALID_PASSWORD = ( INVALID_PASSWORD = (
"The password provided is incorrect. Please check for typos and try again." "The password provided is incorrect. Please check for typos and try again."
) )
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
UNAUTHORIZED = "401 Unauthorized" UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = ( ACTION_PROHIBITED = (
@ -58,7 +60,8 @@ class ERROR_MESSAGES(str, Enum):
RATE_LIMIT_EXCEEDED = "API rate limit exceeded" RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."

View file

@ -62,6 +62,21 @@ class SPAStaticFiles(StaticFiles):
raise ex raise ex
print(
f"""
___ __ __ _ _ _ ___
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || |
\___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___|
|_|
v{VERSION} - building the best open-source AI user interface.
https://github.com/open-webui/open-webui
"""
)
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
@ -179,6 +194,7 @@ async def get_app_config():
"images": images_app.state.ENABLED, "images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS, "default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
} }

View file

@ -14,6 +14,7 @@ uuid
requests requests
aiohttp aiohttp
peewee peewee
peewee-migrate
bcrypt bcrypt
litellm==1.30.7 litellm==1.30.7

View file

@ -1,6 +1,8 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends from fastapi import HTTPException, status, Depends
from apps.web.models.users import Users from apps.web.models.users import Users
from pydantic import BaseModel from pydantic import BaseModel
from typing import Union, Optional from typing import Union, Optional
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
@ -8,6 +10,7 @@ from passlib.context import CryptContext
from datetime import datetime, timedelta from datetime import datetime, timedelta
import requests import requests
import jwt import jwt
import uuid
import logging import logging
import config import config
@ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str):
return auth_header[len("Bearer ") :] return auth_header[len("Bearer ") :]
def create_api_key():
key = str(uuid.uuid4()).replace("-", "")
return f"sk-{key}"
def get_http_authorization_cred(auth_header: str): def get_http_authorization_cred(auth_header: str):
try: try:
scheme, credentials = auth_header.split(" ") scheme, credentials = auth_header.split(" ")
@ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str):
def get_current_user( def get_current_user(
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
): ):
# auth by api key
if auth_token.credentials.startswith("sk-"):
return get_current_user_by_api_key(auth_token.credentials)
# auth by jwt token
data = decode_token(auth_token.credentials) data = decode_token(auth_token.credentials)
if data != None and "id" in data: if data != None and "id" in data:
user = Users.get_user_by_id(data["id"]) user = Users.get_user_by_id(data["id"])
@ -85,6 +97,16 @@ def get_current_user(
) )
def get_current_user_by_api_key(api_key: str):
user = Users.get_user_by_api_key(api_key)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
return user
def get_verified_user(user=Depends(get_current_user)): def get_verified_user(user=Depends(get_current_user)):
if user.role not in {"user", "admin"}: if user.role not in {"user", "admin"}:
raise HTTPException( raise HTTPException(

View file

@ -318,3 +318,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
return res; return res;
}; };
export const createAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res.api_key;
};
export const getAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res.api_key;
};
export const deleteAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};

View file

@ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => {
return res; return res;
}; };
export const getChatByShareId = async (token: string, share_id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, {
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 shareChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
method: 'POST',
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 deleteSharedChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
method: 'DELETE',
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 updateChatById = async (token: string, id: string, chat: object) => { export const updateChatById = async (token: string, id: string, chat: object) => {
let error = null; let error = null;

View file

@ -16,6 +16,7 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let chatId = ''; export let chatId = '';
export let readOnly = false;
export let sendPrompt: Function; export let sendPrompt: Function;
export let continueGeneration: Function; export let continueGeneration: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
@ -317,6 +318,7 @@
<UserMessage <UserMessage
on:delete={() => messageDeleteHandler(message.id)} on:delete={() => messageDeleteHandler(message.id)}
user={$user} user={$user}
{readOnly}
{message} {message}
isFirstMessage={messageIdx === 0} isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null siblings={message.parentId !== null
@ -335,6 +337,7 @@
modelfiles={selectedModelfiles} modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []} siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length} isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{confirmEditResponseMessage} {confirmEditResponseMessage}
{showPreviousMessage} {showPreviousMessage}
{showNextMessage} {showNextMessage}

View file

@ -33,6 +33,8 @@
export let isLastMessage = true; export let isLastMessage = true;
export let readOnly = false;
export let confirmEditResponseMessage: Function; export let confirmEditResponseMessage: Function;
export let showPreviousMessage: Function; export let showPreviousMessage: Function;
export let showNextMessage: Function; export let showNextMessage: Function;
@ -128,7 +130,7 @@
// • auto-render specific keys, e.g.: // • auto-render specific keys, e.g.:
delimiters: [ delimiters: [
{ left: '$$', right: '$$', display: false }, { left: '$$', right: '$$', display: false },
{ left: '$', right: '$', display: false }, { left: '$ ', right: ' $', display: false },
{ left: '\\(', right: '\\)', display: false }, { left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: false }, { left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false } { left: '[ ', right: ' ]', display: false }
@ -422,7 +424,7 @@
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
> >
{#if siblings.length > 1} {#if siblings.length > 1}
<div class="flex self-center min-w-fit -mt-1"> <div class="flex self-center min-w-fit">
<button <button
class="self-center dark:hover:text-white hover:text-black transition" class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
@ -469,6 +471,7 @@
</div> </div>
{/if} {/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom"> <Tooltip content="Edit" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
@ -494,6 +497,7 @@
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom"> <Tooltip content="Copy" placement="bottom">
<button <button
@ -521,6 +525,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if !readOnly}
<Tooltip content="Good Response" placement="bottom"> <Tooltip content="Good Response" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
@ -574,6 +579,7 @@
> >
</button> </button>
</Tooltip> </Tooltip>
{/if}
<Tooltip content="Read Aloud" placement="bottom"> <Tooltip content="Read Aloud" placement="bottom">
<button <button
@ -656,7 +662,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if $config.images} {#if $config.images && !readOnly}
<Tooltip content="Generate Image" placement="bottom"> <Tooltip content="Generate Image" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
@ -752,7 +758,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{#if isLastMessage} {#if isLastMessage && !readOnly}
<Tooltip content="Continue Response" placement="bottom"> <Tooltip content="Continue Response" placement="bottom">
<button <button
type="button" type="button"

View file

@ -15,6 +15,7 @@
export let message; export let message;
export let siblings; export let siblings;
export let isFirstMessage: boolean; export let isFirstMessage: boolean;
export let readOnly: boolean;
export let confirmEditMessage: Function; export let confirmEditMessage: Function;
export let showPreviousMessage: Function; export let showPreviousMessage: Function;
@ -203,7 +204,7 @@
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500"> <div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
{#if siblings.length > 1} {#if siblings.length > 1}
<div class="flex self-center -mt-1"> <div class="flex self-center">
<button <button
class="self-center dark:hover:text-white hover:text-black transition" class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
@ -250,6 +251,7 @@
</div> </div>
{/if} {/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom"> <Tooltip content="Edit" placement="bottom">
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
@ -273,6 +275,7 @@
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom"> <Tooltip content="Copy" placement="bottom">
<button <button
@ -298,7 +301,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if !isFirstMessage} {#if !isFirstMessage && !readOnly}
<Tooltip content="Delete" placement="bottom"> <Tooltip content="Delete" placement="bottom">
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"

View file

@ -3,11 +3,13 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths'; import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
import UpdatePassword from './Account/UpdatePassword.svelte'; import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils'; import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Plus from '$lib/components/icons/Plus.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -15,8 +17,14 @@
let profileImageUrl = ''; let profileImageUrl = '';
let name = ''; let name = '';
let showJWTToken = false; let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
let APIKey = '';
let showAPIKey = false;
let APIKeyCopied = false;
let profileImageInputElement: HTMLInputElement; let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => { const submitHandler = async () => {
@ -33,9 +41,23 @@
return false; return false;
}; };
onMount(() => { const createAPIKeyHandler = async () => {
APIKey = await createAPIKey(localStorage.token);
if (APIKey) {
toast.success($i18n.t('API Key created.'));
} else {
toast.error($i18n.t('Failed to create API Key.'));
}
};
onMount(async () => {
name = $user.name; name = $user.name;
profileImageUrl = $user.profile_image_url; profileImageUrl = $user.profile_image_url;
APIKey = await getAPIKey(localStorage.token).catch((error) => {
console.log(error);
return '';
});
}); });
</script> </script>
@ -170,22 +192,23 @@
<hr class=" dark:border-gray-700 my-4" /> <hr class=" dark:border-gray-700 my-4" />
<div class=" w-full justify-between"> <div class="flex flex-col gap-4">
<div class="flex w-full justify-between"> <div class="justify-between w-full">
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> <div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
</div> </div>
<div class="flex mt-2"> <div class="flex mt-2">
<div class="flex w-full"> <div class="flex w-full">
<input <input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none"
type={showJWTToken ? 'text' : 'password'} type={showJWTToken ? 'text' : 'password'}
value={localStorage.token} value={localStorage.token}
disabled disabled
/> />
<button <button
class="dark:bg-gray-800 px-2 transition rounded-r-lg" class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
on:click={() => { on:click={() => {
showJWTToken = !showJWTToken; showJWTToken = !showJWTToken;
}} }}
@ -225,7 +248,7 @@
</div> </div>
<button <button
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
on:click={() => { on:click={() => {
copyToClipboard(localStorage.token); copyToClipboard(localStorage.token);
JWTTokenCopied = true; JWTTokenCopied = true;
@ -269,6 +292,143 @@
</button> </button>
</div> </div>
</div> </div>
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
</div>
<div class="flex mt-2">
{#if APIKey}
<div class="flex w-full">
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none"
type={showAPIKey ? 'text' : 'password'}
value={APIKey}
disabled
/>
<button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
on:click={() => {
showAPIKey = !showAPIKey;
}}
>
{#if showAPIKey}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
<button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
on:click={() => {
copyToClipboard(APIKey);
APIKeyCopied = true;
setTimeout(() => {
APIKeyCopied = false;
}, 2000);
}}
>
{#if APIKeyCopied}
<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>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
<Tooltip content="Create new key">
<button
class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg"
on:click={() => {
createAPIKeyHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{:else}
<button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition"
on:click={() => {
createAPIKeyHandler();
}}
>
<Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button
>
{/if}
</div>
</div>
</div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">

View file

@ -1,19 +1,138 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext, onMount } from 'svelte';
import Modal from '../common/Modal.svelte';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { toast } from 'svelte-sonner';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import { chatId, modelfiles } from '$lib/stores';
import { copyToClipboard } from '$lib/utils';
import Modal from '../common/Modal.svelte';
import Link from '../icons/Link.svelte';
let chat = null;
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let downloadChat: Function; const shareLocalChat = async () => {
export let shareChat: Function; const _chat = chat;
const sharedChat = await shareChatById(localStorage.token, $chatId);
const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`;
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
copyToClipboard(chatShareUrl);
chat = await getChatById(localStorage.token, $chatId);
};
const shareChat = async () => {
const _chat = chat.chat;
console.log('share', _chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
const tab = await window.open(`${url}/chats/upload`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(
JSON.stringify({
chat: _chat,
modelfiles: $modelfiles.filter((modelfile) =>
_chat.models.includes(modelfile.tagName)
)
}),
'*'
);
}
},
false
);
};
const downloadChat = async () => {
const _chat = chat.chat;
console.log('download', chat);
const chatText = _chat.messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${_chat.title}.txt`);
};
export let show = false; export let show = false;
onMount(async () => {
chatId.subscribe(async (value) => {
chat = await getChatById(localStorage.token, value);
console.log(chat);
});
});
</script> </script>
<Modal bind:show size="xs"> <Modal bind:show size="sm">
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
<button <button
class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white" 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>
<hr class=" dark:border-gray-800" />
{#if chat}
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
<div class=" text-sm dark:text-gray-300 mb-1">
{#if chat.share_id}
<a href="/s/{chat.share_id}" target="_blank"
>You have shared this chat <span class=" underline">before</span>.</a
>
Click here to
<button
class="underline"
on:click={async () => {
const res = await deleteSharedChatById(localStorage.token, $chatId);
if (res) {
chat = await getChatById(localStorage.token, $chatId);
}
}}>delete this link</button
> and create a new shared link.
{:else}
Messages you send after creating your link won't be shared. Users with the URL will be
able to view the shared chat.
{/if}
</div>
<div class="flex justify-end">
<div class="flex flex-col items-end space-x-1 mt-1.5">
<div class="flex gap-1">
<button
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
type="button" type="button"
on:click={() => { on:click={() => {
shareChat(); shareChat();
@ -23,11 +142,27 @@
{$i18n.t('Share to OpenWebUI Community')} {$i18n.t('Share to OpenWebUI Community')}
</button> </button>
<div class="flex justify-center space-x-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button <button
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
type="button"
on:click={() => {
shareLocalChat();
show = false;
}}
>
<Link />
{#if chat.share_id}
{$i18n.t('Update and Copy Link')}
{:else}
{$i18n.t('Copy Link')}
{/if}
</button>
</div>
<div class="flex gap-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button
class=" text-right rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
type="button" type="button"
on:click={() => { on:click={() => {
downloadChat(); downloadChat();
@ -38,4 +173,8 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
{/if}
</div>
</Modal> </Modal>

View file

@ -1,24 +1,25 @@
<script lang="ts"> <script lang="ts">
export let className: string = 'text-white'; export let className: string = '';
export let theme: 'blue' | 'white' | 'black' = 'white';
</script> </script>
<div class="flex justify-center text-center {className}"> <div class="flex justify-center text-center {className}">
<svg <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue' ><style>
? 'text-sky-600' .spinner_ajPY {
: theme === 'white' transform-origin: center;
? 'text-white' animation: spinner_AtaB 0.75s infinite linear;
: 'text-gray-600'} " }
xmlns="http://www.w3.org/2000/svg" @keyframes spinner_AtaB {
fill="none" 100% {
viewBox="0 0 24 24" transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
> >
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div> </div>

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>

View file

@ -0,0 +1,15 @@
<script lang="ts">
export let className = 'w-4 h-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="M12 4.5v15m7.5-7.5h-15" />
</svg>

View file

@ -1,11 +1,9 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { Separator } from 'bits-ui'; import { Separator } from 'bits-ui';
import { getChatById } from '$lib/apis/chats'; import { getChatById, shareChatById } from '$lib/apis/chats';
import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
@ -32,55 +30,13 @@
export let addTag: Function; export let addTag: Function;
export let deleteTag: Function; export let deleteTag: Function;
export let showModelSelector = false; export let showModelSelector = true;
let showShareChatModal = false; let showShareChatModal = false;
let showTagChatModal = false; let showTagChatModal = false;
const shareChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('share', chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
const tab = await window.open(`${url}/chats/upload`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(
JSON.stringify({
chat: chat,
modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName))
}),
'*'
);
}
},
false
);
};
const downloadChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('download', chat);
const chatText = chat.messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${chat.title}.txt`);
};
</script> </script>
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> <ShareChatModal bind:show={showShareChatModal} />
<!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> --> <!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> -->
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div <div
@ -135,12 +91,14 @@
</div> --> </div> -->
<div class="flex items-center w-full max-w-full"> <div class="flex items-center w-full max-w-full">
<div class="w-full flex-1 overflow-hidden max-w-full"> <div class="flex-1 overflow-hidden max-w-full">
{#if showModelSelector}
<ModelSelector bind:selectedModels /> <ModelSelector bind:selectedModels />
{/if}
</div> </div>
<div class="self-start flex flex-none items-center"> <div class="self-start flex flex-none items-center">
<div class="flex self-center w-[1px] h-5 mx-2 bg-stone-700" /> <div class="flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
{#if !shareEnabled} {#if !shareEnabled}
<Tooltip content="Settings"> <Tooltip content="Settings">

View file

@ -5,7 +5,7 @@
"(latest)": "(nieuwste)", "(latest)": "(nieuwste)",
"{{modelName}} is thinking...": "{{modelName}} is aan het denken...", "{{modelName}} is thinking...": "{{modelName}} is aan het denken...",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht", "{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht",
"a user": "", "a user": "een gebruiker",
"About": "Over", "About": "Over",
"Account": "Account", "Account": "Account",
"Action": "Actie", "Action": "Actie",
@ -35,12 +35,12 @@
"API Key": "API Key", "API Key": "API Key",
"API RPM": "API RPM", "API RPM": "API RPM",
"are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen", "are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen",
"Are you sure?": "", "Are you sure?": "Zeker weten?",
"Audio": "Audio", "Audio": "Audio",
"Auto-playback response": "Automatisch afspelen van antwoord", "Auto-playback response": "Automatisch afspelen van antwoord",
"Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.", "Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "", "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Basis URL is verplicht",
"available!": "beschikbaar!", "available!": "beschikbaar!",
"Back": "Terug", "Back": "Terug",
"Builder Mode": "Bouwer Modus", "Builder Mode": "Bouwer Modus",
@ -60,8 +60,8 @@
"Chunk Size": "Chunk Grootte", "Chunk Size": "Chunk Grootte",
"Click here for help.": "Klik hier voor help.", "Click here for help.": "Klik hier voor help.",
"Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.", "Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.",
"Click here to select": "", "Click here to select": "Klik hier om te selecteren",
"Click here to select documents.": "", "Click here to select documents.": "Klik hier om documenten te selecteren",
"click here.": "click here.", "click here.": "click here.",
"Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.", "Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.",
"Close": "Sluiten", "Close": "Sluiten",
@ -88,7 +88,7 @@
"Database": "Database", "Database": "Database",
"DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm", "DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm",
"Default": "Standaard", "Default": "Standaard",
"Default (Automatic1111)": "", "Default (Automatic1111)": "Standaard (Automatic1111)",
"Default (Web API)": "Standaard (Web API)", "Default (Web API)": "Standaard (Web API)",
"Default model updated": "Standaard model bijgewerkt", "Default model updated": "Standaard model bijgewerkt",
"Default Prompt Suggestions": "Standaard Prompt Suggesties", "Default Prompt Suggestions": "Standaard Prompt Suggesties",
@ -123,21 +123,21 @@
"Enable Chat History": "Schakel Chat Geschiedenis in", "Enable Chat History": "Schakel Chat Geschiedenis in",
"Enable New Sign Ups": "Schakel Nieuwe Registraties in", "Enable New Sign Ups": "Schakel Nieuwe Registraties in",
"Enabled": "Ingeschakeld", "Enabled": "Ingeschakeld",
"Enter {{role}} message here": "", "Enter {{role}} message here": "Voeg {{role}} bericht hier toe",
"Enter API Key": "", "Enter API Key": "Voeg API Key toe",
"Enter Chunk Overlap": "", "Enter Chunk Overlap": "Voeg Chunk Overlap toe",
"Enter Chunk Size": "", "Enter Chunk Size": "Voeg Chunk Size toe",
"Enter Image Size (e.g. 512x512)": "", "Enter Image Size (e.g. 512x512)": "Voeg afbeelding formaat toe (Bijv. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "", "Enter LiteLLM API Base URL (litellm_params.api_base)": "Voeg LiteLLM API Base URL toe (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "", "Enter LiteLLM API Key (litellm_params.api_key)": "Voeg LiteLLM API Sleutel toe (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "", "Enter LiteLLM API RPM (litellm_params.rpm)": "Voeg LiteLLM API RPM toe (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "", "Enter LiteLLM Model (litellm_params.model)": "Voeg LiteLLM Model toe (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "", "Enter Max Tokens (litellm_params.max_tokens)": "Voeg maximum aantal tokens toe (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "", "Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "", "Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)",
"Enter stop sequence": "Zet stop sequentie", "Enter stop sequence": "Zet stop sequentie",
"Enter Top K": "", "Enter Top K": "Voeg Top K toe",
"Enter URL (e.g. http://127.0.0.1:7860/)": "", "Enter URL (e.g. http://127.0.0.1:7860/)": "Zet URL (Bijv. http://127.0.0.1:7860/)",
"Enter Your Email": "Voer je Email in", "Enter Your Email": "Voer je Email in",
"Enter Your Full Name": "Voer je Volledige Naam in", "Enter Your Full Name": "Voer je Volledige Naam in",
"Enter Your Password": "Voer je Wachtwoord in", "Enter Your Password": "Voer je Wachtwoord in",
@ -161,7 +161,7 @@
"Hide Additional Params": "Verberg Extra Params", "Hide Additional Params": "Verberg Extra Params",
"How can I help you today?": "Hoe kan ik je vandaag helpen?", "How can I help you today?": "Hoe kan ik je vandaag helpen?",
"Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)", "Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)",
"Image Generation Engine": "", "Image Generation Engine": "Afbeelding Generatie Engine",
"Image Settings": "Afbeelding Instellingen", "Image Settings": "Afbeelding Instellingen",
"Images": "Afbeeldingen", "Images": "Afbeeldingen",
"Import Chats": "Importeer Chats", "Import Chats": "Importeer Chats",
@ -217,7 +217,7 @@
"Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar", "Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar",
"Off": "Uit", "Off": "Uit",
"Okay, Let's Go!": "Okay, Laten we gaan!", "Okay, Let's Go!": "Okay, Laten we gaan!",
"Ollama Base URL": "", "Ollama Base URL": "Ollama Basis URL",
"Ollama Version": "Ollama Versie", "Ollama Version": "Ollama Versie",
"On": "Aan", "On": "Aan",
"Only": "Alleen", "Only": "Alleen",
@ -227,11 +227,11 @@
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.",
"Open": "Open", "Open": "Open",
"Open AI": "Open AI", "Open AI": "Open AI",
"Open AI (Dall-E)": "", "Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Open nieuwe chat", "Open new chat": "Open nieuwe chat",
"OpenAI API": "OpenAI API", "OpenAI API": "OpenAI API",
"OpenAI API Key": "", "OpenAI API Key": "OpenAI API Sleutel",
"OpenAI API Key is required.": "", "OpenAI API Key is required.": "OpenAI API Sleutel is verplicht",
"or": "of", "or": "of",
"Parameters": "Parameters", "Parameters": "Parameters",
"Password": "Wachtwoord", "Password": "Wachtwoord",
@ -273,9 +273,9 @@
"See readme.md for instructions": "Zie readme.md voor instructies", "See readme.md for instructions": "Zie readme.md voor instructies",
"See what's new": "Zie wat er nieuw is", "See what's new": "Zie wat er nieuw is",
"Seed": "Seed", "Seed": "Seed",
"Select a mode": "", "Select a mode": "Selecteer een modus",
"Select a model": "Selecteer een model", "Select a model": "Selecteer een model",
"Select an Ollama instance": "", "Select an Ollama instance": "Selecteer een Ollama instantie",
"Send a Message": "Stuur een Bericht", "Send a Message": "Stuur een Bericht",
"Send message": "Stuur bericht", "Send message": "Stuur bericht",
"Server connection verified": "Server verbinding geverifieerd", "Server connection verified": "Server verbinding geverifieerd",
@ -330,7 +330,7 @@
"Top P": "Top P", "Top P": "Top P",
"Trouble accessing Ollama?": "Problemen met toegang tot Ollama?", "Trouble accessing Ollama?": "Problemen met toegang tot Ollama?",
"TTS Settings": "TTS instellingen", "TTS Settings": "TTS instellingen",
"Type Hugging Face Resolve (Download) URL": "", "Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.", "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst", "Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst",
"Update password": "Wijzig wachtwoord", "Update password": "Wijzig wachtwoord",
@ -339,7 +339,7 @@
"Upload Progress": "Upload Voortgang", "Upload Progress": "Upload Voortgang",
"URL Mode": "URL Modus", "URL Mode": "URL Modus",
"Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.", "Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.",
"Use Gravatar": "", "Use Gravatar": "Gebruik Gravatar",
"user": "user", "user": "user",
"User Permissions": "Gebruikers Rechten", "User Permissions": "Gebruikers Rechten",
"Users": "Gebruikers", "Users": "Gebruikers",

View file

@ -48,7 +48,7 @@
let messagesContainerElement: HTMLDivElement; let messagesContainerElement: HTMLDivElement;
let currentRequestId = null; let currentRequestId = null;
let showModelSelector = false; let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;

View file

@ -56,7 +56,7 @@
let currentRequestId = null; let currentRequestId = null;
// let chatId = $page.params.id; // let chatId = $page.params.id;
let showModelSelector = false; let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;

View file

@ -1,6 +1,7 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { userSignIn, userSignUp } from '$lib/apis/auths'; import { userSignIn, userSignUp } from '$lib/apis/auths';
import Spinner from '$lib/components/common/Spinner.svelte';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, config, user } from '$lib/stores'; import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
@ -56,6 +57,9 @@
await goto('/'); await goto('/');
} }
loaded = true; loaded = true;
if ($config?.trusted_header_auth ?? false) {
await signInHandler();
}
}); });
</script> </script>
@ -90,6 +94,23 @@
</div> --> </div> -->
<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col"> <div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col">
{#if $config?.trusted_header_auth ?? false}
<div class=" my-auto pb-10 w-full">
<div
class="flex items-center justify-center gap-3 text-xl sm:text-2xl text-center font-bold dark:text-gray-200"
>
<div>
{$i18n.t('Signing in')}
{$i18n.t('to')}
{$WEBUI_NAME}
</div>
<div>
<Spinner />
</div>
</div>
</div>
{:else}
<div class=" my-auto pb-10 w-full"> <div class=" my-auto pb-10 w-full">
<form <form
class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl" class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
@ -184,6 +205,7 @@
</div> </div>
</form> </form>
</div> </div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}

View file

@ -0,0 +1,180 @@
<script lang="ts">
import { onMount, tick, getContext } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import dayjs from 'dayjs';
import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores';
import { convertMessagesToHistory } from '$lib/utils';
import { getChatByShareId } from '$lib/apis/chats';
import Messages from '$lib/components/chat/Messages.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
const i18n = getContext('i18n');
let loaded = false;
let autoScroll = true;
let processing = '';
let messagesContainerElement: HTMLDivElement;
// let chatId = $page.params.id;
let showModelSelector = false;
let selectedModels = [''];
let selectedModelfiles = {};
$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
const modelfile =
$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
return {
...a,
...(modelfile && { [tagName]: modelfile })
};
}, {});
let chat = null;
let title = '';
let files = [];
let messages = [];
let history = {
messages: {},
currentId: null
};
$: if (history.currentId !== null) {
let _messages = [];
let currentMessage = history.messages[history.currentId];
while (currentMessage !== null) {
_messages.unshift({ ...currentMessage });
currentMessage =
currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
}
messages = _messages;
} else {
messages = [];
}
$: if ($page.params.id) {
(async () => {
if (await loadSharedChat()) {
await tick();
loaded = true;
window.setTimeout(() => scrollToBottom(), 0);
const chatInput = document.getElementById('chat-textarea');
chatInput?.focus();
} else {
await goto('/');
}
})();
}
//////////////////////////
// Web functions
//////////////////////////
const loadSharedChat = async () => {
await chatId.set($page.params.id);
chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => {
await goto('/');
return null;
});
if (chat) {
const chatContent = chat.chat;
if (chatContent) {
console.log(chatContent);
selectedModels =
(chatContent?.models ?? undefined) !== undefined
? chatContent.models
: [chatContent.models ?? ''];
history =
(chatContent?.history ?? undefined) !== undefined
? chatContent.history
: convertMessagesToHistory(chatContent.messages);
title = chatContent.title;
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
await settings.set({
..._settings,
system: chatContent.system ?? _settings.system,
options: chatContent.options ?? _settings.options
});
autoScroll = true;
await tick();
if (messages.length > 0) {
history.messages[messages.at(-1).id].done = true;
}
await tick();
return true;
} else {
return null;
}
}
};
</script>
<svelte:head>
<title>
{title
? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
: `${$WEBUI_NAME}`}
</title>
</svelte:head>
{#if loaded}
<div
class="min-h-screen max-h-screen w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900"
>
<div class="flex flex-col flex-auto justify-center py-8">
<div class="px-3 w-full max-w-3xl mx-auto">
<div>
<div class=" text-3xl font-semibold line-clamp-1">
{title}
</div>
<div class=" mt-1 text-gray-400">
{dayjs(chat.chat.timestamp).format('MMMM D, YYYY')}
</div>
</div>
<hr class=" dark:border-gray-800 mt-6 mb-2" />
</div>
<div
class=" flex flex-col justify-center w-full flex-auto overflow-auto h-0"
id="messages-container"
>
<div class=" h-full w-full flex flex-col py-4">
<div class="py-2">
<Messages
chatId={$chatId}
readOnly={true}
{selectedModels}
{selectedModelfiles}
{processing}
bind:history
bind:messages
bind:autoScroll
bottomPadding={files.length > 0}
sendPrompt={() => {}}
continueGeneration={() => {}}
regenerateResponse={() => {}}
/>
</div>
</div>
</div>
</div>
</div>
{/if}