mirror of
				https://github.com/open-webui/open-webui
				synced 2025-06-26 18:26:48 +00:00 
			
		
		
		
	Merge branch 'dev' into feature/support_auth_by_api_key
This commit is contained in:
		
						commit
						56369fea3a
					
				
							
								
								
									
										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):
 | 
			
		||||
    user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
 | 
			
		||||
    if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
 | 
			
		||||
        if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
 | 
			
		||||
            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
 | 
			
		||||
 | 
			
		||||
        trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
 | 
			
		||||
        if not Users.get_user_by_email(trusted_email.lower()):
 | 
			
		||||
            await signup(
 | 
			
		||||
                request,
 | 
			
		||||
                SignupForm(
 | 
			
		||||
                    email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        user = Auths.authenticate_user_by_trusted_header(trusted_email)
 | 
			
		||||
    else:
 | 
			
		||||
        user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
 | 
			
		||||
 | 
			
		||||
    if user:
 | 
			
		||||
        token = create_token(
 | 
			
		||||
            data={"id": user.id},
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -128,7 +130,7 @@
 | 
			
		||||
				// • auto-render specific keys, e.g.:
 | 
			
		||||
				delimiters: [
 | 
			
		||||
					{ left: '$$', right: '$$', display: false },
 | 
			
		||||
					{ left: '$', right: '$', display: false },
 | 
			
		||||
					{ left: '$ ', right: ' $', display: false },
 | 
			
		||||
					{ left: '\\(', right: '\\)', display: false },
 | 
			
		||||
					{ left: '\\[', right: '\\]', display: false },
 | 
			
		||||
					{ left: '[ ', right: ' ]', display: false }
 | 
			
		||||
@ -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,31 +471,33 @@
 | 
			
		||||
											</div>
 | 
			
		||||
										{/if}
 | 
			
		||||
 | 
			
		||||
										<Tooltip content="Edit" placement="bottom">
 | 
			
		||||
											<button
 | 
			
		||||
												class="{isLastMessage
 | 
			
		||||
													? 'visible'
 | 
			
		||||
													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
 | 
			
		||||
												on:click={() => {
 | 
			
		||||
													editMessageHandler();
 | 
			
		||||
												}}
 | 
			
		||||
											>
 | 
			
		||||
												<svg
 | 
			
		||||
													xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
													fill="none"
 | 
			
		||||
													viewBox="0 0 24 24"
 | 
			
		||||
													stroke-width="2"
 | 
			
		||||
													stroke="currentColor"
 | 
			
		||||
													class="w-4 h-4"
 | 
			
		||||
										{#if !readOnly}
 | 
			
		||||
											<Tooltip content="Edit" placement="bottom">
 | 
			
		||||
												<button
 | 
			
		||||
													class="{isLastMessage
 | 
			
		||||
														? 'visible'
 | 
			
		||||
														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
 | 
			
		||||
													on:click={() => {
 | 
			
		||||
														editMessageHandler();
 | 
			
		||||
													}}
 | 
			
		||||
												>
 | 
			
		||||
													<path
 | 
			
		||||
														stroke-linecap="round"
 | 
			
		||||
														stroke-linejoin="round"
 | 
			
		||||
														d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
 | 
			
		||||
													/>
 | 
			
		||||
												</svg>
 | 
			
		||||
											</button>
 | 
			
		||||
										</Tooltip>
 | 
			
		||||
													<svg
 | 
			
		||||
														xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
														fill="none"
 | 
			
		||||
														viewBox="0 0 24 24"
 | 
			
		||||
														stroke-width="2"
 | 
			
		||||
														stroke="currentColor"
 | 
			
		||||
														class="w-4 h-4"
 | 
			
		||||
													>
 | 
			
		||||
														<path
 | 
			
		||||
															stroke-linecap="round"
 | 
			
		||||
															stroke-linejoin="round"
 | 
			
		||||
															d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
 | 
			
		||||
														/>
 | 
			
		||||
													</svg>
 | 
			
		||||
												</button>
 | 
			
		||||
											</Tooltip>
 | 
			
		||||
										{/if}
 | 
			
		||||
 | 
			
		||||
										<Tooltip content="Copy" placement="bottom">
 | 
			
		||||
											<button
 | 
			
		||||
@ -521,59 +525,61 @@
 | 
			
		||||
											</button>
 | 
			
		||||
										</Tooltip>
 | 
			
		||||
 | 
			
		||||
										<Tooltip content="Good Response" placement="bottom">
 | 
			
		||||
											<button
 | 
			
		||||
												class="{isLastMessage
 | 
			
		||||
													? 'visible'
 | 
			
		||||
													: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
 | 
			
		||||
													? 'bg-gray-100 dark:bg-gray-800'
 | 
			
		||||
													: ''} dark:hover:text-white hover:text-black transition"
 | 
			
		||||
												on:click={() => {
 | 
			
		||||
													rateMessage(message.id, 1);
 | 
			
		||||
												}}
 | 
			
		||||
											>
 | 
			
		||||
												<svg
 | 
			
		||||
													stroke="currentColor"
 | 
			
		||||
													fill="none"
 | 
			
		||||
													stroke-width="2"
 | 
			
		||||
													viewBox="0 0 24 24"
 | 
			
		||||
													stroke-linecap="round"
 | 
			
		||||
													stroke-linejoin="round"
 | 
			
		||||
													class="w-4 h-4"
 | 
			
		||||
													xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
													><path
 | 
			
		||||
														d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
 | 
			
		||||
													/></svg
 | 
			
		||||
										{#if !readOnly}
 | 
			
		||||
											<Tooltip content="Good Response" placement="bottom">
 | 
			
		||||
												<button
 | 
			
		||||
													class="{isLastMessage
 | 
			
		||||
														? 'visible'
 | 
			
		||||
														: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
 | 
			
		||||
														? 'bg-gray-100 dark:bg-gray-800'
 | 
			
		||||
														: ''} dark:hover:text-white hover:text-black transition"
 | 
			
		||||
													on:click={() => {
 | 
			
		||||
														rateMessage(message.id, 1);
 | 
			
		||||
													}}
 | 
			
		||||
												>
 | 
			
		||||
											</button>
 | 
			
		||||
										</Tooltip>
 | 
			
		||||
													<svg
 | 
			
		||||
														stroke="currentColor"
 | 
			
		||||
														fill="none"
 | 
			
		||||
														stroke-width="2"
 | 
			
		||||
														viewBox="0 0 24 24"
 | 
			
		||||
														stroke-linecap="round"
 | 
			
		||||
														stroke-linejoin="round"
 | 
			
		||||
														class="w-4 h-4"
 | 
			
		||||
														xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
														><path
 | 
			
		||||
															d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
 | 
			
		||||
														/></svg
 | 
			
		||||
													>
 | 
			
		||||
												</button>
 | 
			
		||||
											</Tooltip>
 | 
			
		||||
 | 
			
		||||
										<Tooltip content="Bad Response" placement="bottom">
 | 
			
		||||
											<button
 | 
			
		||||
												class="{isLastMessage
 | 
			
		||||
													? 'visible'
 | 
			
		||||
													: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
 | 
			
		||||
													? 'bg-gray-100 dark:bg-gray-800'
 | 
			
		||||
													: ''} dark:hover:text-white hover:text-black transition"
 | 
			
		||||
												on:click={() => {
 | 
			
		||||
													rateMessage(message.id, -1);
 | 
			
		||||
												}}
 | 
			
		||||
											>
 | 
			
		||||
												<svg
 | 
			
		||||
													stroke="currentColor"
 | 
			
		||||
													fill="none"
 | 
			
		||||
													stroke-width="2"
 | 
			
		||||
													viewBox="0 0 24 24"
 | 
			
		||||
													stroke-linecap="round"
 | 
			
		||||
													stroke-linejoin="round"
 | 
			
		||||
													class="w-4 h-4"
 | 
			
		||||
													xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
													><path
 | 
			
		||||
														d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
 | 
			
		||||
													/></svg
 | 
			
		||||
											<Tooltip content="Bad Response" placement="bottom">
 | 
			
		||||
												<button
 | 
			
		||||
													class="{isLastMessage
 | 
			
		||||
														? 'visible'
 | 
			
		||||
														: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
 | 
			
		||||
														? 'bg-gray-100 dark:bg-gray-800'
 | 
			
		||||
														: ''} dark:hover:text-white hover:text-black transition"
 | 
			
		||||
													on:click={() => {
 | 
			
		||||
														rateMessage(message.id, -1);
 | 
			
		||||
													}}
 | 
			
		||||
												>
 | 
			
		||||
											</button>
 | 
			
		||||
										</Tooltip>
 | 
			
		||||
													<svg
 | 
			
		||||
														stroke="currentColor"
 | 
			
		||||
														fill="none"
 | 
			
		||||
														stroke-width="2"
 | 
			
		||||
														viewBox="0 0 24 24"
 | 
			
		||||
														stroke-linecap="round"
 | 
			
		||||
														stroke-linejoin="round"
 | 
			
		||||
														class="w-4 h-4"
 | 
			
		||||
														xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
														><path
 | 
			
		||||
															d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
 | 
			
		||||
														/></svg
 | 
			
		||||
													>
 | 
			
		||||
												</button>
 | 
			
		||||
											</Tooltip>
 | 
			
		||||
										{/if}
 | 
			
		||||
 | 
			
		||||
										<Tooltip content="Read Aloud" placement="bottom">
 | 
			
		||||
											<button
 | 
			
		||||
@ -656,7 +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,29 +251,31 @@
 | 
			
		||||
							</div>
 | 
			
		||||
						{/if}
 | 
			
		||||
 | 
			
		||||
						<Tooltip content="Edit" placement="bottom">
 | 
			
		||||
							<button
 | 
			
		||||
								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									editMessageHandler();
 | 
			
		||||
								}}
 | 
			
		||||
							>
 | 
			
		||||
								<svg
 | 
			
		||||
									xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
									fill="none"
 | 
			
		||||
									viewBox="0 0 24 24"
 | 
			
		||||
									stroke-width="2"
 | 
			
		||||
									stroke="currentColor"
 | 
			
		||||
									class="w-4 h-4"
 | 
			
		||||
						{#if !readOnly}
 | 
			
		||||
							<Tooltip content="Edit" placement="bottom">
 | 
			
		||||
								<button
 | 
			
		||||
									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
 | 
			
		||||
									on:click={() => {
 | 
			
		||||
										editMessageHandler();
 | 
			
		||||
									}}
 | 
			
		||||
								>
 | 
			
		||||
									<path
 | 
			
		||||
										stroke-linecap="round"
 | 
			
		||||
										stroke-linejoin="round"
 | 
			
		||||
										d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
 | 
			
		||||
									/>
 | 
			
		||||
								</svg>
 | 
			
		||||
							</button>
 | 
			
		||||
						</Tooltip>
 | 
			
		||||
									<svg
 | 
			
		||||
										xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
										fill="none"
 | 
			
		||||
										viewBox="0 0 24 24"
 | 
			
		||||
										stroke-width="2"
 | 
			
		||||
										stroke="currentColor"
 | 
			
		||||
										class="w-4 h-4"
 | 
			
		||||
									>
 | 
			
		||||
										<path
 | 
			
		||||
											stroke-linecap="round"
 | 
			
		||||
											stroke-linejoin="round"
 | 
			
		||||
											d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
 | 
			
		||||
										/>
 | 
			
		||||
									</svg>
 | 
			
		||||
								</button>
 | 
			
		||||
							</Tooltip>
 | 
			
		||||
						{/if}
 | 
			
		||||
 | 
			
		||||
						<Tooltip content="Copy" placement="bottom">
 | 
			
		||||
							<button
 | 
			
		||||
@ -298,7 +301,7 @@
 | 
			
		||||
							</button>
 | 
			
		||||
						</Tooltip>
 | 
			
		||||
 | 
			
		||||
						{#if !isFirstMessage}
 | 
			
		||||
						{#if !isFirstMessage && !readOnly}
 | 
			
		||||
							<Tooltip content="Delete" placement="bottom">
 | 
			
		||||
								<button
 | 
			
		||||
									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
 | 
			
		||||
 | 
			
		||||
@ -258,6 +258,9 @@
 | 
			
		||||
					console.log(error);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			const error = await fileResponse?.json();
 | 
			
		||||
			toast.error(error?.detail ?? error);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (uploaded) {
 | 
			
		||||
 | 
			
		||||
@ -1,41 +1,180 @@
 | 
			
		||||
<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">
 | 
			
		||||
		<button
 | 
			
		||||
			class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white"
 | 
			
		||||
			type="button"
 | 
			
		||||
			on:click={() => {
 | 
			
		||||
				shareChat();
 | 
			
		||||
				show = false;
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			{$i18n.t('Share to OpenWebUI Community')}
 | 
			
		||||
		</button>
 | 
			
		||||
 | 
			
		||||
		<div class="flex justify-center space-x-1 mt-1.5">
 | 
			
		||||
			<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
 | 
			
		||||
 | 
			
		||||
<Modal bind:show size="sm">
 | 
			
		||||
	<div>
 | 
			
		||||
		<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
 | 
			
		||||
			<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
 | 
			
		||||
			<button
 | 
			
		||||
				class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
 | 
			
		||||
				type="button"
 | 
			
		||||
				class="self-center"
 | 
			
		||||
				on:click={() => {
 | 
			
		||||
					downloadChat();
 | 
			
		||||
					show = false;
 | 
			
		||||
				}}
 | 
			
		||||
			>
 | 
			
		||||
				{$i18n.t('Download as a File')}
 | 
			
		||||
				<svg
 | 
			
		||||
					xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
					viewBox="0 0 20 20"
 | 
			
		||||
					fill="currentColor"
 | 
			
		||||
					class="w-5 h-5"
 | 
			
		||||
				>
 | 
			
		||||
					<path
 | 
			
		||||
						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
 | 
			
		||||
					/>
 | 
			
		||||
				</svg>
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<hr class=" dark:border-gray-800" />
 | 
			
		||||
 | 
			
		||||
		{#if chat}
 | 
			
		||||
			<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
 | 
			
		||||
				<div class=" text-sm dark:text-gray-300 mb-1">
 | 
			
		||||
					{#if chat.share_id}
 | 
			
		||||
						<a href="/s/{chat.share_id}" target="_blank"
 | 
			
		||||
							>You have shared this chat <span class=" underline">before</span>.</a
 | 
			
		||||
						>
 | 
			
		||||
						Click here to
 | 
			
		||||
						<button
 | 
			
		||||
							class="underline"
 | 
			
		||||
							on:click={async () => {
 | 
			
		||||
								const res = await deleteSharedChatById(localStorage.token, $chatId);
 | 
			
		||||
 | 
			
		||||
								if (res) {
 | 
			
		||||
									chat = await getChatById(localStorage.token, $chatId);
 | 
			
		||||
								}
 | 
			
		||||
							}}>delete this link</button
 | 
			
		||||
						> and create a new shared link.
 | 
			
		||||
					{:else}
 | 
			
		||||
						Messages you send after creating your link won't be shared. Users with the URL will be
 | 
			
		||||
						able to view the shared chat.
 | 
			
		||||
					{/if}
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="flex justify-end">
 | 
			
		||||
					<div class="flex flex-col items-end space-x-1 mt-1.5">
 | 
			
		||||
						<div class="flex gap-1">
 | 
			
		||||
							<button
 | 
			
		||||
								class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
 | 
			
		||||
								type="button"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									shareChat();
 | 
			
		||||
									show = false;
 | 
			
		||||
								}}
 | 
			
		||||
							>
 | 
			
		||||
								{$i18n.t('Share to OpenWebUI Community')}
 | 
			
		||||
							</button>
 | 
			
		||||
 | 
			
		||||
							<button
 | 
			
		||||
								class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
 | 
			
		||||
								type="button"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									shareLocalChat();
 | 
			
		||||
									show = false;
 | 
			
		||||
								}}
 | 
			
		||||
							>
 | 
			
		||||
								<Link />
 | 
			
		||||
 | 
			
		||||
								{#if chat.share_id}
 | 
			
		||||
									{$i18n.t('Update and Copy Link')}
 | 
			
		||||
								{:else}
 | 
			
		||||
									{$i18n.t('Copy Link')}
 | 
			
		||||
								{/if}
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div 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();
 | 
			
		||||
									show = false;
 | 
			
		||||
								}}
 | 
			
		||||
							>
 | 
			
		||||
								{$i18n.t('Download as a File')}
 | 
			
		||||
							</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">
 | 
			
		||||
				<ModelSelector bind:selectedModels />
 | 
			
		||||
			<div class="flex-1 overflow-hidden max-w-full">
 | 
			
		||||
				{#if showModelSelector}
 | 
			
		||||
					<ModelSelector bind:selectedModels />
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="self-start flex flex-none items-center">
 | 
			
		||||
				<div class="flex self-center w-[1px] h-5 mx-2 bg-stone-700" />
 | 
			
		||||
				<div class="flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
 | 
			
		||||
 | 
			
		||||
				{#if !shareEnabled}
 | 
			
		||||
					<Tooltip content="Settings">
 | 
			
		||||
 | 
			
		||||
@ -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,100 +106,118 @@
 | 
			
		||||
		</div> -->
 | 
			
		||||
 | 
			
		||||
		<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col">
 | 
			
		||||
			<div class=" my-auto pb-10 w-full">
 | 
			
		||||
				<form
 | 
			
		||||
					class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
 | 
			
		||||
					on:submit|preventDefault={() => {
 | 
			
		||||
						submitHandler();
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" text-xl sm:text-2xl font-bold">
 | 
			
		||||
						{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Sign up')}
 | 
			
		||||
						{$i18n.t('to')}
 | 
			
		||||
						{$WEBUI_NAME}
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					{#if mode === 'signup'}
 | 
			
		||||
						<div class=" mt-1 text-xs font-medium text-gray-500">
 | 
			
		||||
							ⓘ {$WEBUI_NAME}
 | 
			
		||||
							{$i18n.t(
 | 
			
		||||
								'does not make any external connections, and your data stays securely on your locally hosted server.'
 | 
			
		||||
							)}
 | 
			
		||||
			{#if $config?.trusted_header_auth ?? false}
 | 
			
		||||
				<div class=" my-auto pb-10 w-full">
 | 
			
		||||
					<div
 | 
			
		||||
						class="flex items-center justify-center gap-3 text-xl sm:text-2xl text-center font-bold dark:text-gray-200"
 | 
			
		||||
					>
 | 
			
		||||
						<div>
 | 
			
		||||
							{$i18n.t('Signing in')}
 | 
			
		||||
							{$i18n.t('to')}
 | 
			
		||||
							{$WEBUI_NAME}
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<Spinner />
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<div class=" my-auto pb-10 w-full">
 | 
			
		||||
					<form
 | 
			
		||||
						class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
 | 
			
		||||
						on:submit|preventDefault={() => {
 | 
			
		||||
							submitHandler();
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<div class=" text-xl sm:text-2xl font-bold">
 | 
			
		||||
							{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Sign up')}
 | 
			
		||||
							{$i18n.t('to')}
 | 
			
		||||
							{$WEBUI_NAME}
 | 
			
		||||
						</div>
 | 
			
		||||
					{/if}
 | 
			
		||||
 | 
			
		||||
					<div class="flex flex-col mt-4">
 | 
			
		||||
						{#if mode === 'signup'}
 | 
			
		||||
							<div>
 | 
			
		||||
								<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Name')}</div>
 | 
			
		||||
							<div class=" mt-1 text-xs font-medium text-gray-500">
 | 
			
		||||
								ⓘ {$WEBUI_NAME}
 | 
			
		||||
								{$i18n.t(
 | 
			
		||||
									'does not make any external connections, and your data stays securely on your locally hosted server.'
 | 
			
		||||
								)}
 | 
			
		||||
							</div>
 | 
			
		||||
						{/if}
 | 
			
		||||
 | 
			
		||||
						<div class="flex flex-col mt-4">
 | 
			
		||||
							{#if mode === 'signup'}
 | 
			
		||||
								<div>
 | 
			
		||||
									<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Name')}</div>
 | 
			
		||||
									<input
 | 
			
		||||
										bind:value={name}
 | 
			
		||||
										type="text"
 | 
			
		||||
										class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
 | 
			
		||||
										autocomplete="name"
 | 
			
		||||
										placeholder={$i18n.t('Enter Your Full Name')}
 | 
			
		||||
										required
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								<hr class=" my-3" />
 | 
			
		||||
							{/if}
 | 
			
		||||
 | 
			
		||||
							<div class="mb-2">
 | 
			
		||||
								<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Email')}</div>
 | 
			
		||||
								<input
 | 
			
		||||
									bind:value={name}
 | 
			
		||||
									type="text"
 | 
			
		||||
									bind:value={email}
 | 
			
		||||
									type="email"
 | 
			
		||||
									class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
 | 
			
		||||
									autocomplete="name"
 | 
			
		||||
									placeholder={$i18n.t('Enter Your Full Name')}
 | 
			
		||||
									autocomplete="email"
 | 
			
		||||
									placeholder={$i18n.t('Enter Your Email')}
 | 
			
		||||
									required
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<hr class=" my-3" />
 | 
			
		||||
						{/if}
 | 
			
		||||
 | 
			
		||||
						<div class="mb-2">
 | 
			
		||||
							<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Email')}</div>
 | 
			
		||||
							<input
 | 
			
		||||
								bind:value={email}
 | 
			
		||||
								type="email"
 | 
			
		||||
								class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
 | 
			
		||||
								autocomplete="email"
 | 
			
		||||
								placeholder={$i18n.t('Enter Your Email')}
 | 
			
		||||
								required
 | 
			
		||||
							/>
 | 
			
		||||
							<div>
 | 
			
		||||
								<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Password')}</div>
 | 
			
		||||
								<input
 | 
			
		||||
									bind:value={password}
 | 
			
		||||
									type="password"
 | 
			
		||||
									class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
 | 
			
		||||
									placeholder={$i18n.t('Enter Your Password')}
 | 
			
		||||
									autocomplete="current-password"
 | 
			
		||||
									required
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<div class=" text-sm font-semibold text-left mb-1">{$i18n.t('Password')}</div>
 | 
			
		||||
							<input
 | 
			
		||||
								bind:value={password}
 | 
			
		||||
								type="password"
 | 
			
		||||
								class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
 | 
			
		||||
								placeholder={$i18n.t('Enter Your Password')}
 | 
			
		||||
								autocomplete="current-password"
 | 
			
		||||
								required
 | 
			
		||||
							/>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="mt-5">
 | 
			
		||||
						<button
 | 
			
		||||
							class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition"
 | 
			
		||||
							type="submit"
 | 
			
		||||
						>
 | 
			
		||||
							{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Create Account')}
 | 
			
		||||
						</button>
 | 
			
		||||
 | 
			
		||||
						<div class=" mt-4 text-sm text-center">
 | 
			
		||||
							{mode === 'signin'
 | 
			
		||||
								? $i18n.t("Don't have an account?")
 | 
			
		||||
								: $i18n.t('Already have an account?')}
 | 
			
		||||
 | 
			
		||||
						<div class="mt-5">
 | 
			
		||||
							<button
 | 
			
		||||
								class=" font-medium underline"
 | 
			
		||||
								type="button"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									if (mode === 'signin') {
 | 
			
		||||
										mode = 'signup';
 | 
			
		||||
									} else {
 | 
			
		||||
										mode = 'signin';
 | 
			
		||||
									}
 | 
			
		||||
								}}
 | 
			
		||||
								class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition"
 | 
			
		||||
								type="submit"
 | 
			
		||||
							>
 | 
			
		||||
								{mode === 'signin' ? $i18n.t('Sign up') : $i18n.t('Sign in')}
 | 
			
		||||
								{mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Create Account')}
 | 
			
		||||
							</button>
 | 
			
		||||
 | 
			
		||||
							<div class=" mt-4 text-sm text-center">
 | 
			
		||||
								{mode === 'signin'
 | 
			
		||||
									? $i18n.t("Don't have an account?")
 | 
			
		||||
									: $i18n.t('Already have an account?')}
 | 
			
		||||
 | 
			
		||||
								<button
 | 
			
		||||
									class=" font-medium underline"
 | 
			
		||||
									type="button"
 | 
			
		||||
									on:click={() => {
 | 
			
		||||
										if (mode === 'signin') {
 | 
			
		||||
											mode = 'signup';
 | 
			
		||||
										} else {
 | 
			
		||||
											mode = 'signin';
 | 
			
		||||
										}
 | 
			
		||||
									}}
 | 
			
		||||
								>
 | 
			
		||||
									{mode === 'signin' ? $i18n.t('Sign up') : $i18n.t('Sign in')}
 | 
			
		||||
								</button>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</form>
 | 
			
		||||
			</div>
 | 
			
		||||
					</form>
 | 
			
		||||
				</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										180
									
								
								src/routes/s/[id]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/routes/s/[id]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,180 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { onMount, tick, getContext } from 'svelte';
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
 | 
			
		||||
	import dayjs from 'dayjs';
 | 
			
		||||
 | 
			
		||||
	import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores';
 | 
			
		||||
	import { convertMessagesToHistory } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
	import { getChatByShareId } from '$lib/apis/chats';
 | 
			
		||||
 | 
			
		||||
	import Messages from '$lib/components/chat/Messages.svelte';
 | 
			
		||||
	import Navbar from '$lib/components/layout/Navbar.svelte';
 | 
			
		||||
 | 
			
		||||
	const i18n = getContext('i18n');
 | 
			
		||||
 | 
			
		||||
	let loaded = false;
 | 
			
		||||
 | 
			
		||||
	let autoScroll = true;
 | 
			
		||||
	let processing = '';
 | 
			
		||||
	let messagesContainerElement: HTMLDivElement;
 | 
			
		||||
 | 
			
		||||
	// let chatId = $page.params.id;
 | 
			
		||||
	let showModelSelector = false;
 | 
			
		||||
	let selectedModels = [''];
 | 
			
		||||
 | 
			
		||||
	let selectedModelfiles = {};
 | 
			
		||||
	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
 | 
			
		||||
		const modelfile =
 | 
			
		||||
			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			...a,
 | 
			
		||||
			...(modelfile && { [tagName]: modelfile })
 | 
			
		||||
		};
 | 
			
		||||
	}, {});
 | 
			
		||||
 | 
			
		||||
	let chat = null;
 | 
			
		||||
 | 
			
		||||
	let title = '';
 | 
			
		||||
	let files = [];
 | 
			
		||||
 | 
			
		||||
	let messages = [];
 | 
			
		||||
	let history = {
 | 
			
		||||
		messages: {},
 | 
			
		||||
		currentId: null
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	$: if (history.currentId !== null) {
 | 
			
		||||
		let _messages = [];
 | 
			
		||||
 | 
			
		||||
		let currentMessage = history.messages[history.currentId];
 | 
			
		||||
		while (currentMessage !== null) {
 | 
			
		||||
			_messages.unshift({ ...currentMessage });
 | 
			
		||||
			currentMessage =
 | 
			
		||||
				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
 | 
			
		||||
		}
 | 
			
		||||
		messages = _messages;
 | 
			
		||||
	} else {
 | 
			
		||||
		messages = [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$: if ($page.params.id) {
 | 
			
		||||
		(async () => {
 | 
			
		||||
			if (await loadSharedChat()) {
 | 
			
		||||
				await tick();
 | 
			
		||||
				loaded = true;
 | 
			
		||||
 | 
			
		||||
				window.setTimeout(() => scrollToBottom(), 0);
 | 
			
		||||
				const chatInput = document.getElementById('chat-textarea');
 | 
			
		||||
				chatInput?.focus();
 | 
			
		||||
			} else {
 | 
			
		||||
				await goto('/');
 | 
			
		||||
			}
 | 
			
		||||
		})();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//////////////////////////
 | 
			
		||||
	// Web functions
 | 
			
		||||
	//////////////////////////
 | 
			
		||||
 | 
			
		||||
	const loadSharedChat = async () => {
 | 
			
		||||
		await chatId.set($page.params.id);
 | 
			
		||||
		chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => {
 | 
			
		||||
			await goto('/');
 | 
			
		||||
			return null;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (chat) {
 | 
			
		||||
			const chatContent = chat.chat;
 | 
			
		||||
 | 
			
		||||
			if (chatContent) {
 | 
			
		||||
				console.log(chatContent);
 | 
			
		||||
 | 
			
		||||
				selectedModels =
 | 
			
		||||
					(chatContent?.models ?? undefined) !== undefined
 | 
			
		||||
						? chatContent.models
 | 
			
		||||
						: [chatContent.models ?? ''];
 | 
			
		||||
				history =
 | 
			
		||||
					(chatContent?.history ?? undefined) !== undefined
 | 
			
		||||
						? chatContent.history
 | 
			
		||||
						: convertMessagesToHistory(chatContent.messages);
 | 
			
		||||
				title = chatContent.title;
 | 
			
		||||
 | 
			
		||||
				let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 | 
			
		||||
				await settings.set({
 | 
			
		||||
					..._settings,
 | 
			
		||||
					system: chatContent.system ?? _settings.system,
 | 
			
		||||
					options: chatContent.options ?? _settings.options
 | 
			
		||||
				});
 | 
			
		||||
				autoScroll = true;
 | 
			
		||||
				await tick();
 | 
			
		||||
 | 
			
		||||
				if (messages.length > 0) {
 | 
			
		||||
					history.messages[messages.at(-1).id].done = true;
 | 
			
		||||
				}
 | 
			
		||||
				await tick();
 | 
			
		||||
 | 
			
		||||
				return true;
 | 
			
		||||
			} else {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{title
 | 
			
		||||
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
 | 
			
		||||
			: `${$WEBUI_NAME}`}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
{#if loaded}
 | 
			
		||||
	<div
 | 
			
		||||
		class="min-h-screen max-h-screen w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900"
 | 
			
		||||
	>
 | 
			
		||||
		<div class="flex flex-col flex-auto justify-center py-8">
 | 
			
		||||
			<div class="px-3 w-full max-w-3xl mx-auto">
 | 
			
		||||
				<div>
 | 
			
		||||
					<div class=" text-3xl font-semibold line-clamp-1">
 | 
			
		||||
						{title}
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class=" mt-1 text-gray-400">
 | 
			
		||||
						{dayjs(chat.chat.timestamp).format('MMMM D, YYYY')}
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<hr class=" dark:border-gray-800 mt-6 mb-2" />
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				class=" flex flex-col justify-center w-full flex-auto overflow-auto h-0"
 | 
			
		||||
				id="messages-container"
 | 
			
		||||
			>
 | 
			
		||||
				<div class=" h-full w-full flex flex-col py-4">
 | 
			
		||||
					<div class="py-2">
 | 
			
		||||
						<Messages
 | 
			
		||||
							chatId={$chatId}
 | 
			
		||||
							readOnly={true}
 | 
			
		||||
							{selectedModels}
 | 
			
		||||
							{selectedModelfiles}
 | 
			
		||||
							{processing}
 | 
			
		||||
							bind:history
 | 
			
		||||
							bind:messages
 | 
			
		||||
							bind:autoScroll
 | 
			
		||||
							bottomPadding={files.length > 0}
 | 
			
		||||
							sendPrompt={() => {}}
 | 
			
		||||
							continueGeneration={() => {}}
 | 
			
		||||
							regenerateResponse={() => {}}
 | 
			
		||||
						/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
{/if}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user