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/), | 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). | 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 | ## [0.1.116] - 2024-03-31 | ||||||
| 
 | 
 | ||||||
| ### Added | ### Added | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ ENV OPENAI_API_BASE_URL "" | ||||||
| ENV OPENAI_API_KEY "" | ENV OPENAI_API_KEY "" | ||||||
| 
 | 
 | ||||||
| ENV WEBUI_SECRET_KEY "" | ENV WEBUI_SECRET_KEY "" | ||||||
|  | ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER "" | ||||||
| 
 | 
 | ||||||
| ENV SCARF_NO_ANALYTICS true | ENV SCARF_NO_ANALYTICS true | ||||||
| ENV DO_NOT_TRACK true | ENV DO_NOT_TRACK true | ||||||
|  |  | ||||||
|  | @ -81,6 +81,12 @@ async def check_url(request: Request, call_next): | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @app.head("/") | ||||||
|  | @app.get("/") | ||||||
|  | async def get_status(): | ||||||
|  |     return {"status": True} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @app.get("/urls") | @app.get("/urls") | ||||||
| async def get_ollama_api_urls(user=Depends(get_admin_user)): | async def get_ollama_api_urls(user=Depends(get_admin_user)): | ||||||
|     return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} |     return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ from fastapi import ( | ||||||
|     Form, |     Form, | ||||||
| ) | ) | ||||||
| from fastapi.middleware.cors import CORSMiddleware | from fastapi.middleware.cors import CORSMiddleware | ||||||
| import os, shutil, logging | import os, shutil, logging, re | ||||||
| 
 | 
 | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import List | from typing import List | ||||||
|  | @ -450,7 +450,7 @@ def store_doc( | ||||||
|     try: |     try: | ||||||
|         is_valid_filename = True |         is_valid_filename = True | ||||||
|         unsanitized_filename = file.filename |         unsanitized_filename = file.filename | ||||||
|         if not unsanitized_filename.isascii(): |         if re.search(r'[\\/:"\*\?<>|\n\t ]', unsanitized_filename) is not None: | ||||||
|             is_valid_filename = False |             is_valid_filename = False | ||||||
| 
 | 
 | ||||||
|         unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" |         unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| from peewee import * | from peewee import * | ||||||
|  | from peewee_migrate import Router | ||||||
| from config import SRC_LOG_LEVELS, DATA_DIR | from config import SRC_LOG_LEVELS, DATA_DIR | ||||||
| import os | import os | ||||||
| import logging | import logging | ||||||
|  | @ -16,4 +17,6 @@ else: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| DB = SqliteDatabase(f"{DATA_DIR}/webui.db") | DB = SqliteDatabase(f"{DATA_DIR}/webui.db") | ||||||
| DB.connect() | router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log) | ||||||
|  | router.run() | ||||||
|  | DB.connect(reuse_if_open=True) | ||||||
|  |  | ||||||
							
								
								
									
										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, |     ENABLE_SIGNUP, | ||||||
|     USER_PERMISSIONS, |     USER_PERMISSIONS, | ||||||
|     WEBHOOK_URL, |     WEBHOOK_URL, | ||||||
|  |     WEBUI_AUTH_TRUSTED_EMAIL_HEADER, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| app = FastAPI() | app = FastAPI() | ||||||
|  | @ -34,7 +35,7 @@ app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS | ||||||
| app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE | app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE | ||||||
| app.state.USER_PERMISSIONS = USER_PERMISSIONS | app.state.USER_PERMISSIONS = USER_PERMISSIONS | ||||||
| app.state.WEBHOOK_URL = WEBHOOK_URL | app.state.WEBHOOK_URL = WEBHOOK_URL | ||||||
| 
 | app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER | ||||||
| 
 | 
 | ||||||
| app.add_middleware( | app.add_middleware( | ||||||
|     CORSMiddleware, |     CORSMiddleware, | ||||||
|  |  | ||||||
|  | @ -47,6 +47,10 @@ class Token(BaseModel): | ||||||
|     token_type: str |     token_type: str | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class ApiKey(BaseModel): | ||||||
|  |     api_key: Optional[str] = None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class UserResponse(BaseModel): | class UserResponse(BaseModel): | ||||||
|     id: str |     id: str | ||||||
|     email: str |     email: str | ||||||
|  | @ -123,6 +127,28 @@ class AuthsTable: | ||||||
|         except: |         except: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|  |     def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]: | ||||||
|  |         log.info(f"authenticate_user_by_api_key: {api_key}") | ||||||
|  |         # if no api_key, return None | ||||||
|  |         if not api_key: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             user = Users.get_user_by_api_key(api_key) | ||||||
|  |             return user if user else None | ||||||
|  |         except: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]: | ||||||
|  |         log.info(f"authenticate_user_by_trusted_header: {email}") | ||||||
|  |         try: | ||||||
|  |             auth = Auth.get(Auth.email == email, Auth.active == True) | ||||||
|  |             if auth: | ||||||
|  |                 user = Users.get_user_by_id(auth.id) | ||||||
|  |                 return user | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|     def update_user_password_by_id(self, id: str, new_password: str) -> bool: |     def update_user_password_by_id(self, id: str, new_password: str) -> bool: | ||||||
|         try: |         try: | ||||||
|             query = Auth.update(password=new_password).where(Auth.id == id) |             query = Auth.update(password=new_password).where(Auth.id == id) | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ class Chat(Model): | ||||||
|     title = CharField() |     title = CharField() | ||||||
|     chat = TextField()  # Save Chat JSON as Text |     chat = TextField()  # Save Chat JSON as Text | ||||||
|     timestamp = DateField() |     timestamp = DateField() | ||||||
|  |     share_id = CharField(null=True, unique=True) | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         database = DB |         database = DB | ||||||
|  | @ -31,6 +32,7 @@ class ChatModel(BaseModel): | ||||||
|     title: str |     title: str | ||||||
|     chat: str |     chat: str | ||||||
|     timestamp: int  # timestamp in epoch |     timestamp: int  # timestamp in epoch | ||||||
|  |     share_id: Optional[str] = None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #################### | #################### | ||||||
|  | @ -52,6 +54,7 @@ class ChatResponse(BaseModel): | ||||||
|     title: str |     title: str | ||||||
|     chat: dict |     chat: dict | ||||||
|     timestamp: int  # timestamp in epoch |     timestamp: int  # timestamp in epoch | ||||||
|  |     share_id: Optional[str] = None  # id of the chat to be shared | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ChatTitleIdResponse(BaseModel): | class ChatTitleIdResponse(BaseModel): | ||||||
|  | @ -95,6 +98,71 @@ class ChatTable: | ||||||
|         except: |         except: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|  |     def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: | ||||||
|  |         # Get the existing chat to share | ||||||
|  |         chat = Chat.get(Chat.id == chat_id) | ||||||
|  |         # Check if the chat is already shared | ||||||
|  |         if chat.share_id: | ||||||
|  |             return self.get_chat_by_id_and_user_id(chat.share_id, "shared") | ||||||
|  |         # Create a new chat with the same data, but with a new ID | ||||||
|  |         shared_chat = ChatModel( | ||||||
|  |             **{ | ||||||
|  |                 "id": str(uuid.uuid4()), | ||||||
|  |                 "user_id": f"shared-{chat_id}", | ||||||
|  |                 "title": chat.title, | ||||||
|  |                 "chat": chat.chat, | ||||||
|  |                 "timestamp": int(time.time()), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         shared_result = Chat.create(**shared_chat.model_dump()) | ||||||
|  |         # Update the original chat with the share_id | ||||||
|  |         result = ( | ||||||
|  |             Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         return shared_chat if (shared_result and result) else None | ||||||
|  | 
 | ||||||
|  |     def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: | ||||||
|  |         try: | ||||||
|  |             print("update_shared_chat_by_id") | ||||||
|  |             chat = Chat.get(Chat.id == chat_id) | ||||||
|  |             print(chat) | ||||||
|  | 
 | ||||||
|  |             query = Chat.update( | ||||||
|  |                 title=chat.title, | ||||||
|  |                 chat=chat.chat, | ||||||
|  |             ).where(Chat.id == chat.share_id) | ||||||
|  | 
 | ||||||
|  |             query.execute() | ||||||
|  | 
 | ||||||
|  |             chat = Chat.get(Chat.id == chat.share_id) | ||||||
|  |             return ChatModel(**model_to_dict(chat)) | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool: | ||||||
|  |         try: | ||||||
|  |             query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}") | ||||||
|  |             query.execute()  # Remove the rows, return number of rows removed. | ||||||
|  | 
 | ||||||
|  |             return True | ||||||
|  |         except: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def update_chat_share_id_by_id( | ||||||
|  |         self, id: str, share_id: Optional[str] | ||||||
|  |     ) -> Optional[ChatModel]: | ||||||
|  |         try: | ||||||
|  |             query = Chat.update( | ||||||
|  |                 share_id=share_id, | ||||||
|  |             ).where(Chat.id == id) | ||||||
|  |             query.execute() | ||||||
|  | 
 | ||||||
|  |             chat = Chat.get(Chat.id == id) | ||||||
|  |             return ChatModel(**model_to_dict(chat)) | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|     def get_chat_lists_by_user_id( |     def get_chat_lists_by_user_id( | ||||||
|         self, user_id: str, skip: int = 0, limit: int = 50 |         self, user_id: str, skip: int = 0, limit: int = 50 | ||||||
|     ) -> List[ChatModel]: |     ) -> List[ChatModel]: | ||||||
|  | @ -131,6 +199,13 @@ class ChatTable: | ||||||
|             .order_by(Chat.timestamp.desc()) |             .order_by(Chat.timestamp.desc()) | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|  |     def get_chat_by_id(self, id: str) -> Optional[ChatModel]: | ||||||
|  |         try: | ||||||
|  |             chat = Chat.get(Chat.id == id) | ||||||
|  |             return ChatModel(**model_to_dict(chat)) | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: |     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: | ||||||
|         try: |         try: | ||||||
|             chat = Chat.get(Chat.id == id, Chat.user_id == user_id) |             chat = Chat.get(Chat.id == id, Chat.user_id == user_id) | ||||||
|  | @ -149,12 +224,15 @@ class ChatTable: | ||||||
|             query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) |             query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) | ||||||
|             query.execute()  # Remove the rows, return number of rows removed. |             query.execute()  # Remove the rows, return number of rows removed. | ||||||
| 
 | 
 | ||||||
|             return True |             return True and self.delete_shared_chat_by_chat_id(id) | ||||||
|         except: |         except: | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|     def delete_chats_by_user_id(self, user_id: str) -> bool: |     def delete_chats_by_user_id(self, user_id: str) -> bool: | ||||||
|         try: |         try: | ||||||
|  | 
 | ||||||
|  |             self.delete_shared_chats_by_user_id(user_id) | ||||||
|  | 
 | ||||||
|             query = Chat.delete().where(Chat.user_id == user_id) |             query = Chat.delete().where(Chat.user_id == user_id) | ||||||
|             query.execute()  # Remove the rows, return number of rows removed. |             query.execute()  # Remove the rows, return number of rows removed. | ||||||
| 
 | 
 | ||||||
|  | @ -162,5 +240,19 @@ class ChatTable: | ||||||
|         except: |         except: | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|  |     def delete_shared_chats_by_user_id(self, user_id: str) -> bool: | ||||||
|  |         try: | ||||||
|  |             shared_chat_ids = [ | ||||||
|  |                 f"shared-{chat.id}" | ||||||
|  |                 for chat in Chat.select().where(Chat.user_id == user_id) | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |             query = Chat.delete().where(Chat.user_id << shared_chat_ids) | ||||||
|  |             query.execute()  # Remove the rows, return number of rows removed. | ||||||
|  | 
 | ||||||
|  |             return True | ||||||
|  |         except: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| Chats = ChatTable(DB) | Chats = ChatTable(DB) | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ class User(Model): | ||||||
|     role = CharField() |     role = CharField() | ||||||
|     profile_image_url = CharField() |     profile_image_url = CharField() | ||||||
|     timestamp = DateField() |     timestamp = DateField() | ||||||
|  |     api_key = CharField(null=True, unique=True) | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         database = DB |         database = DB | ||||||
|  | @ -32,6 +33,7 @@ class UserModel(BaseModel): | ||||||
|     role: str = "pending" |     role: str = "pending" | ||||||
|     profile_image_url: str = "/user.png" |     profile_image_url: str = "/user.png" | ||||||
|     timestamp: int  # timestamp in epoch |     timestamp: int  # timestamp in epoch | ||||||
|  |     api_key: Optional[str] = None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #################### | #################### | ||||||
|  | @ -82,6 +84,13 @@ class UsersTable: | ||||||
|         except: |         except: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|  |     def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]: | ||||||
|  |         try: | ||||||
|  |             user = User.get(User.api_key == api_key) | ||||||
|  |             return UserModel(**model_to_dict(user)) | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|     def get_user_by_email(self, email: str) -> Optional[UserModel]: |     def get_user_by_email(self, email: str) -> Optional[UserModel]: | ||||||
|         try: |         try: | ||||||
|             user = User.get(User.email == email) |             user = User.get(User.email == email) | ||||||
|  | @ -149,5 +158,21 @@ class UsersTable: | ||||||
|         except: |         except: | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|  |     def update_user_api_key_by_id(self, id: str, api_key: str) -> str: | ||||||
|  |         try: | ||||||
|  |             query = User.update(api_key=api_key).where(User.id == id) | ||||||
|  |             result = query.execute() | ||||||
|  | 
 | ||||||
|  |             return True if result == 1 else False | ||||||
|  |         except: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def get_user_api_key_by_id(self, id: str) -> Optional[str]: | ||||||
|  |         try: | ||||||
|  |             user = User.get(User.id == id) | ||||||
|  |             return user.api_key | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| Users = UsersTable(DB) | Users = UsersTable(DB) | ||||||
|  |  | ||||||
|  | @ -1,13 +1,10 @@ | ||||||
| from fastapi import Response, Request | from fastapi import Request | ||||||
| from fastapi import Depends, FastAPI, HTTPException, status | from fastapi import Depends, HTTPException, status | ||||||
| from datetime import datetime, timedelta |  | ||||||
| from typing import List, Union |  | ||||||
| 
 | 
 | ||||||
| from fastapi import APIRouter, status | from fastapi import APIRouter | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel | ||||||
| import time |  | ||||||
| import uuid |  | ||||||
| import re | import re | ||||||
|  | import uuid | ||||||
| 
 | 
 | ||||||
| from apps.web.models.auths import ( | from apps.web.models.auths import ( | ||||||
|     SigninForm, |     SigninForm, | ||||||
|  | @ -17,6 +14,7 @@ from apps.web.models.auths import ( | ||||||
|     UserResponse, |     UserResponse, | ||||||
|     SigninResponse, |     SigninResponse, | ||||||
|     Auths, |     Auths, | ||||||
|  |     ApiKey, | ||||||
| ) | ) | ||||||
| from apps.web.models.users import Users | from apps.web.models.users import Users | ||||||
| 
 | 
 | ||||||
|  | @ -25,10 +23,12 @@ from utils.utils import ( | ||||||
|     get_current_user, |     get_current_user, | ||||||
|     get_admin_user, |     get_admin_user, | ||||||
|     create_token, |     create_token, | ||||||
|  |     create_api_key, | ||||||
| ) | ) | ||||||
| from utils.misc import parse_duration, validate_email_format | from utils.misc import parse_duration, validate_email_format | ||||||
| from utils.webhook import post_webhook | from utils.webhook import post_webhook | ||||||
| from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES | from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES | ||||||
|  | from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER | ||||||
| 
 | 
 | ||||||
| router = APIRouter() | router = APIRouter() | ||||||
| 
 | 
 | ||||||
|  | @ -79,6 +79,8 @@ async def update_profile( | ||||||
| async def update_password( | async def update_password( | ||||||
|     form_data: UpdatePasswordForm, session_user=Depends(get_current_user) |     form_data: UpdatePasswordForm, session_user=Depends(get_current_user) | ||||||
| ): | ): | ||||||
|  |     if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: | ||||||
|  |         raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) | ||||||
|     if session_user: |     if session_user: | ||||||
|         user = Auths.authenticate_user(session_user.email, form_data.password) |         user = Auths.authenticate_user(session_user.email, form_data.password) | ||||||
| 
 | 
 | ||||||
|  | @ -98,7 +100,22 @@ async def update_password( | ||||||
| 
 | 
 | ||||||
| @router.post("/signin", response_model=SigninResponse) | @router.post("/signin", response_model=SigninResponse) | ||||||
| async def signin(request: Request, form_data: SigninForm): | async def signin(request: Request, form_data: SigninForm): | ||||||
|  |     if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: | ||||||
|  |         if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers: | ||||||
|  |             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER) | ||||||
|  | 
 | ||||||
|  |         trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower() | ||||||
|  |         if not Users.get_user_by_email(trusted_email.lower()): | ||||||
|  |             await signup( | ||||||
|  |                 request, | ||||||
|  |                 SignupForm( | ||||||
|  |                     email=trusted_email, password=str(uuid.uuid4()), name=trusted_email | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |         user = Auths.authenticate_user_by_trusted_header(trusted_email) | ||||||
|  |     else: | ||||||
|         user = Auths.authenticate_user(form_data.email.lower(), form_data.password) |         user = Auths.authenticate_user(form_data.email.lower(), form_data.password) | ||||||
|  | 
 | ||||||
|     if user: |     if user: | ||||||
|         token = create_token( |         token = create_token( | ||||||
|             data={"id": user.id}, |             data={"id": user.id}, | ||||||
|  | @ -249,3 +266,40 @@ async def update_token_expires_duration( | ||||||
|         return request.app.state.JWT_EXPIRES_IN |         return request.app.state.JWT_EXPIRES_IN | ||||||
|     else: |     else: | ||||||
|         return request.app.state.JWT_EXPIRES_IN |         return request.app.state.JWT_EXPIRES_IN | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # API Key | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # create api key | ||||||
|  | @router.post("/api_key", response_model=ApiKey) | ||||||
|  | async def create_api_key_(user=Depends(get_current_user)): | ||||||
|  |     api_key = create_api_key() | ||||||
|  |     success = Users.update_user_api_key_by_id(user.id, api_key) | ||||||
|  |     if success: | ||||||
|  |         return { | ||||||
|  |             "api_key": api_key, | ||||||
|  |         } | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # delete api key | ||||||
|  | @router.delete("/api_key", response_model=bool) | ||||||
|  | async def delete_api_key(user=Depends(get_current_user)): | ||||||
|  |     success = Users.update_user_api_key_by_id(user.id, None) | ||||||
|  |     return success | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # get api key | ||||||
|  | @router.get("/api_key", response_model=ApiKey) | ||||||
|  | async def get_api_key(user=Depends(get_current_user)): | ||||||
|  |     api_key = Users.get_user_api_key_by_id(user.id) | ||||||
|  |     if api_key: | ||||||
|  |         return { | ||||||
|  |             "api_key": api_key, | ||||||
|  |         } | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) | ||||||
|  |  | ||||||
|  | @ -189,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_ | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | ############################ | ||||||
|  | # ShareChatById | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.post("/{id}/share", response_model=Optional[ChatResponse]) | ||||||
|  | async def share_chat_by_id(id: str, user=Depends(get_current_user)): | ||||||
|  |     chat = Chats.get_chat_by_id_and_user_id(id, user.id) | ||||||
|  |     if chat: | ||||||
|  |         if chat.share_id: | ||||||
|  |             shared_chat = Chats.update_shared_chat_by_chat_id(chat.id) | ||||||
|  |             return ChatResponse( | ||||||
|  |                 **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id) | ||||||
|  |         if not shared_chat: | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||||||
|  |                 detail=ERROR_MESSAGES.DEFAULT(), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         return ChatResponse( | ||||||
|  |             **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # DeletedSharedChatById | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.delete("/{id}/share", response_model=Optional[bool]) | ||||||
|  | async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)): | ||||||
|  |     chat = Chats.get_chat_by_id_and_user_id(id, user.id) | ||||||
|  |     if chat: | ||||||
|  |         if not chat.share_id: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |         result = Chats.delete_shared_chat_by_chat_id(id) | ||||||
|  |         update_result = Chats.update_chat_share_id_by_id(id, None) | ||||||
|  | 
 | ||||||
|  |         return result and update_result != None | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # GetSharedChatById | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.get("/share/{share_id}", response_model=Optional[ChatResponse]) | ||||||
|  | async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): | ||||||
|  |     chat = Chats.get_chat_by_id(share_id) | ||||||
|  | 
 | ||||||
|  |     if chat: | ||||||
|  |         return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| ############################ | ############################ | ||||||
| # GetChatTagsById | # GetChatTagsById | ||||||
| ############################ | ############################ | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ from starlette.responses import StreamingResponse, FileResponse | ||||||
| 
 | 
 | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | import markdown | ||||||
| import requests | import requests | ||||||
| import os | import os | ||||||
| import aiohttp | import aiohttp | ||||||
|  | @ -28,6 +30,17 @@ async def get_gravatar( | ||||||
|     return get_gravatar_url(email) |     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") | @router.get("/db/download") | ||||||
| async def download_db(user=Depends(get_admin_user)): | async def download_db(user=Depends(get_admin_user)): | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,8 +25,9 @@ try: | ||||||
| except ImportError: | except ImportError: | ||||||
|     log.warning("dotenv not installed, skipping...") |     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" | WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" | ||||||
|  | 
 | ||||||
| shutil.copyfile("../build/favicon.png", "./static/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", "") | CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") | ||||||
|  | 
 | ||||||
| if CUSTOM_NAME: | if CUSTOM_NAME: | ||||||
|     try: |     try: | ||||||
|         r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") |         r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") | ||||||
|  | @ -171,7 +173,9 @@ if CUSTOM_NAME: | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         log.exception(e) |         log.exception(e) | ||||||
|         pass |         pass | ||||||
| 
 | else: | ||||||
|  |     if WEBUI_NAME != "Open WebUI": | ||||||
|  |         WEBUI_NAME += " (Open WebUI)" | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # DATA/FRONTEND BUILD DIR | # 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 = True | ||||||
|  | WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get( | ||||||
|  |     "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # WEBUI_SECRET_KEY | # WEBUI_SECRET_KEY | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." |     ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." | ||||||
|     CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." |     CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." | ||||||
|     DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot." |     DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot." | ||||||
|  |     EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again." | ||||||
|     EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." |     EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." | ||||||
|     USERNAME_TAKEN = ( |     USERNAME_TAKEN = ( | ||||||
|         "Uh-oh! This username is already registered. Please choose another username." |         "Uh-oh! This username is already registered. Please choose another username." | ||||||
|  | @ -36,6 +37,7 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     INVALID_PASSWORD = ( |     INVALID_PASSWORD = ( | ||||||
|         "The password provided is incorrect. Please check for typos and try again." |         "The password provided is incorrect. Please check for typos and try again." | ||||||
|     ) |     ) | ||||||
|  |     INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance." | ||||||
|     UNAUTHORIZED = "401 Unauthorized" |     UNAUTHORIZED = "401 Unauthorized" | ||||||
|     ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." |     ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." | ||||||
|     ACTION_PROHIBITED = ( |     ACTION_PROHIBITED = ( | ||||||
|  | @ -58,7 +60,8 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     RATE_LIMIT_EXCEEDED = "API rate limit exceeded" |     RATE_LIMIT_EXCEEDED = "API rate limit exceeded" | ||||||
| 
 | 
 | ||||||
|     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" |     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" | ||||||
|     OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" |     OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found" | ||||||
|     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" |     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" | ||||||
|  |     CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance." | ||||||
| 
 | 
 | ||||||
|     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." |     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." | ||||||
|  |  | ||||||
|  | @ -62,6 +62,21 @@ class SPAStaticFiles(StaticFiles): | ||||||
|                 raise ex |                 raise ex | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | print( | ||||||
|  |     f""" | ||||||
|  |   ___                    __        __   _     _   _ ___  | ||||||
|  |  / _ \ _ __   ___ _ __   \ \      / /__| |__ | | | |_ _| | ||||||
|  | | | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || |  | ||||||
|  | | |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || |  | ||||||
|  |  \___/| .__/ \___|_| |_|    \_/\_/ \___|_.__/ \___/|___| | ||||||
|  |       |_|                                                | ||||||
|  | 
 | ||||||
|  |        | ||||||
|  | v{VERSION} - building the best open-source AI user interface.       | ||||||
|  | https://github.com/open-webui/open-webui | ||||||
|  | """ | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) | app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) | ||||||
| 
 | 
 | ||||||
| app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED | app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED | ||||||
|  | @ -69,7 +84,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST | ||||||
| 
 | 
 | ||||||
| app.state.WEBHOOK_URL = WEBHOOK_URL | app.state.WEBHOOK_URL = WEBHOOK_URL | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| origins = ["*"] | origins = ["*"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -179,6 +193,7 @@ async def get_app_config(): | ||||||
|         "images": images_app.state.ENABLED, |         "images": images_app.state.ENABLED, | ||||||
|         "default_models": webui_app.state.DEFAULT_MODELS, |         "default_models": webui_app.state.DEFAULT_MODELS, | ||||||
|         "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, |         "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, | ||||||
|  |         "trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER), | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -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("/static", StaticFiles(directory="static"), name="static") | ||||||
| app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") | app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,9 +14,12 @@ uuid | ||||||
| requests | requests | ||||||
| aiohttp | aiohttp | ||||||
| peewee | peewee | ||||||
|  | peewee-migrate | ||||||
| bcrypt | bcrypt | ||||||
| 
 | 
 | ||||||
| litellm==1.30.7 | litellm==1.30.7 | ||||||
|  | boto3 | ||||||
|  | 
 | ||||||
| argon2-cffi | argon2-cffi | ||||||
| apscheduler | apscheduler | ||||||
| google-generativeai | google-generativeai | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | ||||||
| from fastapi import HTTPException, status, Depends | from fastapi import HTTPException, status, Depends | ||||||
|  | 
 | ||||||
| from apps.web.models.users import Users | from apps.web.models.users import Users | ||||||
|  | 
 | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel | ||||||
| from typing import Union, Optional | from typing import Union, Optional | ||||||
| from constants import ERROR_MESSAGES | from constants import ERROR_MESSAGES | ||||||
|  | @ -8,6 +10,7 @@ from passlib.context import CryptContext | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| import requests | import requests | ||||||
| import jwt | import jwt | ||||||
|  | import uuid | ||||||
| import logging | import logging | ||||||
| import config | import config | ||||||
| 
 | 
 | ||||||
|  | @ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str): | ||||||
|     return auth_header[len("Bearer ") :] |     return auth_header[len("Bearer ") :] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def create_api_key(): | ||||||
|  |     key = str(uuid.uuid4()).replace("-", "") | ||||||
|  |     return f"sk-{key}" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def get_http_authorization_cred(auth_header: str): | def get_http_authorization_cred(auth_header: str): | ||||||
|     try: |     try: | ||||||
|         scheme, credentials = auth_header.split(" ") |         scheme, credentials = auth_header.split(" ") | ||||||
|  | @ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str): | ||||||
| def get_current_user( | def get_current_user( | ||||||
|     auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), |     auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), | ||||||
| ): | ): | ||||||
|  |     # auth by api key | ||||||
|  |     if auth_token.credentials.startswith("sk-"): | ||||||
|  |         return get_current_user_by_api_key(auth_token.credentials) | ||||||
|  |     # auth by jwt token | ||||||
|     data = decode_token(auth_token.credentials) |     data = decode_token(auth_token.credentials) | ||||||
|     if data != None and "id" in data: |     if data != None and "id" in data: | ||||||
|         user = Users.get_user_by_id(data["id"]) |         user = Users.get_user_by_id(data["id"]) | ||||||
|  | @ -85,6 +97,16 @@ def get_current_user( | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def get_current_user_by_api_key(api_key: str): | ||||||
|  |     user = Users.get_user_by_api_key(api_key) | ||||||
|  |     if user is None: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.INVALID_TOKEN, | ||||||
|  |         ) | ||||||
|  |     return user | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def get_verified_user(user=Depends(get_current_user)): | def get_verified_user(user=Depends(get_current_user)): | ||||||
|     if user.role not in {"user", "admin"}: |     if user.role not in {"user", "admin"}: | ||||||
|         raise HTTPException( |         raise HTTPException( | ||||||
|  |  | ||||||
							
								
								
									
										200
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										200
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -1,12 +1,12 @@ | ||||||
| { | { | ||||||
| 	"name": "open-webui", | 	"name": "open-webui", | ||||||
| 	"version": "0.1.116", | 	"version": "0.1.117", | ||||||
| 	"lockfileVersion": 3, | 	"lockfileVersion": 3, | ||||||
| 	"requires": true, | 	"requires": true, | ||||||
| 	"packages": { | 	"packages": { | ||||||
| 		"": { | 		"": { | ||||||
| 			"name": "open-webui", | 			"name": "open-webui", | ||||||
| 			"version": "0.1.116", | 			"version": "0.1.117", | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"@sveltejs/adapter-node": "^1.3.1", | 				"@sveltejs/adapter-node": "^1.3.1", | ||||||
| 				"async": "^3.2.5", | 				"async": "^3.2.5", | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
| 				"i18next-resources-to-backend": "^1.2.0", | 				"i18next-resources-to-backend": "^1.2.0", | ||||||
| 				"idb": "^7.1.1", | 				"idb": "^7.1.1", | ||||||
| 				"js-sha256": "^0.10.1", | 				"js-sha256": "^0.10.1", | ||||||
|  | 				"jspdf": "^2.5.1", | ||||||
| 				"katex": "^0.16.9", | 				"katex": "^0.16.9", | ||||||
| 				"marked": "^9.1.0", | 				"marked": "^9.1.0", | ||||||
| 				"svelte-sonner": "^0.3.19", | 				"svelte-sonner": "^0.3.19", | ||||||
|  | @ -1067,6 +1068,12 @@ | ||||||
| 			"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", | 			"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", | ||||||
| 			"dev": true | 			"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": { | 		"node_modules/@types/resolve": { | ||||||
| 			"version": "1.20.2", | 			"version": "1.20.2", | ||||||
| 			"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", | 			"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", | 			"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", | ||||||
| 			"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" | 			"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": { | 		"node_modules/autoprefixer": { | ||||||
| 			"version": "10.4.19", | 			"version": "10.4.19", | ||||||
| 			"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", | 			"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", | ||||||
|  | @ -1459,6 +1477,15 @@ | ||||||
| 			"dev": true, | 			"dev": true, | ||||||
| 			"optional": 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": { | 		"node_modules/base64-js": { | ||||||
| 			"version": "1.5.1", | 			"version": "1.5.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", | 			"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": "^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": { | 		"node_modules/buffer": { | ||||||
| 			"version": "6.0.3", | 			"version": "6.0.3", | ||||||
| 			"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", | 			"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": { | 		"node_modules/chalk": { | ||||||
| 			"version": "4.1.2", | 			"version": "4.1.2", | ||||||
| 			"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", | 			"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", | ||||||
|  | @ -1944,6 +2007,17 @@ | ||||||
| 				"node": ">= 0.6" | 				"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": { | 		"node_modules/core-util-is": { | ||||||
| 			"version": "1.0.3", | 			"version": "1.0.3", | ||||||
| 			"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", | 			"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", | ||||||
|  | @ -1964,6 +2038,15 @@ | ||||||
| 				"node": ">= 8" | 				"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": { | 		"node_modules/css-select": { | ||||||
| 			"version": "5.1.0", | 			"version": "5.1.0", | ||||||
| 			"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", | 			"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" | 				"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": { | 		"node_modules/domutils": { | ||||||
| 			"version": "3.1.0", | 			"version": "3.1.0", | ||||||
| 			"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", | 			"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", | ||||||
|  | @ -2584,6 +2673,11 @@ | ||||||
| 				"reusify": "^1.0.4" | 				"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": { | 		"node_modules/file-entry-cache": { | ||||||
| 			"version": "6.0.1", | 			"version": "6.0.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", | 			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", | ||||||
|  | @ -3003,6 +3097,19 @@ | ||||||
| 				"node": ">=12.0.0" | 				"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": { | 		"node_modules/htmlparser2": { | ||||||
| 			"version": "8.0.2", | 			"version": "8.0.2", | ||||||
| 			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", | 			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", | ||||||
|  | @ -3403,10 +3510,27 @@ | ||||||
| 				"graceful-fs": "^4.1.6" | 				"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": { | 		"node_modules/katex": { | ||||||
| 			"version": "0.16.9", | 			"version": "0.16.10", | ||||||
| 			"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", | 			"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", | ||||||
| 			"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", | 			"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", | ||||||
| 			"funding": [ | 			"funding": [ | ||||||
| 				"https://opencollective.com/katex", | 				"https://opencollective.com/katex", | ||||||
| 				"https://github.com/sponsors/katex" | 				"https://github.com/sponsors/katex" | ||||||
|  | @ -3971,6 +4095,12 @@ | ||||||
| 				"node": ">=8" | 				"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": { | 		"node_modules/periscopic": { | ||||||
| 			"version": "3.1.0", | 			"version": "3.1.0", | ||||||
| 			"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", | 			"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", | ||||||
|  | @ -4391,6 +4521,15 @@ | ||||||
| 				"rimraf": "bin.js" | 				"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": { | 		"node_modules/read-cache": { | ||||||
| 			"version": "1.0.0", | 			"version": "1.0.0", | ||||||
| 			"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", | 			"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", | ||||||
|  | @ -4494,6 +4633,15 @@ | ||||||
| 				"node": ">=0.10.0" | 				"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": { | 		"node_modules/rimraf": { | ||||||
| 			"version": "3.0.2", | 			"version": "3.0.2", | ||||||
| 			"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", | 			"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", | ||||||
|  | @ -4814,6 +4962,15 @@ | ||||||
| 			"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", | 			"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", | ||||||
| 			"dev": true | 			"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": { | 		"node_modules/stream-composer": { | ||||||
| 			"version": "1.0.2", | 			"version": "1.0.2", | ||||||
| 			"resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", | 			"resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", | ||||||
|  | @ -5215,6 +5372,15 @@ | ||||||
| 				"@types/estree": "*" | 				"@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": { | 		"node_modules/symlink-or-copy": { | ||||||
| 			"version": "1.3.1", | 			"version": "1.3.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", | 			"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", | ||||||
|  | @ -5353,6 +5519,15 @@ | ||||||
| 				"streamx": "^2.12.5" | 				"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": { | 		"node_modules/text-table": { | ||||||
| 			"version": "0.2.0", | 			"version": "0.2.0", | ||||||
| 			"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", | 			"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", | ||||||
|  | @ -5583,6 +5758,15 @@ | ||||||
| 			"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | 			"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | ||||||
| 			"dev": true | 			"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": { | 		"node_modules/uuid": { | ||||||
| 			"version": "9.0.1", | 			"version": "9.0.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", | 			"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", | ||||||
|  | @ -5676,9 +5860,9 @@ | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		"node_modules/vite": { | 		"node_modules/vite": { | ||||||
| 			"version": "4.5.2", | 			"version": "4.5.3", | ||||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", | 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", | ||||||
| 			"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", | 			"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"esbuild": "^0.18.10", | 				"esbuild": "^0.18.10", | ||||||
| 				"postcss": "^8.4.27", | 				"postcss": "^8.4.27", | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
| 	"name": "open-webui", | 	"name": "open-webui", | ||||||
| 	"version": "0.1.116", | 	"version": "0.1.117", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"dev": "vite dev --host", | 		"dev": "vite dev --host", | ||||||
|  | @ -53,6 +53,7 @@ | ||||||
| 		"i18next-resources-to-backend": "^1.2.0", | 		"i18next-resources-to-backend": "^1.2.0", | ||||||
| 		"idb": "^7.1.1", | 		"idb": "^7.1.1", | ||||||
| 		"js-sha256": "^0.10.1", | 		"js-sha256": "^0.10.1", | ||||||
|  | 		"jspdf": "^2.5.1", | ||||||
| 		"katex": "^0.16.9", | 		"katex": "^0.16.9", | ||||||
| 		"marked": "^9.1.0", | 		"marked": "^9.1.0", | ||||||
| 		"svelte-sonner": "^0.3.19", | 		"svelte-sonner": "^0.3.19", | ||||||
|  |  | ||||||
|  | @ -318,3 +318,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string) | ||||||
| 
 | 
 | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const createAPIKey = async (token: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 	return res.api_key; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getAPIKey = async (token: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 	return res.api_key; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const deleteAPIKey = async (token: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { | ||||||
|  | 		method: 'DELETE', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => { | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const getChatByShareId = async (token: string, share_id: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, { | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err; | ||||||
|  | 
 | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const shareChatById = async (token: string, id: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err; | ||||||
|  | 
 | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const deleteSharedChatById = async (token: string, id: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { | ||||||
|  | 		method: 'DELETE', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err; | ||||||
|  | 
 | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const updateChatById = async (token: string, id: string, chat: object) => { | export const updateChatById = async (token: string, id: string, chat: object) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -22,6 +22,31 @@ export const getGravatarUrl = async (email: string) => { | ||||||
| 	return res; | 	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) => { | export const downloadDatabase = async (token: string) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
| 	export let chatId = ''; | 	export let chatId = ''; | ||||||
|  | 	export let readOnly = false; | ||||||
| 	export let sendPrompt: Function; | 	export let sendPrompt: Function; | ||||||
| 	export let continueGeneration: Function; | 	export let continueGeneration: Function; | ||||||
| 	export let regenerateResponse: Function; | 	export let regenerateResponse: Function; | ||||||
|  | @ -317,6 +318,7 @@ | ||||||
| 							<UserMessage | 							<UserMessage | ||||||
| 								on:delete={() => messageDeleteHandler(message.id)} | 								on:delete={() => messageDeleteHandler(message.id)} | ||||||
| 								user={$user} | 								user={$user} | ||||||
|  | 								{readOnly} | ||||||
| 								{message} | 								{message} | ||||||
| 								isFirstMessage={messageIdx === 0} | 								isFirstMessage={messageIdx === 0} | ||||||
| 								siblings={message.parentId !== null | 								siblings={message.parentId !== null | ||||||
|  | @ -335,6 +337,7 @@ | ||||||
| 								modelfiles={selectedModelfiles} | 								modelfiles={selectedModelfiles} | ||||||
| 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | ||||||
| 								isLastMessage={messageIdx + 1 === messages.length} | 								isLastMessage={messageIdx + 1 === messages.length} | ||||||
|  | 								{readOnly} | ||||||
| 								{confirmEditResponseMessage} | 								{confirmEditResponseMessage} | ||||||
| 								{showPreviousMessage} | 								{showPreviousMessage} | ||||||
| 								{showNextMessage} | 								{showNextMessage} | ||||||
|  |  | ||||||
|  | @ -17,7 +17,11 @@ | ||||||
| 	import { config, settings } from '$lib/stores'; | 	import { config, settings } from '$lib/stores'; | ||||||
| 	import { synthesizeOpenAISpeech } from '$lib/apis/openai'; | 	import { synthesizeOpenAISpeech } from '$lib/apis/openai'; | ||||||
| 	import { imageGenerations } from '$lib/apis/images'; | 	import { imageGenerations } from '$lib/apis/images'; | ||||||
| 	import { extractSentences } from '$lib/utils'; | 	import { | ||||||
|  | 		extractSentences, | ||||||
|  | 		revertSanitizedResponseContent, | ||||||
|  | 		sanitizeResponseContent | ||||||
|  | 	} from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
| 	import Name from './Name.svelte'; | 	import Name from './Name.svelte'; | ||||||
| 	import ProfileImage from './ProfileImage.svelte'; | 	import ProfileImage from './ProfileImage.svelte'; | ||||||
|  | @ -33,6 +37,8 @@ | ||||||
| 
 | 
 | ||||||
| 	export let isLastMessage = true; | 	export let isLastMessage = true; | ||||||
| 
 | 
 | ||||||
|  | 	export let readOnly = false; | ||||||
|  | 
 | ||||||
| 	export let confirmEditResponseMessage: Function; | 	export let confirmEditResponseMessage: Function; | ||||||
| 	export let showPreviousMessage: Function; | 	export let showPreviousMessage: Function; | ||||||
| 	export let showNextMessage: Function; | 	export let showNextMessage: Function; | ||||||
|  | @ -54,7 +60,7 @@ | ||||||
| 	let loadingSpeech = false; | 	let loadingSpeech = false; | ||||||
| 	let generatingImage = false; | 	let generatingImage = false; | ||||||
| 
 | 
 | ||||||
| 	$: tokens = marked.lexer(message.content); | 	$: tokens = marked.lexer(sanitizeResponseContent(message.content)); | ||||||
| 
 | 
 | ||||||
| 	const renderer = new marked.Renderer(); | 	const renderer = new marked.Renderer(); | ||||||
| 
 | 
 | ||||||
|  | @ -128,7 +134,7 @@ | ||||||
| 				// • auto-render specific keys, e.g.: | 				// • auto-render specific keys, e.g.: | ||||||
| 				delimiters: [ | 				delimiters: [ | ||||||
| 					{ left: '$$', right: '$$', display: false }, | 					{ left: '$$', right: '$$', display: false }, | ||||||
| 					{ left: '$', right: '$', display: false }, | 					{ left: '$ ', right: ' $', display: false }, | ||||||
| 					{ left: '\\(', right: '\\)', display: false }, | 					{ left: '\\(', right: '\\)', display: false }, | ||||||
| 					{ left: '\\[', right: '\\]', display: false }, | 					{ left: '\\[', right: '\\]', display: false }, | ||||||
| 					{ left: '[ ', right: ' ]', display: false } | 					{ left: '[ ', right: ' ]', display: false } | ||||||
|  | @ -403,8 +409,10 @@ | ||||||
| 								{:else} | 								{:else} | ||||||
| 									{#each tokens as token} | 									{#each tokens as token} | ||||||
| 										{#if token.type === 'code'} | 										{#if token.type === 'code'} | ||||||
| 											<!-- {token.text} --> | 											<CodeBlock | ||||||
| 											<CodeBlock lang={token.lang} code={token.text} /> | 												lang={token.lang} | ||||||
|  | 												code={revertSanitizedResponseContent(token.text)} | ||||||
|  | 											/> | ||||||
| 										{:else} | 										{:else} | ||||||
| 											{@html marked.parse(token.raw, { | 											{@html marked.parse(token.raw, { | ||||||
| 												...defaults, | 												...defaults, | ||||||
|  | @ -422,7 +430,7 @@ | ||||||
| 										class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" | 										class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" | ||||||
| 									> | 									> | ||||||
| 										{#if siblings.length > 1} | 										{#if siblings.length > 1} | ||||||
| 											<div class="flex self-center min-w-fit -mt-1"> | 											<div class="flex self-center min-w-fit"> | ||||||
| 												<button | 												<button | ||||||
| 													class="self-center dark:hover:text-white hover:text-black transition" | 													class="self-center dark:hover:text-white hover:text-black transition" | ||||||
| 													on:click={() => { | 													on:click={() => { | ||||||
|  | @ -469,6 +477,7 @@ | ||||||
| 											</div> | 											</div> | ||||||
| 										{/if} | 										{/if} | ||||||
| 
 | 
 | ||||||
|  | 										{#if !readOnly} | ||||||
| 											<Tooltip content="Edit" placement="bottom"> | 											<Tooltip content="Edit" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
|  | @ -494,6 +503,7 @@ | ||||||
| 													</svg> | 													</svg> | ||||||
| 												</button> | 												</button> | ||||||
| 											</Tooltip> | 											</Tooltip> | ||||||
|  | 										{/if} | ||||||
| 
 | 
 | ||||||
| 										<Tooltip content="Copy" placement="bottom"> | 										<Tooltip content="Copy" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
|  | @ -521,6 +531,7 @@ | ||||||
| 											</button> | 											</button> | ||||||
| 										</Tooltip> | 										</Tooltip> | ||||||
| 
 | 
 | ||||||
|  | 										{#if !readOnly} | ||||||
| 											<Tooltip content="Good Response" placement="bottom"> | 											<Tooltip content="Good Response" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
|  | @ -574,6 +585,7 @@ | ||||||
| 													> | 													> | ||||||
| 												</button> | 												</button> | ||||||
| 											</Tooltip> | 											</Tooltip> | ||||||
|  | 										{/if} | ||||||
| 
 | 
 | ||||||
| 										<Tooltip content="Read Aloud" placement="bottom"> | 										<Tooltip content="Read Aloud" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
|  | @ -656,7 +668,7 @@ | ||||||
| 											</button> | 											</button> | ||||||
| 										</Tooltip> | 										</Tooltip> | ||||||
| 
 | 
 | ||||||
| 										{#if $config.images} | 										{#if $config.images && !readOnly} | ||||||
| 											<Tooltip content="Generate Image" placement="bottom"> | 											<Tooltip content="Generate Image" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
|  | @ -752,7 +764,7 @@ | ||||||
| 											</Tooltip> | 											</Tooltip> | ||||||
| 										{/if} | 										{/if} | ||||||
| 
 | 
 | ||||||
| 										{#if isLastMessage} | 										{#if isLastMessage && !readOnly} | ||||||
| 											<Tooltip content="Continue Response" placement="bottom"> | 											<Tooltip content="Continue Response" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													type="button" | 													type="button" | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
| 	export let message; | 	export let message; | ||||||
| 	export let siblings; | 	export let siblings; | ||||||
| 	export let isFirstMessage: boolean; | 	export let isFirstMessage: boolean; | ||||||
|  | 	export let readOnly: boolean; | ||||||
| 
 | 
 | ||||||
| 	export let confirmEditMessage: Function; | 	export let confirmEditMessage: Function; | ||||||
| 	export let showPreviousMessage: Function; | 	export let showPreviousMessage: Function; | ||||||
|  | @ -203,7 +204,7 @@ | ||||||
| 
 | 
 | ||||||
| 					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500"> | 					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500"> | ||||||
| 						{#if siblings.length > 1} | 						{#if siblings.length > 1} | ||||||
| 							<div class="flex self-center -mt-1"> | 							<div class="flex self-center"> | ||||||
| 								<button | 								<button | ||||||
| 									class="self-center dark:hover:text-white hover:text-black transition" | 									class="self-center dark:hover:text-white hover:text-black transition" | ||||||
| 									on:click={() => { | 									on:click={() => { | ||||||
|  | @ -250,6 +251,7 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 						{/if} | 						{/if} | ||||||
| 
 | 
 | ||||||
|  | 						{#if !readOnly} | ||||||
| 							<Tooltip content="Edit" placement="bottom"> | 							<Tooltip content="Edit" placement="bottom"> | ||||||
| 								<button | 								<button | ||||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" | 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" | ||||||
|  | @ -273,6 +275,7 @@ | ||||||
| 									</svg> | 									</svg> | ||||||
| 								</button> | 								</button> | ||||||
| 							</Tooltip> | 							</Tooltip> | ||||||
|  | 						{/if} | ||||||
| 
 | 
 | ||||||
| 						<Tooltip content="Copy" placement="bottom"> | 						<Tooltip content="Copy" placement="bottom"> | ||||||
| 							<button | 							<button | ||||||
|  | @ -298,7 +301,7 @@ | ||||||
| 							</button> | 							</button> | ||||||
| 						</Tooltip> | 						</Tooltip> | ||||||
| 
 | 
 | ||||||
| 						{#if !isFirstMessage} | 						{#if !isFirstMessage && !readOnly} | ||||||
| 							<Tooltip content="Delete" placement="bottom"> | 							<Tooltip content="Delete" placement="bottom"> | ||||||
| 								<button | 								<button | ||||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | ||||||
|  |  | ||||||
|  | @ -6,6 +6,8 @@ | ||||||
| 	import { compareVersion } from '$lib/utils'; | 	import { compareVersion } from '$lib/utils'; | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 
 | 
 | ||||||
|  | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
|  | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
| 	let ollamaVersion = ''; | 	let ollamaVersion = ''; | ||||||
|  | @ -51,8 +53,10 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="flex w-full justify-between items-center"> | 			<div class="flex w-full justify-between items-center"> | ||||||
| 				<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200"> | 				<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200"> | ||||||
| 					<div> | 					<div class="flex gap-1"> | ||||||
|  | 						<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}> | ||||||
| 							v{WEBUI_VERSION} | 							v{WEBUI_VERSION} | ||||||
|  | 						</Tooltip> | ||||||
| 
 | 
 | ||||||
| 						<a | 						<a | ||||||
| 							href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}" | 							href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}" | ||||||
|  | @ -126,7 +130,9 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | 		<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 | 			<a | ||||||
| 				class=" text-gray-500 dark:text-gray-300 font-medium" | 				class=" text-gray-500 dark:text-gray-300 font-medium" | ||||||
| 				href="https://github.com/tjbck" | 				href="https://github.com/tjbck" | ||||||
|  |  | ||||||
|  | @ -3,11 +3,13 @@ | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	import { user } from '$lib/stores'; | 	import { user } from '$lib/stores'; | ||||||
| 	import { updateUserProfile } from '$lib/apis/auths'; | 	import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths'; | ||||||
| 
 | 
 | ||||||
| 	import UpdatePassword from './Account/UpdatePassword.svelte'; | 	import UpdatePassword from './Account/UpdatePassword.svelte'; | ||||||
| 	import { getGravatarUrl } from '$lib/apis/utils'; | 	import { getGravatarUrl } from '$lib/apis/utils'; | ||||||
| 	import { copyToClipboard } from '$lib/utils'; | 	import { copyToClipboard } from '$lib/utils'; | ||||||
|  | 	import Plus from '$lib/components/icons/Plus.svelte'; | ||||||
|  | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
|  | @ -15,8 +17,14 @@ | ||||||
| 
 | 
 | ||||||
| 	let profileImageUrl = ''; | 	let profileImageUrl = ''; | ||||||
| 	let name = ''; | 	let name = ''; | ||||||
|  | 
 | ||||||
| 	let showJWTToken = false; | 	let showJWTToken = false; | ||||||
| 	let JWTTokenCopied = false; | 	let JWTTokenCopied = false; | ||||||
|  | 
 | ||||||
|  | 	let APIKey = ''; | ||||||
|  | 	let showAPIKey = false; | ||||||
|  | 	let APIKeyCopied = false; | ||||||
|  | 
 | ||||||
| 	let profileImageInputElement: HTMLInputElement; | 	let profileImageInputElement: HTMLInputElement; | ||||||
| 
 | 
 | ||||||
| 	const submitHandler = async () => { | 	const submitHandler = async () => { | ||||||
|  | @ -33,9 +41,23 @@ | ||||||
| 		return false; | 		return false; | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	onMount(() => { | 	const createAPIKeyHandler = async () => { | ||||||
|  | 		APIKey = await createAPIKey(localStorage.token); | ||||||
|  | 		if (APIKey) { | ||||||
|  | 			toast.success($i18n.t('API Key created.')); | ||||||
|  | 		} else { | ||||||
|  | 			toast.error($i18n.t('Failed to create API Key.')); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	onMount(async () => { | ||||||
| 		name = $user.name; | 		name = $user.name; | ||||||
| 		profileImageUrl = $user.profile_image_url; | 		profileImageUrl = $user.profile_image_url; | ||||||
|  | 
 | ||||||
|  | 		APIKey = await getAPIKey(localStorage.token).catch((error) => { | ||||||
|  | 			console.log(error); | ||||||
|  | 			return ''; | ||||||
|  | 		}); | ||||||
| 	}); | 	}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -170,22 +192,23 @@ | ||||||
| 
 | 
 | ||||||
| 		<hr class=" dark:border-gray-700 my-4" /> | 		<hr class=" dark:border-gray-700 my-4" /> | ||||||
| 
 | 
 | ||||||
| 		<div class=" w-full justify-between"> | 		<div class="flex flex-col gap-4"> | ||||||
| 			<div class="flex w-full justify-between"> | 			<div class="justify-between w-full"> | ||||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> | 				<div class="flex justify-between w-full"> | ||||||
|  | 					<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div class="flex mt-2"> | 				<div class="flex mt-2"> | ||||||
| 					<div class="flex w-full"> | 					<div class="flex w-full"> | ||||||
| 						<input | 						<input | ||||||
| 						class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | 							class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||||
| 							type={showJWTToken ? 'text' : 'password'} | 							type={showJWTToken ? 'text' : 'password'} | ||||||
| 							value={localStorage.token} | 							value={localStorage.token} | ||||||
| 							disabled | 							disabled | ||||||
| 						/> | 						/> | ||||||
| 
 | 
 | ||||||
| 						<button | 						<button | ||||||
| 						class="dark:bg-gray-800 px-2 transition rounded-r-lg" | 							class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" | ||||||
| 							on:click={() => { | 							on:click={() => { | ||||||
| 								showJWTToken = !showJWTToken; | 								showJWTToken = !showJWTToken; | ||||||
| 							}} | 							}} | ||||||
|  | @ -225,7 +248,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 
 | 
 | ||||||
| 					<button | 					<button | ||||||
| 					class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg" | 						class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" | ||||||
| 						on:click={() => { | 						on:click={() => { | ||||||
| 							copyToClipboard(localStorage.token); | 							copyToClipboard(localStorage.token); | ||||||
| 							JWTTokenCopied = true; | 							JWTTokenCopied = true; | ||||||
|  | @ -269,6 +292,143 @@ | ||||||
| 					</button> | 					</button> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  | 			<div class="justify-between w-full"> | ||||||
|  | 				<div class="flex justify-between w-full"> | ||||||
|  | 					<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="flex mt-2"> | ||||||
|  | 					{#if APIKey} | ||||||
|  | 						<div class="flex w-full"> | ||||||
|  | 							<input | ||||||
|  | 								class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||||
|  | 								type={showAPIKey ? 'text' : 'password'} | ||||||
|  | 								value={APIKey} | ||||||
|  | 								disabled | ||||||
|  | 							/> | ||||||
|  | 
 | ||||||
|  | 							<button | ||||||
|  | 								class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" | ||||||
|  | 								on:click={() => { | ||||||
|  | 									showAPIKey = !showAPIKey; | ||||||
|  | 								}} | ||||||
|  | 							> | ||||||
|  | 								{#if showAPIKey} | ||||||
|  | 									<svg | ||||||
|  | 										xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 										viewBox="0 0 16 16" | ||||||
|  | 										fill="currentColor" | ||||||
|  | 										class="w-4 h-4" | ||||||
|  | 									> | ||||||
|  | 										<path | ||||||
|  | 											fill-rule="evenodd" | ||||||
|  | 											d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z" | ||||||
|  | 											clip-rule="evenodd" | ||||||
|  | 										/> | ||||||
|  | 										<path | ||||||
|  | 											d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z" | ||||||
|  | 										/> | ||||||
|  | 									</svg> | ||||||
|  | 								{:else} | ||||||
|  | 									<svg | ||||||
|  | 										xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 										viewBox="0 0 16 16" | ||||||
|  | 										fill="currentColor" | ||||||
|  | 										class="w-4 h-4" | ||||||
|  | 									> | ||||||
|  | 										<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /> | ||||||
|  | 										<path | ||||||
|  | 											fill-rule="evenodd" | ||||||
|  | 											d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" | ||||||
|  | 											clip-rule="evenodd" | ||||||
|  | 										/> | ||||||
|  | 									</svg> | ||||||
|  | 								{/if} | ||||||
|  | 							</button> | ||||||
|  | 						</div> | ||||||
|  | 
 | ||||||
|  | 						<button | ||||||
|  | 							class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" | ||||||
|  | 							on:click={() => { | ||||||
|  | 								copyToClipboard(APIKey); | ||||||
|  | 								APIKeyCopied = true; | ||||||
|  | 								setTimeout(() => { | ||||||
|  | 									APIKeyCopied = false; | ||||||
|  | 								}, 2000); | ||||||
|  | 							}} | ||||||
|  | 						> | ||||||
|  | 							{#if APIKeyCopied} | ||||||
|  | 								<svg | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									viewBox="0 0 20 20" | ||||||
|  | 									fill="currentColor" | ||||||
|  | 									class="w-4 h-4" | ||||||
|  | 								> | ||||||
|  | 									<path | ||||||
|  | 										fill-rule="evenodd" | ||||||
|  | 										d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" | ||||||
|  | 										clip-rule="evenodd" | ||||||
|  | 									/> | ||||||
|  | 								</svg> | ||||||
|  | 							{:else} | ||||||
|  | 								<svg | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									viewBox="0 0 16 16" | ||||||
|  | 									fill="currentColor" | ||||||
|  | 									class="w-4 h-4" | ||||||
|  | 								> | ||||||
|  | 									<path | ||||||
|  | 										fill-rule="evenodd" | ||||||
|  | 										d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z" | ||||||
|  | 										clip-rule="evenodd" | ||||||
|  | 									/> | ||||||
|  | 									<path | ||||||
|  | 										fill-rule="evenodd" | ||||||
|  | 										d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z" | ||||||
|  | 										clip-rule="evenodd" | ||||||
|  | 									/> | ||||||
|  | 								</svg> | ||||||
|  | 							{/if} | ||||||
|  | 						</button> | ||||||
|  | 
 | ||||||
|  | 						<Tooltip content="Create new key"> | ||||||
|  | 							<button | ||||||
|  | 								class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg" | ||||||
|  | 								on:click={() => { | ||||||
|  | 									createAPIKeyHandler(); | ||||||
|  | 								}} | ||||||
|  | 							> | ||||||
|  | 								<svg | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									fill="none" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									stroke-width="2" | ||||||
|  | 									stroke="currentColor" | ||||||
|  | 									class="size-4" | ||||||
|  | 								> | ||||||
|  | 									<path | ||||||
|  | 										stroke-linecap="round" | ||||||
|  | 										stroke-linejoin="round" | ||||||
|  | 										d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" | ||||||
|  | 									/> | ||||||
|  | 								</svg> | ||||||
|  | 							</button> | ||||||
|  | 						</Tooltip> | ||||||
|  | 					{:else} | ||||||
|  | 						<button | ||||||
|  | 							class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition" | ||||||
|  | 							on:click={() => { | ||||||
|  | 								createAPIKeyHandler(); | ||||||
|  | 							}} | ||||||
|  | 						> | ||||||
|  | 							<Plus strokeWidth="2" className=" size-3.5" /> | ||||||
|  | 
 | ||||||
|  | 							Create new secret key</button | ||||||
|  | 						> | ||||||
|  | 					{/if} | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||||
|  |  | ||||||
|  | @ -1,19 +1,124 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { getContext } from 'svelte'; | 	import { getContext, onMount } from 'svelte'; | ||||||
| 	import Modal from '../common/Modal.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'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
| 	export let downloadChat: Function; | 	const shareLocalChat = async () => { | ||||||
| 	export let shareChat: Function; | 		const _chat = chat; | ||||||
|  | 
 | ||||||
|  | 		const sharedChat = await shareChatById(localStorage.token, $chatId); | ||||||
|  | 		const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`; | ||||||
|  | 
 | ||||||
|  | 		toast.success($i18n.t('Copied shared chat URL to clipboard!')); | ||||||
|  | 		copyToClipboard(chatShareUrl); | ||||||
|  | 		chat = await getChatById(localStorage.token, $chatId); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const shareChat = async () => { | ||||||
|  | 		const _chat = chat.chat; | ||||||
|  | 		console.log('share', _chat); | ||||||
|  | 
 | ||||||
|  | 		toast.success($i18n.t('Redirecting you to OpenWebUI Community')); | ||||||
|  | 		const url = 'https://openwebui.com'; | ||||||
|  | 		// const url = 'http://localhost:5173'; | ||||||
|  | 
 | ||||||
|  | 		const tab = await window.open(`${url}/chats/upload`, '_blank'); | ||||||
|  | 		window.addEventListener( | ||||||
|  | 			'message', | ||||||
|  | 			(event) => { | ||||||
|  | 				if (event.origin !== url) return; | ||||||
|  | 				if (event.data === 'loaded') { | ||||||
|  | 					tab.postMessage( | ||||||
|  | 						JSON.stringify({ | ||||||
|  | 							chat: _chat, | ||||||
|  | 							modelfiles: $modelfiles.filter((modelfile) => | ||||||
|  | 								_chat.models.includes(modelfile.tagName) | ||||||
|  | 							) | ||||||
|  | 						}), | ||||||
|  | 						'*' | ||||||
|  | 					); | ||||||
|  | 				} | ||||||
|  | 			}, | ||||||
|  | 			false | ||||||
|  | 		); | ||||||
|  | 	}; | ||||||
| 
 | 
 | ||||||
| 	export let show = false; | 	export let show = false; | ||||||
|  | 
 | ||||||
|  | 	$: if (show) { | ||||||
|  | 		(async () => { | ||||||
|  | 			if ($chatId) { | ||||||
|  | 				chat = await getChatById(localStorage.token, $chatId); | ||||||
|  | 			} else { | ||||||
|  | 				chat = null; | ||||||
|  | 				console.log(chat); | ||||||
|  | 			} | ||||||
|  | 		})(); | ||||||
|  | 	} | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <Modal bind:show size="xs"> | <Modal bind:show size="sm"> | ||||||
| 	<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> | 	<div> | ||||||
|  | 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||||
|  | 			<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div> | ||||||
| 			<button | 			<button | ||||||
| 			class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white" | 				class="self-center" | ||||||
|  | 				on:click={() => { | ||||||
|  | 					show = false; | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<svg | ||||||
|  | 					xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 					viewBox="0 0 20 20" | ||||||
|  | 					fill="currentColor" | ||||||
|  | 					class="w-5 h-5" | ||||||
|  | 				> | ||||||
|  | 					<path | ||||||
|  | 						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" | ||||||
|  | 					/> | ||||||
|  | 				</svg> | ||||||
|  | 			</button> | ||||||
|  | 		</div> | ||||||
|  | 		<hr class=" dark:border-gray-800" /> | ||||||
|  | 
 | ||||||
|  | 		{#if chat} | ||||||
|  | 			<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> | ||||||
|  | 				<div class=" text-sm dark:text-gray-300 mb-1"> | ||||||
|  | 					{#if chat.share_id} | ||||||
|  | 						<a href="/s/{chat.share_id}" target="_blank" | ||||||
|  | 							>You have shared this chat <span class=" underline">before</span>.</a | ||||||
|  | 						> | ||||||
|  | 						Click here to | ||||||
|  | 						<button | ||||||
|  | 							class="underline" | ||||||
|  | 							on:click={async () => { | ||||||
|  | 								const res = await deleteSharedChatById(localStorage.token, $chatId); | ||||||
|  | 
 | ||||||
|  | 								if (res) { | ||||||
|  | 									chat = await getChatById(localStorage.token, $chatId); | ||||||
|  | 								} | ||||||
|  | 							}}>delete this link</button | ||||||
|  | 						> and create a new shared link. | ||||||
|  | 					{:else} | ||||||
|  | 						Messages you send after creating your link won't be shared. Users with the URL will be | ||||||
|  | 						able to view the shared chat. | ||||||
|  | 					{/if} | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="flex justify-end"> | ||||||
|  | 					<div class="flex flex-col items-end space-x-1 mt-1.5"> | ||||||
|  | 						<div class="flex gap-1"> | ||||||
|  | 							<button | ||||||
|  | 								class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white" | ||||||
| 								type="button" | 								type="button" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									shareChat(); | 									shareChat(); | ||||||
|  | @ -23,19 +128,26 @@ | ||||||
| 								{$i18n.t('Share to OpenWebUI Community')} | 								{$i18n.t('Share to OpenWebUI Community')} | ||||||
| 							</button> | 							</button> | ||||||
| 
 | 
 | ||||||
| 		<div class="flex justify-center space-x-1 mt-1.5"> |  | ||||||
| 			<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div> |  | ||||||
| 
 |  | ||||||
| 							<button | 							<button | ||||||
| 				class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" | 								class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white" | ||||||
| 								type="button" | 								type="button" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 					downloadChat(); | 									shareLocalChat(); | ||||||
| 									show = false; | 									show = false; | ||||||
| 								}} | 								}} | ||||||
| 							> | 							> | ||||||
| 				{$i18n.t('Download as a File')} | 								<Link /> | ||||||
|  | 
 | ||||||
|  | 								{#if chat.share_id} | ||||||
|  | 									{$i18n.t('Update and Copy Link')} | ||||||
|  | 								{:else} | ||||||
|  | 									{$i18n.t('Copy Link')} | ||||||
|  | 								{/if} | ||||||
| 							</button> | 							</button> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		{/if} | ||||||
|  | 	</div> | ||||||
| </Modal> | </Modal> | ||||||
|  |  | ||||||
|  | @ -1,24 +1,25 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	export let className: string = 'text-white'; | 	export let className: string = ''; | ||||||
| 	export let theme: 'blue' | 'white' | 'black' = 'white'; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="flex justify-center text-center {className}"> | <div class="flex justify-center text-center {className}"> | ||||||
| 	<svg | 	<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" | ||||||
| 		class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue' | 		><style> | ||||||
| 			? 'text-sky-600' | 			.spinner_ajPY { | ||||||
| 			: theme === 'white' | 				transform-origin: center; | ||||||
| 			? 'text-white' | 				animation: spinner_AtaB 0.75s infinite linear; | ||||||
| 			: 'text-gray-600'} " | 			} | ||||||
| 		xmlns="http://www.w3.org/2000/svg" | 			@keyframes spinner_AtaB { | ||||||
| 		fill="none" | 				100% { | ||||||
| 		viewBox="0 0 24 24" | 					transform: rotate(360deg); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		</style><path | ||||||
|  | 			d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||||
|  | 			opacity=".25" | ||||||
|  | 		/><path | ||||||
|  | 			d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||||
|  | 			class="spinner_ajPY" | ||||||
|  | 		/></svg | ||||||
| 	> | 	> | ||||||
| 		<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> |  | ||||||
| 		<path |  | ||||||
| 			class="opacity-75" |  | ||||||
| 			fill="currentColor" |  | ||||||
| 			d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |  | ||||||
| 		/> |  | ||||||
| 	</svg> |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
							
								
								
									
										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"> | <script lang="ts"> | ||||||
| 	import { getContext } from 'svelte'; | 	import { getContext } from 'svelte'; | ||||||
| 	import { toast } from 'svelte-sonner'; | 	import { toast } from 'svelte-sonner'; | ||||||
| 	import fileSaver from 'file-saver'; |  | ||||||
| 	const { saveAs } = fileSaver; |  | ||||||
| 
 | 
 | ||||||
| 	import { Separator } from 'bits-ui'; |  | ||||||
| 	import { getChatById } from '$lib/apis/chats'; |  | ||||||
| 	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; | 	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { slide } from 'svelte/transition'; | 	import { slide } from 'svelte/transition'; | ||||||
| 	import ShareChatModal from '../chat/ShareChatModal.svelte'; | 	import ShareChatModal from '../chat/ShareChatModal.svelte'; | ||||||
| 	import TagInput from '../common/Tags/TagInput.svelte'; |  | ||||||
| 	import ModelSelector from '../chat/ModelSelector.svelte'; | 	import ModelSelector from '../chat/ModelSelector.svelte'; | ||||||
| 	import Tooltip from '../common/Tooltip.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 Menu from './Navbar/Menu.svelte'; | ||||||
| 	import TagChatModal from '../chat/TagChatModal.svelte'; |  | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
|  | @ -26,121 +16,34 @@ | ||||||
| 	export let title: string = $WEBUI_NAME; | 	export let title: string = $WEBUI_NAME; | ||||||
| 	export let shareEnabled: boolean = false; | 	export let shareEnabled: boolean = false; | ||||||
| 
 | 
 | ||||||
|  | 	export let chat; | ||||||
| 	export let selectedModels; | 	export let selectedModels; | ||||||
| 
 | 
 | ||||||
| 	export let tags = []; | 	export let tags = []; | ||||||
| 	export let addTag: Function; | 	export let addTag: Function; | ||||||
| 	export let deleteTag: Function; | 	export let deleteTag: Function; | ||||||
| 
 | 
 | ||||||
| 	export let showModelSelector = false; | 	export let showModelSelector = true; | ||||||
| 
 | 
 | ||||||
| 	let showShareChatModal = false; | 	let showShareChatModal = false; | ||||||
| 	let showTagChatModal = false; | 	let showDownloadChatModal = false; | ||||||
| 
 |  | ||||||
| 	const shareChat = async () => { |  | ||||||
| 		const chat = (await getChatById(localStorage.token, $chatId)).chat; |  | ||||||
| 		console.log('share', chat); |  | ||||||
| 
 |  | ||||||
| 		toast.success($i18n.t('Redirecting you to OpenWebUI Community')); |  | ||||||
| 		const url = 'https://openwebui.com'; |  | ||||||
| 		// const url = 'http://localhost:5173'; |  | ||||||
| 
 |  | ||||||
| 		const tab = await window.open(`${url}/chats/upload`, '_blank'); |  | ||||||
| 		window.addEventListener( |  | ||||||
| 			'message', |  | ||||||
| 			(event) => { |  | ||||||
| 				if (event.origin !== url) return; |  | ||||||
| 				if (event.data === 'loaded') { |  | ||||||
| 					tab.postMessage( |  | ||||||
| 						JSON.stringify({ |  | ||||||
| 							chat: chat, |  | ||||||
| 							modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName)) |  | ||||||
| 						}), |  | ||||||
| 						'*' |  | ||||||
| 					); |  | ||||||
| 				} |  | ||||||
| 			}, |  | ||||||
| 			false |  | ||||||
| 		); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const downloadChat = async () => { |  | ||||||
| 		const chat = (await getChatById(localStorage.token, $chatId)).chat; |  | ||||||
| 		console.log('download', chat); |  | ||||||
| 
 |  | ||||||
| 		const chatText = chat.messages.reduce((a, message, i, arr) => { |  | ||||||
| 			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; |  | ||||||
| 		}, ''); |  | ||||||
| 
 |  | ||||||
| 		let blob = new Blob([chatText], { |  | ||||||
| 			type: 'text/plain' |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 		saveAs(blob, `chat-${chat.title}.txt`); |  | ||||||
| 	}; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> | <ShareChatModal bind:show={showShareChatModal} /> | ||||||
| <!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> --> |  | ||||||
| <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> | <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> | ||||||
| 	<div | 	<div | ||||||
| 		class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}  | 		class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}  | ||||||
| 		 w-full mx-auto px-3" | 		 w-full mx-auto px-3" | ||||||
| 	> | 	> | ||||||
| 		<!-- {#if shareEnabled} |  | ||||||
| 		<div class="flex items-center w-full max-w-full"> | 		<div class="flex items-center w-full max-w-full"> | ||||||
| 				<div class=" flex-1 self-center font-medium line-clamp-1"> | 			<div class="flex-1 overflow-hidden max-w-full"> | ||||||
| 					<div> | 				{#if showModelSelector} | ||||||
| 						{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 /> | 					<ModelSelector bind:selectedModels /> | ||||||
|  | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div class="self-start flex flex-none items-center"> | 			<div class="self-start flex flex-none items-center"> | ||||||
| 				<div class="flex self-center w-[1px] h-5 mx-2 bg-stone-700" /> | 				<div class="flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> | ||||||
| 
 | 
 | ||||||
| 				{#if !shareEnabled} | 				{#if !shareEnabled} | ||||||
| 					<Tooltip content="Settings"> | 					<Tooltip content="Settings"> | ||||||
|  | @ -174,10 +77,14 @@ | ||||||
| 					</Tooltip> | 					</Tooltip> | ||||||
| 				{:else} | 				{:else} | ||||||
| 					<Menu | 					<Menu | ||||||
|  | 						{chat} | ||||||
| 						{shareEnabled} | 						{shareEnabled} | ||||||
| 						shareHandler={() => { | 						shareHandler={() => { | ||||||
| 							showShareChatModal = !showShareChatModal; | 							showShareChatModal = !showShareChatModal; | ||||||
| 						}} | 						}} | ||||||
|  | 						downloadHandler={() => { | ||||||
|  | 							showDownloadChatModal = !showDownloadChatModal; | ||||||
|  | 						}} | ||||||
| 						{tags} | 						{tags} | ||||||
| 						{deleteTag} | 						{deleteTag} | ||||||
| 						{addTag} | 						{addTag} | ||||||
|  |  | ||||||
|  | @ -1,23 +1,94 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { DropdownMenu } from 'bits-ui'; | 	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 { flyAndScale } from '$lib/utils/transitions'; | ||||||
| 
 | 
 | ||||||
| 	import Dropdown from '$lib/components/common/Dropdown.svelte'; | 	import Dropdown from '$lib/components/common/Dropdown.svelte'; | ||||||
| 	import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; |  | ||||||
| 	import Pencil from '$lib/components/icons/Pencil.svelte'; |  | ||||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; |  | ||||||
| 	import { showSettings } from '$lib/stores'; |  | ||||||
| 	import Tags from '$lib/components/common/Tags.svelte'; | 	import Tags from '$lib/components/common/Tags.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let shareEnabled: boolean = false; | 	export let shareEnabled: boolean = false; | ||||||
| 	export let shareHandler: Function; | 	export let shareHandler: Function; | ||||||
|  | 	export let downloadHandler: Function; | ||||||
|  | 
 | ||||||
| 	// export let tagHandler: Function; | 	// export let tagHandler: Function; | ||||||
| 
 | 
 | ||||||
|  | 	export let chat; | ||||||
| 	export let tags; | 	export let tags; | ||||||
| 	export let deleteTag: Function; | 	export let deleteTag: Function; | ||||||
| 	export let addTag: Function; | 	export let addTag: Function; | ||||||
| 
 | 
 | ||||||
| 	export let onClose: 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> | </script> | ||||||
| 
 | 
 | ||||||
| <Dropdown | <Dropdown | ||||||
|  | @ -31,14 +102,14 @@ | ||||||
| 
 | 
 | ||||||
| 	<div slot="content"> | 	<div slot="content"> | ||||||
| 		<DropdownMenu.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} | 			sideOffset={8} | ||||||
| 			side="bottom" | 			side="bottom" | ||||||
| 			align="end" | 			align="end" | ||||||
| 			transition={flyAndScale} | 			transition={flyAndScale} | ||||||
| 		> | 		> | ||||||
| 			<DropdownMenu.Item | 			<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 () => { | 				on:click={async () => { | ||||||
| 					await showSettings.set(!$showSettings); | 					await showSettings.set(!$showSettings); | ||||||
| 				}} | 				}} | ||||||
|  | @ -49,7 +120,7 @@ | ||||||
| 					viewBox="0 0 24 24" | 					viewBox="0 0 24 24" | ||||||
| 					stroke-width="1.5" | 					stroke-width="1.5" | ||||||
| 					stroke="currentColor" | 					stroke="currentColor" | ||||||
| 					class="w-5 h-5" | 					class="size-4" | ||||||
| 				> | 				> | ||||||
| 					<path | 					<path | ||||||
| 						stroke-linecap="round" | 						stroke-linecap="round" | ||||||
|  | @ -67,7 +138,7 @@ | ||||||
| 
 | 
 | ||||||
| 			{#if shareEnabled} | 			{#if shareEnabled} | ||||||
| 				<DropdownMenu.Item | 				<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={() => { | 					on:click={() => { | ||||||
| 						shareHandler(); | 						shareHandler(); | ||||||
| 					}} | 					}} | ||||||
|  | @ -87,7 +158,59 @@ | ||||||
| 					<div class="flex items-center">Share</div> | 					<div class="flex items-center">Share</div> | ||||||
| 				</DropdownMenu.Item> | 				</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"> | 				<div class="flex p-1"> | ||||||
| 					<Tags {tags} {deleteTag} {addTag} /> | 					<Tags {tags} {deleteTag} {addTag} /> | ||||||
|  |  | ||||||
|  | @ -581,7 +581,7 @@ | ||||||
| 							<div class="py-2 w-full"> | 							<div class="py-2 w-full"> | ||||||
| 								{#if $user.role === 'admin'} | 								{#if $user.role === 'admin'} | ||||||
| 									<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={() => { | 										on:click={() => { | ||||||
| 											goto('/admin'); | 											goto('/admin'); | ||||||
| 											showDropdown = false; | 											showDropdown = false; | ||||||
|  | @ -607,7 +607,7 @@ | ||||||
| 									</button> | 									</button> | ||||||
| 
 | 
 | ||||||
| 									<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={() => { | 										on:click={() => { | ||||||
| 											goto('/playground'); | 											goto('/playground'); | ||||||
| 											showDropdown = false; | 											showDropdown = false; | ||||||
|  | @ -634,7 +634,7 @@ | ||||||
| 								{/if} | 								{/if} | ||||||
| 
 | 
 | ||||||
| 								<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={async () => { | 									on:click={async () => { | ||||||
| 										await showSettings.set(true); | 										await showSettings.set(true); | ||||||
| 										showDropdown = false; | 										showDropdown = false; | ||||||
|  | @ -669,7 +669,7 @@ | ||||||
| 
 | 
 | ||||||
| 							<div class="py-2 w-full"> | 							<div class="py-2 w-full"> | ||||||
| 								<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={() => { | 									on:click={() => { | ||||||
| 										localStorage.removeItem('token'); | 										localStorage.removeItem('token'); | ||||||
| 										location.href = '/auth'; | 										location.href = '/auth'; | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 	"(latest)": "(nieuwste)", | 	"(latest)": "(nieuwste)", | ||||||
| 	"{{modelName}} is thinking...": "{{modelName}} is aan het denken...", | 	"{{modelName}} is thinking...": "{{modelName}} is aan het denken...", | ||||||
| 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht", | 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht", | ||||||
| 	"a user": "", | 	"a user": "een gebruiker", | ||||||
| 	"About": "Over", | 	"About": "Over", | ||||||
| 	"Account": "Account", | 	"Account": "Account", | ||||||
| 	"Action": "Actie", | 	"Action": "Actie", | ||||||
|  | @ -35,12 +35,12 @@ | ||||||
| 	"API Key": "API Key", | 	"API Key": "API Key", | ||||||
| 	"API RPM": "API RPM", | 	"API RPM": "API RPM", | ||||||
| 	"are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen", | 	"are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen", | ||||||
| 	"Are you sure?": "", | 	"Are you sure?": "Zeker weten?", | ||||||
| 	"Audio": "Audio", | 	"Audio": "Audio", | ||||||
| 	"Auto-playback response": "Automatisch afspelen van antwoord", | 	"Auto-playback response": "Automatisch afspelen van antwoord", | ||||||
| 	"Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.", | 	"Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.", | ||||||
| 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", | 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", | ||||||
| 	"AUTOMATIC1111 Base URL is required.": "", | 	"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Basis URL is verplicht", | ||||||
| 	"available!": "beschikbaar!", | 	"available!": "beschikbaar!", | ||||||
| 	"Back": "Terug", | 	"Back": "Terug", | ||||||
| 	"Builder Mode": "Bouwer Modus", | 	"Builder Mode": "Bouwer Modus", | ||||||
|  | @ -60,8 +60,8 @@ | ||||||
| 	"Chunk Size": "Chunk Grootte", | 	"Chunk Size": "Chunk Grootte", | ||||||
| 	"Click here for help.": "Klik hier voor help.", | 	"Click here for help.": "Klik hier voor help.", | ||||||
| 	"Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.", | 	"Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.", | ||||||
| 	"Click here to select": "", | 	"Click here to select": "Klik hier om te selecteren", | ||||||
| 	"Click here to select documents.": "", | 	"Click here to select documents.": "Klik hier om documenten te selecteren", | ||||||
| 	"click here.": "click here.", | 	"click here.": "click here.", | ||||||
| 	"Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.", | 	"Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.", | ||||||
| 	"Close": "Sluiten", | 	"Close": "Sluiten", | ||||||
|  | @ -88,7 +88,7 @@ | ||||||
| 	"Database": "Database", | 	"Database": "Database", | ||||||
| 	"DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm", | 	"DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm", | ||||||
| 	"Default": "Standaard", | 	"Default": "Standaard", | ||||||
| 	"Default (Automatic1111)": "", | 	"Default (Automatic1111)": "Standaard (Automatic1111)", | ||||||
| 	"Default (Web API)": "Standaard (Web API)", | 	"Default (Web API)": "Standaard (Web API)", | ||||||
| 	"Default model updated": "Standaard model bijgewerkt", | 	"Default model updated": "Standaard model bijgewerkt", | ||||||
| 	"Default Prompt Suggestions": "Standaard Prompt Suggesties", | 	"Default Prompt Suggestions": "Standaard Prompt Suggesties", | ||||||
|  | @ -123,21 +123,21 @@ | ||||||
| 	"Enable Chat History": "Schakel Chat Geschiedenis in", | 	"Enable Chat History": "Schakel Chat Geschiedenis in", | ||||||
| 	"Enable New Sign Ups": "Schakel Nieuwe Registraties in", | 	"Enable New Sign Ups": "Schakel Nieuwe Registraties in", | ||||||
| 	"Enabled": "Ingeschakeld", | 	"Enabled": "Ingeschakeld", | ||||||
| 	"Enter {{role}} message here": "", | 	"Enter {{role}} message here": "Voeg {{role}} bericht hier toe", | ||||||
| 	"Enter API Key": "", | 	"Enter API Key": "Voeg API Key toe", | ||||||
| 	"Enter Chunk Overlap": "", | 	"Enter Chunk Overlap": "Voeg Chunk Overlap toe", | ||||||
| 	"Enter Chunk Size": "", | 	"Enter Chunk Size": "Voeg Chunk Size toe", | ||||||
| 	"Enter Image Size (e.g. 512x512)": "", | 	"Enter Image Size (e.g. 512x512)": "Voeg afbeelding formaat toe (Bijv. 512x512)", | ||||||
| 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "", | 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "Voeg LiteLLM API Base URL toe (litellm_params.api_base)", | ||||||
| 	"Enter LiteLLM API Key (litellm_params.api_key)": "", | 	"Enter LiteLLM API Key (litellm_params.api_key)": "Voeg LiteLLM API Sleutel toe (litellm_params.api_key)", | ||||||
| 	"Enter LiteLLM API RPM (litellm_params.rpm)": "", | 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Voeg LiteLLM API RPM toe (litellm_params.rpm)", | ||||||
| 	"Enter LiteLLM Model (litellm_params.model)": "", | 	"Enter LiteLLM Model (litellm_params.model)": "Voeg LiteLLM Model toe (litellm_params.model)", | ||||||
| 	"Enter Max Tokens (litellm_params.max_tokens)": "", | 	"Enter Max Tokens (litellm_params.max_tokens)": "Voeg maximum aantal tokens toe (litellm_params.max_tokens)", | ||||||
| 	"Enter model tag (e.g. {{modelTag}})": "", | 	"Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})", | ||||||
| 	"Enter Number of Steps (e.g. 50)": "", | 	"Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)", | ||||||
| 	"Enter stop sequence": "Zet stop sequentie", | 	"Enter stop sequence": "Zet stop sequentie", | ||||||
| 	"Enter Top K": "", | 	"Enter Top K": "Voeg Top K toe", | ||||||
| 	"Enter URL (e.g. http://127.0.0.1:7860/)": "", | 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Zet URL (Bijv. http://127.0.0.1:7860/)", | ||||||
| 	"Enter Your Email": "Voer je Email in", | 	"Enter Your Email": "Voer je Email in", | ||||||
| 	"Enter Your Full Name": "Voer je Volledige Naam in", | 	"Enter Your Full Name": "Voer je Volledige Naam in", | ||||||
| 	"Enter Your Password": "Voer je Wachtwoord in", | 	"Enter Your Password": "Voer je Wachtwoord in", | ||||||
|  | @ -161,7 +161,7 @@ | ||||||
| 	"Hide Additional Params": "Verberg Extra Params", | 	"Hide Additional Params": "Verberg Extra Params", | ||||||
| 	"How can I help you today?": "Hoe kan ik je vandaag helpen?", | 	"How can I help you today?": "Hoe kan ik je vandaag helpen?", | ||||||
| 	"Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)", | 	"Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)", | ||||||
| 	"Image Generation Engine": "", | 	"Image Generation Engine": "Afbeelding Generatie Engine", | ||||||
| 	"Image Settings": "Afbeelding Instellingen", | 	"Image Settings": "Afbeelding Instellingen", | ||||||
| 	"Images": "Afbeeldingen", | 	"Images": "Afbeeldingen", | ||||||
| 	"Import Chats": "Importeer Chats", | 	"Import Chats": "Importeer Chats", | ||||||
|  | @ -217,7 +217,7 @@ | ||||||
| 	"Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar", | 	"Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar", | ||||||
| 	"Off": "Uit", | 	"Off": "Uit", | ||||||
| 	"Okay, Let's Go!": "Okay, Laten we gaan!", | 	"Okay, Let's Go!": "Okay, Laten we gaan!", | ||||||
| 	"Ollama Base URL": "", | 	"Ollama Base URL": "Ollama Basis URL", | ||||||
| 	"Ollama Version": "Ollama Versie", | 	"Ollama Version": "Ollama Versie", | ||||||
| 	"On": "Aan", | 	"On": "Aan", | ||||||
| 	"Only": "Alleen", | 	"Only": "Alleen", | ||||||
|  | @ -227,11 +227,11 @@ | ||||||
| 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.", | 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.", | ||||||
| 	"Open": "Open", | 	"Open": "Open", | ||||||
| 	"Open AI": "Open AI", | 	"Open AI": "Open AI", | ||||||
| 	"Open AI (Dall-E)": "", | 	"Open AI (Dall-E)": "Open AI (Dall-E)", | ||||||
| 	"Open new chat": "Open nieuwe chat", | 	"Open new chat": "Open nieuwe chat", | ||||||
| 	"OpenAI API": "OpenAI API", | 	"OpenAI API": "OpenAI API", | ||||||
| 	"OpenAI API Key": "", | 	"OpenAI API Key": "OpenAI API Sleutel", | ||||||
| 	"OpenAI API Key is required.": "", | 	"OpenAI API Key is required.": "OpenAI API Sleutel is verplicht", | ||||||
| 	"or": "of", | 	"or": "of", | ||||||
| 	"Parameters": "Parameters", | 	"Parameters": "Parameters", | ||||||
| 	"Password": "Wachtwoord", | 	"Password": "Wachtwoord", | ||||||
|  | @ -273,9 +273,9 @@ | ||||||
| 	"See readme.md for instructions": "Zie readme.md voor instructies", | 	"See readme.md for instructions": "Zie readme.md voor instructies", | ||||||
| 	"See what's new": "Zie wat er nieuw is", | 	"See what's new": "Zie wat er nieuw is", | ||||||
| 	"Seed": "Seed", | 	"Seed": "Seed", | ||||||
| 	"Select a mode": "", | 	"Select a mode": "Selecteer een modus", | ||||||
| 	"Select a model": "Selecteer een model", | 	"Select a model": "Selecteer een model", | ||||||
| 	"Select an Ollama instance": "", | 	"Select an Ollama instance": "Selecteer een Ollama instantie", | ||||||
| 	"Send a Message": "Stuur een Bericht", | 	"Send a Message": "Stuur een Bericht", | ||||||
| 	"Send message": "Stuur bericht", | 	"Send message": "Stuur bericht", | ||||||
| 	"Server connection verified": "Server verbinding geverifieerd", | 	"Server connection verified": "Server verbinding geverifieerd", | ||||||
|  | @ -330,7 +330,7 @@ | ||||||
| 	"Top P": "Top P", | 	"Top P": "Top P", | ||||||
| 	"Trouble accessing Ollama?": "Problemen met toegang tot Ollama?", | 	"Trouble accessing Ollama?": "Problemen met toegang tot Ollama?", | ||||||
| 	"TTS Settings": "TTS instellingen", | 	"TTS Settings": "TTS instellingen", | ||||||
| 	"Type Hugging Face Resolve (Download) URL": "", | 	"Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL", | ||||||
| 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.", | 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.", | ||||||
| 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst", | 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst", | ||||||
| 	"Update password": "Wijzig wachtwoord", | 	"Update password": "Wijzig wachtwoord", | ||||||
|  | @ -339,7 +339,7 @@ | ||||||
| 	"Upload Progress": "Upload Voortgang", | 	"Upload Progress": "Upload Voortgang", | ||||||
| 	"URL Mode": "URL Modus", | 	"URL Mode": "URL Modus", | ||||||
| 	"Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.", | 	"Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.", | ||||||
| 	"Use Gravatar": "", | 	"Use Gravatar": "Gebruik Gravatar", | ||||||
| 	"user": "user", | 	"user": "user", | ||||||
| 	"User Permissions": "Gebruikers Rechten", | 	"User Permissions": "Gebruikers Rechten", | ||||||
| 	"Users": "Gebruikers", | 	"Users": "Gebruikers", | ||||||
|  |  | ||||||
|  | @ -31,6 +31,21 @@ export const getModels = async (token: string) => { | ||||||
| // Helper functions
 | // 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) => { | export const capitalizeFirstLetter = (string) => { | ||||||
| 	return string.charAt(0).toUpperCase() + string.slice(1); | 	return string.charAt(0).toUpperCase() + string.slice(1); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ | ||||||
| 	let messagesContainerElement: HTMLDivElement; | 	let messagesContainerElement: HTMLDivElement; | ||||||
| 	let currentRequestId = null; | 	let currentRequestId = null; | ||||||
| 
 | 
 | ||||||
| 	let showModelSelector = false; | 	let showModelSelector = true; | ||||||
| 	let selectedModels = ['']; | 	let selectedModels = ['']; | ||||||
| 
 | 
 | ||||||
| 	let selectedModelfile = null; | 	let selectedModelfile = null; | ||||||
|  | @ -847,6 +847,7 @@ | ||||||
| 		bind:selectedModels | 		bind:selectedModels | ||||||
| 		bind:showModelSelector | 		bind:showModelSelector | ||||||
| 		shareEnabled={messages.length > 0} | 		shareEnabled={messages.length > 0} | ||||||
|  | 		{chat} | ||||||
| 		{initNewChat} | 		{initNewChat} | ||||||
| 		{tags} | 		{tags} | ||||||
| 		{addTag} | 		{addTag} | ||||||
|  |  | ||||||
|  | @ -56,7 +56,7 @@ | ||||||
| 	let currentRequestId = null; | 	let currentRequestId = null; | ||||||
| 
 | 
 | ||||||
| 	// let chatId = $page.params.id; | 	// let chatId = $page.params.id; | ||||||
| 	let showModelSelector = false; | 	let showModelSelector = true; | ||||||
| 	let selectedModels = ['']; | 	let selectedModels = ['']; | ||||||
| 	let selectedModelfile = null; | 	let selectedModelfile = null; | ||||||
| 
 | 
 | ||||||
|  | @ -865,6 +865,7 @@ | ||||||
| 	<div class="min-h-screen max-h-screen w-full flex flex-col"> | 	<div class="min-h-screen max-h-screen w-full flex flex-col"> | ||||||
| 		<Navbar | 		<Navbar | ||||||
| 			{title} | 			{title} | ||||||
|  | 			{chat} | ||||||
| 			bind:selectedModels | 			bind:selectedModels | ||||||
| 			bind:showModelSelector | 			bind:showModelSelector | ||||||
| 			shareEnabled={messages.length > 0} | 			shareEnabled={messages.length > 0} | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| <script> | <script> | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
| 	import { userSignIn, userSignUp } from '$lib/apis/auths'; | 	import { userSignIn, userSignUp } from '$lib/apis/auths'; | ||||||
|  | 	import Spinner from '$lib/components/common/Spinner.svelte'; | ||||||
| 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; | 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; | ||||||
| 	import { WEBUI_NAME, config, user } from '$lib/stores'; | 	import { WEBUI_NAME, config, user } from '$lib/stores'; | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
|  | @ -56,6 +57,9 @@ | ||||||
| 			await goto('/'); | 			await goto('/'); | ||||||
| 		} | 		} | ||||||
| 		loaded = true; | 		loaded = true; | ||||||
|  | 		if ($config?.trusted_header_auth ?? false) { | ||||||
|  | 			await signInHandler(); | ||||||
|  | 		} | ||||||
| 	}); | 	}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -90,6 +94,23 @@ | ||||||
| 		</div> --> | 		</div> --> | ||||||
| 
 | 
 | ||||||
| 		<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col"> | 		<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col"> | ||||||
|  | 			{#if $config?.trusted_header_auth ?? false} | ||||||
|  | 				<div class=" my-auto pb-10 w-full"> | ||||||
|  | 					<div | ||||||
|  | 						class="flex items-center justify-center gap-3 text-xl sm:text-2xl text-center font-bold dark:text-gray-200" | ||||||
|  | 					> | ||||||
|  | 						<div> | ||||||
|  | 							{$i18n.t('Signing in')} | ||||||
|  | 							{$i18n.t('to')} | ||||||
|  | 							{$WEBUI_NAME} | ||||||
|  | 						</div> | ||||||
|  | 
 | ||||||
|  | 						<div> | ||||||
|  | 							<Spinner /> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			{:else} | ||||||
| 				<div class=" my-auto pb-10 w-full"> | 				<div class=" my-auto pb-10 w-full"> | ||||||
| 					<form | 					<form | ||||||
| 						class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl" | 						class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl" | ||||||
|  | @ -184,6 +205,7 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 					</form> | 					</form> | ||||||
| 				</div> | 				</div> | ||||||
|  | 			{/if} | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek