mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-10 04:20:44 +02:00
refac: shared chat
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""Add shared_chat table and migrate existing shares
|
||||
|
||||
Revision ID: c1d2e3f4a5b6
|
||||
Revises: e1f2a3b4c5d6
|
||||
Create Date: 2026-04-16 23:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = 'c1d2e3f4a5b6'
|
||||
down_revision = 'e1f2a3b4c5d6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# Lightweight table references for data migration (no ORM models needed)
|
||||
chat_t = sa.table(
|
||||
'chat',
|
||||
sa.column('id', sa.Text),
|
||||
sa.column('user_id', sa.Text),
|
||||
sa.column('title', sa.Text),
|
||||
sa.column('chat', sa.JSON),
|
||||
sa.column('share_id', sa.Text),
|
||||
sa.column('created_at', sa.BigInteger),
|
||||
sa.column('updated_at', sa.BigInteger),
|
||||
sa.column('archived', sa.Boolean),
|
||||
sa.column('meta', sa.JSON),
|
||||
)
|
||||
|
||||
shared_chat_t = sa.table(
|
||||
'shared_chat',
|
||||
sa.column('id', sa.Text),
|
||||
sa.column('chat_id', sa.Text),
|
||||
sa.column('user_id', sa.Text),
|
||||
sa.column('title', sa.Text),
|
||||
sa.column('chat', sa.JSON),
|
||||
sa.column('created_at', sa.BigInteger),
|
||||
sa.column('updated_at', sa.BigInteger),
|
||||
)
|
||||
|
||||
chat_message_t = sa.table(
|
||||
'chat_message',
|
||||
sa.column('chat_id', sa.Text),
|
||||
)
|
||||
|
||||
access_grant_t = sa.table(
|
||||
'access_grant',
|
||||
sa.column('id', sa.Text),
|
||||
sa.column('resource_type', sa.Text),
|
||||
sa.column('resource_id', sa.Text),
|
||||
sa.column('principal_type', sa.Text),
|
||||
sa.column('principal_id', sa.Text),
|
||||
sa.column('permission', sa.Text),
|
||||
sa.column('created_at', sa.BigInteger),
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
|
||||
# 1. Create shared_chat table
|
||||
op.create_table(
|
||||
'shared_chat',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
sa.Column('chat', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
# 2. Migrate existing shared-* rows
|
||||
shared_rows = conn.execute(
|
||||
sa.select(
|
||||
chat_t.c.id,
|
||||
chat_t.c.user_id,
|
||||
chat_t.c.title,
|
||||
chat_t.c.chat,
|
||||
chat_t.c.created_at,
|
||||
chat_t.c.updated_at,
|
||||
).where(chat_t.c.user_id.like('shared-%'))
|
||||
).fetchall()
|
||||
|
||||
for row in shared_rows:
|
||||
share_token = row.id
|
||||
original_chat_id = row.user_id.replace('shared-', '', 1)
|
||||
|
||||
# Verify original chat still exists
|
||||
original = conn.execute(
|
||||
sa.select(chat_t.c.user_id).where(chat_t.c.id == original_chat_id)
|
||||
).fetchone()
|
||||
|
||||
if not original:
|
||||
continue
|
||||
|
||||
# Insert snapshot into shared_chat
|
||||
conn.execute(shared_chat_t.insert().values(
|
||||
id=share_token,
|
||||
chat_id=original_chat_id,
|
||||
user_id=original.user_id,
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
))
|
||||
|
||||
# Create user:*:read grant for backward compat
|
||||
conn.execute(access_grant_t.insert().values(
|
||||
id=str(uuid.uuid4()),
|
||||
resource_type='shared_chat',
|
||||
resource_id=original_chat_id,
|
||||
principal_type='user',
|
||||
principal_id='*',
|
||||
permission='read',
|
||||
created_at=row.created_at or int(time.time()),
|
||||
))
|
||||
|
||||
# 3. Clean up old phantom rows
|
||||
conn.execute(
|
||||
chat_message_t.delete().where(
|
||||
chat_message_t.c.chat_id.in_(
|
||||
sa.select(chat_t.c.id).where(chat_t.c.user_id.like('shared-%'))
|
||||
)
|
||||
)
|
||||
)
|
||||
conn.execute(chat_t.delete().where(chat_t.c.user_id.like('shared-%')))
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
|
||||
shared_rows = conn.execute(
|
||||
sa.select(
|
||||
shared_chat_t.c.id,
|
||||
shared_chat_t.c.chat_id,
|
||||
shared_chat_t.c.user_id,
|
||||
shared_chat_t.c.title,
|
||||
shared_chat_t.c.chat,
|
||||
shared_chat_t.c.created_at,
|
||||
shared_chat_t.c.updated_at,
|
||||
)
|
||||
).fetchall()
|
||||
|
||||
for row in shared_rows:
|
||||
conn.execute(chat_t.insert().values(
|
||||
id=row.id,
|
||||
user_id=f'shared-{row.chat_id}',
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
archived=False,
|
||||
meta={},
|
||||
))
|
||||
|
||||
conn.execute(
|
||||
access_grant_t.delete().where(access_grant_t.c.resource_type == 'shared_chat')
|
||||
)
|
||||
op.drop_table('shared_chat')
|
||||
@@ -555,77 +555,51 @@ class ChatTable:
|
||||
async def insert_shared_chat_by_chat_id(
|
||||
self, chat_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[ChatModel]:
|
||||
"""Create a shared snapshot for a chat. Returns the original chat with share_id set."""
|
||||
from open_webui.models.shared_chats import SharedChats
|
||||
|
||||
async with get_async_db_context(db) as db:
|
||||
# Get the existing chat to share
|
||||
chat = await db.get(Chat, chat_id)
|
||||
# Check if chat exists
|
||||
if not chat:
|
||||
return None
|
||||
# Check if the chat is already shared
|
||||
if chat.share_id:
|
||||
return await self.get_chat_by_id_and_user_id(chat.share_id, 'shared', db=db)
|
||||
# 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,
|
||||
'meta': chat.meta,
|
||||
'pinned': chat.pinned,
|
||||
'folder_id': chat.folder_id,
|
||||
'created_at': chat.created_at,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
shared_result = Chat(**shared_chat.model_dump())
|
||||
db.add(shared_result)
|
||||
await db.commit()
|
||||
await db.refresh(shared_result)
|
||||
|
||||
# Update the original chat with the share_id
|
||||
await db.execute(update(Chat).filter_by(id=chat_id).values(share_id=shared_chat.id))
|
||||
# If already shared, just update the existing snapshot
|
||||
if chat.share_id:
|
||||
return await self.update_shared_chat_by_chat_id(chat_id, db=db)
|
||||
|
||||
shared = await SharedChats.create(chat_id, chat.user_id, db=db)
|
||||
if not shared:
|
||||
return None
|
||||
|
||||
# Set share_id on the original chat
|
||||
chat.share_id = shared.id
|
||||
await db.commit()
|
||||
return shared_chat if shared_result else None
|
||||
await db.refresh(chat)
|
||||
return ChatModel.model_validate(chat)
|
||||
|
||||
async def update_shared_chat_by_chat_id(
|
||||
self, chat_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[ChatModel]:
|
||||
"""Re-snapshot the shared chat with current chat data."""
|
||||
from open_webui.models.shared_chats import SharedChats
|
||||
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
chat = await db.get(Chat, chat_id)
|
||||
result = await db.execute(select(Chat).filter_by(user_id=f'shared-{chat_id}'))
|
||||
shared_chat = result.scalars().first()
|
||||
|
||||
if shared_chat is None:
|
||||
if not chat or not chat.share_id:
|
||||
return await self.insert_shared_chat_by_chat_id(chat_id, db=db)
|
||||
|
||||
shared_chat.title = chat.title
|
||||
shared_chat.chat = chat.chat
|
||||
shared_chat.meta = chat.meta
|
||||
shared_chat.pinned = chat.pinned
|
||||
shared_chat.folder_id = chat.folder_id
|
||||
shared_chat.updated_at = int(time.time())
|
||||
await db.commit()
|
||||
await db.refresh(shared_chat)
|
||||
|
||||
return ChatModel.model_validate(shared_chat)
|
||||
await SharedChats.update(chat.share_id, db=db)
|
||||
return ChatModel.model_validate(chat)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def delete_shared_chat_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
"""Delete shared snapshot for a chat."""
|
||||
from open_webui.models.shared_chats import SharedChats
|
||||
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
# Get shared chat IDs
|
||||
result = await db.execute(select(Chat.id).filter_by(user_id=f'shared-{chat_id}'))
|
||||
shared_ids = [row[0] for row in result.all()]
|
||||
|
||||
if shared_ids:
|
||||
await db.execute(delete(ChatMessage).filter(ChatMessage.chat_id.in_(shared_ids)))
|
||||
await db.execute(delete(Chat).filter_by(user_id=f'shared-{chat_id}'))
|
||||
await db.commit()
|
||||
|
||||
return True
|
||||
return await SharedChats.delete_by_chat_id(chat_id, db=db)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -746,53 +720,12 @@ class ChatTable:
|
||||
limit: int = 50,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> list[SharedChatResponse]:
|
||||
async with get_async_db_context(db) as db:
|
||||
stmt = (
|
||||
select(Chat.id, Chat.title, Chat.share_id, Chat.updated_at, Chat.created_at)
|
||||
.filter_by(user_id=user_id)
|
||||
.filter(Chat.share_id.isnot(None))
|
||||
)
|
||||
"""Delegate to SharedChats for listing shared chats by user."""
|
||||
from open_webui.models.shared_chats import SharedChats
|
||||
|
||||
if filter:
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
stmt = stmt.filter(Chat.title.ilike(f'%{query_key}%'))
|
||||
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by and direction:
|
||||
if not getattr(Chat, order_by, None):
|
||||
raise ValueError('Invalid order_by field')
|
||||
|
||||
if direction.lower() == 'asc':
|
||||
stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id)
|
||||
elif direction.lower() == 'desc':
|
||||
stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id)
|
||||
else:
|
||||
raise ValueError('Invalid direction for ordering')
|
||||
else:
|
||||
stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id)
|
||||
|
||||
if skip:
|
||||
stmt = stmt.offset(skip)
|
||||
if limit:
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
all_chats = result.all()
|
||||
return [
|
||||
SharedChatResponse.model_validate(
|
||||
{
|
||||
'id': chat[0],
|
||||
'title': chat[1],
|
||||
'share_id': chat[2],
|
||||
'updated_at': chat[3],
|
||||
'created_at': chat[4],
|
||||
}
|
||||
)
|
||||
for chat in all_chats
|
||||
]
|
||||
return await SharedChats.get_by_user_id(
|
||||
user_id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
async def get_chat_list_by_user_id(
|
||||
self,
|
||||
@@ -925,15 +858,23 @@ class ChatTable:
|
||||
return None
|
||||
|
||||
async def get_chat_by_share_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatModel]:
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(select(Chat).filter_by(share_id=id))
|
||||
chat = result.scalars().first()
|
||||
"""Look up a shared chat snapshot by its share token."""
|
||||
from open_webui.models.shared_chats import SharedChats
|
||||
|
||||
if chat:
|
||||
return await self.get_chat_by_id(id, db=db)
|
||||
else:
|
||||
return None
|
||||
try:
|
||||
shared = await SharedChats.get_by_id(id, db=db)
|
||||
if shared:
|
||||
# Return a ChatModel-compatible view of the snapshot
|
||||
return ChatModel(
|
||||
id=shared.id,
|
||||
user_id=shared.user_id,
|
||||
title=shared.title,
|
||||
chat=shared.chat,
|
||||
created_at=shared.created_at,
|
||||
updated_at=shared.updated_at,
|
||||
share_id=shared.id,
|
||||
)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -1568,20 +1509,17 @@ class ChatTable:
|
||||
return False
|
||||
|
||||
async def delete_shared_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
"""Delete all shared chat snapshots created by a user."""
|
||||
from open_webui.models.shared_chats import SharedChats, SharedChat as SharedChatTable
|
||||
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(select(Chat.id).filter_by(user_id=user_id))
|
||||
id_rows = result.all()
|
||||
shared_chat_ids = [f'shared-{row[0]}' for row in id_rows]
|
||||
# Delete shared_chat rows for this user's chats
|
||||
await db.execute(delete(SharedChatTable).filter_by(user_id=user_id))
|
||||
|
||||
if shared_chat_ids:
|
||||
# Get shared chat IDs to delete associated messages
|
||||
shared_result = await db.execute(select(Chat.id).filter(Chat.user_id.in_(shared_chat_ids)))
|
||||
shared_ids = [row[0] for row in shared_result.all()]
|
||||
if shared_ids:
|
||||
await db.execute(delete(ChatMessage).filter(ChatMessage.chat_id.in_(shared_ids)))
|
||||
await db.execute(delete(Chat).filter(Chat.user_id.in_(shared_chat_ids)))
|
||||
await db.commit()
|
||||
# Clear share_id on all of this user's chats
|
||||
await db.execute(update(Chat).filter_by(user_id=user_id).values(share_id=None))
|
||||
await db.commit()
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
@@ -1651,16 +1589,15 @@ class ChatTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def get_shared_chats_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[ChatModel]:
|
||||
async def get_shared_chat_ids_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[str]:
|
||||
"""Return IDs of chats that contain this file and have an active share link."""
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(
|
||||
select(Chat)
|
||||
select(Chat.id)
|
||||
.join(ChatFile, Chat.id == ChatFile.chat_id)
|
||||
.filter(ChatFile.file_id == file_id, Chat.share_id.isnot(None))
|
||||
)
|
||||
all_chats = result.scalars().all()
|
||||
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
async def update_chat_tasks_by_id(self, id: str, tasks: list[dict]) -> Optional[ChatModel]:
|
||||
"""Update the tasks list on a chat."""
|
||||
|
||||
222
backend/open_webui/models/shared_chats.py
Normal file
222
backend/open_webui/models/shared_chats.py
Normal file
@@ -0,0 +1,222 @@
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from open_webui.internal.db import Base, JSONField, get_async_db_context
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Text, JSON
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
####################
|
||||
# SharedChat DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class SharedChat(Base):
|
||||
__tablename__ = 'shared_chat'
|
||||
|
||||
id = Column(Text, primary_key=True) # The share token (UUID) — used in /s/{id} URL
|
||||
chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = Column(Text, nullable=False) # Who created this share
|
||||
|
||||
title = Column(Text)
|
||||
chat = Column(JSON) # Snapshot of chat JSON at share time
|
||||
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
|
||||
class SharedChatModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
chat_id: str
|
||||
user_id: str
|
||||
|
||||
title: str
|
||||
chat: dict
|
||||
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
|
||||
class SharedChatResponse(BaseModel):
|
||||
id: str
|
||||
chat_id: str
|
||||
title: str
|
||||
share_id: Optional[str] = None # Alias for id, for backward compat
|
||||
updated_at: int
|
||||
created_at: int
|
||||
|
||||
|
||||
####################
|
||||
# Table Operations
|
||||
####################
|
||||
|
||||
|
||||
class SharedChatsTable:
|
||||
async def create(
|
||||
self, chat_id: str, user_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
"""
|
||||
Create a snapshot of the chat for link sharing.
|
||||
Returns the SharedChatModel with the share token as its id.
|
||||
"""
|
||||
async with get_async_db_context(db) as db:
|
||||
from open_webui.models.chats import Chat
|
||||
|
||||
chat = await db.get(Chat, chat_id)
|
||||
if not chat:
|
||||
return None
|
||||
|
||||
share_id = str(uuid.uuid4())
|
||||
now = int(time.time())
|
||||
|
||||
shared_chat = SharedChat(
|
||||
id=share_id,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
title=chat.title,
|
||||
chat=chat.chat,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
db.add(shared_chat)
|
||||
await db.commit()
|
||||
await db.refresh(shared_chat)
|
||||
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
|
||||
async def update(
|
||||
self, share_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
"""
|
||||
Re-snapshot: update the shared chat with the current state of the original chat.
|
||||
"""
|
||||
async with get_async_db_context(db) as db:
|
||||
from open_webui.models.chats import Chat
|
||||
|
||||
shared_chat = await db.get(SharedChat, share_id)
|
||||
if not shared_chat:
|
||||
return None
|
||||
|
||||
chat = await db.get(Chat, shared_chat.chat_id)
|
||||
if not chat:
|
||||
return None
|
||||
|
||||
shared_chat.title = chat.title
|
||||
shared_chat.chat = chat.chat
|
||||
shared_chat.updated_at = int(time.time())
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(shared_chat)
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
|
||||
async def get_by_id(
|
||||
self, share_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
"""Get a shared chat by its share token."""
|
||||
async with get_async_db_context(db) as db:
|
||||
shared_chat = await db.get(SharedChat, share_id)
|
||||
if shared_chat:
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
return None
|
||||
|
||||
async def get_by_chat_id(
|
||||
self, chat_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
"""Get the shared chat for a given original chat. Returns the most recent one."""
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(
|
||||
select(SharedChat)
|
||||
.filter_by(chat_id=chat_id)
|
||||
.order_by(SharedChat.updated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
shared_chat = result.scalars().first()
|
||||
if shared_chat:
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
return None
|
||||
|
||||
async def get_by_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
filter: Optional[dict] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> list[SharedChatResponse]:
|
||||
"""List all shared chats created by a user."""
|
||||
async with get_async_db_context(db) as db:
|
||||
stmt = select(SharedChat).filter_by(user_id=user_id)
|
||||
|
||||
if filter:
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
stmt = stmt.filter(SharedChat.title.ilike(f'%{query_key}%'))
|
||||
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by and direction:
|
||||
col = getattr(SharedChat, order_by, None)
|
||||
if not col:
|
||||
raise ValueError('Invalid order_by field')
|
||||
if direction.lower() == 'asc':
|
||||
stmt = stmt.order_by(col.asc())
|
||||
elif direction.lower() == 'desc':
|
||||
stmt = stmt.order_by(col.desc())
|
||||
else:
|
||||
raise ValueError('Invalid direction for ordering')
|
||||
else:
|
||||
stmt = stmt.order_by(SharedChat.updated_at.desc())
|
||||
|
||||
if skip:
|
||||
stmt = stmt.offset(skip)
|
||||
if limit:
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
return [
|
||||
SharedChatResponse(
|
||||
id=sc.chat_id,
|
||||
chat_id=sc.chat_id,
|
||||
title=sc.title,
|
||||
share_id=sc.id,
|
||||
updated_at=sc.updated_at,
|
||||
created_at=sc.created_at,
|
||||
)
|
||||
for sc in result.scalars().all()
|
||||
]
|
||||
|
||||
async def delete_by_id(
|
||||
self, share_id: str, db: Optional[AsyncSession] = None
|
||||
) -> bool:
|
||||
"""Delete a shared chat by its share token."""
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
await db.execute(delete(SharedChat).filter_by(id=share_id))
|
||||
await db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def delete_by_chat_id(
|
||||
self, chat_id: str, db: Optional[AsyncSession] = None
|
||||
) -> bool:
|
||||
"""Delete all shared chats for a given original chat."""
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
await db.execute(delete(SharedChat).filter_by(chat_id=chat_id))
|
||||
await db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
SharedChats = SharedChatsTable()
|
||||
@@ -17,13 +17,14 @@ from open_webui.models.chats import (
|
||||
ChatResponse,
|
||||
Chats,
|
||||
ChatTitleIdResponse,
|
||||
SharedChatResponse,
|
||||
ChatStatsExport,
|
||||
AggregateChatStats,
|
||||
ChatBody,
|
||||
ChatHistoryStats,
|
||||
MessageStats,
|
||||
)
|
||||
from open_webui.models.shared_chats import SharedChats, SharedChatResponse
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.tags import TagModel, Tags
|
||||
from open_webui.models.folders import Folders
|
||||
from open_webui.internal.db import get_async_session
|
||||
@@ -35,7 +36,7 @@ from pydantic import BaseModel
|
||||
|
||||
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.utils.access_control import has_permission, filter_allowed_access_grants
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -807,7 +808,7 @@ async def get_shared_session_user_chat_list(
|
||||
if direction:
|
||||
filter['direction'] = direction
|
||||
|
||||
return await Chats.get_shared_chat_list_by_user_id(
|
||||
return await SharedChats.get_by_user_id(
|
||||
user.id,
|
||||
filter=filter,
|
||||
skip=skip,
|
||||
@@ -828,17 +829,32 @@ async def get_shared_chat_by_id(
|
||||
if user.role == 'pending':
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
|
||||
|
||||
if user.role == 'user' or (user.role == 'admin' and not ENABLE_ADMIN_CHAT_ACCESS):
|
||||
chat = await Chats.get_chat_by_share_id(share_id, db=db)
|
||||
elif user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
|
||||
if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
|
||||
chat = await Chats.get_chat_by_id(share_id, db=db)
|
||||
|
||||
if chat:
|
||||
return ChatResponse(**chat.model_dump())
|
||||
|
||||
else:
|
||||
chat = await Chats.get_chat_by_share_id(share_id, db=db)
|
||||
|
||||
if not chat:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
|
||||
|
||||
# Look up the original chat_id to check access grants
|
||||
shared = await SharedChats.get_by_id(share_id, db=db)
|
||||
if shared:
|
||||
has_grant = await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='shared_chat',
|
||||
resource_id=shared.chat_id,
|
||||
permission='read',
|
||||
db=db,
|
||||
)
|
||||
if not has_grant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
return ChatResponse(**chat.model_dump())
|
||||
|
||||
|
||||
############################
|
||||
# GetChatsByTags
|
||||
@@ -878,11 +894,25 @@ async def get_user_chat_list_by_tag_name(
|
||||
async def get_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
|
||||
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
||||
|
||||
if not chat:
|
||||
# Check if user has access via access grants (shared_chat grants)
|
||||
if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
|
||||
chat = await Chats.get_chat_by_id(id, db=db)
|
||||
else:
|
||||
has_grant = await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='shared_chat',
|
||||
resource_id=id,
|
||||
permission='read',
|
||||
db=db,
|
||||
)
|
||||
if has_grant:
|
||||
chat = await Chats.get_chat_by_id(id, db=db)
|
||||
|
||||
if chat:
|
||||
return ChatResponse(**chat.model_dump())
|
||||
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
|
||||
|
||||
|
||||
############################
|
||||
@@ -1158,39 +1188,58 @@ async def clone_shared_chat_by_id(
|
||||
else:
|
||||
chat = await Chats.get_chat_by_share_id(id, db=db)
|
||||
|
||||
if chat:
|
||||
updated_chat = {
|
||||
**chat.chat,
|
||||
'originalChatId': chat.id,
|
||||
'branchPointMessageId': chat.chat['history']['currentId'],
|
||||
'title': f'Clone of {chat.title}',
|
||||
}
|
||||
|
||||
chats = await Chats.import_chats(
|
||||
user.id,
|
||||
[
|
||||
ChatImportForm(
|
||||
**{
|
||||
'chat': updated_chat,
|
||||
'meta': chat.meta,
|
||||
'pinned': chat.pinned,
|
||||
'folder_id': chat.folder_id,
|
||||
}
|
||||
)
|
||||
],
|
||||
db=db,
|
||||
if not chat:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if chats:
|
||||
chat = chats[0]
|
||||
return ChatResponse(**chat.model_dump())
|
||||
else:
|
||||
# Enforce access grants
|
||||
shared = await SharedChats.get_by_id(id, db=db)
|
||||
if shared and user.role != 'admin':
|
||||
has_grant = await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='shared_chat',
|
||||
resource_id=shared.chat_id,
|
||||
permission='read',
|
||||
db=db,
|
||||
)
|
||||
if not has_grant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
updated_chat = {
|
||||
**chat.chat,
|
||||
'originalChatId': chat.id,
|
||||
'branchPointMessageId': chat.chat['history']['currentId'],
|
||||
'title': f'Clone of {chat.title}',
|
||||
}
|
||||
|
||||
chats = await Chats.import_chats(
|
||||
user.id,
|
||||
[
|
||||
ChatImportForm(
|
||||
**{
|
||||
'chat': updated_chat,
|
||||
'meta': chat.meta,
|
||||
'pinned': chat.pinned,
|
||||
'folder_id': chat.folder_id,
|
||||
}
|
||||
)
|
||||
],
|
||||
db=db,
|
||||
)
|
||||
|
||||
if chats:
|
||||
chat = chats[0]
|
||||
return ChatResponse(**chat.model_dump())
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
@@ -1241,16 +1290,28 @@ async def share_chat_by_id(
|
||||
|
||||
if chat:
|
||||
if chat.share_id:
|
||||
shared_chat = await Chats.update_shared_chat_by_chat_id(chat.id, db=db)
|
||||
return ChatResponse(**shared_chat.model_dump())
|
||||
# Re-snapshot existing share
|
||||
shared = await SharedChats.update(chat.share_id, db=db)
|
||||
if shared:
|
||||
# Re-fetch the original chat to return
|
||||
chat = await Chats.get_chat_by_id(id, db=db)
|
||||
return ChatResponse(**chat.model_dump())
|
||||
|
||||
shared_chat = await Chats.insert_shared_chat_by_chat_id(chat.id, db=db)
|
||||
if not shared_chat:
|
||||
# Create new share
|
||||
shared = await SharedChats.create(id, user.id, db=db)
|
||||
if not shared:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
return ChatResponse(**shared_chat.model_dump())
|
||||
# Set share_id on the original chat
|
||||
chat = await Chats.update_chat_share_id_by_id(id, shared.id, db=db)
|
||||
if not chat:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
return ChatResponse(**chat.model_dump())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -1260,7 +1321,7 @@ async def share_chat_by_id(
|
||||
|
||||
|
||||
############################
|
||||
# DeletedSharedChatById
|
||||
# DeleteSharedChatById
|
||||
############################
|
||||
|
||||
|
||||
@@ -1273,10 +1334,13 @@ async def delete_shared_chat_by_id(
|
||||
if not chat.share_id:
|
||||
return False
|
||||
|
||||
result = await Chats.delete_shared_chat_by_chat_id(id, db=db)
|
||||
update_result = await Chats.update_chat_share_id_by_id(id, None, db=db)
|
||||
await SharedChats.delete_by_chat_id(id, db=db)
|
||||
await Chats.update_chat_share_id_by_id(id, None, db=db)
|
||||
|
||||
return result and update_result != None
|
||||
# Revoke all access grants for this shared chat
|
||||
await AccessGrants.set_access_grants('shared_chat', id, [], db=db)
|
||||
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -1284,6 +1348,85 @@ async def delete_shared_chat_by_id(
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateSharedChatAccessById
|
||||
############################
|
||||
|
||||
|
||||
class ChatAccessGrantsForm(BaseModel):
|
||||
access_grants: list[dict]
|
||||
|
||||
|
||||
@router.post('/shared/{id}/access/update', response_model=Optional[ChatResponse])
|
||||
async def update_shared_chat_access_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: ChatAccessGrantsForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
||||
if not chat:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if chat.user_id != user.id and user.role != 'admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
form_data.access_grants = await filter_allowed_access_grants(
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
user.id,
|
||||
user.role,
|
||||
form_data.access_grants,
|
||||
'sharing.public_chats',
|
||||
)
|
||||
|
||||
await AccessGrants.set_access_grants('shared_chat', id, form_data.access_grants, db=db)
|
||||
|
||||
return ChatResponse(**chat.model_dump())
|
||||
|
||||
|
||||
############################
|
||||
# GetSharedChatAccessById
|
||||
############################
|
||||
|
||||
|
||||
@router.get('/shared/{id}/access', response_model=list)
|
||||
async def get_shared_chat_access_by_id(
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
||||
if not chat:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if chat.user_id != user.id and user.role != 'admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db)
|
||||
return [
|
||||
{
|
||||
'id': g.id,
|
||||
'principal_type': g.principal_type,
|
||||
'principal_id': g.principal_id,
|
||||
'permission': g.permission,
|
||||
}
|
||||
for g in grants
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# UpdateChatFolderIdById
|
||||
############################
|
||||
|
||||
@@ -66,10 +66,18 @@ async def has_access_to_file(
|
||||
return True
|
||||
|
||||
# Check if the file is associated with any chats the user has access to
|
||||
# TODO: Granular access control for chats
|
||||
chats = await Chats.get_shared_chats_by_file_id(file_id, db=db)
|
||||
if chats:
|
||||
return True
|
||||
shared_chat_ids = await Chats.get_shared_chat_ids_by_file_id(file_id, db=db)
|
||||
if shared_chat_ids:
|
||||
accessible_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type='shared_chat',
|
||||
resource_ids=shared_chat_ids,
|
||||
permission='read',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
if accessible_ids:
|
||||
return True
|
||||
|
||||
# Check if the file is directly attached to a shared workspace model
|
||||
for model in await Models.get_models_by_user_id(user.id, permission=access_type, db=db):
|
||||
|
||||
@@ -953,6 +953,77 @@ export const deleteSharedChatById = async (token: string, id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateChatAccessGrants = async (
|
||||
token: string,
|
||||
id: string,
|
||||
accessGrants: object[]
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/shared/${id}/access/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
access_grants: accessGrants
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChatAccessGrants = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/shared/${id}/access`, {
|
||||
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.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateChatById = async (token: string, id: string, chat: object) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -3,24 +3,32 @@
|
||||
import { models, config } from '$lib/stores';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
|
||||
import {
|
||||
deleteSharedChatById,
|
||||
getChatById,
|
||||
shareChatById,
|
||||
getChatAccessGrants,
|
||||
updateChatAccessGrants
|
||||
} from '$lib/apis/chats';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Link from '../icons/Link.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||
|
||||
export let chatId;
|
||||
|
||||
let chat = null;
|
||||
let shareUrl = null;
|
||||
let accessGrants: any[] = [];
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const shareLocalChat = async () => {
|
||||
const _chat = chat;
|
||||
|
||||
const sharedChat = await shareChatById(localStorage.token, chatId);
|
||||
shareUrl = `${window.location.origin}/s/${sharedChat.id}`;
|
||||
shareUrl = `${window.location.origin}/s/${sharedChat.share_id}`;
|
||||
console.log(shareUrl);
|
||||
chat = await getChatById(localStorage.token, chatId);
|
||||
|
||||
@@ -54,6 +62,25 @@
|
||||
);
|
||||
};
|
||||
|
||||
const loadAccessGrants = async () => {
|
||||
if (!chatId) return;
|
||||
try {
|
||||
accessGrants = (await getChatAccessGrants(localStorage.token, chatId)) ?? [];
|
||||
} catch (e) {
|
||||
console.error('Failed to load access grants', e);
|
||||
accessGrants = [];
|
||||
}
|
||||
};
|
||||
|
||||
const saveAccessGrants = async () => {
|
||||
try {
|
||||
await updateChatAccessGrants(localStorage.token, chatId, accessGrants);
|
||||
toast.success($i18n.t('Access updated'));
|
||||
} catch (e) {
|
||||
toast.error(`${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
export let show = false;
|
||||
|
||||
const isDifferentChat = (_chat) => {
|
||||
@@ -73,8 +100,10 @@
|
||||
if (isDifferentChat(_chat)) {
|
||||
chat = _chat;
|
||||
}
|
||||
await loadAccessGrants();
|
||||
} else {
|
||||
chat = null;
|
||||
accessGrants = [];
|
||||
console.log(chat);
|
||||
}
|
||||
})();
|
||||
@@ -97,8 +126,8 @@
|
||||
</div>
|
||||
|
||||
{#if chat}
|
||||
<div class="px-5 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||
<div class=" text-sm dark:text-gray-300 mb-1">
|
||||
<div class="px-5 pt-4 pb-5 w-full flex flex-col">
|
||||
<div class="text-sm dark:text-gray-300">
|
||||
{#if chat.share_id}
|
||||
<a href="/s/{chat.share_id}" target="_blank"
|
||||
>{$i18n.t('You have shared this chat')}
|
||||
@@ -124,70 +153,73 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-col items-end space-x-1 mt-3">
|
||||
<div class="flex gap-1">
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<button
|
||||
class="self-center flex items-center gap-1 px-3.5 py-2 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
shareChat();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="self-center flex items-center gap-1 px-3.5 py-2 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
id="copy-and-share-chat-button"
|
||||
on:click={async () => {
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
if (isSafari) {
|
||||
// Oh, Safari, you're so special, let's give you some extra love and attention
|
||||
console.log('isSafari');
|
||||
|
||||
const getUrlPromise = async () => {
|
||||
const url = await shareLocalChat();
|
||||
return new Blob([url], { type: 'text/plain' });
|
||||
};
|
||||
|
||||
navigator.clipboard
|
||||
.write([
|
||||
new ClipboardItem({
|
||||
'text/plain': getUrlPromise()
|
||||
})
|
||||
])
|
||||
.then(() => {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Async: Could not copy text: ', error);
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
copyToClipboard(await shareLocalChat());
|
||||
}
|
||||
|
||||
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<Link />
|
||||
|
||||
{#if chat.share_id}
|
||||
{$i18n.t('Update and Copy Link')}
|
||||
{:else}
|
||||
{$i18n.t('Copy Link')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if chat.share_id}
|
||||
<div class="mt-3">
|
||||
<AccessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read']}
|
||||
onChange={saveAccessGrants}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-1 mt-3">
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<button
|
||||
class="flex items-center gap-1 px-3.5 py-2 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
shareChat();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="flex items-center gap-1 px-3.5 py-2 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
id="copy-and-share-chat-button"
|
||||
on:click={async () => {
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
if (isSafari) {
|
||||
console.log('isSafari');
|
||||
|
||||
const getUrlPromise = async () => {
|
||||
const url = await shareLocalChat();
|
||||
return new Blob([url], { type: 'text/plain' });
|
||||
};
|
||||
|
||||
navigator.clipboard
|
||||
.write([
|
||||
new ClipboardItem({
|
||||
'text/plain': getUrlPromise()
|
||||
})
|
||||
])
|
||||
.then(() => {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Async: Could not copy text: ', error);
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
copyToClipboard(await shareLocalChat());
|
||||
}
|
||||
|
||||
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
|
||||
}}
|
||||
>
|
||||
<Link />
|
||||
|
||||
{#if chat.share_id}
|
||||
{$i18n.t('Update and Copy Link')}
|
||||
{:else}
|
||||
{$i18n.t('Copy Link')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user