forked from open-webui/open-webui
		
	Merge branch 'dev' into feature/support_auth_by_api_key
This commit is contained in:
		
						commit
						56369fea3a
					
				
					 35 changed files with 1231 additions and 325 deletions
				
			
		
							
								
								
									
										2
									
								
								.github/workflows/format-backend.yaml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/format-backend.yaml
									
										
									
									
										vendored
									
									
								
							|  | @ -33,7 +33,7 @@ jobs: | |||
|           pip install black | ||||
| 
 | ||||
|       - name: Format backend | ||||
|         run: black . --exclude "/venv/" | ||||
|         run: npm run format:backend | ||||
| 
 | ||||
|       - name: Check for changes after format | ||||
|         run: git diff --exit-code | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ ENV OPENAI_API_BASE_URL "" | |||
| ENV OPENAI_API_KEY "" | ||||
| 
 | ||||
| ENV WEBUI_SECRET_KEY "" | ||||
| ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER "" | ||||
| 
 | ||||
| ENV SCARF_NO_ANALYTICS true | ||||
| ENV DO_NOT_TRACK true | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
										
									
									
									
								
							|  | @ -8,6 +8,8 @@ remove: | |||
| 
 | ||||
| start: | ||||
| 	@docker-compose start | ||||
| startAndBuild:  | ||||
| 	docker-compose up -d --build | ||||
| 
 | ||||
| stop: | ||||
| 	@docker-compose stop | ||||
|  |  | |||
|  | @ -81,6 +81,12 @@ async def check_url(request: Request, call_next): | |||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| @app.head("/") | ||||
| @app.get("/") | ||||
| async def get_status(): | ||||
|     return {"status": True} | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/urls") | ||||
| async def get_ollama_api_urls(user=Depends(get_admin_user)): | ||||
|     return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} | ||||
|  | @ -1029,6 +1035,14 @@ async def download_model( | |||
|     url_idx: Optional[int] = None, | ||||
| ): | ||||
| 
 | ||||
|     allowed_hosts = ["https://huggingface.co/", "https://github.com/"] | ||||
| 
 | ||||
|     if not any(form_data.url.startswith(host) for host in allowed_hosts): | ||||
|         raise HTTPException( | ||||
|             status_code=400, | ||||
|             detail="Invalid file_url. Only URLs from allowed hosts are permitted.", | ||||
|         ) | ||||
| 
 | ||||
|     if url_idx == None: | ||||
|         url_idx = 0 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|  | @ -1037,6 +1051,7 @@ async def download_model( | |||
| 
 | ||||
|     if file_name: | ||||
|         file_path = f"{UPLOAD_DIR}/{file_name}" | ||||
| 
 | ||||
|         return StreamingResponse( | ||||
|             download_file_stream(url, form_data.url, file_path, file_name), | ||||
|         ) | ||||
|  |  | |||
|  | @ -448,8 +448,25 @@ def store_doc( | |||
| 
 | ||||
|     log.info(f"file.content_type: {file.content_type}") | ||||
|     try: | ||||
|         is_valid_filename = True | ||||
|         unsanitized_filename = file.filename | ||||
|         if not unsanitized_filename.isascii(): | ||||
|             is_valid_filename = False | ||||
| 
 | ||||
|         unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" | ||||
|         dereferenced_file_path = str(Path(unvalidated_file_path).resolve(strict=False)) | ||||
|         if not dereferenced_file_path.startswith(UPLOAD_DIR): | ||||
|             is_valid_filename = False | ||||
| 
 | ||||
|         if is_valid_filename: | ||||
|             file_path = dereferenced_file_path | ||||
|         else: | ||||
|             raise HTTPException( | ||||
|                 status_code=status.HTTP_400_BAD_REQUEST, | ||||
|                 detail=ERROR_MESSAGES.DEFAULT(), | ||||
|             ) | ||||
| 
 | ||||
|         filename = file.filename | ||||
|         file_path = f"{UPLOAD_DIR}/{filename}" | ||||
|         contents = file.file.read() | ||||
|         with open(file_path, "wb") as f: | ||||
|             f.write(contents) | ||||
|  |  | |||
|  | @ -156,6 +156,8 @@ def rag_messages(docs, messages, template, k, embedding_function): | |||
| 
 | ||||
|         relevant_contexts.append(context) | ||||
| 
 | ||||
|     log.debug(f"relevant_contexts: {relevant_contexts}") | ||||
| 
 | ||||
|     context_string = "" | ||||
|     for context in relevant_contexts: | ||||
|         if context: | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| from peewee import * | ||||
| from peewee_migrate import Router | ||||
| from config import SRC_LOG_LEVELS, DATA_DIR | ||||
| import os | ||||
| import logging | ||||
|  | @ -16,4 +17,6 @@ else: | |||
| 
 | ||||
| 
 | ||||
| DB = SqliteDatabase(f"{DATA_DIR}/webui.db") | ||||
| DB.connect() | ||||
| router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log) | ||||
| router.run() | ||||
| DB.connect(reuse_if_open=True) | ||||
|  |  | |||
							
								
								
									
										149
									
								
								backend/apps/web/internal/migrations/001_initial_schema.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								backend/apps/web/internal/migrations/001_initial_schema.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,149 @@ | |||
| """Peewee migrations -- 001_initial_schema.py. | ||||
| 
 | ||||
| Some examples (model - class or model name):: | ||||
| 
 | ||||
|     > Model = migrator.orm['table_name']            # Return model in current state by name | ||||
|     > Model = migrator.ModelClass                   # Return model in current state by name | ||||
| 
 | ||||
|     > migrator.sql(sql)                             # Run custom SQL | ||||
|     > migrator.run(func, *args, **kwargs)           # Run python function with the given args | ||||
|     > migrator.create_model(Model)                  # Create a model (could be used as decorator) | ||||
|     > migrator.remove_model(model, cascade=True)    # Remove a model | ||||
|     > migrator.add_fields(model, **fields)          # Add fields to a model | ||||
|     > migrator.change_fields(model, **fields)       # Change fields | ||||
|     > migrator.remove_fields(model, *field_names, cascade=True) | ||||
|     > migrator.rename_field(model, old_field_name, new_field_name) | ||||
|     > migrator.rename_table(model, new_table_name) | ||||
|     > migrator.add_index(model, *col_names, unique=False) | ||||
|     > migrator.add_not_null(model, *field_names) | ||||
|     > migrator.add_default(model, field_name, default) | ||||
|     > migrator.add_constraint(model, name, sql) | ||||
|     > migrator.drop_index(model, *col_names) | ||||
|     > migrator.drop_not_null(model, *field_names) | ||||
|     > migrator.drop_constraints(model, *constraints) | ||||
| 
 | ||||
| """ | ||||
| 
 | ||||
| from contextlib import suppress | ||||
| 
 | ||||
| import peewee as pw | ||||
| from peewee_migrate import Migrator | ||||
| 
 | ||||
| 
 | ||||
| with suppress(ImportError): | ||||
|     import playhouse.postgres_ext as pw_pext | ||||
| 
 | ||||
| 
 | ||||
| def migrate(migrator: Migrator, database: pw.Database, *, fake=False): | ||||
|     """Write your migrations here.""" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class Auth(pw.Model): | ||||
|         id = pw.CharField(max_length=255, unique=True) | ||||
|         email = pw.CharField(max_length=255) | ||||
|         password = pw.CharField(max_length=255) | ||||
|         active = pw.BooleanField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "auth" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class Chat(pw.Model): | ||||
|         id = pw.CharField(max_length=255, unique=True) | ||||
|         user_id = pw.CharField(max_length=255) | ||||
|         title = pw.CharField() | ||||
|         chat = pw.TextField() | ||||
|         timestamp = pw.DateField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "chat" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class ChatIdTag(pw.Model): | ||||
|         id = pw.CharField(max_length=255, unique=True) | ||||
|         tag_name = pw.CharField(max_length=255) | ||||
|         chat_id = pw.CharField(max_length=255) | ||||
|         user_id = pw.CharField(max_length=255) | ||||
|         timestamp = pw.DateField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "chatidtag" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class Document(pw.Model): | ||||
|         id = pw.AutoField() | ||||
|         collection_name = pw.CharField(max_length=255, unique=True) | ||||
|         name = pw.CharField(max_length=255, unique=True) | ||||
|         title = pw.CharField() | ||||
|         filename = pw.CharField() | ||||
|         content = pw.TextField(null=True) | ||||
|         user_id = pw.CharField(max_length=255) | ||||
|         timestamp = pw.DateField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "document" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class Modelfile(pw.Model): | ||||
|         id = pw.AutoField() | ||||
|         tag_name = pw.CharField(max_length=255, unique=True) | ||||
|         user_id = pw.CharField(max_length=255) | ||||
|         modelfile = pw.TextField() | ||||
|         timestamp = pw.DateField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "modelfile" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class Prompt(pw.Model): | ||||
|         id = pw.AutoField() | ||||
|         command = pw.CharField(max_length=255, unique=True) | ||||
|         user_id = pw.CharField(max_length=255) | ||||
|         title = pw.CharField() | ||||
|         content = pw.TextField() | ||||
|         timestamp = pw.DateField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "prompt" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class Tag(pw.Model): | ||||
|         id = pw.CharField(max_length=255, unique=True) | ||||
|         name = pw.CharField(max_length=255) | ||||
|         user_id = pw.CharField(max_length=255) | ||||
|         data = pw.TextField(null=True) | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "tag" | ||||
| 
 | ||||
|     @migrator.create_model | ||||
|     class User(pw.Model): | ||||
|         id = pw.CharField(max_length=255, unique=True) | ||||
|         name = pw.CharField(max_length=255) | ||||
|         email = pw.CharField(max_length=255) | ||||
|         role = pw.CharField(max_length=255) | ||||
|         profile_image_url = pw.CharField(max_length=255) | ||||
|         timestamp = pw.DateField() | ||||
| 
 | ||||
|         class Meta: | ||||
|             table_name = "user" | ||||
| 
 | ||||
| 
 | ||||
| def rollback(migrator: Migrator, database: pw.Database, *, fake=False): | ||||
|     """Write your rollback migrations here.""" | ||||
| 
 | ||||
|     migrator.remove_model("user") | ||||
| 
 | ||||
|     migrator.remove_model("tag") | ||||
| 
 | ||||
|     migrator.remove_model("prompt") | ||||
| 
 | ||||
|     migrator.remove_model("modelfile") | ||||
| 
 | ||||
|     migrator.remove_model("document") | ||||
| 
 | ||||
|     migrator.remove_model("chatidtag") | ||||
| 
 | ||||
|     migrator.remove_model("chat") | ||||
| 
 | ||||
|     migrator.remove_model("auth") | ||||
|  | @ -0,0 +1,48 @@ | |||
| """Peewee migrations -- 002_add_local_sharing.py. | ||||
| 
 | ||||
| Some examples (model - class or model name):: | ||||
| 
 | ||||
|     > Model = migrator.orm['table_name']            # Return model in current state by name | ||||
|     > Model = migrator.ModelClass                   # Return model in current state by name | ||||
| 
 | ||||
|     > migrator.sql(sql)                             # Run custom SQL | ||||
|     > migrator.run(func, *args, **kwargs)           # Run python function with the given args | ||||
|     > migrator.create_model(Model)                  # Create a model (could be used as decorator) | ||||
|     > migrator.remove_model(model, cascade=True)    # Remove a model | ||||
|     > migrator.add_fields(model, **fields)          # Add fields to a model | ||||
|     > migrator.change_fields(model, **fields)       # Change fields | ||||
|     > migrator.remove_fields(model, *field_names, cascade=True) | ||||
|     > migrator.rename_field(model, old_field_name, new_field_name) | ||||
|     > migrator.rename_table(model, new_table_name) | ||||
|     > migrator.add_index(model, *col_names, unique=False) | ||||
|     > migrator.add_not_null(model, *field_names) | ||||
|     > migrator.add_default(model, field_name, default) | ||||
|     > migrator.add_constraint(model, name, sql) | ||||
|     > migrator.drop_index(model, *col_names) | ||||
|     > migrator.drop_not_null(model, *field_names) | ||||
|     > migrator.drop_constraints(model, *constraints) | ||||
| 
 | ||||
| """ | ||||
| 
 | ||||
| from contextlib import suppress | ||||
| 
 | ||||
| import peewee as pw | ||||
| from peewee_migrate import Migrator | ||||
| 
 | ||||
| 
 | ||||
| with suppress(ImportError): | ||||
|     import playhouse.postgres_ext as pw_pext | ||||
| 
 | ||||
| 
 | ||||
| def migrate(migrator: Migrator, database: pw.Database, *, fake=False): | ||||
|     """Write your migrations here.""" | ||||
| 
 | ||||
|     migrator.add_fields( | ||||
|         "chat", share_id=pw.CharField(max_length=255, null=True, unique=True) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def rollback(migrator: Migrator, database: pw.Database, *, fake=False): | ||||
|     """Write your rollback migrations here.""" | ||||
| 
 | ||||
|     migrator.remove_fields("chat", "share_id") | ||||
							
								
								
									
										21
									
								
								backend/apps/web/internal/migrations/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/apps/web/internal/migrations/README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| # Database Migrations | ||||
| 
 | ||||
| This directory contains all the database migrations for the web app. | ||||
| Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library. | ||||
| 
 | ||||
| Migrations are automatically ran at app startup. | ||||
| 
 | ||||
| ## Creating a migration | ||||
| 
 | ||||
| Have you made a change to the schema of an existing model? | ||||
| You will need to create a migration file to ensure that existing databases are updated for backwards compatibility. | ||||
| 
 | ||||
| 1. Have a database file (`webui.db`) that has the old schema prior to any of your changes. | ||||
| 2. Make your changes to the models. | ||||
| 3. From the `backend` directory, run the following command: | ||||
|    ```bash | ||||
|    pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME} | ||||
|    ``` | ||||
|    - `$SQLITE_DB` should be the path to the database file. | ||||
|    - `$MIGRATION_NAME` should be a descriptive name for the migration. | ||||
| 4. The migration file will be created in the `apps/web/internal/migrations` directory. | ||||
|  | @ -20,6 +20,7 @@ from config import ( | |||
|     ENABLE_SIGNUP, | ||||
|     USER_PERMISSIONS, | ||||
|     WEBHOOK_URL, | ||||
|     WEBUI_AUTH_TRUSTED_EMAIL_HEADER, | ||||
| ) | ||||
| 
 | ||||
| app = FastAPI() | ||||
|  | @ -34,7 +35,7 @@ app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS | |||
| app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE | ||||
| app.state.USER_PERMISSIONS = USER_PERMISSIONS | ||||
| app.state.WEBHOOK_URL = WEBHOOK_URL | ||||
| 
 | ||||
| app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER | ||||
| 
 | ||||
| app.add_middleware( | ||||
|     CORSMiddleware, | ||||
|  |  | |||
|  | @ -139,6 +139,14 @@ class AuthsTable: | |||
|                 return user | ||||
|             else: | ||||
|                 return None | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ class Chat(Model): | |||
|     title = CharField() | ||||
|     chat = TextField()  # Save Chat JSON as Text | ||||
|     timestamp = DateField() | ||||
|     share_id = CharField(null=True, unique=True) | ||||
| 
 | ||||
|     class Meta: | ||||
|         database = DB | ||||
|  | @ -31,6 +32,7 @@ class ChatModel(BaseModel): | |||
|     title: str | ||||
|     chat: str | ||||
|     timestamp: int  # timestamp in epoch | ||||
|     share_id: Optional[str] = None | ||||
| 
 | ||||
| 
 | ||||
| #################### | ||||
|  | @ -52,6 +54,7 @@ class ChatResponse(BaseModel): | |||
|     title: str | ||||
|     chat: dict | ||||
|     timestamp: int  # timestamp in epoch | ||||
|     share_id: Optional[str] = None  # id of the chat to be shared | ||||
| 
 | ||||
| 
 | ||||
| class ChatTitleIdResponse(BaseModel): | ||||
|  | @ -95,6 +98,71 @@ class ChatTable: | |||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: | ||||
|         # Get the existing chat to share | ||||
|         chat = Chat.get(Chat.id == chat_id) | ||||
|         # Check if the chat is already shared | ||||
|         if chat.share_id: | ||||
|             return self.get_chat_by_id_and_user_id(chat.share_id, "shared") | ||||
|         # Create a new chat with the same data, but with a new ID | ||||
|         shared_chat = ChatModel( | ||||
|             **{ | ||||
|                 "id": str(uuid.uuid4()), | ||||
|                 "user_id": f"shared-{chat_id}", | ||||
|                 "title": chat.title, | ||||
|                 "chat": chat.chat, | ||||
|                 "timestamp": int(time.time()), | ||||
|             } | ||||
|         ) | ||||
|         shared_result = Chat.create(**shared_chat.model_dump()) | ||||
|         # Update the original chat with the share_id | ||||
|         result = ( | ||||
|             Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute() | ||||
|         ) | ||||
| 
 | ||||
|         return shared_chat if (shared_result and result) else None | ||||
| 
 | ||||
|     def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: | ||||
|         try: | ||||
|             print("update_shared_chat_by_id") | ||||
|             chat = Chat.get(Chat.id == chat_id) | ||||
|             print(chat) | ||||
| 
 | ||||
|             query = Chat.update( | ||||
|                 title=chat.title, | ||||
|                 chat=chat.chat, | ||||
|             ).where(Chat.id == chat.share_id) | ||||
| 
 | ||||
|             query.execute() | ||||
| 
 | ||||
|             chat = Chat.get(Chat.id == chat.share_id) | ||||
|             return ChatModel(**model_to_dict(chat)) | ||||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool: | ||||
|         try: | ||||
|             query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}") | ||||
|             query.execute()  # Remove the rows, return number of rows removed. | ||||
| 
 | ||||
|             return True | ||||
|         except: | ||||
|             return False | ||||
| 
 | ||||
|     def update_chat_share_id_by_id( | ||||
|         self, id: str, share_id: Optional[str] | ||||
|     ) -> Optional[ChatModel]: | ||||
|         try: | ||||
|             query = Chat.update( | ||||
|                 share_id=share_id, | ||||
|             ).where(Chat.id == id) | ||||
|             query.execute() | ||||
| 
 | ||||
|             chat = Chat.get(Chat.id == id) | ||||
|             return ChatModel(**model_to_dict(chat)) | ||||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def get_chat_lists_by_user_id( | ||||
|         self, user_id: str, skip: int = 0, limit: int = 50 | ||||
|     ) -> List[ChatModel]: | ||||
|  | @ -131,6 +199,13 @@ class ChatTable: | |||
|             .order_by(Chat.timestamp.desc()) | ||||
|         ] | ||||
| 
 | ||||
|     def get_chat_by_id(self, id: str) -> Optional[ChatModel]: | ||||
|         try: | ||||
|             chat = Chat.get(Chat.id == id) | ||||
|             return ChatModel(**model_to_dict(chat)) | ||||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: | ||||
|         try: | ||||
|             chat = Chat.get(Chat.id == id, Chat.user_id == user_id) | ||||
|  | @ -149,12 +224,15 @@ class ChatTable: | |||
|             query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) | ||||
|             query.execute()  # Remove the rows, return number of rows removed. | ||||
| 
 | ||||
|             return True | ||||
|             return True and self.delete_shared_chat_by_chat_id(id) | ||||
|         except: | ||||
|             return False | ||||
| 
 | ||||
|     def delete_chats_by_user_id(self, user_id: str) -> bool: | ||||
|         try: | ||||
| 
 | ||||
|             self.delete_shared_chats_by_user_id(user_id) | ||||
| 
 | ||||
|             query = Chat.delete().where(Chat.user_id == user_id) | ||||
|             query.execute()  # Remove the rows, return number of rows removed. | ||||
| 
 | ||||
|  | @ -162,5 +240,19 @@ class ChatTable: | |||
|         except: | ||||
|             return False | ||||
| 
 | ||||
|     def delete_shared_chats_by_user_id(self, user_id: str) -> bool: | ||||
|         try: | ||||
|             shared_chat_ids = [ | ||||
|                 f"shared-{chat.id}" | ||||
|                 for chat in Chat.select().where(Chat.user_id == user_id) | ||||
|             ] | ||||
| 
 | ||||
|             query = Chat.delete().where(Chat.user_id << shared_chat_ids) | ||||
|             query.execute()  # Remove the rows, return number of rows removed. | ||||
| 
 | ||||
|             return True | ||||
|         except: | ||||
|             return False | ||||
| 
 | ||||
| 
 | ||||
| Chats = ChatTable(DB) | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ from utils.utils import ( | |||
| from utils.misc import parse_duration, validate_email_format | ||||
| from utils.webhook import post_webhook | ||||
| from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES | ||||
| from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER | ||||
| 
 | ||||
| router = APIRouter() | ||||
| 
 | ||||
|  | @ -77,6 +78,8 @@ async def update_profile( | |||
| async def update_password( | ||||
|     form_data: UpdatePasswordForm, session_user=Depends(get_current_user) | ||||
| ): | ||||
|     if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: | ||||
|         raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) | ||||
|     if session_user: | ||||
|         user = Auths.authenticate_user(session_user.email, form_data.password) | ||||
| 
 | ||||
|  | @ -96,7 +99,22 @@ async def update_password( | |||
| 
 | ||||
| @router.post("/signin", response_model=SigninResponse) | ||||
| 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) | ||||
| 
 | ||||
|     if user: | ||||
|         token = create_token( | ||||
|             data={"id": user.id}, | ||||
|  |  | |||
|  | @ -189,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_ | |||
|     return result | ||||
| 
 | ||||
| 
 | ||||
| ############################ | ||||
| # ShareChatById | ||||
| ############################ | ||||
| 
 | ||||
| 
 | ||||
| @router.post("/{id}/share", response_model=Optional[ChatResponse]) | ||||
| async def share_chat_by_id(id: str, user=Depends(get_current_user)): | ||||
|     chat = Chats.get_chat_by_id_and_user_id(id, user.id) | ||||
|     if chat: | ||||
|         if chat.share_id: | ||||
|             shared_chat = Chats.update_shared_chat_by_chat_id(chat.id) | ||||
|             return ChatResponse( | ||||
|                 **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} | ||||
|             ) | ||||
| 
 | ||||
|         shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id) | ||||
|         if not shared_chat: | ||||
|             raise HTTPException( | ||||
|                 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||||
|                 detail=ERROR_MESSAGES.DEFAULT(), | ||||
|             ) | ||||
| 
 | ||||
|         return ChatResponse( | ||||
|             **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} | ||||
|         ) | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_401_UNAUTHORIZED, | ||||
|             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| ############################ | ||||
| # DeletedSharedChatById | ||||
| ############################ | ||||
| 
 | ||||
| 
 | ||||
| @router.delete("/{id}/share", response_model=Optional[bool]) | ||||
| async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)): | ||||
|     chat = Chats.get_chat_by_id_and_user_id(id, user.id) | ||||
|     if chat: | ||||
|         if not chat.share_id: | ||||
|             return False | ||||
| 
 | ||||
|         result = Chats.delete_shared_chat_by_chat_id(id) | ||||
|         update_result = Chats.update_chat_share_id_by_id(id, None) | ||||
| 
 | ||||
|         return result and update_result != None | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_401_UNAUTHORIZED, | ||||
|             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| ############################ | ||||
| # GetSharedChatById | ||||
| ############################ | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/share/{share_id}", response_model=Optional[ChatResponse]) | ||||
| async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): | ||||
|     chat = Chats.get_chat_by_id(share_id) | ||||
| 
 | ||||
|     if chat: | ||||
|         return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| ############################ | ||||
| # GetChatTagsById | ||||
| ############################ | ||||
|  |  | |||
|  | @ -362,6 +362,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100") | |||
| #################################### | ||||
| 
 | ||||
| WEBUI_AUTH = True | ||||
| WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get( | ||||
|     "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None | ||||
| ) | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI_SECRET_KEY | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ class ERROR_MESSAGES(str, Enum): | |||
|     ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." | ||||
|     CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." | ||||
|     DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot." | ||||
|     EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again." | ||||
|     EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." | ||||
|     USERNAME_TAKEN = ( | ||||
|         "Uh-oh! This username is already registered. Please choose another username." | ||||
|  | @ -36,6 +37,7 @@ class ERROR_MESSAGES(str, Enum): | |||
|     INVALID_PASSWORD = ( | ||||
|         "The password provided is incorrect. Please check for typos and try again." | ||||
|     ) | ||||
|     INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance." | ||||
|     UNAUTHORIZED = "401 Unauthorized" | ||||
|     ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." | ||||
|     ACTION_PROHIBITED = ( | ||||
|  |  | |||
|  | @ -62,6 +62,21 @@ class SPAStaticFiles(StaticFiles): | |||
|                 raise ex | ||||
| 
 | ||||
| 
 | ||||
| print( | ||||
|     f""" | ||||
|   ___                    __        __   _     _   _ ___  | ||||
|  / _ \ _ __   ___ _ __   \ \      / /__| |__ | | | |_ _| | ||||
| | | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || |  | ||||
| | |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || |  | ||||
|  \___/| .__/ \___|_| |_|    \_/\_/ \___|_.__/ \___/|___| | ||||
|       |_|                                                | ||||
| 
 | ||||
|        | ||||
| v{VERSION} - building the best open-source AI user interface.       | ||||
| https://github.com/open-webui/open-webui | ||||
| """ | ||||
| ) | ||||
| 
 | ||||
| app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) | ||||
| 
 | ||||
| app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED | ||||
|  | @ -164,18 +179,22 @@ app.mount("/rag/api/v1", rag_app) | |||
| 
 | ||||
| @app.get("/api/config") | ||||
| async def get_app_config(): | ||||
|     # Checking and Handling the Absence of 'ui' in CONFIG_DATA | ||||
| 
 | ||||
|     default_locale = "en-US" | ||||
|     if "ui" in CONFIG_DATA: | ||||
|         default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US") | ||||
| 
 | ||||
|     # The Rest of the Function Now Uses the Variables Defined Above | ||||
|     return { | ||||
|         "status": True, | ||||
|         "name": WEBUI_NAME, | ||||
|         "version": VERSION, | ||||
|         "default_locale": ( | ||||
|             CONFIG_DATA["ui"]["default_locale"] | ||||
|             if "ui" in CONFIG_DATA and "default_locale" in CONFIG_DATA["ui"] | ||||
|             else "en-US" | ||||
|         ), | ||||
|         "default_locale": default_locale, | ||||
|         "images": images_app.state.ENABLED, | ||||
|         "default_models": webui_app.state.DEFAULT_MODELS, | ||||
|         "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, | ||||
|         "trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER), | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ uuid | |||
| requests | ||||
| aiohttp | ||||
| peewee | ||||
| peewee-migrate | ||||
| bcrypt | ||||
| 
 | ||||
| litellm==1.30.7 | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
| 		"lint:types": "npm run check", | ||||
| 		"lint:backend": "pylint backend/", | ||||
| 		"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'", | ||||
| 		"format:backend": "black . --exclude \"/venv/\"", | ||||
| 		"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
|  |  | |||
|  | @ -78,3 +78,7 @@ select { | |||
| 	/* for Chrome */ | ||||
| 	-webkit-appearance: none; | ||||
| } | ||||
| 
 | ||||
| .katex-mathml { | ||||
| 	display: none; | ||||
| } | ||||
|  |  | |||
|  | @ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => { | |||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const getChatByShareId = async (token: string, share_id: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const shareChatById = async (token: string, id: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const deleteSharedChatById = async (token: string, id: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { | ||||
| 		method: 'DELETE', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const updateChatById = async (token: string, id: string, chat: object) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let chatId = ''; | ||||
| 	export let readOnly = false; | ||||
| 	export let sendPrompt: Function; | ||||
| 	export let continueGeneration: Function; | ||||
| 	export let regenerateResponse: Function; | ||||
|  | @ -317,6 +318,7 @@ | |||
| 							<UserMessage | ||||
| 								on:delete={() => messageDeleteHandler(message.id)} | ||||
| 								user={$user} | ||||
| 								{readOnly} | ||||
| 								{message} | ||||
| 								isFirstMessage={messageIdx === 0} | ||||
| 								siblings={message.parentId !== null | ||||
|  | @ -335,6 +337,7 @@ | |||
| 								modelfiles={selectedModelfiles} | ||||
| 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | ||||
| 								isLastMessage={messageIdx + 1 === messages.length} | ||||
| 								{readOnly} | ||||
| 								{confirmEditResponseMessage} | ||||
| 								{showPreviousMessage} | ||||
| 								{showNextMessage} | ||||
|  |  | |||
|  | @ -33,6 +33,8 @@ | |||
| 
 | ||||
| 	export let isLastMessage = true; | ||||
| 
 | ||||
| 	export let readOnly = false; | ||||
| 
 | ||||
| 	export let confirmEditResponseMessage: Function; | ||||
| 	export let showPreviousMessage: Function; | ||||
| 	export let showNextMessage: Function; | ||||
|  | @ -422,7 +424,7 @@ | |||
| 										class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" | ||||
| 									> | ||||
| 										{#if siblings.length > 1} | ||||
| 											<div class="flex self-center min-w-fit -mt-1"> | ||||
| 											<div class="flex self-center min-w-fit"> | ||||
| 												<button | ||||
| 													class="self-center dark:hover:text-white hover:text-black transition" | ||||
| 													on:click={() => { | ||||
|  | @ -469,6 +471,7 @@ | |||
| 											</div> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										{#if !readOnly} | ||||
| 											<Tooltip content="Edit" placement="bottom"> | ||||
| 												<button | ||||
| 													class="{isLastMessage | ||||
|  | @ -494,6 +497,7 @@ | |||
| 													</svg> | ||||
| 												</button> | ||||
| 											</Tooltip> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										<Tooltip content="Copy" placement="bottom"> | ||||
| 											<button | ||||
|  | @ -521,6 +525,7 @@ | |||
| 											</button> | ||||
| 										</Tooltip> | ||||
| 
 | ||||
| 										{#if !readOnly} | ||||
| 											<Tooltip content="Good Response" placement="bottom"> | ||||
| 												<button | ||||
| 													class="{isLastMessage | ||||
|  | @ -574,6 +579,7 @@ | |||
| 													> | ||||
| 												</button> | ||||
| 											</Tooltip> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										<Tooltip content="Read Aloud" placement="bottom"> | ||||
| 											<button | ||||
|  | @ -656,7 +662,7 @@ | |||
| 											</button> | ||||
| 										</Tooltip> | ||||
| 
 | ||||
| 										{#if $config.images} | ||||
| 										{#if $config.images && !readOnly} | ||||
| 											<Tooltip content="Generate Image" placement="bottom"> | ||||
| 												<button | ||||
| 													class="{isLastMessage | ||||
|  | @ -752,7 +758,7 @@ | |||
| 											</Tooltip> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										{#if isLastMessage} | ||||
| 										{#if isLastMessage && !readOnly} | ||||
| 											<Tooltip content="Continue Response" placement="bottom"> | ||||
| 												<button | ||||
| 													type="button" | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
| 	export let message; | ||||
| 	export let siblings; | ||||
| 	export let isFirstMessage: boolean; | ||||
| 	export let readOnly: boolean; | ||||
| 
 | ||||
| 	export let confirmEditMessage: Function; | ||||
| 	export let showPreviousMessage: Function; | ||||
|  | @ -203,7 +204,7 @@ | |||
| 
 | ||||
| 					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500"> | ||||
| 						{#if siblings.length > 1} | ||||
| 							<div class="flex self-center -mt-1"> | ||||
| 							<div class="flex self-center"> | ||||
| 								<button | ||||
| 									class="self-center dark:hover:text-white hover:text-black transition" | ||||
| 									on:click={() => { | ||||
|  | @ -250,6 +251,7 @@ | |||
| 							</div> | ||||
| 						{/if} | ||||
| 
 | ||||
| 						{#if !readOnly} | ||||
| 							<Tooltip content="Edit" placement="bottom"> | ||||
| 								<button | ||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" | ||||
|  | @ -273,6 +275,7 @@ | |||
| 									</svg> | ||||
| 								</button> | ||||
| 							</Tooltip> | ||||
| 						{/if} | ||||
| 
 | ||||
| 						<Tooltip content="Copy" placement="bottom"> | ||||
| 							<button | ||||
|  | @ -298,7 +301,7 @@ | |||
| 							</button> | ||||
| 						</Tooltip> | ||||
| 
 | ||||
| 						{#if !isFirstMessage} | ||||
| 						{#if !isFirstMessage && !readOnly} | ||||
| 							<Tooltip content="Delete" placement="bottom"> | ||||
| 								<button | ||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | ||||
|  |  | |||
|  | @ -258,6 +258,9 @@ | |||
| 					console.log(error); | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			const error = await fileResponse?.json(); | ||||
| 			toast.error(error?.detail ?? error); | ||||
| 		} | ||||
| 
 | ||||
| 		if (uploaded) { | ||||
|  |  | |||
|  | @ -1,19 +1,138 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 	import { getContext, onMount } from 'svelte'; | ||||
| 
 | ||||
| 	import fileSaver from 'file-saver'; | ||||
| 	const { saveAs } = fileSaver; | ||||
| 
 | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; | ||||
| 	import { chatId, modelfiles } from '$lib/stores'; | ||||
| 	import { copyToClipboard } from '$lib/utils'; | ||||
| 
 | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 	import Link from '../icons/Link.svelte'; | ||||
| 
 | ||||
| 	let chat = null; | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let downloadChat: Function; | ||||
| 	export let shareChat: Function; | ||||
| 	const shareLocalChat = async () => { | ||||
| 		const _chat = chat; | ||||
| 
 | ||||
| 		const sharedChat = await shareChatById(localStorage.token, $chatId); | ||||
| 		const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`; | ||||
| 
 | ||||
| 		toast.success($i18n.t('Copied shared chat URL to clipboard!')); | ||||
| 		copyToClipboard(chatShareUrl); | ||||
| 		chat = await getChatById(localStorage.token, $chatId); | ||||
| 	}; | ||||
| 
 | ||||
| 	const shareChat = async () => { | ||||
| 		const _chat = chat.chat; | ||||
| 		console.log('share', _chat); | ||||
| 
 | ||||
| 		toast.success($i18n.t('Redirecting you to OpenWebUI Community')); | ||||
| 		const url = 'https://openwebui.com'; | ||||
| 		// const url = 'http://localhost:5173'; | ||||
| 
 | ||||
| 		const tab = await window.open(`${url}/chats/upload`, '_blank'); | ||||
| 		window.addEventListener( | ||||
| 			'message', | ||||
| 			(event) => { | ||||
| 				if (event.origin !== url) return; | ||||
| 				if (event.data === 'loaded') { | ||||
| 					tab.postMessage( | ||||
| 						JSON.stringify({ | ||||
| 							chat: _chat, | ||||
| 							modelfiles: $modelfiles.filter((modelfile) => | ||||
| 								_chat.models.includes(modelfile.tagName) | ||||
| 							) | ||||
| 						}), | ||||
| 						'*' | ||||
| 					); | ||||
| 				} | ||||
| 			}, | ||||
| 			false | ||||
| 		); | ||||
| 	}; | ||||
| 
 | ||||
| 	const downloadChat = async () => { | ||||
| 		const _chat = chat.chat; | ||||
| 		console.log('download', chat); | ||||
| 
 | ||||
| 		const chatText = _chat.messages.reduce((a, message, i, arr) => { | ||||
| 			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; | ||||
| 		}, ''); | ||||
| 
 | ||||
| 		let blob = new Blob([chatText], { | ||||
| 			type: 'text/plain' | ||||
| 		}); | ||||
| 
 | ||||
| 		saveAs(blob, `chat-${_chat.title}.txt`); | ||||
| 	}; | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		chatId.subscribe(async (value) => { | ||||
| 			chat = await getChatById(localStorage.token, value); | ||||
| 			console.log(chat); | ||||
| 		}); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <Modal bind:show size="xs"> | ||||
| 	<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> | ||||
| <Modal bind:show size="sm"> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div> | ||||
| 			<button | ||||
| 			class=" self-center 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" | ||||
| 								on:click={() => { | ||||
| 									shareChat(); | ||||
|  | @ -23,11 +142,27 @@ | |||
| 								{$i18n.t('Share to OpenWebUI Community')} | ||||
| 							</button> | ||||
| 
 | ||||
| 		<div class="flex justify-center space-x-1 mt-1.5"> | ||||
| 			<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div> | ||||
| 
 | ||||
| 							<button | ||||
| 				class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" | ||||
| 								class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white" | ||||
| 								type="button" | ||||
| 								on:click={() => { | ||||
| 									shareLocalChat(); | ||||
| 									show = false; | ||||
| 								}} | ||||
| 							> | ||||
| 								<Link /> | ||||
| 
 | ||||
| 								{#if chat.share_id} | ||||
| 									{$i18n.t('Update and Copy Link')} | ||||
| 								{:else} | ||||
| 									{$i18n.t('Copy Link')} | ||||
| 								{/if} | ||||
| 							</button> | ||||
| 						</div> | ||||
| 						<div class="flex gap-1 mt-1.5"> | ||||
| 							<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div> | ||||
| 							<button | ||||
| 								class=" text-right rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" | ||||
| 								type="button" | ||||
| 								on:click={() => { | ||||
| 									downloadChat(); | ||||
|  | @ -38,4 +173,8 @@ | |||
| 							</button> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </Modal> | ||||
|  |  | |||
|  | @ -1,24 +1,25 @@ | |||
| <script lang="ts"> | ||||
| 	export let className: string = 'text-white'; | ||||
| 	export let theme: 'blue' | 'white' | 'black' = 'white'; | ||||
| 	export let className: string = ''; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex justify-center text-center {className}"> | ||||
| 	<svg | ||||
| 		class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue' | ||||
| 			? 'text-sky-600' | ||||
| 			: theme === 'white' | ||||
| 			? 'text-white' | ||||
| 			: 'text-gray-600'} " | ||||
| 		xmlns="http://www.w3.org/2000/svg" | ||||
| 		fill="none" | ||||
| 		viewBox="0 0 24 24" | ||||
| 	<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" | ||||
| 		><style> | ||||
| 			.spinner_ajPY { | ||||
| 				transform-origin: center; | ||||
| 				animation: spinner_AtaB 0.75s infinite linear; | ||||
| 			} | ||||
| 			@keyframes spinner_AtaB { | ||||
| 				100% { | ||||
| 					transform: rotate(360deg); | ||||
| 				} | ||||
| 			} | ||||
| 		</style><path | ||||
| 			d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||
| 			opacity=".25" | ||||
| 		/><path | ||||
| 			d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||
| 			class="spinner_ajPY" | ||||
| 		/></svg | ||||
| 	> | ||||
| 		<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> | ||||
| 		<path | ||||
| 			class="opacity-75" | ||||
| 			fill="currentColor" | ||||
| 			d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" | ||||
| 		/> | ||||
| 	</svg> | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										16
									
								
								src/lib/components/icons/Link.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/components/icons/Link.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| <script lang="ts"> | ||||
| 	export let className = 'w-4 h-4'; | ||||
| </script> | ||||
| 
 | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}> | ||||
| 	<path | ||||
| 		fill-rule="evenodd" | ||||
| 		d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z" | ||||
| 		clip-rule="evenodd" | ||||
| 	/> | ||||
| 	<path | ||||
| 		fill-rule="evenodd" | ||||
| 		d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z" | ||||
| 		clip-rule="evenodd" | ||||
| 	/> | ||||
| </svg> | ||||
|  | @ -1,11 +1,9 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import fileSaver from 'file-saver'; | ||||
| 	const { saveAs } = fileSaver; | ||||
| 
 | ||||
| 	import { Separator } from 'bits-ui'; | ||||
| 	import { getChatById } from '$lib/apis/chats'; | ||||
| 	import { getChatById, shareChatById } from '$lib/apis/chats'; | ||||
| 	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; | ||||
| 
 | ||||
| 	import { slide } from 'svelte/transition'; | ||||
|  | @ -32,55 +30,13 @@ | |||
| 	export let addTag: Function; | ||||
| 	export let deleteTag: Function; | ||||
| 
 | ||||
| 	export let showModelSelector = false; | ||||
| 	export let showModelSelector = true; | ||||
| 
 | ||||
| 	let showShareChatModal = false; | ||||
| 	let showTagChatModal = false; | ||||
| 
 | ||||
| 	const shareChat = async () => { | ||||
| 		const chat = (await getChatById(localStorage.token, $chatId)).chat; | ||||
| 		console.log('share', chat); | ||||
| 
 | ||||
| 		toast.success($i18n.t('Redirecting you to OpenWebUI Community')); | ||||
| 		const url = 'https://openwebui.com'; | ||||
| 		// const url = 'http://localhost:5173'; | ||||
| 
 | ||||
| 		const tab = await window.open(`${url}/chats/upload`, '_blank'); | ||||
| 		window.addEventListener( | ||||
| 			'message', | ||||
| 			(event) => { | ||||
| 				if (event.origin !== url) return; | ||||
| 				if (event.data === 'loaded') { | ||||
| 					tab.postMessage( | ||||
| 						JSON.stringify({ | ||||
| 							chat: chat, | ||||
| 							modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName)) | ||||
| 						}), | ||||
| 						'*' | ||||
| 					); | ||||
| 				} | ||||
| 			}, | ||||
| 			false | ||||
| 		); | ||||
| 	}; | ||||
| 
 | ||||
| 	const downloadChat = async () => { | ||||
| 		const chat = (await getChatById(localStorage.token, $chatId)).chat; | ||||
| 		console.log('download', chat); | ||||
| 
 | ||||
| 		const chatText = chat.messages.reduce((a, message, i, arr) => { | ||||
| 			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; | ||||
| 		}, ''); | ||||
| 
 | ||||
| 		let blob = new Blob([chatText], { | ||||
| 			type: 'text/plain' | ||||
| 		}); | ||||
| 
 | ||||
| 		saveAs(blob, `chat-${chat.title}.txt`); | ||||
| 	}; | ||||
| </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"> | ||||
| 	<div | ||||
|  | @ -135,12 +91,14 @@ | |||
| 		</div> --> | ||||
| 
 | ||||
| 		<div class="flex items-center w-full max-w-full"> | ||||
| 			<div class="w-full flex-1 overflow-hidden max-w-full"> | ||||
| 			<div class="flex-1 overflow-hidden max-w-full"> | ||||
| 				{#if showModelSelector} | ||||
| 					<ModelSelector bind:selectedModels /> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="self-start flex flex-none items-center"> | ||||
| 				<div class="flex self-center w-[1px] h-5 mx-2 bg-stone-700" /> | ||||
| 				<div class="flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> | ||||
| 
 | ||||
| 				{#if !shareEnabled} | ||||
| 					<Tooltip content="Settings"> | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 	"(latest)": "(nieuwste)", | ||||
| 	"{{modelName}} is thinking...": "{{modelName}} is aan het denken...", | ||||
| 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht", | ||||
| 	"a user": "", | ||||
| 	"a user": "een gebruiker", | ||||
| 	"About": "Over", | ||||
| 	"Account": "Account", | ||||
| 	"Action": "Actie", | ||||
|  | @ -35,12 +35,12 @@ | |||
| 	"API Key": "API Key", | ||||
| 	"API RPM": "API RPM", | ||||
| 	"are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen", | ||||
| 	"Are you sure?": "", | ||||
| 	"Are you sure?": "Zeker weten?", | ||||
| 	"Audio": "Audio", | ||||
| 	"Auto-playback response": "Automatisch afspelen van antwoord", | ||||
| 	"Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.", | ||||
| 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", | ||||
| 	"AUTOMATIC1111 Base URL is required.": "", | ||||
| 	"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Basis URL is verplicht", | ||||
| 	"available!": "beschikbaar!", | ||||
| 	"Back": "Terug", | ||||
| 	"Builder Mode": "Bouwer Modus", | ||||
|  | @ -60,8 +60,8 @@ | |||
| 	"Chunk Size": "Chunk Grootte", | ||||
| 	"Click here for help.": "Klik hier voor help.", | ||||
| 	"Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.", | ||||
| 	"Click here to select": "", | ||||
| 	"Click here to select documents.": "", | ||||
| 	"Click here to select": "Klik hier om te selecteren", | ||||
| 	"Click here to select documents.": "Klik hier om documenten te selecteren", | ||||
| 	"click here.": "click here.", | ||||
| 	"Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.", | ||||
| 	"Close": "Sluiten", | ||||
|  | @ -88,7 +88,7 @@ | |||
| 	"Database": "Database", | ||||
| 	"DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm", | ||||
| 	"Default": "Standaard", | ||||
| 	"Default (Automatic1111)": "", | ||||
| 	"Default (Automatic1111)": "Standaard (Automatic1111)", | ||||
| 	"Default (Web API)": "Standaard (Web API)", | ||||
| 	"Default model updated": "Standaard model bijgewerkt", | ||||
| 	"Default Prompt Suggestions": "Standaard Prompt Suggesties", | ||||
|  | @ -123,21 +123,21 @@ | |||
| 	"Enable Chat History": "Schakel Chat Geschiedenis in", | ||||
| 	"Enable New Sign Ups": "Schakel Nieuwe Registraties in", | ||||
| 	"Enabled": "Ingeschakeld", | ||||
| 	"Enter {{role}} message here": "", | ||||
| 	"Enter API Key": "", | ||||
| 	"Enter Chunk Overlap": "", | ||||
| 	"Enter Chunk Size": "", | ||||
| 	"Enter Image Size (e.g. 512x512)": "", | ||||
| 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "", | ||||
| 	"Enter LiteLLM API Key (litellm_params.api_key)": "", | ||||
| 	"Enter LiteLLM API RPM (litellm_params.rpm)": "", | ||||
| 	"Enter LiteLLM Model (litellm_params.model)": "", | ||||
| 	"Enter Max Tokens (litellm_params.max_tokens)": "", | ||||
| 	"Enter model tag (e.g. {{modelTag}})": "", | ||||
| 	"Enter Number of Steps (e.g. 50)": "", | ||||
| 	"Enter {{role}} message here": "Voeg {{role}} bericht hier toe", | ||||
| 	"Enter API Key": "Voeg API Key toe", | ||||
| 	"Enter Chunk Overlap": "Voeg Chunk Overlap toe", | ||||
| 	"Enter Chunk Size": "Voeg Chunk Size toe", | ||||
| 	"Enter Image Size (e.g. 512x512)": "Voeg afbeelding formaat toe (Bijv. 512x512)", | ||||
| 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "Voeg LiteLLM API Base URL toe (litellm_params.api_base)", | ||||
| 	"Enter LiteLLM API Key (litellm_params.api_key)": "Voeg LiteLLM API Sleutel toe (litellm_params.api_key)", | ||||
| 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Voeg LiteLLM API RPM toe (litellm_params.rpm)", | ||||
| 	"Enter LiteLLM Model (litellm_params.model)": "Voeg LiteLLM Model toe (litellm_params.model)", | ||||
| 	"Enter Max Tokens (litellm_params.max_tokens)": "Voeg maximum aantal tokens toe (litellm_params.max_tokens)", | ||||
| 	"Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})", | ||||
| 	"Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)", | ||||
| 	"Enter stop sequence": "Zet stop sequentie", | ||||
| 	"Enter Top K": "", | ||||
| 	"Enter URL (e.g. http://127.0.0.1:7860/)": "", | ||||
| 	"Enter Top K": "Voeg Top K toe", | ||||
| 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Zet URL (Bijv. http://127.0.0.1:7860/)", | ||||
| 	"Enter Your Email": "Voer je Email in", | ||||
| 	"Enter Your Full Name": "Voer je Volledige Naam in", | ||||
| 	"Enter Your Password": "Voer je Wachtwoord in", | ||||
|  | @ -161,7 +161,7 @@ | |||
| 	"Hide Additional Params": "Verberg Extra Params", | ||||
| 	"How can I help you today?": "Hoe kan ik je vandaag helpen?", | ||||
| 	"Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)", | ||||
| 	"Image Generation Engine": "", | ||||
| 	"Image Generation Engine": "Afbeelding Generatie Engine", | ||||
| 	"Image Settings": "Afbeelding Instellingen", | ||||
| 	"Images": "Afbeeldingen", | ||||
| 	"Import Chats": "Importeer Chats", | ||||
|  | @ -217,7 +217,7 @@ | |||
| 	"Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar", | ||||
| 	"Off": "Uit", | ||||
| 	"Okay, Let's Go!": "Okay, Laten we gaan!", | ||||
| 	"Ollama Base URL": "", | ||||
| 	"Ollama Base URL": "Ollama Basis URL", | ||||
| 	"Ollama Version": "Ollama Versie", | ||||
| 	"On": "Aan", | ||||
| 	"Only": "Alleen", | ||||
|  | @ -227,11 +227,11 @@ | |||
| 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.", | ||||
| 	"Open": "Open", | ||||
| 	"Open AI": "Open AI", | ||||
| 	"Open AI (Dall-E)": "", | ||||
| 	"Open AI (Dall-E)": "Open AI (Dall-E)", | ||||
| 	"Open new chat": "Open nieuwe chat", | ||||
| 	"OpenAI API": "OpenAI API", | ||||
| 	"OpenAI API Key": "", | ||||
| 	"OpenAI API Key is required.": "", | ||||
| 	"OpenAI API Key": "OpenAI API Sleutel", | ||||
| 	"OpenAI API Key is required.": "OpenAI API Sleutel is verplicht", | ||||
| 	"or": "of", | ||||
| 	"Parameters": "Parameters", | ||||
| 	"Password": "Wachtwoord", | ||||
|  | @ -273,9 +273,9 @@ | |||
| 	"See readme.md for instructions": "Zie readme.md voor instructies", | ||||
| 	"See what's new": "Zie wat er nieuw is", | ||||
| 	"Seed": "Seed", | ||||
| 	"Select a mode": "", | ||||
| 	"Select a mode": "Selecteer een modus", | ||||
| 	"Select a model": "Selecteer een model", | ||||
| 	"Select an Ollama instance": "", | ||||
| 	"Select an Ollama instance": "Selecteer een Ollama instantie", | ||||
| 	"Send a Message": "Stuur een Bericht", | ||||
| 	"Send message": "Stuur bericht", | ||||
| 	"Server connection verified": "Server verbinding geverifieerd", | ||||
|  | @ -330,7 +330,7 @@ | |||
| 	"Top P": "Top P", | ||||
| 	"Trouble accessing Ollama?": "Problemen met toegang tot Ollama?", | ||||
| 	"TTS Settings": "TTS instellingen", | ||||
| 	"Type Hugging Face Resolve (Download) URL": "", | ||||
| 	"Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL", | ||||
| 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.", | ||||
| 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst", | ||||
| 	"Update password": "Wijzig wachtwoord", | ||||
|  | @ -339,7 +339,7 @@ | |||
| 	"Upload Progress": "Upload Voortgang", | ||||
| 	"URL Mode": "URL Modus", | ||||
| 	"Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.", | ||||
| 	"Use Gravatar": "", | ||||
| 	"Use Gravatar": "Gebruik Gravatar", | ||||
| 	"user": "user", | ||||
| 	"User Permissions": "Gebruikers Rechten", | ||||
| 	"Users": "Gebruikers", | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ | |||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 	let currentRequestId = null; | ||||
| 
 | ||||
| 	let showModelSelector = false; | ||||
| 	let showModelSelector = true; | ||||
| 	let selectedModels = ['']; | ||||
| 
 | ||||
| 	let selectedModelfile = null; | ||||
|  | @ -520,11 +520,6 @@ | |||
| 	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { | ||||
| 		const responseMessage = history.messages[responseMessageId]; | ||||
| 
 | ||||
| 		// Wait until history/message have been updated | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		scrollToBottom(); | ||||
| 
 | ||||
| 		const docs = messages | ||||
| 			.filter((message) => message?.files ?? null) | ||||
| 			.map((message) => | ||||
|  | @ -593,6 +588,11 @@ | |||
| 				: `${OPENAI_API_BASE_URL}` | ||||
| 		); | ||||
| 
 | ||||
| 		// Wait until history/message have been updated | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		scrollToBottom(); | ||||
| 
 | ||||
| 		if (res && res.ok) { | ||||
| 			const reader = res.body | ||||
| 				.pipeThrough(new TextDecoderStream()) | ||||
|  |  | |||
|  | @ -56,7 +56,7 @@ | |||
| 	let currentRequestId = null; | ||||
| 
 | ||||
| 	// let chatId = $page.params.id; | ||||
| 	let showModelSelector = false; | ||||
| 	let showModelSelector = true; | ||||
| 	let selectedModels = ['']; | ||||
| 	let selectedModelfile = null; | ||||
| 
 | ||||
|  | @ -536,11 +536,6 @@ | |||
| 	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { | ||||
| 		const responseMessage = history.messages[responseMessageId]; | ||||
| 
 | ||||
| 		// Wait until history/message have been updated | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		scrollToBottom(); | ||||
| 
 | ||||
| 		const docs = messages | ||||
| 			.filter((message) => message?.files ?? null) | ||||
| 			.map((message) => | ||||
|  | @ -607,6 +602,11 @@ | |||
| 				: `${OPENAI_API_BASE_URL}` | ||||
| 		); | ||||
| 
 | ||||
| 		// Wait until history/message have been updated | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		scrollToBottom(); | ||||
| 
 | ||||
| 		if (res && res.ok) { | ||||
| 			const reader = res.body | ||||
| 				.pipeThrough(new TextDecoderStream()) | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <script> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { userSignIn, userSignUp, getApiKey } from '$lib/apis/auths'; | ||||
| 	import Spinner from '$lib/components/common/Spinner.svelte'; | ||||
| 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; | ||||
| 	import { WEBUI_NAME, config, user } from '$lib/stores'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
|  | @ -68,6 +69,9 @@ | |||
| 			await goto('/'); | ||||
| 		} | ||||
| 		loaded = true; | ||||
| 		if ($config?.trusted_header_auth ?? false) { | ||||
| 			await signInHandler(); | ||||
| 		} | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
|  | @ -102,6 +106,23 @@ | |||
| 		</div> --> | ||||
| 
 | ||||
| 		<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"> | ||||
| 					<form | ||||
| 						class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl" | ||||
|  | @ -196,6 +217,7 @@ | |||
| 						</div> | ||||
| 					</form> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
|  |  | |||
							
								
								
									
										180
									
								
								src/routes/s/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/routes/s/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,180 @@ | |||
| <script lang="ts"> | ||||
| 	import { onMount, tick, getContext } from 'svelte'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { page } from '$app/stores'; | ||||
| 
 | ||||
| 	import dayjs from 'dayjs'; | ||||
| 
 | ||||
| 	import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores'; | ||||
| 	import { convertMessagesToHistory } from '$lib/utils'; | ||||
| 
 | ||||
| 	import { getChatByShareId } from '$lib/apis/chats'; | ||||
| 
 | ||||
| 	import Messages from '$lib/components/chat/Messages.svelte'; | ||||
| 	import Navbar from '$lib/components/layout/Navbar.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	let loaded = false; | ||||
| 
 | ||||
| 	let autoScroll = true; | ||||
| 	let processing = ''; | ||||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 
 | ||||
| 	// let chatId = $page.params.id; | ||||
| 	let showModelSelector = false; | ||||
| 	let selectedModels = ['']; | ||||
| 
 | ||||
| 	let selectedModelfiles = {}; | ||||
| 	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => { | ||||
| 		const modelfile = | ||||
| 			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined; | ||||
| 
 | ||||
| 		return { | ||||
| 			...a, | ||||
| 			...(modelfile && { [tagName]: modelfile }) | ||||
| 		}; | ||||
| 	}, {}); | ||||
| 
 | ||||
| 	let chat = null; | ||||
| 
 | ||||
| 	let title = ''; | ||||
| 	let files = []; | ||||
| 
 | ||||
| 	let messages = []; | ||||
| 	let history = { | ||||
| 		messages: {}, | ||||
| 		currentId: null | ||||
| 	}; | ||||
| 
 | ||||
| 	$: if (history.currentId !== null) { | ||||
| 		let _messages = []; | ||||
| 
 | ||||
| 		let currentMessage = history.messages[history.currentId]; | ||||
| 		while (currentMessage !== null) { | ||||
| 			_messages.unshift({ ...currentMessage }); | ||||
| 			currentMessage = | ||||
| 				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null; | ||||
| 		} | ||||
| 		messages = _messages; | ||||
| 	} else { | ||||
| 		messages = []; | ||||
| 	} | ||||
| 
 | ||||
| 	$: if ($page.params.id) { | ||||
| 		(async () => { | ||||
| 			if (await loadSharedChat()) { | ||||
| 				await tick(); | ||||
| 				loaded = true; | ||||
| 
 | ||||
| 				window.setTimeout(() => scrollToBottom(), 0); | ||||
| 				const chatInput = document.getElementById('chat-textarea'); | ||||
| 				chatInput?.focus(); | ||||
| 			} else { | ||||
| 				await goto('/'); | ||||
| 			} | ||||
| 		})(); | ||||
| 	} | ||||
| 
 | ||||
| 	////////////////////////// | ||||
| 	// Web functions | ||||
| 	////////////////////////// | ||||
| 
 | ||||
| 	const loadSharedChat = async () => { | ||||
| 		await chatId.set($page.params.id); | ||||
| 		chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => { | ||||
| 			await goto('/'); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (chat) { | ||||
| 			const chatContent = chat.chat; | ||||
| 
 | ||||
| 			if (chatContent) { | ||||
| 				console.log(chatContent); | ||||
| 
 | ||||
| 				selectedModels = | ||||
| 					(chatContent?.models ?? undefined) !== undefined | ||||
| 						? chatContent.models | ||||
| 						: [chatContent.models ?? '']; | ||||
| 				history = | ||||
| 					(chatContent?.history ?? undefined) !== undefined | ||||
| 						? chatContent.history | ||||
| 						: convertMessagesToHistory(chatContent.messages); | ||||
| 				title = chatContent.title; | ||||
| 
 | ||||
| 				let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 				await settings.set({ | ||||
| 					..._settings, | ||||
| 					system: chatContent.system ?? _settings.system, | ||||
| 					options: chatContent.options ?? _settings.options | ||||
| 				}); | ||||
| 				autoScroll = true; | ||||
| 				await tick(); | ||||
| 
 | ||||
| 				if (messages.length > 0) { | ||||
| 					history.messages[messages.at(-1).id].done = true; | ||||
| 				} | ||||
| 				await tick(); | ||||
| 
 | ||||
| 				return true; | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <svelte:head> | ||||
| 	<title> | ||||
| 		{title | ||||
| 			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}` | ||||
| 			: `${$WEBUI_NAME}`} | ||||
| 	</title> | ||||
| </svelte:head> | ||||
| 
 | ||||
| {#if loaded} | ||||
| 	<div | ||||
| 		class="min-h-screen max-h-screen w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900" | ||||
| 	> | ||||
| 		<div class="flex flex-col flex-auto justify-center py-8"> | ||||
| 			<div class="px-3 w-full max-w-3xl mx-auto"> | ||||
| 				<div> | ||||
| 					<div class=" text-3xl font-semibold line-clamp-1"> | ||||
| 						{title} | ||||
| 					</div> | ||||
| 
 | ||||
| 					<div class=" mt-1 text-gray-400"> | ||||
| 						{dayjs(chat.chat.timestamp).format('MMMM D, YYYY')} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<hr class=" dark:border-gray-800 mt-6 mb-2" /> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div | ||||
| 				class=" flex flex-col justify-center w-full flex-auto overflow-auto h-0" | ||||
| 				id="messages-container" | ||||
| 			> | ||||
| 				<div class=" h-full w-full flex flex-col py-4"> | ||||
| 					<div class="py-2"> | ||||
| 						<Messages | ||||
| 							chatId={$chatId} | ||||
| 							readOnly={true} | ||||
| 							{selectedModels} | ||||
| 							{selectedModelfiles} | ||||
| 							{processing} | ||||
| 							bind:history | ||||
| 							bind:messages | ||||
| 							bind:autoScroll | ||||
| 							bottomPadding={files.length > 0} | ||||
| 							sendPrompt={() => {}} | ||||
| 							continueGeneration={() => {}} | ||||
| 							regenerateResponse={() => {}} | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek