forked from open-webui/open-webui
commit
46774aa5cd
45 changed files with 2067 additions and 485 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.117] - 2024-04-03
|
||||
|
||||
### Added
|
||||
|
||||
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
|
||||
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
|
||||
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
|
||||
- 📝 **Improved Logging**: Enhancements to logging functionality.
|
||||
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
|
||||
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
|
||||
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
|
||||
- 🔒 **Security Patches**: Applied necessary security patches.
|
||||
|
||||
## [0.1.116] - 2024-03-31
|
||||
|
||||
### Added
|
||||
|
|
|
@ -26,6 +26,7 @@ ENV OPENAI_API_BASE_URL ""
|
|||
ENV OPENAI_API_KEY ""
|
||||
|
||||
ENV WEBUI_SECRET_KEY ""
|
||||
ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER ""
|
||||
|
||||
ENV SCARF_NO_ANALYTICS true
|
||||
ENV DO_NOT_TRACK true
|
||||
|
|
|
@ -81,6 +81,12 @@ async def check_url(request: Request, call_next):
|
|||
return response
|
||||
|
||||
|
||||
@app.head("/")
|
||||
@app.get("/")
|
||||
async def get_status():
|
||||
return {"status": True}
|
||||
|
||||
|
||||
@app.get("/urls")
|
||||
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||
|
|
|
@ -8,7 +8,7 @@ from fastapi import (
|
|||
Form,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import os, shutil, logging
|
||||
import os, shutil, logging, re
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
@ -450,7 +450,7 @@ def store_doc(
|
|||
try:
|
||||
is_valid_filename = True
|
||||
unsanitized_filename = file.filename
|
||||
if not unsanitized_filename.isascii():
|
||||
if re.search(r'[\\/:"\*\?<>|\n\t ]', unsanitized_filename) is not None:
|
||||
is_valid_filename = False
|
||||
|
||||
unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from peewee import *
|
||||
from peewee_migrate import Router
|
||||
from config import SRC_LOG_LEVELS, DATA_DIR
|
||||
import os
|
||||
import logging
|
||||
|
@ -16,4 +17,6 @@ else:
|
|||
|
||||
|
||||
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)
|
||||
|
|
149
backend/apps/web/internal/migrations/001_initial_schema.py
Normal file
149
backend/apps/web/internal/migrations/001_initial_schema.py
Normal 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")
|
|
@ -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")
|
48
backend/apps/web/internal/migrations/003_add_auth_api_key.py
Normal file
48
backend/apps/web/internal/migrations/003_add_auth_api_key.py
Normal 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")
|
21
backend/apps/web/internal/migrations/README.md
Normal file
21
backend/apps/web/internal/migrations/README.md
Normal 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.
|
|
@ -20,6 +20,7 @@ from config import (
|
|||
ENABLE_SIGNUP,
|
||||
USER_PERMISSIONS,
|
||||
WEBHOOK_URL,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
|
@ -34,7 +35,7 @@ app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
|||
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
app.state.USER_PERMISSIONS = USER_PERMISSIONS
|
||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
|
@ -47,6 +47,10 @@ class Token(BaseModel):
|
|||
token_type: str
|
||||
|
||||
|
||||
class ApiKey(BaseModel):
|
||||
api_key: Optional[str] = None
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
|
@ -123,6 +127,28 @@ class AuthsTable:
|
|||
except:
|
||||
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:
|
||||
try:
|
||||
query = Auth.update(password=new_password).where(Auth.id == id)
|
||||
|
|
|
@ -20,6 +20,7 @@ class Chat(Model):
|
|||
title = CharField()
|
||||
chat = TextField() # Save Chat JSON as Text
|
||||
timestamp = DateField()
|
||||
share_id = CharField(null=True, unique=True)
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
@ -31,6 +32,7 @@ class ChatModel(BaseModel):
|
|||
title: str
|
||||
chat: str
|
||||
timestamp: int # timestamp in epoch
|
||||
share_id: Optional[str] = None
|
||||
|
||||
|
||||
####################
|
||||
|
@ -52,6 +54,7 @@ class ChatResponse(BaseModel):
|
|||
title: str
|
||||
chat: dict
|
||||
timestamp: int # timestamp in epoch
|
||||
share_id: Optional[str] = None # id of the chat to be shared
|
||||
|
||||
|
||||
class ChatTitleIdResponse(BaseModel):
|
||||
|
@ -95,6 +98,71 @@ class ChatTable:
|
|||
except:
|
||||
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(
|
||||
self, user_id: str, skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
|
@ -131,6 +199,13 @@ class ChatTable:
|
|||
.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]:
|
||||
try:
|
||||
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.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
return True and self.delete_shared_chat_by_chat_id(id)
|
||||
except:
|
||||
return False
|
||||
|
||||
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
||||
try:
|
||||
|
||||
self.delete_shared_chats_by_user_id(user_id)
|
||||
|
||||
query = Chat.delete().where(Chat.user_id == user_id)
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
|
@ -162,5 +240,19 @@ class ChatTable:
|
|||
except:
|
||||
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)
|
||||
|
|
|
@ -20,6 +20,7 @@ class User(Model):
|
|||
role = CharField()
|
||||
profile_image_url = CharField()
|
||||
timestamp = DateField()
|
||||
api_key = CharField(null=True, unique=True)
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
@ -32,6 +33,7 @@ class UserModel(BaseModel):
|
|||
role: str = "pending"
|
||||
profile_image_url: str = "/user.png"
|
||||
timestamp: int # timestamp in epoch
|
||||
api_key: Optional[str] = None
|
||||
|
||||
|
||||
####################
|
||||
|
@ -82,6 +84,13 @@ class UsersTable:
|
|||
except:
|
||||
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]:
|
||||
try:
|
||||
user = User.get(User.email == email)
|
||||
|
@ -149,5 +158,21 @@ class UsersTable:
|
|||
except:
|
||||
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)
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
from fastapi import Response, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union
|
||||
from fastapi import Request
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import time
|
||||
import uuid
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from apps.web.models.auths import (
|
||||
SigninForm,
|
||||
|
@ -17,6 +14,7 @@ from apps.web.models.auths import (
|
|||
UserResponse,
|
||||
SigninResponse,
|
||||
Auths,
|
||||
ApiKey,
|
||||
)
|
||||
from apps.web.models.users import Users
|
||||
|
||||
|
@ -25,10 +23,12 @@ from utils.utils import (
|
|||
get_current_user,
|
||||
get_admin_user,
|
||||
create_token,
|
||||
create_api_key,
|
||||
)
|
||||
from utils.misc import parse_duration, validate_email_format
|
||||
from utils.webhook import post_webhook
|
||||
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
@ -79,6 +79,8 @@ async def update_profile(
|
|||
async def update_password(
|
||||
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:
|
||||
user = Auths.authenticate_user(session_user.email, form_data.password)
|
||||
|
||||
|
@ -98,7 +100,22 @@ async def update_password(
|
|||
|
||||
@router.post("/signin", response_model=SigninResponse)
|
||||
async def signin(request: Request, form_data: SigninForm):
|
||||
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
||||
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)
|
||||
|
||||
if user:
|
||||
token = create_token(
|
||||
data={"id": user.id},
|
||||
|
@ -249,3 +266,40 @@ async def update_token_expires_duration(
|
|||
return request.app.state.JWT_EXPIRES_IN
|
||||
else:
|
||||
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)
|
||||
|
|
|
@ -189,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
|
|||
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
|
||||
############################
|
||||
|
|
|
@ -5,6 +5,8 @@ from starlette.responses import StreamingResponse, FileResponse
|
|||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
import markdown
|
||||
import requests
|
||||
import os
|
||||
import aiohttp
|
||||
|
@ -28,6 +30,17 @@ async def get_gravatar(
|
|||
return get_gravatar_url(email)
|
||||
|
||||
|
||||
class MarkdownForm(BaseModel):
|
||||
md: str
|
||||
|
||||
|
||||
@router.post("/markdown")
|
||||
async def get_html_from_markdown(
|
||||
form_data: MarkdownForm,
|
||||
):
|
||||
return {"html": markdown.markdown(form_data.md)}
|
||||
|
||||
|
||||
@router.get("/db/download")
|
||||
async def download_db(user=Depends(get_admin_user)):
|
||||
|
||||
|
|
|
@ -25,8 +25,9 @@ try:
|
|||
except ImportError:
|
||||
log.warning("dotenv not installed, skipping...")
|
||||
|
||||
WEBUI_NAME = "Open WebUI"
|
||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||
|
||||
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
||||
|
||||
####################################
|
||||
|
@ -149,6 +150,7 @@ log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
|||
####################################
|
||||
|
||||
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
||||
|
||||
if CUSTOM_NAME:
|
||||
try:
|
||||
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
|
||||
|
@ -171,7 +173,9 @@ if CUSTOM_NAME:
|
|||
except Exception as e:
|
||||
log.exception(e)
|
||||
pass
|
||||
|
||||
else:
|
||||
if WEBUI_NAME != "Open WebUI":
|
||||
WEBUI_NAME += " (Open WebUI)"
|
||||
|
||||
####################################
|
||||
# DATA/FRONTEND BUILD DIR
|
||||
|
@ -362,6 +366,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
|
|||
####################################
|
||||
|
||||
WEBUI_AUTH = True
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI_SECRET_KEY
|
||||
|
|
|
@ -20,6 +20,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
||||
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
||||
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
||||
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."
|
||||
USERNAME_TAKEN = (
|
||||
"Uh-oh! This username is already registered. Please choose another username."
|
||||
|
@ -36,6 +37,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||
INVALID_PASSWORD = (
|
||||
"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"
|
||||
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
||||
ACTION_PROHIBITED = (
|
||||
|
@ -58,7 +60,8 @@ class ERROR_MESSAGES(str, Enum):
|
|||
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
||||
|
||||
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"
|
||||
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."
|
||||
|
|
|
@ -62,6 +62,21 @@ class SPAStaticFiles(StaticFiles):
|
|||
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.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||
|
@ -69,7 +84,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
|||
|
||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
|
||||
|
@ -179,6 +193,7 @@ async def get_app_config():
|
|||
"images": images_app.state.ENABLED,
|
||||
"default_models": webui_app.state.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||
}
|
||||
|
||||
|
||||
|
@ -268,6 +283,20 @@ async def get_app_latest_release_version():
|
|||
)
|
||||
|
||||
|
||||
@app.get("/manifest.json")
|
||||
async def get_manifest_json():
|
||||
return {
|
||||
"name": WEBUI_NAME,
|
||||
"short_name": WEBUI_NAME,
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#343541",
|
||||
"theme_color": "#343541",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
|
||||
}
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
|
||||
|
||||
|
|
|
@ -14,9 +14,12 @@ uuid
|
|||
requests
|
||||
aiohttp
|
||||
peewee
|
||||
peewee-migrate
|
||||
bcrypt
|
||||
|
||||
litellm==1.30.7
|
||||
boto3
|
||||
|
||||
argon2-cffi
|
||||
apscheduler
|
||||
google-generativeai
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi import HTTPException, status, Depends
|
||||
|
||||
from apps.web.models.users import Users
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Union, Optional
|
||||
from constants import ERROR_MESSAGES
|
||||
|
@ -8,6 +10,7 @@ from passlib.context import CryptContext
|
|||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
import jwt
|
||||
import uuid
|
||||
import logging
|
||||
import config
|
||||
|
||||
|
@ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str):
|
|||
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):
|
||||
try:
|
||||
scheme, credentials = auth_header.split(" ")
|
||||
|
@ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str):
|
|||
def get_current_user(
|
||||
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)
|
||||
if data != None and "id" in data:
|
||||
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)):
|
||||
if user.role not in {"user", "admin"}:
|
||||
raise HTTPException(
|
||||
|
|
200
package-lock.json
generated
200
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.1.116",
|
||||
"version": "0.1.117",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.1.116",
|
||||
"version": "0.1.117",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
|
@ -19,6 +19,7 @@
|
|||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
|
@ -1067,6 +1068,12 @@
|
|||
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
|
@ -1402,6 +1409,17 @@
|
|||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
|
@ -1459,6 +1477,15 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
|
@ -1666,6 +1693,17 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"bin": {
|
||||
"btoa": "bin/btoa.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
@ -1758,6 +1796,31 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg/node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
@ -1944,6 +2007,17 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.36.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
|
||||
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
|
@ -1964,6 +2038,15 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
|
@ -2156,6 +2239,12 @@
|
|||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz",
|
||||
"integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
|
@ -2584,6 +2673,11 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
|
@ -3003,6 +3097,19 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
|
@ -3403,10 +3510,27 @@
|
|||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
|
||||
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"fflate": "^0.4.8"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.6",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^2.2.0",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.9",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
|
||||
"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
|
||||
"version": "0.16.10",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
|
||||
"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
|
@ -3971,6 +4095,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/periscopic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||
|
@ -4391,6 +4521,15 @@
|
|||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -4494,6 +4633,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
|
@ -4814,6 +4962,15 @@
|
|||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-composer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz",
|
||||
|
@ -5215,6 +5372,15 @@
|
|||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symlink-or-copy": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
|
||||
|
@ -5353,6 +5519,15 @@
|
|||
"streamx": "^2.12.5"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
@ -5583,6 +5758,15 @@
|
|||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
|
@ -5676,9 +5860,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
|
||||
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
||||
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.1.116",
|
||||
"version": "0.1.117",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
|
@ -53,6 +53,7 @@
|
|||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
|
|
|
@ -318,3 +318,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
|
|||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => {
|
|||
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) => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
@ -22,6 +22,31 @@ export const getGravatarUrl = async (email: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getHTMLFromMarkdown = async (md: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
md: md
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
return res.html;
|
||||
};
|
||||
|
||||
export const downloadDatabase = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
export let chatId = '';
|
||||
export let readOnly = false;
|
||||
export let sendPrompt: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
@ -317,6 +318,7 @@
|
|||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
user={$user}
|
||||
{readOnly}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
|
@ -335,6 +337,7 @@
|
|||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{readOnly}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
import { config, settings } from '$lib/stores';
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import { extractSentences } from '$lib/utils';
|
||||
import {
|
||||
extractSentences,
|
||||
revertSanitizedResponseContent,
|
||||
sanitizeResponseContent
|
||||
} from '$lib/utils';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
|
@ -33,6 +37,8 @@
|
|||
|
||||
export let isLastMessage = true;
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
export let confirmEditResponseMessage: Function;
|
||||
export let showPreviousMessage: Function;
|
||||
export let showNextMessage: Function;
|
||||
|
@ -54,7 +60,7 @@
|
|||
let loadingSpeech = false;
|
||||
let generatingImage = false;
|
||||
|
||||
$: tokens = marked.lexer(message.content);
|
||||
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
|
@ -128,7 +134,7 @@
|
|||
// • auto-render specific keys, e.g.:
|
||||
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 }
|
||||
|
@ -403,8 +409,10 @@
|
|||
{:else}
|
||||
{#each tokens as token}
|
||||
{#if token.type === 'code'}
|
||||
<!-- {token.text} -->
|
||||
<CodeBlock lang={token.lang} code={token.text} />
|
||||
<CodeBlock
|
||||
lang={token.lang}
|
||||
code={revertSanitizedResponseContent(token.text)}
|
||||
/>
|
||||
{:else}
|
||||
{@html marked.parse(token.raw, {
|
||||
...defaults,
|
||||
|
@ -422,7 +430,7 @@
|
|||
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||
>
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center min-w-fit -mt-1">
|
||||
<div class="flex self-center min-w-fit">
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
|
@ -469,31 +477,33 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
{#if !readOnly}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<button
|
||||
|
@ -521,59 +531,61 @@
|
|||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Good Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, 1);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||
/></svg
|
||||
{#if !readOnly}
|
||||
<Tooltip content="Good Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, 1);
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Bad Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, -1);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||
/></svg
|
||||
<Tooltip content="Bad Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, -1);
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Read Aloud" placement="bottom">
|
||||
<button
|
||||
|
@ -656,7 +668,7 @@
|
|||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if $config.images}
|
||||
{#if $config.images && !readOnly}
|
||||
<Tooltip content="Generate Image" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
|
@ -752,7 +764,7 @@
|
|||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if isLastMessage}
|
||||
{#if isLastMessage && !readOnly}
|
||||
<Tooltip content="Continue Response" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
export let message;
|
||||
export let siblings;
|
||||
export let isFirstMessage: boolean;
|
||||
export let readOnly: boolean;
|
||||
|
||||
export let confirmEditMessage: Function;
|
||||
export let showPreviousMessage: Function;
|
||||
|
@ -203,7 +204,7 @@
|
|||
|
||||
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center -mt-1">
|
||||
<div class="flex self-center">
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
|
@ -250,29 +251,31 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
{#if !readOnly}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<button
|
||||
|
@ -298,7 +301,7 @@
|
|||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if !isFirstMessage}
|
||||
{#if !isFirstMessage && !readOnly}
|
||||
<Tooltip content="Delete" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
import { compareVersion } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let ollamaVersion = '';
|
||||
|
@ -51,8 +53,10 @@
|
|||
</div>
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||
<div>
|
||||
v{WEBUI_VERSION}
|
||||
<div class="flex gap-1">
|
||||
<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}>
|
||||
v{WEBUI_VERSION}
|
||||
</Tooltip>
|
||||
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
|
@ -126,7 +130,9 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Created by')}
|
||||
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
|
||||
{/if}{$i18n.t('Created by')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||
href="https://github.com/tjbck"
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
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 { getGravatarUrl } from '$lib/apis/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');
|
||||
|
||||
|
@ -15,8 +17,14 @@
|
|||
|
||||
let profileImageUrl = '';
|
||||
let name = '';
|
||||
|
||||
let showJWTToken = false;
|
||||
let JWTTokenCopied = false;
|
||||
|
||||
let APIKey = '';
|
||||
let showAPIKey = false;
|
||||
let APIKeyCopied = false;
|
||||
|
||||
let profileImageInputElement: HTMLInputElement;
|
||||
|
||||
const submitHandler = async () => {
|
||||
|
@ -33,9 +41,23 @@
|
|||
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;
|
||||
profileImageUrl = $user.profile_image_url;
|
||||
|
||||
APIKey = await getAPIKey(localStorage.token).catch((error) => {
|
||||
console.log(error);
|
||||
return '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -170,41 +192,83 @@
|
|||
|
||||
<hr class=" dark:border-gray-700 my-4" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="justify-between w-full">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2">
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
type={showJWTToken ? 'text' : 'password'}
|
||||
value={localStorage.token}
|
||||
disabled
|
||||
/>
|
||||
<div class="flex mt-2">
|
||||
<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={showJWTToken ? 'text' : 'password'}
|
||||
value={localStorage.token}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<button
|
||||
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
|
||||
on:click={() => {
|
||||
showJWTToken = !showJWTToken;
|
||||
}}
|
||||
>
|
||||
{#if showJWTToken}
|
||||
<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="dark:bg-gray-800 px-2 transition rounded-r-lg"
|
||||
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
|
||||
on:click={() => {
|
||||
showJWTToken = !showJWTToken;
|
||||
copyToClipboard(localStorage.token);
|
||||
JWTTokenCopied = true;
|
||||
setTimeout(() => {
|
||||
JWTTokenCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if showJWTToken}
|
||||
{#if JWTTokenCopied}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
viewBox="0 0 20 20"
|
||||
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"
|
||||
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"
|
||||
/>
|
||||
<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
|
||||
|
@ -213,60 +277,156 @@
|
|||
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"
|
||||
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>
|
||||
</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>
|
||||
|
||||
<button
|
||||
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
|
||||
on:click={() => {
|
||||
copyToClipboard(localStorage.token);
|
||||
JWTTokenCopied = true;
|
||||
setTimeout(() => {
|
||||
JWTTokenCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if JWTTokenCopied}
|
||||
<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"
|
||||
<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
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<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}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<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
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,41 +1,153 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
|
||||
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');
|
||||
|
||||
export let downloadChat: Function;
|
||||
export let shareChat: Function;
|
||||
const shareLocalChat = async () => {
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: if (show) {
|
||||
(async () => {
|
||||
if ($chatId) {
|
||||
chat = await getChatById(localStorage.token, $chatId);
|
||||
} else {
|
||||
chat = null;
|
||||
console.log(chat);
|
||||
}
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="xs">
|
||||
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||
<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"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
shareChat();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</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>
|
||||
|
||||
<Modal bind:show size="sm">
|
||||
<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
|
||||
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
||||
type="button"
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
downloadChat();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Download as a File')}
|
||||
<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"
|
||||
on:click={() => {
|
||||
shareChat();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
<script lang="ts">
|
||||
export let className: string = 'text-white';
|
||||
export let theme: 'blue' | 'white' | 'black' = 'white';
|
||||
export let className: string = '';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center text-center {className}">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue'
|
||||
? 'text-sky-600'
|
||||
: theme === 'white'
|
||||
? 'text-white'
|
||||
: 'text-gray-600'} "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
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>
|
||||
|
|
16
src/lib/components/icons/Link.svelte
Normal file
16
src/lib/components/icons/Link.svelte
Normal 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>
|
15
src/lib/components/icons/Plus.svelte
Normal file
15
src/lib/components/icons/Plus.svelte
Normal 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>
|
|
@ -1,24 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { Separator } from 'bits-ui';
|
||||
import { getChatById } from '$lib/apis/chats';
|
||||
import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
|
||||
|
||||
import { slide } from 'svelte/transition';
|
||||
import ShareChatModal from '../chat/ShareChatModal.svelte';
|
||||
import TagInput from '../common/Tags/TagInput.svelte';
|
||||
import ModelSelector from '../chat/ModelSelector.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
||||
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronUpDown from '../icons/ChevronUpDown.svelte';
|
||||
import Menu from './Navbar/Menu.svelte';
|
||||
import TagChatModal from '../chat/TagChatModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
@ -26,121 +16,34 @@
|
|||
export let title: string = $WEBUI_NAME;
|
||||
export let shareEnabled: boolean = false;
|
||||
|
||||
export let chat;
|
||||
export let selectedModels;
|
||||
|
||||
export let tags = [];
|
||||
export let addTag: Function;
|
||||
export let deleteTag: Function;
|
||||
|
||||
export let showModelSelector = false;
|
||||
export let showModelSelector = true;
|
||||
|
||||
let showShareChatModal = 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`);
|
||||
};
|
||||
let showDownloadChatModal = false;
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} />
|
||||
<!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> -->
|
||||
<ShareChatModal bind:show={showShareChatModal} />
|
||||
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
|
||||
<div
|
||||
class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
|
||||
w-full mx-auto px-3"
|
||||
>
|
||||
<!-- {#if shareEnabled}
|
||||
<div class="flex items-center w-full max-w-full">
|
||||
<div class=" flex-1 self-center font-medium line-clamp-1">
|
||||
<div>
|
||||
{title != '' ? title : $WEBUI_NAME}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-2 self-center flex items-center">
|
||||
<div class=" mr-1">
|
||||
<Tags {tags} {deleteTag} {addTag} />
|
||||
</div>
|
||||
|
||||
<Tooltip content="Share">
|
||||
<button
|
||||
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
|
||||
on:click={async () => {
|
||||
showShareChatModal = !showShareChatModal;
|
||||
|
||||
// console.log(showShareChatModal);
|
||||
}}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
<!-- <div class=" flex-1 self-center font-medium line-clamp-1">
|
||||
<div>
|
||||
{title != '' ? title : $WEBUI_NAME}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex items-center w-full max-w-full">
|
||||
<div class="w-full flex-1 overflow-hidden max-w-full">
|
||||
<ModelSelector bind:selectedModels />
|
||||
<div class="flex-1 overflow-hidden max-w-full">
|
||||
{#if showModelSelector}
|
||||
<ModelSelector bind:selectedModels />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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}
|
||||
<Tooltip content="Settings">
|
||||
|
@ -174,10 +77,14 @@
|
|||
</Tooltip>
|
||||
{:else}
|
||||
<Menu
|
||||
{chat}
|
||||
{shareEnabled}
|
||||
shareHandler={() => {
|
||||
showShareChatModal = !showShareChatModal;
|
||||
}}
|
||||
downloadHandler={() => {
|
||||
showDownloadChatModal = !showDownloadChatModal;
|
||||
}}
|
||||
{tags}
|
||||
{deleteTag}
|
||||
{addTag}
|
||||
|
|
|
@ -1,23 +1,94 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
import { showSettings } from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { showSettings } from '$lib/stores';
|
||||
import Tags from '$lib/components/common/Tags.svelte';
|
||||
|
||||
export let shareEnabled: boolean = false;
|
||||
export let shareHandler: Function;
|
||||
export let downloadHandler: Function;
|
||||
|
||||
// export let tagHandler: Function;
|
||||
|
||||
export let chat;
|
||||
export let tags;
|
||||
export let deleteTag: Function;
|
||||
export let addTag: Function;
|
||||
|
||||
export let onClose: Function = () => {};
|
||||
|
||||
const downloadChatAsTxt = 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`);
|
||||
};
|
||||
|
||||
const downloadChatAsPdf = async () => {
|
||||
const _chat = chat.chat;
|
||||
console.log('download', chat);
|
||||
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Initialize y-coordinate for text placement
|
||||
let yPos = 10;
|
||||
const pageHeight = doc.internal.pageSize.height;
|
||||
|
||||
// Function to check if new text exceeds the current page height
|
||||
function checkAndAddNewPage() {
|
||||
if (yPos > pageHeight - 10) {
|
||||
doc.addPage();
|
||||
yPos = 10; // Reset yPos for the new page
|
||||
}
|
||||
}
|
||||
|
||||
// Function to add text with specific style
|
||||
function addStyledText(text, isTitle = false) {
|
||||
// Set font style and size based on the parameters
|
||||
doc.setFont('helvetica', isTitle ? 'bold' : 'normal');
|
||||
doc.setFontSize(isTitle ? 12 : 10);
|
||||
|
||||
const textMargin = 7;
|
||||
|
||||
// Split text into lines to ensure it fits within the page width
|
||||
const lines = doc.splitTextToSize(text, 180); // Adjust the width as needed
|
||||
|
||||
lines.forEach((line) => {
|
||||
checkAndAddNewPage(); // Check if we need a new page before adding more text
|
||||
doc.text(line, 10, yPos);
|
||||
yPos += textMargin; // Increment yPos for the next line
|
||||
});
|
||||
|
||||
// Add extra space after a block of text
|
||||
yPos += 2;
|
||||
}
|
||||
|
||||
_chat.messages.forEach((message, i) => {
|
||||
// Add user text in bold
|
||||
doc.setFont('helvetica', 'normal', 'bold');
|
||||
|
||||
addStyledText(message.role.toUpperCase(), { isTitle: true });
|
||||
addStyledText(message.content);
|
||||
});
|
||||
|
||||
doc.save(`chat-${_chat.title}.pdf`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
|
@ -31,14 +102,14 @@
|
|||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="end"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||
on:click={async () => {
|
||||
await showSettings.set(!$showSettings);
|
||||
}}
|
||||
|
@ -49,7 +120,7 @@
|
|||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
@ -67,7 +138,7 @@
|
|||
|
||||
{#if shareEnabled}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
|
@ -87,7 +158,59 @@
|
|||
<div class="flex items-center">Share</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
<!-- <DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||
on:click={() => {
|
||||
downloadHandler();
|
||||
}}
|
||||
/> -->
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="flex items-center">Download</div>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
|
||||
transition={flyAndScale}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||
on:click={() => {
|
||||
downloadChatAsTxt();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">Plain text (.txt)</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
|
||||
on:click={() => {
|
||||
downloadChatAsPdf();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">PDF document (.pdf)</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags {tags} {deleteTag} {addTag} />
|
||||
|
|
|
@ -581,7 +581,7 @@
|
|||
<div class="py-2 w-full">
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
|
||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
goto('/admin');
|
||||
showDropdown = false;
|
||||
|
@ -607,7 +607,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
|
||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
goto('/playground');
|
||||
showDropdown = false;
|
||||
|
@ -634,7 +634,7 @@
|
|||
{/if}
|
||||
|
||||
<button
|
||||
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
|
||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||
on:click={async () => {
|
||||
await showSettings.set(true);
|
||||
showDropdown = false;
|
||||
|
@ -669,7 +669,7 @@
|
|||
|
||||
<div class="py-2 w-full">
|
||||
<button
|
||||
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
|
||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
localStorage.removeItem('token');
|
||||
location.href = '/auth';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"(latest)": "(nieuwste)",
|
||||
"{{modelName}} is thinking...": "{{modelName}} is aan het denken...",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht",
|
||||
"a user": "",
|
||||
"a user": "een gebruiker",
|
||||
"About": "Over",
|
||||
"Account": "Account",
|
||||
"Action": "Actie",
|
||||
|
@ -35,12 +35,12 @@
|
|||
"API Key": "API Key",
|
||||
"API RPM": "API RPM",
|
||||
"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",
|
||||
"Auto-playback response": "Automatisch afspelen van antwoord",
|
||||
"Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
|
||||
"AUTOMATIC1111 Base URL is required.": "",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Basis URL is verplicht",
|
||||
"available!": "beschikbaar!",
|
||||
"Back": "Terug",
|
||||
"Builder Mode": "Bouwer Modus",
|
||||
|
@ -60,8 +60,8 @@
|
|||
"Chunk Size": "Chunk Grootte",
|
||||
"Click here for help.": "Klik hier voor help.",
|
||||
"Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.",
|
||||
"Click here to select": "",
|
||||
"Click here to select documents.": "",
|
||||
"Click here to select": "Klik hier om te selecteren",
|
||||
"Click here to select documents.": "Klik hier om documenten te selecteren",
|
||||
"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.",
|
||||
"Close": "Sluiten",
|
||||
|
@ -88,7 +88,7 @@
|
|||
"Database": "Database",
|
||||
"DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm",
|
||||
"Default": "Standaard",
|
||||
"Default (Automatic1111)": "",
|
||||
"Default (Automatic1111)": "Standaard (Automatic1111)",
|
||||
"Default (Web API)": "Standaard (Web API)",
|
||||
"Default model updated": "Standaard model bijgewerkt",
|
||||
"Default Prompt Suggestions": "Standaard Prompt Suggesties",
|
||||
|
@ -123,21 +123,21 @@
|
|||
"Enable Chat History": "Schakel Chat Geschiedenis in",
|
||||
"Enable New Sign Ups": "Schakel Nieuwe Registraties in",
|
||||
"Enabled": "Ingeschakeld",
|
||||
"Enter {{role}} message here": "",
|
||||
"Enter API Key": "",
|
||||
"Enter Chunk Overlap": "",
|
||||
"Enter Chunk Size": "",
|
||||
"Enter Image Size (e.g. 512x512)": "",
|
||||
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
|
||||
"Enter LiteLLM API Key (litellm_params.api_key)": "",
|
||||
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
|
||||
"Enter LiteLLM Model (litellm_params.model)": "",
|
||||
"Enter Max Tokens (litellm_params.max_tokens)": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "",
|
||||
"Enter Number of Steps (e.g. 50)": "",
|
||||
"Enter {{role}} message here": "Voeg {{role}} bericht hier toe",
|
||||
"Enter API Key": "Voeg API Key toe",
|
||||
"Enter Chunk Overlap": "Voeg Chunk Overlap toe",
|
||||
"Enter Chunk Size": "Voeg Chunk Size toe",
|
||||
"Enter Image Size (e.g. 512x512)": "Voeg afbeelding formaat toe (Bijv. 512x512)",
|
||||
"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)": "Voeg LiteLLM API Sleutel toe (litellm_params.api_key)",
|
||||
"Enter LiteLLM API RPM (litellm_params.rpm)": "Voeg LiteLLM API RPM toe (litellm_params.rpm)",
|
||||
"Enter LiteLLM Model (litellm_params.model)": "Voeg LiteLLM Model toe (litellm_params.model)",
|
||||
"Enter Max Tokens (litellm_params.max_tokens)": "Voeg maximum aantal tokens toe (litellm_params.max_tokens)",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)",
|
||||
"Enter stop sequence": "Zet stop sequentie",
|
||||
"Enter Top K": "",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
|
||||
"Enter Top K": "Voeg Top K toe",
|
||||
"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 Full Name": "Voer je Volledige Naam in",
|
||||
"Enter Your Password": "Voer je Wachtwoord in",
|
||||
|
@ -161,7 +161,7 @@
|
|||
"Hide Additional Params": "Verberg Extra Params",
|
||||
"How can I help you today?": "Hoe kan ik je vandaag helpen?",
|
||||
"Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)",
|
||||
"Image Generation Engine": "",
|
||||
"Image Generation Engine": "Afbeelding Generatie Engine",
|
||||
"Image Settings": "Afbeelding Instellingen",
|
||||
"Images": "Afbeeldingen",
|
||||
"Import Chats": "Importeer Chats",
|
||||
|
@ -217,7 +217,7 @@
|
|||
"Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar",
|
||||
"Off": "Uit",
|
||||
"Okay, Let's Go!": "Okay, Laten we gaan!",
|
||||
"Ollama Base URL": "",
|
||||
"Ollama Base URL": "Ollama Basis URL",
|
||||
"Ollama Version": "Ollama Versie",
|
||||
"On": "Aan",
|
||||
"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.",
|
||||
"Open": "Open",
|
||||
"Open AI": "Open AI",
|
||||
"Open AI (Dall-E)": "",
|
||||
"Open AI (Dall-E)": "Open AI (Dall-E)",
|
||||
"Open new chat": "Open nieuwe chat",
|
||||
"OpenAI API": "OpenAI API",
|
||||
"OpenAI API Key": "",
|
||||
"OpenAI API Key is required.": "",
|
||||
"OpenAI API Key": "OpenAI API Sleutel",
|
||||
"OpenAI API Key is required.": "OpenAI API Sleutel is verplicht",
|
||||
"or": "of",
|
||||
"Parameters": "Parameters",
|
||||
"Password": "Wachtwoord",
|
||||
|
@ -273,9 +273,9 @@
|
|||
"See readme.md for instructions": "Zie readme.md voor instructies",
|
||||
"See what's new": "Zie wat er nieuw is",
|
||||
"Seed": "Seed",
|
||||
"Select a mode": "",
|
||||
"Select a mode": "Selecteer een modus",
|
||||
"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 message": "Stuur bericht",
|
||||
"Server connection verified": "Server verbinding geverifieerd",
|
||||
|
@ -330,7 +330,7 @@
|
|||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Problemen met toegang tot Ollama?",
|
||||
"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}}.",
|
||||
"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",
|
||||
|
@ -339,7 +339,7 @@
|
|||
"Upload Progress": "Upload Voortgang",
|
||||
"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 Gravatar": "",
|
||||
"Use Gravatar": "Gebruik Gravatar",
|
||||
"user": "user",
|
||||
"User Permissions": "Gebruikers Rechten",
|
||||
"Users": "Gebruikers",
|
||||
|
|
|
@ -31,6 +31,21 @@ export const getModels = async (token: string) => {
|
|||
// Helper functions
|
||||
//////////////////////////
|
||||
|
||||
export const sanitizeResponseContent = (content: string) => {
|
||||
return content
|
||||
.replace(/<\|[a-z]*$/, '')
|
||||
.replace(/<\|[a-z]+\|$/, '')
|
||||
.replace(/<$/, '')
|
||||
.replaceAll(/<\|[a-z]+\|>/g, ' ')
|
||||
.replaceAll(/<br\s?\/?>/gi, '\n')
|
||||
.replaceAll('<', '<')
|
||||
.trim();
|
||||
};
|
||||
|
||||
export const revertSanitizedResponseContent = (content: string) => {
|
||||
return content.replaceAll('<', '<');
|
||||
};
|
||||
|
||||
export const capitalizeFirstLetter = (string) => {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
let messagesContainerElement: HTMLDivElement;
|
||||
let currentRequestId = null;
|
||||
|
||||
let showModelSelector = false;
|
||||
let showModelSelector = true;
|
||||
let selectedModels = [''];
|
||||
|
||||
let selectedModelfile = null;
|
||||
|
@ -847,6 +847,7 @@
|
|||
bind:selectedModels
|
||||
bind:showModelSelector
|
||||
shareEnabled={messages.length > 0}
|
||||
{chat}
|
||||
{initNewChat}
|
||||
{tags}
|
||||
{addTag}
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
let currentRequestId = null;
|
||||
|
||||
// let chatId = $page.params.id;
|
||||
let showModelSelector = false;
|
||||
let showModelSelector = true;
|
||||
let selectedModels = [''];
|
||||
let selectedModelfile = null;
|
||||
|
||||
|
@ -865,6 +865,7 @@
|
|||
<div class="min-h-screen max-h-screen w-full flex flex-col">
|
||||
<Navbar
|
||||
{title}
|
||||
{chat}
|
||||
bind:selectedModels
|
||||
bind:showModelSelector
|
||||
shareEnabled={messages.length > 0}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
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_NAME, config, user } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
@ -56,6 +57,9 @@
|
|||
await goto('/');
|
||||
}
|
||||
loaded = true;
|
||||
if ($config?.trusted_header_auth ?? false) {
|
||||
await signInHandler();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -90,100 +94,118 @@
|
|||
</div> -->
|
||||
|
||||
<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col">
|
||||
<div class=" my-auto pb-10 w-full">
|
||||
<form
|
||||
class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" text-xl sm:text-2xl font-bold">
|
||||
{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Sign up')}
|
||||
{$i18n.t('to')}
|
||||
{$WEBUI_NAME}
|
||||
</div>
|
||||
|
||||
{#if mode === 'signup'}
|
||||
<div class=" mt-1 text-xs font-medium text-gray-500">
|
||||
ⓘ {$WEBUI_NAME}
|
||||
{$i18n.t(
|
||||
'does not make any external connections, and your data stays securely on your locally hosted server.'
|
||||
)}
|
||||
{#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">
|
||||
<form
|
||||
class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" text-xl sm:text-2xl font-bold">
|
||||
{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Sign up')}
|
||||
{$i18n.t('to')}
|
||||
{$WEBUI_NAME}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col mt-4">
|
||||
{#if mode === 'signup'}
|
||||
<div>
|
||||
<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Name')}</div>
|
||||
<div class=" mt-1 text-xs font-medium text-gray-500">
|
||||
ⓘ {$WEBUI_NAME}
|
||||
{$i18n.t(
|
||||
'does not make any external connections, and your data stays securely on your locally hosted server.'
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col mt-4">
|
||||
{#if mode === 'signup'}
|
||||
<div>
|
||||
<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Name')}</div>
|
||||
<input
|
||||
bind:value={name}
|
||||
type="text"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
autocomplete="name"
|
||||
placeholder={$i18n.t('Enter Your Full Name')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class=" my-3" />
|
||||
{/if}
|
||||
|
||||
<div class="mb-2">
|
||||
<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Email')}</div>
|
||||
<input
|
||||
bind:value={name}
|
||||
type="text"
|
||||
bind:value={email}
|
||||
type="email"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
autocomplete="name"
|
||||
placeholder={$i18n.t('Enter Your Full Name')}
|
||||
autocomplete="email"
|
||||
placeholder={$i18n.t('Enter Your Email')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class=" my-3" />
|
||||
{/if}
|
||||
|
||||
<div class="mb-2">
|
||||
<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Email')}</div>
|
||||
<input
|
||||
bind:value={email}
|
||||
type="email"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
autocomplete="email"
|
||||
placeholder={$i18n.t('Enter Your Email')}
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Password')}</div>
|
||||
<input
|
||||
bind:value={password}
|
||||
type="password"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
placeholder={$i18n.t('Enter Your Password')}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Password')}</div>
|
||||
<input
|
||||
bind:value={password}
|
||||
type="password"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
placeholder={$i18n.t('Enter Your Password')}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<button
|
||||
class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition"
|
||||
type="submit"
|
||||
>
|
||||
{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Create Account')}
|
||||
</button>
|
||||
|
||||
<div class=" mt-4 text-sm text-center">
|
||||
{mode === 'signin'
|
||||
? $i18n.t("Don't have an account?")
|
||||
: $i18n.t('Already have an account?')}
|
||||
|
||||
<div class="mt-5">
|
||||
<button
|
||||
class=" font-medium underline"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (mode === 'signin') {
|
||||
mode = 'signup';
|
||||
} else {
|
||||
mode = 'signin';
|
||||
}
|
||||
}}
|
||||
class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition"
|
||||
type="submit"
|
||||
>
|
||||
{mode === 'signin' ? $i18n.t('Sign up') : $i18n.t('Sign in')}
|
||||
{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Create Account')}
|
||||
</button>
|
||||
|
||||
<div class=" mt-4 text-sm text-center">
|
||||
{mode === 'signin'
|
||||
? $i18n.t("Don't have an account?")
|
||||
: $i18n.t('Already have an account?')}
|
||||
|
||||
<button
|
||||
class=" font-medium underline"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (mode === 'signin') {
|
||||
mode = 'signup';
|
||||
} else {
|
||||
mode = 'signin';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{mode === 'signin' ? $i18n.t('Sign up') : $i18n.t('Sign in')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
180
src/routes/s/[id]/+page.svelte
Normal file
180
src/routes/s/[id]/+page.svelte
Normal 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}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"name": "Open WebUI",
|
||||
"short_name": "Open WebUI",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#343541",
|
||||
"theme_color": "#343541",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "844x884"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue