mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
Merge remote-tracking branch 'origin/dev' into feature/tools-system
This commit is contained in:
commit
9ba90414eb
122 changed files with 3151 additions and 1190 deletions
|
|
@ -1455,6 +1455,10 @@ USER_PERMISSIONS_FEATURES_NOTES = (
|
||||||
os.environ.get("USER_PERMISSIONS_FEATURES_NOTES", "True").lower() == "true"
|
os.environ.get("USER_PERMISSIONS_FEATURES_NOTES", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS_FEATURES_CHANNELS = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_FEATURES_CHANNELS", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
USER_PERMISSIONS_FEATURES_API_KEYS = (
|
USER_PERMISSIONS_FEATURES_API_KEYS = (
|
||||||
os.environ.get("USER_PERMISSIONS_FEATURES_API_KEYS", "False").lower() == "true"
|
os.environ.get("USER_PERMISSIONS_FEATURES_API_KEYS", "False").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
@ -1509,8 +1513,9 @@ DEFAULT_USER_PERMISSIONS = {
|
||||||
"features": {
|
"features": {
|
||||||
# General features
|
# General features
|
||||||
"api_keys": USER_PERMISSIONS_FEATURES_API_KEYS,
|
"api_keys": USER_PERMISSIONS_FEATURES_API_KEYS,
|
||||||
"folders": USER_PERMISSIONS_FEATURES_FOLDERS,
|
|
||||||
"notes": USER_PERMISSIONS_FEATURES_NOTES,
|
"notes": USER_PERMISSIONS_FEATURES_NOTES,
|
||||||
|
"folders": USER_PERMISSIONS_FEATURES_FOLDERS,
|
||||||
|
"channels": USER_PERMISSIONS_FEATURES_CHANNELS,
|
||||||
"direct_tool_servers": USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS,
|
"direct_tool_servers": USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS,
|
||||||
# Chat features
|
# Chat features
|
||||||
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
|
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
"""Update channel and channel members table
|
||||||
|
|
||||||
|
Revision ID: 90ef40d4714e
|
||||||
|
Revises: b10670c03dd5
|
||||||
|
Create Date: 2025-11-30 06:33:38.790341
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import open_webui.internal.db
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "90ef40d4714e"
|
||||||
|
down_revision: Union[str, None] = "b10670c03dd5"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Update 'channel' table
|
||||||
|
op.add_column("channel", sa.Column("is_private", sa.Boolean(), nullable=True))
|
||||||
|
|
||||||
|
op.add_column("channel", sa.Column("archived_at", sa.BigInteger(), nullable=True))
|
||||||
|
op.add_column("channel", sa.Column("archived_by", sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
op.add_column("channel", sa.Column("deleted_at", sa.BigInteger(), nullable=True))
|
||||||
|
op.add_column("channel", sa.Column("deleted_by", sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
op.add_column("channel", sa.Column("updated_by", sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
# Update 'channel_member' table
|
||||||
|
op.add_column("channel_member", sa.Column("role", sa.Text(), nullable=True))
|
||||||
|
op.add_column("channel_member", sa.Column("invited_by", sa.Text(), nullable=True))
|
||||||
|
op.add_column(
|
||||||
|
"channel_member", sa.Column("invited_at", sa.BigInteger(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create 'channel_webhook' table
|
||||||
|
op.create_table(
|
||||||
|
"channel_webhook",
|
||||||
|
sa.Column("id", sa.Text(), primary_key=True, unique=True, nullable=False),
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"channel_id",
|
||||||
|
sa.Text(),
|
||||||
|
sa.ForeignKey("channel.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("name", sa.Text(), nullable=False),
|
||||||
|
sa.Column("profile_image_url", sa.Text(), nullable=True),
|
||||||
|
sa.Column("token", sa.Text(), nullable=False),
|
||||||
|
sa.Column("last_used_at", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Downgrade 'channel' table
|
||||||
|
op.drop_column("channel", "is_private")
|
||||||
|
op.drop_column("channel", "archived_at")
|
||||||
|
op.drop_column("channel", "archived_by")
|
||||||
|
op.drop_column("channel", "deleted_at")
|
||||||
|
op.drop_column("channel", "deleted_by")
|
||||||
|
op.drop_column("channel", "updated_by")
|
||||||
|
|
||||||
|
# Downgrade 'channel_member' table
|
||||||
|
op.drop_column("channel_member", "role")
|
||||||
|
op.drop_column("channel_member", "invited_by")
|
||||||
|
op.drop_column("channel_member", "invited_at")
|
||||||
|
|
||||||
|
# Drop 'channel_webhook' table
|
||||||
|
op.drop_table("channel_webhook")
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
@ -4,10 +4,13 @@ import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
from open_webui.internal.db import Base, get_db
|
||||||
from open_webui.utils.access_control import has_access
|
from open_webui.models.groups import Groups
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, case
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, case, cast
|
||||||
from sqlalchemy import or_, func, select, and_, text
|
from sqlalchemy import or_, func, select, and_, text
|
||||||
from sqlalchemy.sql import exists
|
from sqlalchemy.sql import exists
|
||||||
|
|
||||||
|
|
@ -26,12 +29,23 @@ class Channel(Base):
|
||||||
name = Column(Text)
|
name = Column(Text)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Used to indicate if the channel is private (for 'group' type channels)
|
||||||
|
is_private = Column(Boolean, nullable=True)
|
||||||
|
|
||||||
data = Column(JSON, nullable=True)
|
data = Column(JSON, nullable=True)
|
||||||
meta = Column(JSON, nullable=True)
|
meta = Column(JSON, nullable=True)
|
||||||
access_control = Column(JSON, nullable=True)
|
access_control = Column(JSON, nullable=True)
|
||||||
|
|
||||||
created_at = Column(BigInteger)
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
updated_at = Column(BigInteger)
|
updated_at = Column(BigInteger)
|
||||||
|
updated_by = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
archived_at = Column(BigInteger, nullable=True)
|
||||||
|
archived_by = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
deleted_at = Column(BigInteger, nullable=True)
|
||||||
|
deleted_by = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class ChannelModel(BaseModel):
|
class ChannelModel(BaseModel):
|
||||||
|
|
@ -39,17 +53,28 @@ class ChannelModel(BaseModel):
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
|
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
is_private: Optional[bool] = None
|
||||||
|
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
access_control: Optional[dict] = None
|
access_control: Optional[dict] = None
|
||||||
|
|
||||||
created_at: int # timestamp in epoch (time_ns)
|
created_at: int # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
updated_at: int # timestamp in epoch (time_ns)
|
updated_at: int # timestamp in epoch (time_ns)
|
||||||
|
updated_by: Optional[str] = None
|
||||||
|
|
||||||
|
archived_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
archived_by: Optional[str] = None
|
||||||
|
|
||||||
|
deleted_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
deleted_by: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelMember(Base):
|
class ChannelMember(Base):
|
||||||
|
|
@ -59,7 +84,9 @@ class ChannelMember(Base):
|
||||||
channel_id = Column(Text, nullable=False)
|
channel_id = Column(Text, nullable=False)
|
||||||
user_id = Column(Text, nullable=False)
|
user_id = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
role = Column(Text, nullable=True)
|
||||||
status = Column(Text, nullable=True)
|
status = Column(Text, nullable=True)
|
||||||
|
|
||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
|
|
||||||
is_channel_muted = Column(Boolean, nullable=False, default=False)
|
is_channel_muted = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
@ -68,6 +95,9 @@ class ChannelMember(Base):
|
||||||
data = Column(JSON, nullable=True)
|
data = Column(JSON, nullable=True)
|
||||||
meta = Column(JSON, nullable=True)
|
meta = Column(JSON, nullable=True)
|
||||||
|
|
||||||
|
invited_at = Column(BigInteger, nullable=True)
|
||||||
|
invited_by = Column(Text, nullable=True)
|
||||||
|
|
||||||
joined_at = Column(BigInteger)
|
joined_at = Column(BigInteger)
|
||||||
left_at = Column(BigInteger, nullable=True)
|
left_at = Column(BigInteger, nullable=True)
|
||||||
|
|
||||||
|
|
@ -84,7 +114,9 @@ class ChannelMemberModel(BaseModel):
|
||||||
channel_id: str
|
channel_id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
|
|
||||||
|
role: Optional[str] = None
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
|
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
|
||||||
is_channel_muted: bool = False
|
is_channel_muted: bool = False
|
||||||
|
|
@ -93,6 +125,9 @@ class ChannelMemberModel(BaseModel):
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
|
|
||||||
|
invited_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
invited_by: Optional[str] = None
|
||||||
|
|
||||||
joined_at: Optional[int] = None # timestamp in epoch (time_ns)
|
joined_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
left_at: Optional[int] = None # timestamp in epoch (time_ns)
|
left_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
|
|
@ -102,29 +137,128 @@ class ChannelMemberModel(BaseModel):
|
||||||
updated_at: Optional[int] = None # timestamp in epoch (time_ns)
|
updated_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelWebhook(Base):
|
||||||
|
__tablename__ = "channel_webhook"
|
||||||
|
|
||||||
|
id = Column(Text, primary_key=True, unique=True)
|
||||||
|
channel_id = Column(Text, nullable=False)
|
||||||
|
user_id = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
name = Column(Text, nullable=False)
|
||||||
|
profile_image_url = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
token = Column(Text, nullable=False)
|
||||||
|
last_used_at = Column(BigInteger, nullable=True)
|
||||||
|
|
||||||
|
created_at = Column(BigInteger, nullable=False)
|
||||||
|
updated_at = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelWebhookModel(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: str
|
||||||
|
channel_id: str
|
||||||
|
user_id: str
|
||||||
|
|
||||||
|
name: str
|
||||||
|
profile_image_url: Optional[str] = None
|
||||||
|
|
||||||
|
token: str
|
||||||
|
last_used_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
|
created_at: int # timestamp in epoch (time_ns)
|
||||||
|
updated_at: int # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class ChannelResponse(ChannelModel):
|
class ChannelResponse(ChannelModel):
|
||||||
|
is_manager: bool = False
|
||||||
write_access: bool = False
|
write_access: bool = False
|
||||||
|
|
||||||
user_count: Optional[int] = None
|
user_count: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelForm(BaseModel):
|
class ChannelForm(BaseModel):
|
||||||
type: Optional[str] = None
|
name: str = ""
|
||||||
name: str
|
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
is_private: Optional[bool] = None
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
access_control: Optional[dict] = None
|
access_control: Optional[dict] = None
|
||||||
|
group_ids: Optional[list[str]] = None
|
||||||
user_ids: Optional[list[str]] = None
|
user_ids: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CreateChannelForm(ChannelForm):
|
||||||
|
type: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelTable:
|
class ChannelTable:
|
||||||
|
|
||||||
|
def _collect_unique_user_ids(
|
||||||
|
self,
|
||||||
|
invited_by: str,
|
||||||
|
user_ids: Optional[list[str]] = None,
|
||||||
|
group_ids: Optional[list[str]] = None,
|
||||||
|
) -> set[str]:
|
||||||
|
"""
|
||||||
|
Collect unique user ids from:
|
||||||
|
- invited_by
|
||||||
|
- user_ids
|
||||||
|
- each group in group_ids
|
||||||
|
Returns a set for efficient SQL diffing.
|
||||||
|
"""
|
||||||
|
users = set(user_ids or [])
|
||||||
|
users.add(invited_by)
|
||||||
|
|
||||||
|
for group_id in group_ids or []:
|
||||||
|
users.update(Groups.get_group_user_ids_by_id(group_id))
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
|
def _create_membership_models(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
invited_by: str,
|
||||||
|
user_ids: set[str],
|
||||||
|
) -> list[ChannelMember]:
|
||||||
|
"""
|
||||||
|
Takes a set of NEW user IDs (already filtered to exclude existing members).
|
||||||
|
Returns ORM ChannelMember objects to be added.
|
||||||
|
"""
|
||||||
|
now = int(time.time_ns())
|
||||||
|
memberships = []
|
||||||
|
|
||||||
|
for uid in user_ids:
|
||||||
|
model = ChannelMemberModel(
|
||||||
|
**{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"channel_id": channel_id,
|
||||||
|
"user_id": uid,
|
||||||
|
"status": "joined",
|
||||||
|
"is_active": True,
|
||||||
|
"is_channel_muted": False,
|
||||||
|
"is_channel_pinned": False,
|
||||||
|
"invited_at": now,
|
||||||
|
"invited_by": invited_by,
|
||||||
|
"joined_at": now,
|
||||||
|
"left_at": None,
|
||||||
|
"last_read_at": now,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
memberships.append(ChannelMember(**model.model_dump()))
|
||||||
|
|
||||||
|
return memberships
|
||||||
|
|
||||||
def insert_new_channel(
|
def insert_new_channel(
|
||||||
self, form_data: ChannelForm, user_id: str
|
self, form_data: CreateChannelForm, user_id: str
|
||||||
) -> Optional[ChannelModel]:
|
) -> Optional[ChannelModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
channel = ChannelModel(
|
channel = ChannelModel(
|
||||||
|
|
@ -140,32 +274,19 @@ class ChannelTable:
|
||||||
)
|
)
|
||||||
new_channel = Channel(**channel.model_dump())
|
new_channel = Channel(**channel.model_dump())
|
||||||
|
|
||||||
if form_data.type == "dm":
|
if form_data.type in ["group", "dm"]:
|
||||||
# For direct message channels, automatically add the specified users as members
|
users = self._collect_unique_user_ids(
|
||||||
user_ids = form_data.user_ids or []
|
invited_by=user_id,
|
||||||
if user_id not in user_ids:
|
user_ids=form_data.user_ids,
|
||||||
user_ids.append(user_id) # Ensure the creator is also a member
|
group_ids=form_data.group_ids,
|
||||||
|
)
|
||||||
for uid in user_ids:
|
memberships = self._create_membership_models(
|
||||||
channel_member = ChannelMemberModel(
|
channel_id=new_channel.id,
|
||||||
**{
|
invited_by=user_id,
|
||||||
"id": str(uuid.uuid4()),
|
user_ids=users,
|
||||||
"channel_id": channel.id,
|
|
||||||
"user_id": uid,
|
|
||||||
"status": "joined",
|
|
||||||
"is_active": True,
|
|
||||||
"is_channel_muted": False,
|
|
||||||
"is_channel_pinned": False,
|
|
||||||
"joined_at": int(time.time_ns()),
|
|
||||||
"left_at": None,
|
|
||||||
"last_read_at": int(time.time_ns()),
|
|
||||||
"created_at": int(time.time_ns()),
|
|
||||||
"updated_at": int(time.time_ns()),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
new_membership = ChannelMember(**channel_member.model_dump())
|
|
||||||
db.add(new_membership)
|
|
||||||
|
|
||||||
|
db.add_all(memberships)
|
||||||
db.add(new_channel)
|
db.add(new_channel)
|
||||||
db.commit()
|
db.commit()
|
||||||
return channel
|
return channel
|
||||||
|
|
@ -175,24 +296,84 @@ class ChannelTable:
|
||||||
channels = db.query(Channel).all()
|
channels = db.query(Channel).all()
|
||||||
return [ChannelModel.model_validate(channel) for channel in channels]
|
return [ChannelModel.model_validate(channel) for channel in channels]
|
||||||
|
|
||||||
def get_channels_by_user_id(
|
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
|
||||||
self, user_id: str, permission: str = "read"
|
group_ids = filter.get("group_ids", [])
|
||||||
) -> list[ChannelModel]:
|
user_id = filter.get("user_id")
|
||||||
channels = self.get_channels()
|
|
||||||
|
|
||||||
channel_list = []
|
dialect_name = db.bind.dialect.name
|
||||||
for channel in channels:
|
|
||||||
if channel.type == "dm":
|
|
||||||
membership = self.get_member_by_channel_and_user_id(channel.id, user_id)
|
|
||||||
if membership and membership.is_active:
|
|
||||||
channel_list.append(channel)
|
|
||||||
else:
|
|
||||||
if channel.user_id == user_id or has_access(
|
|
||||||
user_id, permission, channel.access_control
|
|
||||||
):
|
|
||||||
channel_list.append(channel)
|
|
||||||
|
|
||||||
return channel_list
|
# Public access
|
||||||
|
conditions = []
|
||||||
|
if group_ids or user_id:
|
||||||
|
conditions.extend(
|
||||||
|
[
|
||||||
|
Channel.access_control.is_(None),
|
||||||
|
cast(Channel.access_control, String) == "null",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# User-level permission
|
||||||
|
if user_id:
|
||||||
|
conditions.append(Channel.user_id == user_id)
|
||||||
|
|
||||||
|
# Group-level permission
|
||||||
|
if group_ids:
|
||||||
|
group_conditions = []
|
||||||
|
for gid in group_ids:
|
||||||
|
if dialect_name == "sqlite":
|
||||||
|
group_conditions.append(
|
||||||
|
Channel.access_control[permission]["group_ids"].contains([gid])
|
||||||
|
)
|
||||||
|
elif dialect_name == "postgresql":
|
||||||
|
group_conditions.append(
|
||||||
|
cast(
|
||||||
|
Channel.access_control[permission]["group_ids"],
|
||||||
|
JSONB,
|
||||||
|
).contains([gid])
|
||||||
|
)
|
||||||
|
conditions.append(or_(*group_conditions))
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
query = query.filter(or_(*conditions))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def get_channels_by_user_id(self, user_id: str) -> list[ChannelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
user_group_ids = [
|
||||||
|
group.id for group in Groups.get_groups_by_member_id(user_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
membership_channels = (
|
||||||
|
db.query(Channel)
|
||||||
|
.join(ChannelMember, Channel.id == ChannelMember.channel_id)
|
||||||
|
.filter(
|
||||||
|
Channel.deleted_at.is_(None),
|
||||||
|
Channel.archived_at.is_(None),
|
||||||
|
Channel.type.in_(["group", "dm"]),
|
||||||
|
ChannelMember.user_id == user_id,
|
||||||
|
ChannelMember.is_active.is_(True),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
query = db.query(Channel).filter(
|
||||||
|
Channel.deleted_at.is_(None),
|
||||||
|
Channel.archived_at.is_(None),
|
||||||
|
or_(
|
||||||
|
Channel.type.is_(None), # True NULL/None
|
||||||
|
Channel.type == "", # Empty string
|
||||||
|
and_(Channel.type != "group", Channel.type != "dm"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
query = self._has_permission(
|
||||||
|
db, query, {"user_id": user_id, "group_ids": user_group_ids}
|
||||||
|
)
|
||||||
|
|
||||||
|
standard_channels = query.all()
|
||||||
|
|
||||||
|
all_channels = membership_channels + standard_channels
|
||||||
|
return [ChannelModel.model_validate(c) for c in all_channels]
|
||||||
|
|
||||||
def get_dm_channel_by_user_ids(self, user_ids: list[str]) -> Optional[ChannelModel]:
|
def get_dm_channel_by_user_ids(self, user_ids: list[str]) -> Optional[ChannelModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -227,6 +408,78 @@ class ChannelTable:
|
||||||
|
|
||||||
return ChannelModel.model_validate(channel) if channel else None
|
return ChannelModel.model_validate(channel) if channel else None
|
||||||
|
|
||||||
|
def add_members_to_channel(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
invited_by: str,
|
||||||
|
user_ids: Optional[list[str]] = None,
|
||||||
|
group_ids: Optional[list[str]] = None,
|
||||||
|
) -> list[ChannelMemberModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
# 1. Collect all user_ids including groups + inviter
|
||||||
|
requested_users = self._collect_unique_user_ids(
|
||||||
|
invited_by, user_ids, group_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_users = {
|
||||||
|
row.user_id
|
||||||
|
for row in db.query(ChannelMember.user_id)
|
||||||
|
.filter(ChannelMember.channel_id == channel_id)
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user_ids = requested_users - existing_users
|
||||||
|
if not new_user_ids:
|
||||||
|
return [] # Nothing to add
|
||||||
|
|
||||||
|
new_memberships = self._create_membership_models(
|
||||||
|
channel_id, invited_by, new_user_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add_all(new_memberships)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return [
|
||||||
|
ChannelMemberModel.model_validate(membership)
|
||||||
|
for membership in new_memberships
|
||||||
|
]
|
||||||
|
|
||||||
|
def remove_members_from_channel(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
user_ids: list[str],
|
||||||
|
) -> int:
|
||||||
|
with get_db() as db:
|
||||||
|
result = (
|
||||||
|
db.query(ChannelMember)
|
||||||
|
.filter(
|
||||||
|
ChannelMember.channel_id == channel_id,
|
||||||
|
ChannelMember.user_id.in_(user_ids),
|
||||||
|
)
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return result # number of rows deleted
|
||||||
|
|
||||||
|
def is_user_channel_manager(self, channel_id: str, user_id: str) -> bool:
|
||||||
|
with get_db() as db:
|
||||||
|
# Check if the user is the creator of the channel
|
||||||
|
# or has a 'manager' role in ChannelMember
|
||||||
|
channel = db.query(Channel).filter(Channel.id == channel_id).first()
|
||||||
|
if channel and channel.user_id == user_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
membership = (
|
||||||
|
db.query(ChannelMember)
|
||||||
|
.filter(
|
||||||
|
ChannelMember.channel_id == channel_id,
|
||||||
|
ChannelMember.user_id == user_id,
|
||||||
|
ChannelMember.role == "manager",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return membership is not None
|
||||||
|
|
||||||
def join_channel(
|
def join_channel(
|
||||||
self, channel_id: str, user_id: str
|
self, channel_id: str, user_id: str
|
||||||
) -> Optional[ChannelMemberModel]:
|
) -> Optional[ChannelMemberModel]:
|
||||||
|
|
@ -398,8 +651,12 @@ class ChannelTable:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
channel.name = form_data.name
|
channel.name = form_data.name
|
||||||
|
channel.description = form_data.description
|
||||||
|
channel.is_private = form_data.is_private
|
||||||
|
|
||||||
channel.data = form_data.data
|
channel.data = form_data.data
|
||||||
channel.meta = form_data.meta
|
channel.meta = form_data.meta
|
||||||
|
|
||||||
channel.access_control = form_data.access_control
|
channel.access_control = form_data.access_control
|
||||||
channel.updated_at = int(time.time_ns())
|
channel.updated_at = int(time.time_ns())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,18 @@ from open_webui.models.files import FileMetadataResponse
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text, JSON, func, ForeignKey
|
from sqlalchemy import (
|
||||||
|
BigInteger,
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
JSON,
|
||||||
|
and_,
|
||||||
|
func,
|
||||||
|
ForeignKey,
|
||||||
|
cast,
|
||||||
|
or_,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -41,7 +52,6 @@ class Group(Base):
|
||||||
|
|
||||||
|
|
||||||
class GroupModel(BaseModel):
|
class GroupModel(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
id: str
|
id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
|
|
||||||
|
|
@ -56,6 +66,8 @@ class GroupModel(BaseModel):
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
class GroupMember(Base):
|
class GroupMember(Base):
|
||||||
__tablename__ = "group_member"
|
__tablename__ = "group_member"
|
||||||
|
|
@ -84,17 +96,8 @@ class GroupMemberModel(BaseModel):
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
class GroupResponse(BaseModel):
|
class GroupResponse(GroupModel):
|
||||||
id: str
|
|
||||||
user_id: str
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
permissions: Optional[dict] = None
|
|
||||||
data: Optional[dict] = None
|
|
||||||
meta: Optional[dict] = None
|
|
||||||
member_count: Optional[int] = None
|
member_count: Optional[int] = None
|
||||||
created_at: int # timestamp in epoch
|
|
||||||
updated_at: int # timestamp in epoch
|
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(BaseModel):
|
class GroupForm(BaseModel):
|
||||||
|
|
@ -112,6 +115,11 @@ class GroupUpdateForm(GroupForm):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GroupListResponse(BaseModel):
|
||||||
|
items: list[GroupResponse] = []
|
||||||
|
total: int = 0
|
||||||
|
|
||||||
|
|
||||||
class GroupTable:
|
class GroupTable:
|
||||||
def insert_new_group(
|
def insert_new_group(
|
||||||
self, user_id: str, form_data: GroupForm
|
self, user_id: str, form_data: GroupForm
|
||||||
|
|
@ -140,13 +148,87 @@ class GroupTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_groups(self) -> list[GroupModel]:
|
def get_all_groups(self) -> list[GroupModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
groups = db.query(Group).order_by(Group.updated_at.desc()).all()
|
||||||
|
return [GroupModel.model_validate(group) for group in groups]
|
||||||
|
|
||||||
|
def get_groups(self, filter) -> list[GroupResponse]:
|
||||||
|
with get_db() as db:
|
||||||
|
query = db.query(Group)
|
||||||
|
|
||||||
|
if filter:
|
||||||
|
if "query" in filter:
|
||||||
|
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
|
||||||
|
if "member_id" in filter:
|
||||||
|
query = query.join(
|
||||||
|
GroupMember, GroupMember.group_id == Group.id
|
||||||
|
).filter(GroupMember.user_id == filter["member_id"])
|
||||||
|
|
||||||
|
if "share" in filter:
|
||||||
|
share_value = filter["share"]
|
||||||
|
json_share = Group.data["config"]["share"].as_boolean()
|
||||||
|
|
||||||
|
if share_value:
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
Group.data.is_(None),
|
||||||
|
json_share.is_(None),
|
||||||
|
json_share == True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = query.filter(
|
||||||
|
and_(Group.data.isnot(None), json_share == False)
|
||||||
|
)
|
||||||
|
groups = query.order_by(Group.updated_at.desc()).all()
|
||||||
return [
|
return [
|
||||||
GroupModel.model_validate(group)
|
GroupResponse.model_validate(
|
||||||
for group in db.query(Group).order_by(Group.updated_at.desc()).all()
|
{
|
||||||
|
**GroupModel.model_validate(group).model_dump(),
|
||||||
|
"member_count": self.get_group_member_count_by_id(group.id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for group in groups
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def search_groups(
|
||||||
|
self, filter: Optional[dict] = None, skip: int = 0, limit: int = 30
|
||||||
|
) -> GroupListResponse:
|
||||||
|
with get_db() as db:
|
||||||
|
query = db.query(Group)
|
||||||
|
|
||||||
|
if filter:
|
||||||
|
if "query" in filter:
|
||||||
|
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
|
||||||
|
if "member_id" in filter:
|
||||||
|
query = query.join(
|
||||||
|
GroupMember, GroupMember.group_id == Group.id
|
||||||
|
).filter(GroupMember.user_id == filter["member_id"])
|
||||||
|
|
||||||
|
if "share" in filter:
|
||||||
|
# 'share' is stored in data JSON, support both sqlite and postgres
|
||||||
|
share_value = filter["share"]
|
||||||
|
print("Filtering by share:", share_value)
|
||||||
|
query = query.filter(
|
||||||
|
Group.data.op("->>")("share") == str(share_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
query = query.order_by(Group.updated_at.desc())
|
||||||
|
groups = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
GroupResponse.model_validate(
|
||||||
|
**GroupModel.model_validate(group).model_dump(),
|
||||||
|
member_count=self.get_group_member_count_by_id(group.id),
|
||||||
|
)
|
||||||
|
for group in groups
|
||||||
|
],
|
||||||
|
"total": total,
|
||||||
|
}
|
||||||
|
|
||||||
def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]:
|
def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [
|
return [
|
||||||
|
|
@ -293,7 +375,7 @@ class GroupTable:
|
||||||
) -> list[GroupModel]:
|
) -> list[GroupModel]:
|
||||||
|
|
||||||
# check for existing groups
|
# check for existing groups
|
||||||
existing_groups = self.get_groups()
|
existing_groups = self.get_all_groups()
|
||||||
existing_group_names = {group.name for group in existing_groups}
|
existing_group_names = {group.name for group in existing_groups}
|
||||||
|
|
||||||
new_groups = []
|
new_groups = []
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
from open_webui.internal.db import Base, get_db
|
||||||
from open_webui.models.tags import TagModel, Tag, Tags
|
from open_webui.models.tags import TagModel, Tag, Tags
|
||||||
from open_webui.models.users import Users, UserNameResponse
|
from open_webui.models.users import Users, User, UserNameResponse
|
||||||
from open_webui.models.channels import Channels, ChannelMember
|
from open_webui.models.channels import Channels, ChannelMember
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ class MessageForm(BaseModel):
|
||||||
|
|
||||||
class Reactions(BaseModel):
|
class Reactions(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
user_ids: list[str]
|
users: list[dict]
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -373,6 +373,15 @@ class MessageTable:
|
||||||
self, id: str, user_id: str, name: str
|
self, id: str, user_id: str, name: str
|
||||||
) -> Optional[MessageReactionModel]:
|
) -> Optional[MessageReactionModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
# check for existing reaction
|
||||||
|
existing_reaction = (
|
||||||
|
db.query(MessageReaction)
|
||||||
|
.filter_by(message_id=id, user_id=user_id, name=name)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing_reaction:
|
||||||
|
return MessageReactionModel.model_validate(existing_reaction)
|
||||||
|
|
||||||
reaction_id = str(uuid.uuid4())
|
reaction_id = str(uuid.uuid4())
|
||||||
reaction = MessageReactionModel(
|
reaction = MessageReactionModel(
|
||||||
id=reaction_id,
|
id=reaction_id,
|
||||||
|
|
@ -389,17 +398,30 @@ class MessageTable:
|
||||||
|
|
||||||
def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
|
def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
all_reactions = db.query(MessageReaction).filter_by(message_id=id).all()
|
# JOIN User so all user info is fetched in one query
|
||||||
|
results = (
|
||||||
|
db.query(MessageReaction, User)
|
||||||
|
.join(User, MessageReaction.user_id == User.id)
|
||||||
|
.filter(MessageReaction.message_id == id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
reactions = {}
|
reactions = {}
|
||||||
for reaction in all_reactions:
|
|
||||||
|
for reaction, user in results:
|
||||||
if reaction.name not in reactions:
|
if reaction.name not in reactions:
|
||||||
reactions[reaction.name] = {
|
reactions[reaction.name] = {
|
||||||
"name": reaction.name,
|
"name": reaction.name,
|
||||||
"user_ids": [],
|
"users": [],
|
||||||
"count": 0,
|
"count": 0,
|
||||||
}
|
}
|
||||||
reactions[reaction.name]["user_ids"].append(reaction.user_id)
|
|
||||||
|
reactions[reaction.name]["users"].append(
|
||||||
|
{
|
||||||
|
"id": user.id,
|
||||||
|
"name": user.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
reactions[reaction.name]["count"] += 1
|
reactions[reaction.name]["count"] += 1
|
||||||
|
|
||||||
return [Reactions(**reaction) for reaction in reactions.values()]
|
return [Reactions(**reaction) for reaction in reactions.values()]
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
from sqlalchemy import String, cast, or_, and_, func
|
from sqlalchemy import String, cast, or_, and_, func
|
||||||
from sqlalchemy.dialects import postgresql, sqlite
|
from sqlalchemy.dialects import postgresql, sqlite
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
|
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -220,30 +222,44 @@ class ModelsTable:
|
||||||
or has_access(user_id, permission, model.access_control, user_group_ids)
|
or has_access(user_id, permission, model.access_control, user_group_ids)
|
||||||
]
|
]
|
||||||
|
|
||||||
def _has_write_permission(self, query, filter: dict):
|
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
|
||||||
if filter.get("group_ids") or filter.get("user_id"):
|
group_ids = filter.get("group_ids", [])
|
||||||
|
user_id = filter.get("user_id")
|
||||||
|
|
||||||
|
dialect_name = db.bind.dialect.name
|
||||||
|
|
||||||
|
# Public access
|
||||||
conditions = []
|
conditions = []
|
||||||
|
if group_ids or user_id:
|
||||||
|
conditions.extend(
|
||||||
|
[
|
||||||
|
Model.access_control.is_(None),
|
||||||
|
cast(Model.access_control, String) == "null",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# --- ANY group_ids match ("write".group_ids) ---
|
# User-level permission
|
||||||
if filter.get("group_ids"):
|
if user_id:
|
||||||
group_ids = filter["group_ids"]
|
conditions.append(Model.user_id == user_id)
|
||||||
like_clauses = []
|
|
||||||
|
|
||||||
|
# Group-level permission
|
||||||
|
if group_ids:
|
||||||
|
group_conditions = []
|
||||||
for gid in group_ids:
|
for gid in group_ids:
|
||||||
like_clauses.append(
|
if dialect_name == "sqlite":
|
||||||
cast(Model.access_control, String).like(
|
group_conditions.append(
|
||||||
f'%"write"%"group_ids"%"{gid}"%'
|
Model.access_control[permission]["group_ids"].contains([gid])
|
||||||
)
|
)
|
||||||
|
elif dialect_name == "postgresql":
|
||||||
|
group_conditions.append(
|
||||||
|
cast(
|
||||||
|
Model.access_control[permission]["group_ids"],
|
||||||
|
JSONB,
|
||||||
|
).contains([gid])
|
||||||
)
|
)
|
||||||
|
conditions.append(or_(*group_conditions))
|
||||||
|
|
||||||
# ANY → OR
|
if conditions:
|
||||||
conditions.append(or_(*like_clauses))
|
|
||||||
|
|
||||||
# --- user_id match (owner) ---
|
|
||||||
if filter.get("user_id"):
|
|
||||||
conditions.append(Model.user_id == filter["user_id"])
|
|
||||||
|
|
||||||
# Apply OR across the two groups of conditions
|
|
||||||
query = query.filter(or_(*conditions))
|
query = query.filter(or_(*conditions))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
@ -266,15 +282,20 @@ class ModelsTable:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply access control filtering
|
|
||||||
query = self._has_write_permission(query, filter)
|
|
||||||
|
|
||||||
view_option = filter.get("view_option")
|
view_option = filter.get("view_option")
|
||||||
if view_option == "created":
|
if view_option == "created":
|
||||||
query = query.filter(Model.user_id == user_id)
|
query = query.filter(Model.user_id == user_id)
|
||||||
elif view_option == "shared":
|
elif view_option == "shared":
|
||||||
query = query.filter(Model.user_id != user_id)
|
query = query.filter(Model.user_id != user_id)
|
||||||
|
|
||||||
|
# Apply access control filtering
|
||||||
|
query = self._has_permission(
|
||||||
|
db,
|
||||||
|
query,
|
||||||
|
filter,
|
||||||
|
permission="write",
|
||||||
|
)
|
||||||
|
|
||||||
tag = filter.get("tag")
|
tag = filter.get("tag")
|
||||||
if tag:
|
if tag:
|
||||||
# TODO: This is a simple implementation and should be improved for performance
|
# TODO: This is a simple implementation and should be improved for performance
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ from open_webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.groups import Groups, GroupMember
|
from open_webui.models.groups import Groups, GroupMember
|
||||||
|
from open_webui.models.channels import ChannelMember
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.misc import throttle
|
from open_webui.utils.misc import throttle
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,6 +107,12 @@ class UserModel(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserStatusModel(UserModel):
|
||||||
|
is_active: bool = False
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
class ApiKey(Base):
|
class ApiKey(Base):
|
||||||
__tablename__ = "api_key"
|
__tablename__ = "api_key"
|
||||||
|
|
||||||
|
|
@ -161,7 +170,13 @@ class UserGroupIdsListResponse(BaseModel):
|
||||||
total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
class UserInfoResponse(BaseModel):
|
class UserStatus(BaseModel):
|
||||||
|
status_emoji: Optional[str] = None
|
||||||
|
status_message: Optional[str] = None
|
||||||
|
status_expires_at: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfoResponse(UserStatus):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
|
|
@ -176,7 +191,7 @@ class UserIdNameResponse(BaseModel):
|
||||||
class UserIdNameStatusResponse(BaseModel):
|
class UserIdNameStatusResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
is_active: bool = False
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class UserInfoListResponse(BaseModel):
|
class UserInfoListResponse(BaseModel):
|
||||||
|
|
@ -311,6 +326,17 @@ class UsersTable:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
channel_id = filter.get("channel_id")
|
||||||
|
if channel_id:
|
||||||
|
query = query.filter(
|
||||||
|
exists(
|
||||||
|
select(ChannelMember.id).where(
|
||||||
|
ChannelMember.user_id == User.id,
|
||||||
|
ChannelMember.channel_id == channel_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
user_ids = filter.get("user_ids")
|
user_ids = filter.get("user_ids")
|
||||||
group_ids = filter.get("group_ids")
|
group_ids = filter.get("group_ids")
|
||||||
|
|
||||||
|
|
@ -417,7 +443,7 @@ class UsersTable:
|
||||||
"total": total,
|
"total": total,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
|
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserStatusModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||||
return [UserModel.model_validate(user) for user in users]
|
return [UserModel.model_validate(user) for user in users]
|
||||||
|
|
@ -473,6 +499,21 @@ class UsersTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def update_user_status_by_id(
|
||||||
|
self, id: str, form_data: UserStatus
|
||||||
|
) -> Optional[UserModel]:
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
db.query(User).filter_by(id=id).update(
|
||||||
|
{**form_data.model_dump(exclude_none=True)}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
user = db.query(User).filter_by(id=id).first()
|
||||||
|
return UserModel.model_validate(user)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_user_profile_image_url_by_id(
|
def update_user_profile_image_url_by_id(
|
||||||
self, id: str, profile_image_url: str
|
self, id: str, profile_image_url: str
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,12 @@ from open_webui.models.auths import (
|
||||||
SignupForm,
|
SignupForm,
|
||||||
UpdatePasswordForm,
|
UpdatePasswordForm,
|
||||||
)
|
)
|
||||||
from open_webui.models.users import UserProfileImageResponse, Users, UpdateProfileForm
|
from open_webui.models.users import (
|
||||||
|
UserProfileImageResponse,
|
||||||
|
Users,
|
||||||
|
UpdateProfileForm,
|
||||||
|
UserStatus,
|
||||||
|
)
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.oauth_sessions import OAuthSessions
|
from open_webui.models.oauth_sessions import OAuthSessions
|
||||||
|
|
||||||
|
|
@ -82,7 +87,7 @@ class SessionUserResponse(Token, UserProfileImageResponse):
|
||||||
permissions: Optional[dict] = None
|
permissions: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class SessionUserInfoResponse(SessionUserResponse):
|
class SessionUserInfoResponse(SessionUserResponse, UserStatus):
|
||||||
bio: Optional[str] = None
|
bio: Optional[str] = None
|
||||||
gender: Optional[str] = None
|
gender: Optional[str] = None
|
||||||
date_of_birth: Optional[datetime.date] = None
|
date_of_birth: Optional[datetime.date] = None
|
||||||
|
|
@ -139,6 +144,9 @@ async def get_session_user(
|
||||||
"bio": user.bio,
|
"bio": user.bio,
|
||||||
"gender": user.gender,
|
"gender": user.gender,
|
||||||
"date_of_birth": user.date_of_birth,
|
"date_of_birth": user.date_of_birth,
|
||||||
|
"status_emoji": user.status_emoji,
|
||||||
|
"status_message": user.status_message,
|
||||||
|
"status_expires_at": user.status_expires_at,
|
||||||
"permissions": user_permissions,
|
"permissions": user_permissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
from open_webui.socket.main import (
|
from open_webui.socket.main import (
|
||||||
|
emit_to_users,
|
||||||
|
enter_room_for_users,
|
||||||
sio,
|
sio,
|
||||||
get_user_ids_from_room,
|
get_user_ids_from_room,
|
||||||
)
|
)
|
||||||
|
|
@ -26,6 +28,7 @@ from open_webui.models.channels import (
|
||||||
ChannelModel,
|
ChannelModel,
|
||||||
ChannelForm,
|
ChannelForm,
|
||||||
ChannelResponse,
|
ChannelResponse,
|
||||||
|
CreateChannelForm,
|
||||||
)
|
)
|
||||||
from open_webui.models.messages import (
|
from open_webui.models.messages import (
|
||||||
Messages,
|
Messages,
|
||||||
|
|
@ -53,6 +56,7 @@ from open_webui.utils.access_control import (
|
||||||
has_access,
|
has_access,
|
||||||
get_users_with_access,
|
get_users_with_access,
|
||||||
get_permitted_group_and_user_ids,
|
get_permitted_group_and_user_ids,
|
||||||
|
has_permission,
|
||||||
)
|
)
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
from open_webui.utils.channels import extract_mentions, replace_mentions
|
from open_webui.utils.channels import extract_mentions, replace_mentions
|
||||||
|
|
@ -76,18 +80,28 @@ class ChannelListItemResponse(ChannelModel):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[ChannelListItemResponse])
|
@router.get("/", response_model=list[ChannelListItemResponse])
|
||||||
async def get_channels(user=Depends(get_verified_user)):
|
async def get_channels(request: Request, user=Depends(get_verified_user)):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
channels = Channels.get_channels_by_user_id(user.id)
|
channels = Channels.get_channels_by_user_id(user.id)
|
||||||
|
|
||||||
channel_list = []
|
channel_list = []
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
last_message = Messages.get_last_message_by_channel_id(channel.id)
|
last_message = Messages.get_last_message_by_channel_id(channel.id)
|
||||||
last_message_at = last_message.created_at if last_message else None
|
last_message_at = last_message.created_at if last_message else None
|
||||||
|
|
||||||
channel_member = Channels.get_member_by_channel_and_user_id(channel.id, user.id)
|
channel_member = Channels.get_member_by_channel_and_user_id(channel.id, user.id)
|
||||||
unread_count = Messages.get_unread_message_count(
|
unread_count = (
|
||||||
channel.id, user.id, channel_member.last_read_at if channel_member else None
|
Messages.get_unread_message_count(
|
||||||
|
channel.id, user.id, channel_member.last_read_at
|
||||||
|
)
|
||||||
|
if channel_member
|
||||||
|
else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
user_ids = None
|
user_ids = None
|
||||||
|
|
@ -124,24 +138,141 @@ async def get_all_channels(user=Depends(get_verified_user)):
|
||||||
return Channels.get_channels_by_user_id(user.id)
|
return Channels.get_channels_by_user_id(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetDMChannelByUserId
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}", response_model=Optional[ChannelModel])
|
||||||
|
async def get_dm_channel_by_user_id(
|
||||||
|
request: Request, user_id: str, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_channel = Channels.get_dm_channel_by_user_ids([user.id, user_id])
|
||||||
|
if existing_channel:
|
||||||
|
participant_ids = [
|
||||||
|
member.user_id
|
||||||
|
for member in Channels.get_members_by_channel_id(existing_channel.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
await emit_to_users(
|
||||||
|
"events:channel",
|
||||||
|
{"data": {"type": "channel:created"}},
|
||||||
|
participant_ids,
|
||||||
|
)
|
||||||
|
await enter_room_for_users(
|
||||||
|
f"channel:{existing_channel.id}", participant_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
Channels.update_member_active_status(existing_channel.id, user.id, True)
|
||||||
|
return ChannelModel(**existing_channel.model_dump())
|
||||||
|
|
||||||
|
channel = Channels.insert_new_channel(
|
||||||
|
CreateChannelForm(
|
||||||
|
type="dm",
|
||||||
|
name="",
|
||||||
|
user_ids=[user_id],
|
||||||
|
),
|
||||||
|
user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
participant_ids = [
|
||||||
|
member.user_id
|
||||||
|
for member in Channels.get_members_by_channel_id(channel.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
await emit_to_users(
|
||||||
|
"events:channel",
|
||||||
|
{"data": {"type": "channel:created"}},
|
||||||
|
participant_ids,
|
||||||
|
)
|
||||||
|
await enter_room_for_users(f"channel:{channel.id}", participant_ids)
|
||||||
|
|
||||||
|
return ChannelModel(**channel.model_dump())
|
||||||
|
else:
|
||||||
|
raise Exception("Error creating channel")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# CreateNewChannel
|
# CreateNewChannel
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[ChannelModel])
|
@router.post("/create", response_model=Optional[ChannelModel])
|
||||||
async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user)):
|
async def create_new_channel(
|
||||||
|
request: Request, form_data: CreateChannelForm, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
if form_data.type not in ["group", "dm"] and user.role != "admin":
|
||||||
|
# Only admins can create standard channels (joined by default)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if form_data.type == "dm":
|
if form_data.type == "dm":
|
||||||
existing_channel = Channels.get_dm_channel_by_user_ids(
|
existing_channel = Channels.get_dm_channel_by_user_ids(
|
||||||
[user.id, *form_data.user_ids]
|
[user.id, *form_data.user_ids]
|
||||||
)
|
)
|
||||||
if existing_channel:
|
if existing_channel:
|
||||||
|
participant_ids = [
|
||||||
|
member.user_id
|
||||||
|
for member in Channels.get_members_by_channel_id(
|
||||||
|
existing_channel.id
|
||||||
|
)
|
||||||
|
]
|
||||||
|
await emit_to_users(
|
||||||
|
"events:channel",
|
||||||
|
{"data": {"type": "channel:created"}},
|
||||||
|
participant_ids,
|
||||||
|
)
|
||||||
|
await enter_room_for_users(
|
||||||
|
f"channel:{existing_channel.id}", participant_ids
|
||||||
|
)
|
||||||
|
|
||||||
Channels.update_member_active_status(existing_channel.id, user.id, True)
|
Channels.update_member_active_status(existing_channel.id, user.id, True)
|
||||||
return ChannelModel(**existing_channel.model_dump())
|
return ChannelModel(**existing_channel.model_dump())
|
||||||
|
|
||||||
channel = Channels.insert_new_channel(form_data, user.id)
|
channel = Channels.insert_new_channel(form_data, user.id)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
participant_ids = [
|
||||||
|
member.user_id
|
||||||
|
for member in Channels.get_members_by_channel_id(channel.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
await emit_to_users(
|
||||||
|
"events:channel",
|
||||||
|
{"data": {"type": "channel:created"}},
|
||||||
|
participant_ids,
|
||||||
|
)
|
||||||
|
await enter_room_for_users(f"channel:{channel.id}", participant_ids)
|
||||||
|
|
||||||
return ChannelModel(**channel.model_dump())
|
return ChannelModel(**channel.model_dump())
|
||||||
|
else:
|
||||||
|
raise Exception("Error creating channel")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -155,8 +286,8 @@ async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user
|
||||||
|
|
||||||
|
|
||||||
class ChannelFullResponse(ChannelResponse):
|
class ChannelFullResponse(ChannelResponse):
|
||||||
user_ids: Optional[list[str]] = None # 'dm' channels only
|
user_ids: Optional[list[str]] = None # 'group'/'dm' channels only
|
||||||
users: Optional[list[UserIdNameResponse]] = None # 'dm' channels only
|
users: Optional[list[UserIdNameStatusResponse]] = None # 'group'/'dm' channels only
|
||||||
|
|
||||||
last_read_at: Optional[int] = None # timestamp in epoch (time_ns)
|
last_read_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
unread_count: int = 0
|
unread_count: int = 0
|
||||||
|
|
@ -173,7 +304,7 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
user_ids = None
|
user_ids = None
|
||||||
users = None
|
users = None
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -182,8 +313,11 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
user_ids = [
|
user_ids = [
|
||||||
member.user_id for member in Channels.get_members_by_channel_id(channel.id)
|
member.user_id for member in Channels.get_members_by_channel_id(channel.id)
|
||||||
]
|
]
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
UserIdNameResponse(**user.model_dump())
|
UserIdNameStatusResponse(
|
||||||
|
**{**user.model_dump(), "is_active": Users.is_user_active(user.id)}
|
||||||
|
)
|
||||||
for user in Users.get_users_by_user_ids(user_ids)
|
for user in Users.get_users_by_user_ids(user_ids)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -197,13 +331,13 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
**channel.model_dump(),
|
**channel.model_dump(),
|
||||||
"user_ids": user_ids,
|
"user_ids": user_ids,
|
||||||
"users": users,
|
"users": users,
|
||||||
|
"is_manager": Channels.is_user_channel_manager(channel.id, user.id),
|
||||||
"write_access": True,
|
"write_access": True,
|
||||||
"user_count": len(user_ids),
|
"user_count": len(user_ids),
|
||||||
"last_read_at": channel_member.last_read_at if channel_member else None,
|
"last_read_at": channel_member.last_read_at if channel_member else None,
|
||||||
"unread_count": unread_count,
|
"unread_count": unread_count,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if user.role != "admin" and not has_access(
|
if user.role != "admin" and not has_access(
|
||||||
user.id, type="read", access_control=channel.access_control
|
user.id, type="read", access_control=channel.access_control
|
||||||
|
|
@ -228,6 +362,7 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
**channel.model_dump(),
|
**channel.model_dump(),
|
||||||
"user_ids": user_ids,
|
"user_ids": user_ids,
|
||||||
"users": users,
|
"users": users,
|
||||||
|
"is_manager": Channels.is_user_channel_manager(channel.id, user.id),
|
||||||
"write_access": write_access or user.role == "admin",
|
"write_access": write_access or user.role == "admin",
|
||||||
"user_count": user_count,
|
"user_count": user_count,
|
||||||
"last_read_at": channel_member.last_read_at if channel_member else None,
|
"last_read_at": channel_member.last_read_at if channel_member else None,
|
||||||
|
|
@ -265,17 +400,17 @@ async def get_channel_members_by_id(
|
||||||
page = max(1, page)
|
page = max(1, page)
|
||||||
skip = (page - 1) * limit
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if channel.type == "dm":
|
||||||
user_ids = [
|
user_ids = [
|
||||||
member.user_id for member in Channels.get_members_by_channel_id(channel.id)
|
member.user_id for member in Channels.get_members_by_channel_id(channel.id)
|
||||||
]
|
]
|
||||||
users = Users.get_users_by_user_ids(user_ids)
|
users = Users.get_users_by_user_ids(user_ids)
|
||||||
|
|
||||||
total = len(users)
|
total = len(users)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -287,11 +422,8 @@ async def get_channel_members_by_id(
|
||||||
],
|
],
|
||||||
"total": total,
|
"total": total,
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
filter = {
|
filter = {}
|
||||||
"roles": ["!pending"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
filter["query"] = query
|
filter["query"] = query
|
||||||
|
|
@ -300,7 +432,13 @@ async def get_channel_members_by_id(
|
||||||
if direction:
|
if direction:
|
||||||
filter["direction"] = direction
|
filter["direction"] = direction
|
||||||
|
|
||||||
permitted_ids = get_permitted_group_and_user_ids("read", channel.access_control)
|
if channel.type == "group":
|
||||||
|
filter["channel_id"] = channel.id
|
||||||
|
else:
|
||||||
|
filter["roles"] = ["!pending"]
|
||||||
|
permitted_ids = get_permitted_group_and_user_ids(
|
||||||
|
"read", channel.access_control
|
||||||
|
)
|
||||||
if permitted_ids:
|
if permitted_ids:
|
||||||
filter["user_ids"] = permitted_ids.get("user_ids")
|
filter["user_ids"] = permitted_ids.get("user_ids")
|
||||||
filter["group_ids"] = permitted_ids.get("group_ids")
|
filter["group_ids"] = permitted_ids.get("group_ids")
|
||||||
|
|
@ -351,6 +489,101 @@ async def update_is_active_member_by_id_and_user_id(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
# AddMembersById
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateMembersForm(BaseModel):
|
||||||
|
user_ids: list[str] = []
|
||||||
|
group_ids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/update/members/add")
|
||||||
|
async def add_members_by_id(
|
||||||
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
form_data: UpdateMembersForm,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = Channels.get_channel_by_id(id)
|
||||||
|
if not channel:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
if channel.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
memberships = Channels.add_members_to_channel(
|
||||||
|
channel.id, user.id, form_data.user_ids, form_data.group_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
return memberships
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
#
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveMembersForm(BaseModel):
|
||||||
|
user_ids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/update/members/remove")
|
||||||
|
async def remove_members_by_id(
|
||||||
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
form_data: RemoveMembersForm,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = Channels.get_channel_by_id(id)
|
||||||
|
if not channel:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
if channel.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
deleted = Channels.remove_members_from_channel(channel.id, form_data.user_ids)
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# UpdateChannelById
|
# UpdateChannelById
|
||||||
############################
|
############################
|
||||||
|
|
@ -358,14 +591,27 @@ async def update_is_active_member_by_id_and_user_id(
|
||||||
|
|
||||||
@router.post("/{id}/update", response_model=Optional[ChannelModel])
|
@router.post("/{id}/update", response_model=Optional[ChannelModel])
|
||||||
async def update_channel_by_id(
|
async def update_channel_by_id(
|
||||||
id: str, form_data: ChannelForm, user=Depends(get_admin_user)
|
request: Request, id: str, form_data: ChannelForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
channel = Channels.get_channel_by_id(id)
|
channel = Channels.get_channel_by_id(id)
|
||||||
if not channel:
|
if not channel:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if channel.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
channel = Channels.update_channel_by_id(id, form_data)
|
channel = Channels.update_channel_by_id(id, form_data)
|
||||||
return ChannelModel(**channel.model_dump())
|
return ChannelModel(**channel.model_dump())
|
||||||
|
|
@ -382,13 +628,28 @@ async def update_channel_by_id(
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}/delete", response_model=bool)
|
@router.delete("/{id}/delete", response_model=bool)
|
||||||
async def delete_channel_by_id(id: str, user=Depends(get_admin_user)):
|
async def delete_channel_by_id(
|
||||||
|
request: Request, id: str, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
channel = Channels.get_channel_by_id(id)
|
channel = Channels.get_channel_by_id(id)
|
||||||
if not channel:
|
if not channel:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if channel.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Channels.delete_channel_by_id(id)
|
Channels.delete_channel_by_id(id)
|
||||||
return True
|
return True
|
||||||
|
|
@ -418,7 +679,7 @@ async def get_channel_messages(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -481,7 +742,7 @@ async def get_pinned_channel_messages(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -735,7 +996,7 @@ async def new_message_handler(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -751,7 +1012,7 @@ async def new_message_handler(
|
||||||
try:
|
try:
|
||||||
message = Messages.insert_new_message(form_data, channel.id, user.id)
|
message = Messages.insert_new_message(form_data, channel.id, user.id)
|
||||||
if message:
|
if message:
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
members = Channels.get_members_by_channel_id(channel.id)
|
members = Channels.get_members_by_channel_id(channel.id)
|
||||||
for member in members:
|
for member in members:
|
||||||
if not member.is_active:
|
if not member.is_active:
|
||||||
|
|
@ -857,7 +1118,7 @@ async def get_channel_message(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -912,7 +1173,7 @@ async def pin_channel_message(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -975,7 +1236,7 @@ async def get_channel_thread_messages(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -1040,7 +1301,7 @@ async def update_message_by_id(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -1104,7 +1365,7 @@ async def add_reaction_to_message(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -1173,7 +1434,7 @@ async def remove_reaction_by_id_and_user_id_and_name(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
@ -1256,7 +1517,7 @@ async def delete_message_by_id(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
if channel.type == "dm":
|
if channel.type in ["group", "dm"]:
|
||||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
|
|
||||||
|
|
@ -32,31 +32,17 @@ router = APIRouter()
|
||||||
|
|
||||||
@router.get("/", response_model=list[GroupResponse])
|
@router.get("/", response_model=list[GroupResponse])
|
||||||
async def get_groups(share: Optional[bool] = None, user=Depends(get_verified_user)):
|
async def get_groups(share: Optional[bool] = None, user=Depends(get_verified_user)):
|
||||||
if user.role == "admin":
|
|
||||||
groups = Groups.get_groups()
|
|
||||||
else:
|
|
||||||
groups = Groups.get_groups_by_member_id(user.id)
|
|
||||||
|
|
||||||
group_list = []
|
filter = {}
|
||||||
|
if user.role != "admin":
|
||||||
|
filter["member_id"] = user.id
|
||||||
|
|
||||||
for group in groups:
|
|
||||||
if share is not None:
|
if share is not None:
|
||||||
# Check if the group has data and a config with share key
|
filter["share"] = share
|
||||||
if (
|
|
||||||
group.data
|
|
||||||
and "share" in group.data.get("config", {})
|
|
||||||
and group.data["config"]["share"] != share
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
group_list.append(
|
groups = Groups.get_groups(filter=filter)
|
||||||
GroupResponse(
|
|
||||||
**group.model_dump(),
|
|
||||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return group_list
|
return groups
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,11 @@ def remove_file_from_knowledge_by_id(
|
||||||
try:
|
try:
|
||||||
VECTOR_DB_CLIENT.delete(
|
VECTOR_DB_CLIENT.delete(
|
||||||
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
|
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
|
||||||
)
|
) # Remove by file_id first
|
||||||
|
|
||||||
|
VECTOR_DB_CLIENT.delete(
|
||||||
|
collection_name=knowledge.id, filter={"hash": file.hash}
|
||||||
|
) # Remove by hash as well in case of duplicates
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug("This was most likely caused by bypassing embedding processing")
|
log.debug("This was most likely caused by bypassing embedding processing")
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
|
|
@ -579,7 +583,6 @@ def remove_file_from_knowledge_by_id(
|
||||||
data["file_ids"] = file_ids
|
data["file_ids"] = file_ids
|
||||||
|
|
||||||
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
|
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
files = Files.get_file_metadatas_by_ids(file_ids)
|
files = Files.get_file_metadatas_by_ids(file_ids)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -719,7 +719,7 @@ async def get_groups(
|
||||||
):
|
):
|
||||||
"""List SCIM Groups"""
|
"""List SCIM Groups"""
|
||||||
# Get all groups
|
# Get all groups
|
||||||
groups_list = Groups.get_groups()
|
groups_list = Groups.get_all_groups()
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
total = len(groups_list)
|
total = len(groups_list)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ from open_webui.models.users import (
|
||||||
UserGroupIdsModel,
|
UserGroupIdsModel,
|
||||||
UserGroupIdsListResponse,
|
UserGroupIdsListResponse,
|
||||||
UserInfoListResponse,
|
UserInfoListResponse,
|
||||||
UserIdNameListResponse,
|
UserInfoListResponse,
|
||||||
UserRoleUpdateForm,
|
UserRoleUpdateForm,
|
||||||
|
UserStatus,
|
||||||
Users,
|
Users,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
UserUpdateForm,
|
UserUpdateForm,
|
||||||
|
|
@ -102,20 +103,31 @@ async def get_all_users(
|
||||||
return Users.get_users()
|
return Users.get_users()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search", response_model=UserIdNameListResponse)
|
@router.get("/search", response_model=UserInfoListResponse)
|
||||||
async def search_users(
|
async def search_users(
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
|
order_by: Optional[str] = None,
|
||||||
|
direction: Optional[str] = None,
|
||||||
|
page: Optional[int] = 1,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
limit = PAGE_ITEM_COUNT
|
limit = PAGE_ITEM_COUNT
|
||||||
|
|
||||||
page = 1 # Always return the first page for search
|
page = max(1, page)
|
||||||
skip = (page - 1) * limit
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
filter = {}
|
filter = {}
|
||||||
if query:
|
if query:
|
||||||
filter["query"] = query
|
filter["query"] = query
|
||||||
|
|
||||||
|
filter = {}
|
||||||
|
if query:
|
||||||
|
filter["query"] = query
|
||||||
|
if order_by:
|
||||||
|
filter["order_by"] = order_by
|
||||||
|
if direction:
|
||||||
|
filter["direction"] = direction
|
||||||
|
|
||||||
return Users.get_users(filter=filter, skip=skip, limit=limit)
|
return Users.get_users(filter=filter, skip=skip, limit=limit)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -196,8 +208,9 @@ class ChatPermissions(BaseModel):
|
||||||
|
|
||||||
class FeaturesPermissions(BaseModel):
|
class FeaturesPermissions(BaseModel):
|
||||||
api_keys: bool = False
|
api_keys: bool = False
|
||||||
folders: bool = True
|
|
||||||
notes: bool = True
|
notes: bool = True
|
||||||
|
channels: bool = True
|
||||||
|
folders: bool = True
|
||||||
direct_tool_servers: bool = False
|
direct_tool_servers: bool = False
|
||||||
|
|
||||||
web_search: bool = True
|
web_search: bool = True
|
||||||
|
|
@ -287,6 +300,43 @@ async def update_user_settings_by_session_user(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetUserStatusBySessionUser
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/user/status")
|
||||||
|
async def get_user_status_by_session_user(user=Depends(get_verified_user)):
|
||||||
|
user = Users.get_user_by_id(user.id)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# UpdateUserStatusBySessionUser
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/user/status/update")
|
||||||
|
async def update_user_status_by_session_user(
|
||||||
|
form_data: UserStatus, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
user = Users.get_user_by_id(user.id)
|
||||||
|
if user:
|
||||||
|
user = Users.update_user_status_by_id(user.id, form_data)
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetUserInfoBySessionUser
|
# GetUserInfoBySessionUser
|
||||||
############################
|
############################
|
||||||
|
|
@ -338,9 +388,10 @@ async def update_user_info_by_session_user(
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
class UserActiveResponse(BaseModel):
|
class UserActiveResponse(UserStatus):
|
||||||
name: str
|
name: str
|
||||||
profile_image_url: Optional[str] = None
|
profile_image_url: Optional[str] = None
|
||||||
|
|
||||||
is_active: bool
|
is_active: bool
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
@ -365,8 +416,7 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||||
if user:
|
if user:
|
||||||
return UserActiveResponse(
|
return UserActiveResponse(
|
||||||
**{
|
**{
|
||||||
"id": user.id,
|
**user.model_dump(),
|
||||||
"name": user.name,
|
|
||||||
"is_active": Users.is_user_active(user_id),
|
"is_active": Users.is_user_active(user_id),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,38 @@ def get_user_ids_from_room(room):
|
||||||
return active_user_ids
|
return active_user_ids
|
||||||
|
|
||||||
|
|
||||||
|
async def emit_to_users(event: str, data: dict, user_ids: list[str]):
|
||||||
|
"""
|
||||||
|
Send a message to specific users using their user:{id} rooms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (str): The event name to emit.
|
||||||
|
data (dict): The payload/data to send.
|
||||||
|
user_ids (list[str]): The target users' IDs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
for user_id in user_ids:
|
||||||
|
await sio.emit(event, data, room=f"user:{user_id}")
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Failed to emit event {event} to users {user_ids}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def enter_room_for_users(room: str, user_ids: list[str]):
|
||||||
|
"""
|
||||||
|
Make all sessions of a user join a specific room.
|
||||||
|
Args:
|
||||||
|
room (str): The room to join.
|
||||||
|
user_ids (list[str]): The target user's IDs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
for user_id in user_ids:
|
||||||
|
session_ids = get_session_ids_from_room(f"user:{user_id}")
|
||||||
|
for sid in session_ids:
|
||||||
|
await sio.enter_room(sid, room)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Failed to make users {user_ids} join room {room}: {e}")
|
||||||
|
|
||||||
|
|
||||||
@sio.on("usage")
|
@sio.on("usage")
|
||||||
async def usage(sid, data):
|
async def usage(sid, data):
|
||||||
if sid in SESSION_POOL:
|
if sid in SESSION_POOL:
|
||||||
|
|
@ -309,11 +341,13 @@ async def user_join(sid, data):
|
||||||
)
|
)
|
||||||
|
|
||||||
await sio.enter_room(sid, f"user:{user.id}")
|
await sio.enter_room(sid, f"user:{user.id}")
|
||||||
|
|
||||||
# Join all the channels
|
# Join all the channels
|
||||||
channels = Channels.get_channels_by_user_id(user.id)
|
channels = Channels.get_channels_by_user_id(user.id)
|
||||||
log.debug(f"{channels=}")
|
log.debug(f"{channels=}")
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
await sio.enter_room(sid, f"channel:{channel.id}")
|
await sio.enter_room(sid, f"channel:{channel.id}")
|
||||||
|
|
||||||
return {"id": user.id, "name": user.name}
|
return {"id": user.id, "name": user.name}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ class AuditLoggingMiddleware:
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = get_current_user(
|
user = await get_current_user(
|
||||||
request, None, None, get_http_authorization_cred(auth_header)
|
request, None, None, get_http_authorization_cred(auth_header)
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -858,7 +858,7 @@ async def chat_image_generation_handler(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
|
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that the following error occurred: {error_message}</context>"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Create image(s)
|
# Create image(s)
|
||||||
|
|
@ -921,7 +921,7 @@ async def chat_image_generation_handler(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
system_message_content = "<context>The requested image has been created and is now being shown to the user. Let them know that it has been generated.</context>"
|
system_message_content = "<context>The requested image has been created by the system successfully and is now being shown to the user. Let the user know that the image they requested has been generated and is now shown in the chat.</context>"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
|
|
||||||
|
|
@ -942,7 +942,7 @@ async def chat_image_generation_handler(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
|
system_message_content = f"<context>Image generation was attempted but failed because of an error. The system is currently unable to generate the image. Tell the user that the following error occurred: {error_message}</context>"
|
||||||
|
|
||||||
if system_message_content:
|
if system_message_content:
|
||||||
form_data["messages"] = add_or_update_system_message(
|
form_data["messages"] = add_or_update_system_message(
|
||||||
|
|
|
||||||
|
|
@ -1102,7 +1102,7 @@ class OAuthManager:
|
||||||
user_oauth_groups = []
|
user_oauth_groups = []
|
||||||
|
|
||||||
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
|
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
|
||||||
all_available_groups: list[GroupModel] = Groups.get_groups()
|
all_available_groups: list[GroupModel] = Groups.get_all_groups()
|
||||||
|
|
||||||
# Create groups if they don't exist and creation is enabled
|
# Create groups if they don't exist and creation is enabled
|
||||||
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
|
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
|
||||||
|
|
@ -1146,7 +1146,7 @@ class OAuthManager:
|
||||||
|
|
||||||
# Refresh the list of all available groups if any were created
|
# Refresh the list of all available groups if any were created
|
||||||
if groups_created:
|
if groups_created:
|
||||||
all_available_groups = Groups.get_groups()
|
all_available_groups = Groups.get_all_groups()
|
||||||
log.debug("Refreshed list of all available groups after creation.")
|
log.debug("Refreshed list of all available groups after creation.")
|
||||||
|
|
||||||
log.debug(f"Oauth Groups claim: {oauth_claim}")
|
log.debug(f"Oauth Groups claim: {oauth_claim}")
|
||||||
|
|
|
||||||
|
|
@ -139,14 +139,14 @@ ldap3==2.9.1
|
||||||
firecrawl-py==4.5.0
|
firecrawl-py==4.5.0
|
||||||
|
|
||||||
## Trace
|
## Trace
|
||||||
opentelemetry-api==1.37.0
|
opentelemetry-api==1.38.0
|
||||||
opentelemetry-sdk==1.37.0
|
opentelemetry-sdk==1.38.0
|
||||||
opentelemetry-exporter-otlp==1.37.0
|
opentelemetry-exporter-otlp==1.38.0
|
||||||
opentelemetry-instrumentation==0.58b0
|
opentelemetry-instrumentation==0.59b0
|
||||||
opentelemetry-instrumentation-fastapi==0.58b0
|
opentelemetry-instrumentation-fastapi==0.59b0
|
||||||
opentelemetry-instrumentation-sqlalchemy==0.58b0
|
opentelemetry-instrumentation-sqlalchemy==0.59b0
|
||||||
opentelemetry-instrumentation-redis==0.58b0
|
opentelemetry-instrumentation-redis==0.59b0
|
||||||
opentelemetry-instrumentation-requests==0.58b0
|
opentelemetry-instrumentation-requests==0.59b0
|
||||||
opentelemetry-instrumentation-logging==0.58b0
|
opentelemetry-instrumentation-logging==0.59b0
|
||||||
opentelemetry-instrumentation-httpx==0.58b0
|
opentelemetry-instrumentation-httpx==0.59b0
|
||||||
opentelemetry-instrumentation-aiohttp-client==0.58b0
|
opentelemetry-instrumentation-aiohttp-client==0.59b0
|
||||||
|
|
|
||||||
|
|
@ -637,7 +637,7 @@ input[type='number'] {
|
||||||
|
|
||||||
.tiptap th,
|
.tiptap th,
|
||||||
.tiptap td {
|
.tiptap td {
|
||||||
@apply px-3 py-1.5 border border-gray-100 dark:border-gray-850;
|
@apply px-3 py-1.5 border border-gray-100/30 dark:border-gray-850/30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap th {
|
.tiptap th {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
type ChannelForm = {
|
type ChannelForm = {
|
||||||
type?: string;
|
type?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
is_private?: boolean;
|
||||||
data?: object;
|
data?: object;
|
||||||
meta?: object;
|
meta?: object;
|
||||||
access_control?: object;
|
access_control?: object;
|
||||||
|
|
@ -103,6 +104,37 @@ export const getChannelById = async (token: string = '', channel_id: string) =>
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDMChannelByUserId = async (token: string = '', user_id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/users/${user_id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getChannelMembersById = async (
|
export const getChannelMembersById = async (
|
||||||
token: string,
|
token: string,
|
||||||
channel_id: string,
|
channel_id: string,
|
||||||
|
|
@ -193,6 +225,88 @@ export const updateChannelMemberActiveStatusById = async (
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpdateMembersForm = {
|
||||||
|
user_ids?: string[];
|
||||||
|
group_ids?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMembersById = async (
|
||||||
|
token: string = '',
|
||||||
|
channel_id: string,
|
||||||
|
formData: UpdateMembersForm
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...formData })
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RemoveMembersForm = {
|
||||||
|
user_ids?: string[];
|
||||||
|
group_ids?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeMembersById = async (
|
||||||
|
token: string = '',
|
||||||
|
channel_id: string,
|
||||||
|
formData: RemoveMembersForm
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/remove`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...formData })
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateChannelById = async (
|
export const updateChannelById = async (
|
||||||
token: string = '',
|
token: string = '',
|
||||||
channel_id: string,
|
channel_id: string,
|
||||||
|
|
|
||||||
|
|
@ -166,11 +166,33 @@ export const getUsers = async (
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllUsers = async (token: string) => {
|
export const searchUsers = async (
|
||||||
|
token: string,
|
||||||
|
query?: string,
|
||||||
|
orderBy?: string,
|
||||||
|
direction?: string,
|
||||||
|
page = 1
|
||||||
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
res = await fetch(`${WEBUI_API_BASE_URL}/users/all`, {
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
searchParams.set('page', `${page}`);
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
searchParams.set('query', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderBy) {
|
||||||
|
searchParams.set('order_by', orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction) {
|
||||||
|
searchParams.set('direction', direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
res = await fetch(`${WEBUI_API_BASE_URL}/users/search?${searchParams.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -194,11 +216,11 @@ export const getAllUsers = async (token: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const searchUsers = async (token: string, query: string) => {
|
export const getAllUsers = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
res = await fetch(`${WEBUI_API_BASE_URL}/users/search?query=${encodeURIComponent(query)}`, {
|
res = await fetch(`${WEBUI_API_BASE_URL}/users/all`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -305,6 +327,36 @@ export const getUserById = async (token: string, userId: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateUserStatus = async (token: string, formData: object) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/status/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...formData
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getUserInfo = async (token: string) => {
|
export const getUserInfo = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, {
|
||||||
|
|
|
||||||
|
|
@ -818,11 +818,9 @@
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
||||||
|
|
||||||
<div class="my-2 -mx-2">
|
<div class="my-2">
|
||||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
|
|
||||||
<AccessControl bind:accessControl />
|
<AccessControl bind:accessControl />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"
|
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"
|
||||||
>
|
>
|
||||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,7 @@
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
|
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
|
||||||
>
|
>
|
||||||
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
|
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
{#if STT_ENGINE !== 'web'}
|
{#if STT_ENGINE !== 'web'}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
|
|
@ -263,7 +263,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
||||||
|
|
@ -289,7 +289,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
||||||
|
|
@ -323,7 +323,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Azure Region')}</div>
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Azure Region')}</div>
|
||||||
|
|
@ -391,7 +391,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
|
||||||
|
|
@ -416,7 +416,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
|
@ -500,7 +500,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2 py-0.5 flex w-full justify-between">
|
<div class="mb-2 py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||||
|
|
@ -557,7 +557,7 @@
|
||||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_API_KEY} required />
|
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_API_KEY} required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Azure Region')}</div>
|
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Azure Region')}</div>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
|
|
@ -288,7 +288,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="py-0.5 w-full">
|
<div class="py-0.5 w-full">
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="mt-2 space-y-2">
|
<div class="mt-2 space-y-2">
|
||||||
|
|
@ -384,7 +384,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||||
|
|
||||||
{#if $config?.features.enable_admin_export ?? true}
|
{#if $config?.features.enable_admin_export ?? true}
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5 flex flex-col w-full justify-between">
|
<div class="mb-2.5 flex flex-col w-full justify-between">
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
|
|
@ -762,7 +762,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Embedding')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Embedding')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex flex-col w-full justify-between">
|
<div class=" mb-2.5 flex flex-col w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
|
|
@ -953,7 +953,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Retrieval')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Retrieval')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
|
||||||
|
|
@ -1211,7 +1211,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allowed File Extensions')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Allowed File Extensions')}</div>
|
||||||
|
|
@ -1323,7 +1323,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Google Drive')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Google Drive')}</div>
|
||||||
|
|
@ -1343,7 +1343,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5 flex w-full justify-between">
|
<div class="mb-2.5 flex w-full justify-between">
|
||||||
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
|
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
|
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
|
||||||
|
|
|
||||||
|
|
@ -292,11 +292,9 @@
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
||||||
|
|
||||||
<div class="my-2 -mx-2">
|
<div class="my-2">
|
||||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
|
|
||||||
<AccessControl bind:accessControl />
|
<AccessControl bind:accessControl />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class=" mb-1 text-xs font-medium flex space-x-2 items-center">
|
<div class=" mb-1 text-xs font-medium flex space-x-2 items-center">
|
||||||
|
|
@ -287,7 +287,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||||
|
|
@ -660,7 +660,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
|
|
@ -309,7 +309,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Create Image')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Create Image')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
{#if config.ENABLE_IMAGE_GENERATION}
|
{#if config.ENABLE_IMAGE_GENERATION}
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
|
|
@ -882,7 +882,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Edit Image')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Edit Image')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2 font-medium flex items-center">
|
<div class=" mb-2 font-medium flex items-center">
|
||||||
<div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
|
<div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
|
||||||
|
|
@ -423,7 +423,7 @@
|
||||||
<div class="mb-3.5">
|
<div class="mb-3.5">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-3 w-full" />
|
||||||
|
|
||||||
{#if pipelines !== null}
|
{#if pipelines !== null}
|
||||||
{#if pipelines.length > 0}
|
{#if pipelines.length > 0}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class="mb-2.5 flex flex-col w-full justify-between">
|
<div class="mb-2.5 flex flex-col w-full justify-between">
|
||||||
<!-- {$i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
|
<!-- {$i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
|
|
@ -746,7 +746,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
|
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
|
|
||||||
<div class=" mb-2.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@
|
||||||
<EditGroupModal
|
<EditGroupModal
|
||||||
bind:show={showAddGroupModal}
|
bind:show={showAddGroupModal}
|
||||||
edit={false}
|
edit={false}
|
||||||
|
tabs={['general', 'permissions']}
|
||||||
permissions={defaultPermissions}
|
permissions={defaultPermissions}
|
||||||
onSubmit={addGroupHandler}
|
onSubmit={addGroupHandler}
|
||||||
/>
|
/>
|
||||||
|
|
@ -175,7 +176,7 @@
|
||||||
<div class="w-full basis-2/5 text-right">{$i18n.t('Users')}</div>
|
<div class="w-full basis-2/5 text-right">{$i18n.t('Users')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="mt-1.5 border-gray-100 dark:border-gray-850" />
|
<hr class="mt-1.5 border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
{#each filteredGroups as group}
|
{#each filteredGroups as group}
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
|
|
@ -185,7 +186,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="mb-2 border-gray-100 dark:border-gray-850" />
|
<hr class="mb-2 border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
<EditGroupModal
|
<EditGroupModal
|
||||||
bind:show={showDefaultPermissionsModal}
|
bind:show={showDefaultPermissionsModal}
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,9 @@
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
api_keys: false,
|
api_keys: false,
|
||||||
folders: true,
|
|
||||||
notes: true,
|
notes: true,
|
||||||
|
channels: true,
|
||||||
|
folders: true,
|
||||||
direct_tool_servers: false,
|
direct_tool_servers: false,
|
||||||
web_search: true,
|
web_search: true,
|
||||||
image_generation: true,
|
image_generation: true,
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||||
|
|
||||||
<div class="flex flex-col w-full mt-2">
|
<div class="flex flex-col w-full mt-2">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Setting')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Setting')}</div>
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,9 @@
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
api_keys: false,
|
api_keys: false,
|
||||||
folders: true,
|
|
||||||
notes: true,
|
notes: true,
|
||||||
|
channels: true,
|
||||||
|
folders: true,
|
||||||
direct_tool_servers: false,
|
direct_tool_servers: false,
|
||||||
web_search: true,
|
web_search: true,
|
||||||
image_generation: true,
|
image_generation: true,
|
||||||
|
|
@ -215,7 +216,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Sharing Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Sharing Permissions')}</div>
|
||||||
|
|
@ -391,7 +392,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
||||||
|
|
@ -705,7 +706,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
||||||
|
|
@ -726,6 +727,54 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class="flex w-full justify-between my-1">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Notes')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.notes} />
|
||||||
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.notes && !permissions.features.notes}
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class="flex w-full justify-between my-1">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Channels')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.channels} />
|
||||||
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.channels && !permissions.features.channels}
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class="flex w-full justify-between my-1">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Folders')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.features.folders} />
|
||||||
|
</div>
|
||||||
|
{#if defaultPermissions?.features?.folders && !permissions.features.folders}
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="flex w-full justify-between my-1">
|
<div class="flex w-full justify-between my-1">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
|
|
@ -789,21 +838,5 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class="flex w-full justify-between my-1">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
|
||||||
{$i18n.t('Notes')}
|
|
||||||
</div>
|
|
||||||
<Switch bind:state={permissions.features.notes} />
|
|
||||||
</div>
|
|
||||||
{#if defaultPermissions?.features?.notes && !permissions.features.notes}
|
|
||||||
<div>
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{$i18n.t('This is a default user permission and will remain enabled.')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
||||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2.5 w-full" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2.5 w-full" />
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,11 @@
|
||||||
return message;
|
return message;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onUpdate={async () => {
|
||||||
|
channel = await getChannelById(localStorage.token, id).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if channel && messages !== null}
|
{#if channel && messages !== null}
|
||||||
|
|
@ -402,7 +407,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else if threadId !== null}
|
{:else if threadId !== null}
|
||||||
<PaneResizer
|
<PaneResizer
|
||||||
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
||||||
id="controls-resizer"
|
id="controls-resizer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,41 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { removeMembersById } from '$lib/apis/channels';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
|
||||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
|
||||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import Hashtag from '../icons/Hashtag.svelte';
|
import Hashtag from '../icons/Hashtag.svelte';
|
||||||
import Lock from '../icons/Lock.svelte';
|
import Lock from '../icons/Lock.svelte';
|
||||||
import UserList from './ChannelInfoModal/UserList.svelte';
|
import UserList from './ChannelInfoModal/UserList.svelte';
|
||||||
|
import AddMembersModal from './ChannelInfoModal/AddMembersModal.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let channel = null;
|
export let channel = null;
|
||||||
|
|
||||||
|
export let onUpdate = () => {};
|
||||||
|
|
||||||
|
let showAddMembersModal = false;
|
||||||
const submitHandler = async () => {};
|
const submitHandler = async () => {};
|
||||||
|
|
||||||
|
const removeMemberHandler = async (userId) => {
|
||||||
|
const res = await removeMembersById(localStorage.token, channel.id, {
|
||||||
|
user_ids: [userId]
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Member removed successfully'));
|
||||||
|
onUpdate();
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to remove member'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const init = () => {};
|
const init = () => {};
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
|
|
@ -31,29 +50,26 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if channel}
|
{#if channel}
|
||||||
|
<AddMembersModal bind:show={showAddMembersModal} {channel} {onUpdate} />
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||||
<div class="self-center text-base">
|
<div class="self-center text-base">
|
||||||
<div class="flex items-center gap-0.5 shrink-0">
|
<div class="flex items-center gap-0.5 shrink-0">
|
||||||
{#if channel?.type === 'dm'}
|
{#if channel?.type === 'dm'}
|
||||||
<div
|
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
|
||||||
>
|
|
||||||
{$i18n.t('Direct Message')}
|
{$i18n.t('Direct Message')}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" size-4 justify-center flex items-center">
|
<div class=" size-4 justify-center flex items-center">
|
||||||
{#if channel?.access_control === null}
|
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
|
||||||
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
||||||
{:else}
|
{:else}
|
||||||
<Lock className="size-5.5" strokeWidth="2" />
|
<Lock className="size-5.5" strokeWidth="2" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
|
||||||
>
|
|
||||||
{channel.name}
|
{channel.name}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -69,7 +85,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full px-3 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
<form
|
<form
|
||||||
class="flex flex-col w-full"
|
class="flex flex-col w-full"
|
||||||
|
|
@ -79,7 +95,21 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col w-full h-full pb-2">
|
<div class="flex flex-col w-full h-full pb-2">
|
||||||
<UserList {channel} search={channel?.type !== 'dm'} sort={channel?.type !== 'dm'} />
|
<UserList
|
||||||
|
{channel}
|
||||||
|
onAdd={channel?.type === 'group' && channel?.is_manager
|
||||||
|
? () => {
|
||||||
|
showAddMembersModal = true;
|
||||||
|
}
|
||||||
|
: null}
|
||||||
|
onRemove={channel?.type === 'group' && channel?.is_manager
|
||||||
|
? (userId) => {
|
||||||
|
removeMemberHandler(userId);
|
||||||
|
}
|
||||||
|
: null}
|
||||||
|
search={channel?.type !== 'dm'}
|
||||||
|
sort={channel?.type !== 'dm'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { addMembersById } from '$lib/apis/channels';
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let channel = null;
|
||||||
|
|
||||||
|
export let onUpdate = () => {};
|
||||||
|
|
||||||
|
let groupIds = [];
|
||||||
|
let userIds = [];
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
const res = await addMembersById(localStorage.token, channel.id, {
|
||||||
|
user_ids: userIds,
|
||||||
|
group_ids: groupIds
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Members added successfully'));
|
||||||
|
onUpdate();
|
||||||
|
show = false;
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to add members'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if channel}
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||||
|
<div class="self-center text-base">
|
||||||
|
<div class="flex items-center gap-0.5 shrink-0">
|
||||||
|
{$i18n.t('Add Members')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className={'size-5'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-3 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<form
|
||||||
|
class="flex flex-col w-full"
|
||||||
|
on:submit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col w-full h-full pb-2">
|
||||||
|
<MemberSelector bind:userIds bind:groupIds includeGroups={true} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-950 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||||
|
? ' cursor-not-allowed'
|
||||||
|
: ''}"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t('Add')}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="ml-2 self-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
|
import { WEBUI_NAME, config, user as _user, showSidebar } from '$lib/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
|
@ -14,29 +14,21 @@
|
||||||
import { getChannelMembersById } from '$lib/apis/channels';
|
import { getChannelMembersById } from '$lib/apis/channels';
|
||||||
|
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
import EditUserModal from '$lib/components/admin/Users/UserList/EditUserModal.svelte';
|
|
||||||
import UserChatsModal from '$lib/components/admin/Users/UserList/UserChatsModal.svelte';
|
|
||||||
import AddUserModal from '$lib/components/admin/Users/UserList/AddUserModal.svelte';
|
|
||||||
|
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
import RoleUpdateConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
|
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
||||||
import About from '$lib/components/chat/Settings/About.svelte';
|
|
||||||
import Banner from '$lib/components/common/Banner.svelte';
|
|
||||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
|
import ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let channel = null;
|
export let channel = null;
|
||||||
|
|
||||||
|
export let onAdd = null;
|
||||||
|
export let onRemove = null;
|
||||||
|
|
||||||
export let search = true;
|
export let search = true;
|
||||||
export let sort = true;
|
export let sort = true;
|
||||||
|
|
||||||
|
|
@ -85,7 +77,13 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (page !== null && query !== null && orderBy !== null && direction !== null) {
|
$: if (
|
||||||
|
channel !== null &&
|
||||||
|
page !== null &&
|
||||||
|
query !== null &&
|
||||||
|
orderBy !== null &&
|
||||||
|
direction !== null
|
||||||
|
) {
|
||||||
getUserList();
|
getUserList();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -96,10 +94,33 @@
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="flex items-center justify-between px-2 mb-1">
|
||||||
|
<div class="flex gap-1 items-center">
|
||||||
|
<span class="text-sm">
|
||||||
|
{$i18n.t('Members')}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500">{total}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if onAdd}
|
||||||
|
<div class="">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class=" px-3 py-1.5 gap-1 rounded-xl bg-black dark:text-white dark:bg-gray-850/50 text-black transition font-medium text-xs flex items-center justify-center"
|
||||||
|
on:click={onAdd}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5 " />
|
||||||
|
<span>{$i18n.t('Add Member')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- <hr class="my-1 border-gray-100/5- dark:border-gray-850/50" /> -->
|
||||||
|
|
||||||
{#if search}
|
{#if search}
|
||||||
<div class="flex gap-1 px-0.5">
|
<div class="flex gap-1 px-1 mb-1">
|
||||||
<div class=" flex w-full space-x-2">
|
<div class=" flex w-full space-x-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1 items-center">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -127,7 +148,7 @@
|
||||||
{#if users.length > 0}
|
{#if users.length > 0}
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap w-full max-w-full">
|
<div class="scrollbar-hidden relative whitespace-nowrap w-full max-w-full">
|
||||||
<div class=" text-sm text-left text-gray-500 dark:text-gray-400 w-full max-w-full">
|
<div class=" text-sm text-left text-gray-500 dark:text-gray-400 w-full max-w-full">
|
||||||
<div
|
<!-- <div
|
||||||
class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200 w-full mb-0.5"
|
class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200 w-full mb-0.5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -181,11 +202,11 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
{#each users as user, userIdx}
|
{#each users as user, userIdx (user.id)}
|
||||||
<div class=" dark:border-gray-850 text-xs flex items-center justify-between">
|
<div class=" dark:border-gray-850 text-xs flex items-center justify-between">
|
||||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
<div class="px-2 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
||||||
<img
|
<img
|
||||||
|
|
@ -212,8 +233,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3 py-1">
|
<div class="px-2 py-1 flex items-center gap-1 translate-y-0.5">
|
||||||
<div class=" translate-y-0.5">
|
<div class=" ">
|
||||||
<Badge
|
<Badge
|
||||||
type={user.role === 'admin'
|
type={user.role === 'admin'
|
||||||
? 'info'
|
? 'info'
|
||||||
|
|
@ -223,6 +244,21 @@
|
||||||
content={$i18n.t(user.role)}
|
content={$i18n.t(user.role)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if onRemove}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
disabled={user.id === $_user?.id}
|
||||||
|
on:click={() => {
|
||||||
|
onRemove(user.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -775,7 +775,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="message-input-container"
|
id="message-input-container"
|
||||||
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850/30 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
{#if replyToMessage !== null}
|
{#if replyToMessage !== null}
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@
|
||||||
if (
|
if (
|
||||||
(message?.reactions ?? [])
|
(message?.reactions ?? [])
|
||||||
.find((reaction) => reaction.name === name)
|
.find((reaction) => reaction.name === name)
|
||||||
?.user_ids?.includes($user?.id) ??
|
?.users?.some((u) => u.id === $user?.id) ??
|
||||||
false
|
false
|
||||||
) {
|
) {
|
||||||
messages = messages.map((m) => {
|
messages = messages.map((m) => {
|
||||||
|
|
@ -197,8 +197,8 @@
|
||||||
const reaction = m.reactions.find((reaction) => reaction.name === name);
|
const reaction = m.reactions.find((reaction) => reaction.name === name);
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
reaction.user_ids = reaction.user_ids.filter((id) => id !== $user?.id);
|
reaction.users = reaction.users.filter((u) => u.id !== $user?.id);
|
||||||
reaction.count = reaction.user_ids.length;
|
reaction.count = reaction.users.length;
|
||||||
|
|
||||||
if (reaction.count === 0) {
|
if (reaction.count === 0) {
|
||||||
m.reactions = m.reactions.filter((r) => r.name !== name);
|
m.reactions = m.reactions.filter((r) => r.name !== name);
|
||||||
|
|
@ -224,12 +224,12 @@
|
||||||
const reaction = m.reactions.find((reaction) => reaction.name === name);
|
const reaction = m.reactions.find((reaction) => reaction.name === name);
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
reaction.user_ids.push($user?.id);
|
reaction.users.push({ id: $user?.id, name: $user?.name });
|
||||||
reaction.count = reaction.user_ids.length;
|
reaction.count = reaction.users.length;
|
||||||
} else {
|
} else {
|
||||||
m.reactions.push({
|
m.reactions.push({
|
||||||
name: name,
|
name: name,
|
||||||
user_ids: [$user?.id],
|
users: [{ id: $user?.id, name: $user?.name }],
|
||||||
count: 1
|
count: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
|
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100/30 dark:border-gray-850/30"
|
||||||
>
|
>
|
||||||
{#if onReaction}
|
{#if onReaction}
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
|
@ -388,8 +388,9 @@
|
||||||
<Markdown
|
<Markdown
|
||||||
id={message.id}
|
id={message.id}
|
||||||
content={message.content}
|
content={message.content}
|
||||||
|
paragraphTag="span"
|
||||||
/>{#if message.created_at !== message.updated_at && (message?.meta?.model_id ?? null) === null}<span
|
/>{#if message.created_at !== message.updated_at && (message?.meta?.model_id ?? null) === null}<span
|
||||||
class="text-gray-500 text-[10px]">({$i18n.t('edited')})</span
|
class="text-gray-500 text-[10px] pl-1 self-center">({$i18n.t('edited')})</span
|
||||||
>{/if}
|
>{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -398,24 +399,57 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center flex-wrap gap-y-1.5 gap-1 mt-1 mb-2">
|
<div class="flex items-center flex-wrap gap-y-1.5 gap-1 mt-1 mb-2">
|
||||||
{#each message.reactions as reaction}
|
{#each message.reactions as reaction}
|
||||||
<Tooltip content={`:${reaction.name}:`}>
|
<Tooltip
|
||||||
|
content={$i18n.t('{{NAMES}} reacted with {{REACTION}}', {
|
||||||
|
NAMES: reaction.users
|
||||||
|
.reduce((acc, u, idx) => {
|
||||||
|
const name = u.id === $user?.id ? $i18n.t('You') : u.name;
|
||||||
|
const total = reaction.users.length;
|
||||||
|
|
||||||
|
// First three names always added normally
|
||||||
|
if (idx < 3) {
|
||||||
|
const separator =
|
||||||
|
idx === 0
|
||||||
|
? ''
|
||||||
|
: idx === Math.min(2, total - 1)
|
||||||
|
? ` ${$i18n.t('and')} `
|
||||||
|
: ', ';
|
||||||
|
return `${acc}${separator}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// More than 4 → "and X others"
|
||||||
|
if (idx === 3 && total > 4) {
|
||||||
|
return (
|
||||||
|
acc +
|
||||||
|
` ${$i18n.t('and {{COUNT}} others', {
|
||||||
|
COUNT: total - 3
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, '')
|
||||||
|
.trim(),
|
||||||
|
REACTION: `:${reaction.name}:`
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes(
|
class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.users
|
||||||
$user?.id
|
.map((u) => u.id)
|
||||||
)
|
.includes($user?.id)
|
||||||
? ' bg-blue-300/10 outline outline-blue-500/50 outline-1'
|
? ' bg-blue-300/10 outline outline-blue-500/50 outline-1'
|
||||||
: 'bg-gray-300/10 dark:bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
|
: 'bg-gray-300/10 dark:bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (onReaction) {
|
if (onReaction) {
|
||||||
onReaction(name);
|
onReaction(reaction.name);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Emoji shortCode={reaction.name} />
|
<Emoji shortCode={reaction.name} />
|
||||||
|
|
||||||
{#if reaction.user_ids.length > 0}
|
{#if reaction.users.length > 0}
|
||||||
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
{reaction.user_ids?.length}
|
{reaction.users?.length}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
let openPreview = false;
|
let openPreview = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LinkPreview.Root openDelay={0} closeDelay={100} bind:open={openPreview}>
|
<LinkPreview.Root openDelay={0} closeDelay={200} bind:open={openPreview}>
|
||||||
<LinkPreview.Trigger class="flex items-center">
|
<LinkPreview.Trigger class="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,39 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { user as _user, channels, socket } from '$lib/stores';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import { getChannels, getDMChannelByUserId } from '$lib/apis/channels';
|
||||||
|
|
||||||
|
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
||||||
|
import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
|
||||||
|
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
export let user = null;
|
export let user = null;
|
||||||
|
|
||||||
|
const directMessageHandler = async () => {
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getDMChannelByUserId(localStorage.token, user.id).catch((error) => {
|
||||||
|
console.error('Error fetching DM channel:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
goto(`/channels/${res.id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if user}
|
{#if user}
|
||||||
<div class=" flex gap-3.5 w-full py-3 px-3 items-center">
|
<div class="py-2.5">
|
||||||
|
<div class=" flex gap-3.5 w-full px-2.5 items-center">
|
||||||
<div class=" items-center flex shrink-0">
|
<div class=" items-center flex shrink-0">
|
||||||
<img
|
<img
|
||||||
src={`${WEBUI_API_BASE_URL}/users/${user?.id}/profile/image`}
|
src={`${WEBUI_API_BASE_URL}/users/${user?.id}/profile/image`}
|
||||||
|
|
@ -46,4 +72,46 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if user?.status_emoji || user?.status_message}
|
||||||
|
<div class="mx-2 mt-2">
|
||||||
|
<Tooltip content={user?.status_message}>
|
||||||
|
<div
|
||||||
|
class="mb-1 w-full gap-2 px-2.5 py-1.5 rounded-xl bg-gray-50 dark:text-white dark:bg-gray-900/50 text-black transition text-xs flex items-center"
|
||||||
|
>
|
||||||
|
{#if user?.status_emoji}
|
||||||
|
<div class=" self-center shrink-0">
|
||||||
|
<Emoji className="size-4" shortCode={user?.status_emoji} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class=" self-center line-clamp-2 flex-1 text-left">
|
||||||
|
{user?.status_message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $_user?.id !== user.id}
|
||||||
|
<hr class="border-gray-100/50 dark:border-gray-800/50 my-2.5" />
|
||||||
|
|
||||||
|
<div class=" flex flex-col w-full px-2.5 items-center">
|
||||||
|
<button
|
||||||
|
class="w-full text-left px-3 py-1.5 rounded-xl border border-gray-100/50 dark:border-gray-800/50 hover:bg-gray-50 dark:hover:bg-gray-850 transition flex items-center gap-2 text-sm"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
directMessageHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ChatBubbleOval className="size-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="font-medium">
|
||||||
|
{$i18n.t('Message')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
export let sideOffset = 6;
|
export let sideOffset = 6;
|
||||||
|
|
||||||
let user = null;
|
let user = null;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
user = await getUserById(localStorage.token, id).catch((error) => {
|
user = await getUserById(localStorage.token, id).catch((error) => {
|
||||||
|
|
@ -27,7 +26,7 @@
|
||||||
|
|
||||||
{#if user}
|
{#if user}
|
||||||
<LinkPreview.Content
|
<LinkPreview.Content
|
||||||
class="w-full max-w-[260px] rounded-2xl border border-gray-100 dark:border-gray-800 z-[99999] bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
class="w-full max-w-[260px] rounded-2xl border border-gray-100 dark:border-gray-800 z-[9999] bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
||||||
{side}
|
{side}
|
||||||
{align}
|
{align}
|
||||||
{sideOffset}
|
{sideOffset}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@
|
||||||
export let channel;
|
export let channel;
|
||||||
|
|
||||||
export let onPin = (messageId, pinned) => {};
|
export let onPin = (messageId, pinned) => {};
|
||||||
|
export let onUpdate = () => {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} {onPin} />
|
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} {onPin} />
|
||||||
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} />
|
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} {onUpdate} />
|
||||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1 -mb-8 flex items-center drag-region flex flex-col">
|
<nav class="sticky top-0 z-30 w-full px-1.5 py-1 -mb-8 flex items-center drag-region flex flex-col">
|
||||||
<div
|
<div
|
||||||
id="navbar-bg-gradient-to-b"
|
id="navbar-bg-gradient-to-b"
|
||||||
|
|
@ -75,8 +76,9 @@
|
||||||
<div class="flex items-center gap-0.5 shrink-0">
|
<div class="flex items-center gap-0.5 shrink-0">
|
||||||
{#if channel?.type === 'dm'}
|
{#if channel?.type === 'dm'}
|
||||||
{#if channel?.users}
|
{#if channel?.users}
|
||||||
<div class="flex mr-1.5">
|
{@const channelMembers = channel.users.filter((u) => u.id !== $user?.id)}
|
||||||
{#each channel.users.filter((u) => u.id !== $user?.id).slice(0, 2) as u, index}
|
<div class="flex mr-1.5 relative">
|
||||||
|
{#each channelMembers.slice(0, 2) as u, index}
|
||||||
<img
|
<img
|
||||||
src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`}
|
src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`}
|
||||||
alt={u.name}
|
alt={u.name}
|
||||||
|
|
@ -86,13 +88,31 @@
|
||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#if channelMembers.length === 1}
|
||||||
|
<div class="absolute bottom-0 right-0">
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
{#if channelMembers[0]?.is_active}
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
|
||||||
|
></span>
|
||||||
|
{/if}
|
||||||
|
<span
|
||||||
|
class="relative inline-flex size-2 rounded-full {channelMembers[0]
|
||||||
|
?.is_active
|
||||||
|
? 'bg-green-500'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700'} border-[1.5px] border-white dark:border-gray-900"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Users className="size-4 ml-1 mr-0.5" strokeWidth="2" />
|
<Users className="size-4 ml-1 mr-0.5" strokeWidth="2" />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" size-4.5 justify-center flex items-center">
|
<div class=" size-4.5 justify-center flex items-center">
|
||||||
{#if channel?.access_control === null}
|
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
|
||||||
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
||||||
{:else}
|
{:else}
|
||||||
<Lock className="size-5" strokeWidth="2" />
|
<Lock className="size-5" strokeWidth="2" />
|
||||||
|
|
@ -100,9 +120,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
|
||||||
>
|
|
||||||
{#if channel?.name}
|
{#if channel?.name}
|
||||||
{channel.name}
|
{channel.name}
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@
|
||||||
|
|
||||||
{#if $showControls}
|
{#if $showControls}
|
||||||
<PaneResizer
|
<PaneResizer
|
||||||
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
||||||
id="controls-resizer"
|
id="controls-resizer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedId}
|
{#if selectedId}
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1 w-full" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1 w-full" />
|
||||||
|
|
||||||
<div class="my-2 text-xs">
|
<div class="my-2 text-xs">
|
||||||
{#if !loading}
|
{#if !loading}
|
||||||
|
|
|
||||||
|
|
@ -1061,7 +1061,7 @@
|
||||||
id="message-input-container"
|
id="message-input-container"
|
||||||
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
|
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
|
||||||
? 'border-dashed border-gray-100 dark:border-gray-800 hover:border-gray-200 focus-within:border-gray-200 hover:dark:border-gray-700 focus-within:dark:border-gray-700'
|
? 'border-dashed border-gray-100 dark:border-gray-800 hover:border-gray-200 focus-within:border-gray-200 hover:dark:border-gray-700 focus-within:dark:border-gray-700'
|
||||||
: ' border-gray-100 dark:border-gray-850 hover:border-gray-200 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800'} transition px-1 bg-white/5 dark:bg-gray-500/5 backdrop-blur-sm dark:text-gray-100"
|
: ' border-gray-100/30 dark:border-gray-850/30 hover:border-gray-200 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800'} transition px-1 bg-white/5 dark:bg-gray-500/5 backdrop-blur-sm dark:text-gray-100"
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
{#if atSelectedModel !== undefined}
|
{#if atSelectedModel !== undefined}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
{@const placeholder = variableAttributes?.placeholder ?? ''}
|
{@const placeholder = variableAttributes?.placeholder ?? ''}
|
||||||
|
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
>
|
>
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
{:else if variables[variable]?.type === 'date'}
|
{:else if variables[variable]?.type === 'date'}
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
{:else if variables[variable]?.type === 'datetime-local'}
|
{:else if variables[variable]?.type === 'datetime-local'}
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
{:else if variables[variable]?.type === 'email'}
|
{:else if variables[variable]?.type === 'email'}
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -198,7 +198,7 @@
|
||||||
{:else if variables[variable]?.type === 'month'}
|
{:else if variables[variable]?.type === 'month'}
|
||||||
<input
|
<input
|
||||||
type="month"
|
type="month"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -209,7 +209,7 @@
|
||||||
{:else if variables[variable]?.type === 'number'}
|
{:else if variables[variable]?.type === 'number'}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -223,7 +223,7 @@
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
class="w-full rounded-lg py-1 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-1 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
|
|
@ -241,7 +241,7 @@
|
||||||
|
|
||||||
<!-- <input
|
<!-- <input
|
||||||
type="range"
|
type="range"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -251,7 +251,7 @@
|
||||||
{:else if variables[variable]?.type === 'tel'}
|
{:else if variables[variable]?.type === 'tel'}
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -262,7 +262,7 @@
|
||||||
{:else if variables[variable]?.type === 'text'}
|
{:else if variables[variable]?.type === 'text'}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -273,7 +273,7 @@
|
||||||
{:else if variables[variable]?.type === 'time'}
|
{:else if variables[variable]?.type === 'time'}
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -284,7 +284,7 @@
|
||||||
{:else if variables[variable]?.type === 'url'}
|
{:else if variables[variable]?.type === 'url'}
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -316,7 +316,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<textarea
|
<textarea
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={variables[variable]?.placeholder ?? ''}
|
placeholder={variables[variable]?.placeholder ?? ''}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@
|
||||||
{@const urlCitations = citations.filter((c) => c?.source?.name?.startsWith('http'))}
|
{@const urlCitations = citations.filter((c) => c?.source?.name?.startsWith('http'))}
|
||||||
<div class=" py-1 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
<div class=" py-1 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
||||||
<button
|
<button
|
||||||
class="text-xs font-medium text-gray-600 dark:text-gray-300 px-3.5 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition flex items-center gap-1 border border-gray-50 dark:border-gray-850"
|
class="text-xs font-medium text-gray-600 dark:text-gray-300 px-3.5 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition flex items-center gap-1 border border-gray-50 dark:border-gray-850/30"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showCitations = !showCitations;
|
showCitations = !showCitations;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
export let code = '';
|
export let code = '';
|
||||||
export let attributes = {};
|
export let attributes = {};
|
||||||
|
|
||||||
export let className = 'mb-2';
|
export let className = '';
|
||||||
export let editorClassName = '';
|
export let editorClassName = '';
|
||||||
export let stickyButtonsClassName = 'top-0';
|
export let stickyButtonsClassName = 'top-0';
|
||||||
|
|
||||||
|
|
@ -417,7 +417,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="relative {className} flex flex-col rounded-3xl border border-gray-100 dark:border-gray-850 my-0.5"
|
class="relative {className} flex flex-col rounded-3xl border border-gray-100/30 dark:border-gray-850/30 my-0.5"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
>
|
>
|
||||||
{#if ['mermaid', 'vega', 'vega-lite'].includes(lang)}
|
{#if ['mermaid', 'vega', 'vega-lite'].includes(lang)}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0}
|
{#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0}
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||||
<div class=" text-sm font-medium dark:text-gray-300">
|
<div class=" text-sm font-medium dark:text-gray-300">
|
||||||
{$i18n.t('Files')}
|
{$i18n.t('Files')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
export let save = false;
|
export let save = false;
|
||||||
export let preview = false;
|
export let preview = false;
|
||||||
|
|
||||||
|
export let paragraphTag = 'p';
|
||||||
export let editCodeBlock = true;
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@
|
||||||
{done}
|
{done}
|
||||||
{save}
|
{save}
|
||||||
{preview}
|
{preview}
|
||||||
|
{paragraphTag}
|
||||||
{editCodeBlock}
|
{editCodeBlock}
|
||||||
{sourceIds}
|
{sourceIds}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { decode } from 'html-entities';
|
import { decode } from 'html-entities';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -35,6 +34,8 @@
|
||||||
export let save = false;
|
export let save = false;
|
||||||
export let preview = false;
|
export let preview = false;
|
||||||
|
|
||||||
|
export let paragraphTag = 'p';
|
||||||
|
|
||||||
export let editCodeBlock = true;
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
|
|
@ -89,7 +90,7 @@
|
||||||
<!-- {JSON.stringify(tokens)} -->
|
<!-- {JSON.stringify(tokens)} -->
|
||||||
{#each tokens as token, tokenIdx (tokenIdx)}
|
{#each tokens as token, tokenIdx (tokenIdx)}
|
||||||
{#if token.type === 'hr'}
|
{#if token.type === 'hr'}
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
{:else if token.type === 'heading'}
|
{:else if token.type === 'heading'}
|
||||||
<svelte:element this={headerComponent(token.depth)} dir="auto">
|
<svelte:element this={headerComponent(token.depth)} dir="auto">
|
||||||
<MarkdownInlineTokens
|
<MarkdownInlineTokens
|
||||||
|
|
@ -351,6 +352,17 @@
|
||||||
}}
|
}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{:else if token.type === 'paragraph'}
|
{:else if token.type === 'paragraph'}
|
||||||
|
{#if paragraphTag == 'span'}
|
||||||
|
<span dir="auto">
|
||||||
|
<MarkdownInlineTokens
|
||||||
|
id={`${id}-${tokenIdx}-p`}
|
||||||
|
tokens={token.tokens ?? []}
|
||||||
|
{done}
|
||||||
|
{sourceIds}
|
||||||
|
{onSourceClick}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
<p dir="auto">
|
<p dir="auto">
|
||||||
<MarkdownInlineTokens
|
<MarkdownInlineTokens
|
||||||
id={`${id}-${tokenIdx}-p`}
|
id={`${id}-${tokenIdx}-p`}
|
||||||
|
|
@ -360,6 +372,7 @@
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
{/if}
|
||||||
{:else if token.type === 'text'}
|
{:else if token.type === 'text'}
|
||||||
{#if top}
|
{#if top}
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@
|
||||||
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
||||||
$mobile ? 'min-w-full' : 'min-w-80'
|
$mobile ? 'min-w-full' : 'min-w-80'
|
||||||
}`
|
}`
|
||||||
: `border-gray-100 dark:border-gray-850 border-dashed ${
|
: `border-gray-100/30 dark:border-gray-850/30 border-dashed ${
|
||||||
$mobile ? 'min-w-full' : 'min-w-80'
|
$mobile ? 'min-w-full' : 'min-w-80'
|
||||||
}`} transition-all p-5 rounded-2xl"
|
}`} transition-all p-5 rounded-2xl"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-100 dark:border-gray-850"
|
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-100/30 dark:border-gray-850/30"
|
||||||
id="message-feedback-{message.id}"
|
id="message-feedback-{message.id}"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
|
@ -134,7 +134,7 @@
|
||||||
<!-- 1-10 scale -->
|
<!-- 1-10 scale -->
|
||||||
{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
|
{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
|
||||||
<button
|
<button
|
||||||
class="size-7 text-sm border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
|
class="size-7 text-sm border border-gray-100/30 dark:border-gray-850/30 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
|
||||||
rating
|
rating
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white dark:disabled:bg-gray-900"
|
: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white dark:disabled:bg-gray-900"
|
||||||
|
|
@ -167,7 +167,7 @@
|
||||||
<div class="flex flex-wrap gap-1.5 text-sm mt-1.5">
|
<div class="flex flex-wrap gap-1.5 text-sm mt-1.5">
|
||||||
{#each reasons as reason}
|
{#each reasons as reason}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-0.5 border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
|
class="px-3 py-0.5 border border-gray-100/30 dark:border-gray-850/30 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
|
||||||
reason
|
reason
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
: ''} transition rounded-xl"
|
: ''} transition rounded-xl"
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if idx < followUps.length - 1}
|
{#if idx < followUps.length - 1}
|
||||||
<hr class="border-gray-50 dark:border-gray-850" />
|
<hr class="border-gray-50 dark:border-gray-850/30" />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1 mx-2" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1 mx-2" />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="text-sm border border-gray-50 dark:border-gray-850 rounded-xl my-1.5 p-2 w-full"
|
class="text-sm border border-gray-50 dark:border-gray-850/30 rounded-xl my-1.5 p-2 w-full"
|
||||||
slot="content"
|
slot="content"
|
||||||
>
|
>
|
||||||
{#if status?.query}
|
{#if status?.query}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ollamaVersion}
|
{#if ollamaVersion}
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
{#if $config?.license_metadata}
|
{#if $config?.license_metadata}
|
||||||
<div class="mb-2 text-xs">
|
<div class="mb-2 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-4" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-4" />
|
||||||
|
|
||||||
{#if $config?.features.enable_login_form}
|
{#if $config?.features.enable_login_form}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
{#if TTSEngine === 'browser-kokoro'}
|
{#if TTSEngine === 'browser-kokoro'}
|
||||||
{#if TTSModel}
|
{#if TTSModel}
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $user?.role === 'admin' || (($user?.permissions.chat?.controls ?? true) && ($user?.permissions.chat?.system_prompt ?? true))}
|
{#if $user?.role === 'admin' || (($user?.permissions.chat?.controls ?? true) && ($user?.permissions.chat?.system_prompt ?? true))}
|
||||||
<hr class="border-gray-100/50 dark:border-gray-850 my-3" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30 my-3" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-2" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-2" />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
<div class="relative overflow-x-auto">
|
<div class="relative overflow-x-auto">
|
||||||
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
||||||
<thead
|
<thead
|
||||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 border-gray-50 dark:border-gray-850"
|
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 border-gray-50 dark:border-gray-850/30"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each memories as memory}
|
{#each memories as memory}
|
||||||
<tr class="border-b border-gray-50 dark:border-gray-850 items-center">
|
<tr class="border-b border-gray-50 dark:border-gray-850/30 items-center">
|
||||||
<td class="px-3 py-1">
|
<td class="px-3 py-1">
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
{memory.content}
|
{memory.content}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
export let className = 'w-60';
|
export let className = 'w-60';
|
||||||
export let colorClassName =
|
export let colorClassName =
|
||||||
'bg-white dark:bg-gray-850 border border-gray-50 dark:border-gray-800';
|
'bg-white dark:bg-gray-850 border border-gray-50/30 dark:border-gray-800/30';
|
||||||
export let url: string | null = null;
|
export let url: string | null = null;
|
||||||
|
|
||||||
export let dismissible = false;
|
export let dismissible = false;
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if isPDF}
|
{:else if isPDF}
|
||||||
<div
|
<div
|
||||||
class="flex mb-2.5 scrollbar-none overflow-x-auto w-full border-b border-gray-50 dark:border-gray-850 text-center text-sm font-medium bg-transparent dark:text-gray-200"
|
class="flex mb-2.5 scrollbar-none overflow-x-auto w-full border-b border-gray-50 dark:border-gray-850/30 text-center text-sm font-medium bg-transparent dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="min-w-fit py-1.5 px-4 border-b {selectedTab === ''
|
class="min-w-fit py-1.5 px-4 border-b {selectedTab === ''
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,10 @@
|
||||||
document.body.appendChild(modalElement);
|
document.body.appendChild(modalElement);
|
||||||
focusTrap = FocusTrap.createFocusTrap(modalElement, {
|
focusTrap = FocusTrap.createFocusTrap(modalElement, {
|
||||||
allowOutsideClick: (e) => {
|
allowOutsideClick: (e) => {
|
||||||
return e.target.closest('[data-sonner-toast]') !== null;
|
return (
|
||||||
|
e.target.closest('[data-sonner-toast]') !== null ||
|
||||||
|
e.target.closest('.modal-content') === null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
focusTrap.activate();
|
focusTrap.activate();
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850" />
|
<hr class="border-gray-100/30 dark:border-gray-850/30" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="px-3 my-2 max-h-80 overflow-y-auto">
|
<div class="px-3 my-2 max-h-80 overflow-y-auto">
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
{#if valvesSpec.properties[property]?.enum ?? null}
|
{#if valvesSpec.properties[property]?.enum ?? null}
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
bind:value={valves[property]}
|
bind:value={valves[property]}
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
dispatch('change');
|
dispatch('change');
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if (valvesSpec.properties[property]?.type ?? null) !== 'string'}
|
{:else if (valvesSpec.properties[property]?.type ?? null) !== 'string'}
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={valvesSpec.properties[property].title}
|
placeholder={valvesSpec.properties[property].title}
|
||||||
bind:value={valves[property]}
|
bind:value={valves[property]}
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="flex-1 rounded-lg py-2 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="flex-1 rounded-lg py-2 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={$i18n.t('Enter hex color (e.g. #FF0000)')}
|
placeholder={$i18n.t('Enter hex color (e.g. #FF0000)')}
|
||||||
bind:value={valves[property]}
|
bind:value={valves[property]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -146,7 +146,7 @@
|
||||||
{#if valves[property]}
|
{#if valves[property]}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class=" w-full rounded-lg py-1 text-left text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class=" w-full rounded-lg py-1 text-left text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={$i18n.t('Enter coordinates (e.g. 51.505, -0.09)')}
|
placeholder={$i18n.t('Enter coordinates (e.g. 51.505, -0.09)')}
|
||||||
bind:value={valves[property]}
|
bind:value={valves[property]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<textarea
|
<textarea
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||||
placeholder={valvesSpec.properties[property].title}
|
placeholder={valvesSpec.properties[property].title}
|
||||||
bind:value={valves[property]}
|
bind:value={valves[property]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@
|
||||||
class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"
|
class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"
|
||||||
>
|
>
|
||||||
<thead
|
<thead
|
||||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-1 border-gray-50 dark:border-gray-850"
|
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-1 border-gray-50 dark:border-gray-850/30"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
||||||
|
|
@ -367,7 +367,7 @@
|
||||||
{#each chats as chat, idx}
|
{#each chats as chat, idx}
|
||||||
<tr
|
<tr
|
||||||
class="bg-transparent {idx !== chats.length - 1 &&
|
class="bg-transparent {idx !== chats.length - 1 &&
|
||||||
'border-b'} dark:bg-gray-900 border-gray-50 dark:border-gray-850 text-xs"
|
'border-b'} dark:bg-gray-900 border-gray-50 dark:border-gray-850/30 text-xs"
|
||||||
>
|
>
|
||||||
<td class="px-3 py-1 w-2/3">
|
<td class="px-3 py-1 w-2/3">
|
||||||
<a href="/c/{chat.id}" target="_blank">
|
<a href="/c/{chat.id}" target="_blank">
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
|
||||||
|
|
||||||
{#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
|
{#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|
@ -435,7 +435,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
{#if !$temporaryChatEnabled && chat?.id}
|
{#if !$temporaryChatEnabled && chat?.id}
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
|
||||||
|
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger
|
<DropdownMenu.SubTrigger
|
||||||
|
|
@ -475,7 +475,7 @@
|
||||||
<div class="flex items-center">{$i18n.t('Archive')}</div>
|
<div class="flex items-center">{$i18n.t('Archive')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
|
||||||
|
|
||||||
<div class="flex p-1">
|
<div class="flex p-1">
|
||||||
<Tags chatId={chat.id} />
|
<Tags chatId={chat.id} />
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <hr class="border-gray-50 dark:border-gray-850 my-1" /> -->
|
<!-- <hr class="border-gray-50 dark:border-gray-850/30 my-1" /> -->
|
||||||
|
|
||||||
<div class="flex px-4 pb-1">
|
<div class="flex px-4 pb-1">
|
||||||
<div
|
<div
|
||||||
|
|
@ -328,7 +328,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if chatList}
|
{#if chatList}
|
||||||
<hr class="border-gray-50 dark:border-gray-850 my-3" />
|
<hr class="border-gray-50 dark:border-gray-850/30 my-3" />
|
||||||
|
|
||||||
{#if chatList.length === 0}
|
{#if chatList.length === 0}
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4">
|
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,18 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const initChannels = async () => {
|
const initChannels = async () => {
|
||||||
|
// default (none), group, dm type
|
||||||
await channels.set(
|
await channels.set(
|
||||||
(await getChannels(localStorage.token)).sort((a, b) =>
|
(await getChannels(localStorage.token)).sort((a, b) =>
|
||||||
a.type === b.type ? 0 : a.type === 'dm' ? 1 : -1
|
a.type === b.type
|
||||||
|
? 0
|
||||||
|
: a.type === 'dm'
|
||||||
|
? 1
|
||||||
|
: a.type === 'group'
|
||||||
|
? b.type === 'dm'
|
||||||
|
? -1
|
||||||
|
: 0
|
||||||
|
: -1
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -486,7 +495,7 @@
|
||||||
|
|
||||||
<ChannelModal
|
<ChannelModal
|
||||||
bind:show={showCreateChannel}
|
bind:show={showCreateChannel}
|
||||||
onSubmit={async ({ type, name, access_control, user_ids }) => {
|
onSubmit={async ({ type, name, is_private, access_control, group_ids, user_ids }) => {
|
||||||
name = name?.trim();
|
name = name?.trim();
|
||||||
|
|
||||||
if (type === 'dm') {
|
if (type === 'dm') {
|
||||||
|
|
@ -504,7 +513,9 @@
|
||||||
const res = await createNewChannel(localStorage.token, {
|
const res = await createNewChannel(localStorage.token, {
|
||||||
type: type,
|
type: type,
|
||||||
name: name,
|
name: name,
|
||||||
|
is_private: is_private,
|
||||||
access_control: access_control,
|
access_control: access_control,
|
||||||
|
group_ids: group_ids,
|
||||||
user_ids: user_ids
|
user_ids: user_ids
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
|
|
@ -562,7 +573,7 @@
|
||||||
|
|
||||||
{#if !$mobile && !$showSidebar}
|
{#if !$mobile && !$showSidebar}
|
||||||
<div
|
<div
|
||||||
class=" pt-[7px] pb-2 px-1.5 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850"
|
class=" pt-[7px] pb-2 px-2 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850/30"
|
||||||
id="sidebar"
|
id="sidebar"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
@ -703,10 +714,11 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5">
|
<div class=" py-2 flex justify-center items-center">
|
||||||
{#if $user !== undefined && $user !== null}
|
{#if $user !== undefined && $user !== null}
|
||||||
<UserMenu
|
<UserMenu
|
||||||
role={$user?.role}
|
role={$user?.role}
|
||||||
|
profile={true}
|
||||||
showActiveUsers={false}
|
showActiveUsers={false}
|
||||||
on:show={(e) => {
|
on:show={(e) => {
|
||||||
if (e.detail === 'archived-chat') {
|
if (e.detail === 'archived-chat') {
|
||||||
|
|
@ -717,13 +729,26 @@
|
||||||
<div
|
<div
|
||||||
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
|
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
|
||||||
>
|
>
|
||||||
<div class=" self-center flex items-center justify-center size-9">
|
<div class="self-center relative">
|
||||||
<img
|
<img
|
||||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||||
class=" size-6 object-cover rounded-full"
|
class=" size-7 object-cover rounded-full"
|
||||||
alt={$i18n.t('Open User Profile Menu')}
|
alt={$i18n.t('Open User Profile Menu')}
|
||||||
aria-label={$i18n.t('Open User Profile Menu')}
|
aria-label={$i18n.t('Open User Profile Menu')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="absolute -bottom-0.5 -right-0.5">
|
||||||
|
<span class="relative flex size-2.5">
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="relative inline-flex size-2.5 rounded-full {true
|
||||||
|
? 'bg-green-500'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700'} border-2 border-white dark:border-gray-900"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UserMenu>
|
</UserMenu>
|
||||||
|
|
@ -756,7 +781,7 @@
|
||||||
: 'invisible'}"
|
: 'invisible'}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="sidebar px-2 pt-2 pb-1.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 -mb-3"
|
class="sidebar px-[0.5625rem] pt-2 pb-1.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 -mb-3"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="flex items-center rounded-xl size-8.5 h-full justify-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition no-drag-region"
|
class="flex items-center rounded-xl size-8.5 h-full justify-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition no-drag-region"
|
||||||
|
|
@ -817,7 +842,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="pb-1.5">
|
<div class="pb-1.5">
|
||||||
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-[0.4375rem] flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<a
|
<a
|
||||||
id="sidebar-new-chat-button"
|
id="sidebar-new-chat-button"
|
||||||
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||||
|
|
@ -838,7 +863,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-[0.4375rem] flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<button
|
<button
|
||||||
id="sidebar-search-button"
|
id="sidebar-search-button"
|
||||||
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
class="group grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||||
|
|
@ -860,7 +885,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
|
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
|
||||||
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-[0.4375rem] flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<a
|
<a
|
||||||
id="sidebar-notes-button"
|
id="sidebar-notes-button"
|
||||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||||
|
|
@ -881,7 +906,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
|
||||||
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
<div class="px-[0.4375rem] flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
<a
|
<a
|
||||||
id="sidebar-workspace-button"
|
id="sidebar-workspace-button"
|
||||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||||
|
|
@ -927,14 +952,14 @@
|
||||||
</Folder>
|
</Folder>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
|
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || ($user?.permissions?.features?.channels ?? true))}
|
||||||
<Folder
|
<Folder
|
||||||
id="sidebar-channels"
|
id="sidebar-channels"
|
||||||
className="px-2 mt-0.5"
|
className="px-2 mt-0.5"
|
||||||
name={$i18n.t('Channels')}
|
name={$i18n.t('Channels')}
|
||||||
chevron={false}
|
chevron={false}
|
||||||
dragAndDrop={false}
|
dragAndDrop={false}
|
||||||
onAdd={$user?.role === 'admin'
|
onAdd={$user?.role === 'admin' || ($user?.permissions?.features?.channels ?? true)
|
||||||
? async () => {
|
? async () => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
|
|
@ -1257,6 +1282,7 @@
|
||||||
{#if $user !== undefined && $user !== null}
|
{#if $user !== undefined && $user !== null}
|
||||||
<UserMenu
|
<UserMenu
|
||||||
role={$user?.role}
|
role={$user?.role}
|
||||||
|
profile={true}
|
||||||
showActiveUsers={false}
|
showActiveUsers={false}
|
||||||
on:show={(e) => {
|
on:show={(e) => {
|
||||||
if (e.detail === 'archived-chat') {
|
if (e.detail === 'archived-chat') {
|
||||||
|
|
@ -1267,13 +1293,26 @@
|
||||||
<div
|
<div
|
||||||
class=" flex items-center rounded-2xl py-2 px-1.5 w-full hover:bg-gray-100/50 dark:hover:bg-gray-900/50 transition"
|
class=" flex items-center rounded-2xl py-2 px-1.5 w-full hover:bg-gray-100/50 dark:hover:bg-gray-900/50 transition"
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3 relative">
|
||||||
<img
|
<img
|
||||||
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||||
class=" size-6 object-cover rounded-full"
|
class=" size-7 object-cover rounded-full"
|
||||||
alt={$i18n.t('Open User Profile Menu')}
|
alt={$i18n.t('Open User Profile Menu')}
|
||||||
aria-label={$i18n.t('Open User Profile Menu')}
|
aria-label={$i18n.t('Open User Profile Menu')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="absolute -bottom-0.5 -right-0.5">
|
||||||
|
<span class="relative flex size-2.5">
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="relative inline-flex size-2.5 rounded-full {true
|
||||||
|
? 'bg-green-500'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700'} border-2 border-white dark:border-gray-900"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center font-medium">{$user?.name}</div>
|
<div class=" self-center font-medium">{$user?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,13 @@
|
||||||
{channel}
|
{channel}
|
||||||
edit={true}
|
edit={true}
|
||||||
{onUpdate}
|
{onUpdate}
|
||||||
onSubmit={async ({ name, access_control }) => {
|
onSubmit={async ({ name, is_private, access_control, group_ids, user_ids }) => {
|
||||||
const res = await updateChannelById(localStorage.token, channel.id, {
|
const res = await updateChannelById(localStorage.token, channel.id, {
|
||||||
name,
|
name,
|
||||||
access_control
|
is_private,
|
||||||
|
access_control,
|
||||||
|
group_ids,
|
||||||
|
user_ids
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
});
|
});
|
||||||
|
|
@ -119,7 +122,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" size-4 justify-center flex items-center ml-1">
|
<div class=" size-4 justify-center flex items-center ml-1">
|
||||||
{#if channel?.access_control === null}
|
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
|
||||||
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
||||||
{:else}
|
{:else}
|
||||||
<Lock className="size-[15px]" strokeWidth="2" />
|
<Lock className="size-[15px]" strokeWidth="2" />
|
||||||
|
|
@ -154,7 +157,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if channel?.type === 'dm'}
|
{#if ['dm'].includes(channel?.type)}
|
||||||
<div
|
<div
|
||||||
class="ml-0.5 mr-1 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
class="ml-0.5 mr-1 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||||
>
|
>
|
||||||
|
|
@ -181,7 +184,7 @@
|
||||||
<XMark className="size-3.5" />
|
<XMark className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if $user?.role === 'admin'}
|
{:else if $user?.role === 'admin' || channel.user_id === $user?.id}
|
||||||
<div
|
<div
|
||||||
class="ml-0.5 mr-1 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
class="ml-0.5 mr-1 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, createEventDispatcher, onMount } from 'svelte';
|
import { getContext, createEventDispatcher, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { createNewChannel, deleteChannelById } from '$lib/apis/channels';
|
import { createNewChannel, deleteChannelById } from '$lib/apis/channels';
|
||||||
|
import { user } from '$lib/stores';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import Visibility from '$lib/components/workspace/common/Visibility.svelte';
|
||||||
import { page } from '$app/stores';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import UserListSelector from '$lib/components/workspace/common/UserListSelector.svelte';
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let onSubmit: Function = () => {};
|
export let onSubmit: Function = () => {};
|
||||||
|
|
@ -21,10 +25,14 @@
|
||||||
export let channel = null;
|
export let channel = null;
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
|
|
||||||
|
let channelTypes = ['group', 'dm'];
|
||||||
let type = '';
|
let type = '';
|
||||||
let name = '';
|
let name = '';
|
||||||
|
|
||||||
|
let isPrivate = null;
|
||||||
let accessControl = {};
|
let accessControl = {};
|
||||||
|
|
||||||
|
let groupIds = [];
|
||||||
let userIds = [];
|
let userIds = [];
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
@ -33,12 +41,26 @@
|
||||||
name = name.replace(/\s/g, '-').toLocaleLowerCase();
|
name = name.replace(/\s/g, '-').toLocaleLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: onTypeChange(type);
|
||||||
|
|
||||||
|
const onTypeChange = (type) => {
|
||||||
|
if (type === 'group') {
|
||||||
|
if (isPrivate === null) {
|
||||||
|
isPrivate = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isPrivate = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
await onSubmit({
|
await onSubmit({
|
||||||
type: type,
|
type: type,
|
||||||
name: name.replace(/\s/g, '-'),
|
name: name.replace(/\s/g, '-'),
|
||||||
access_control: accessControl,
|
is_private: type === 'group' ? isPrivate : null,
|
||||||
|
access_control: type === '' ? accessControl : {},
|
||||||
|
group_ids: groupIds,
|
||||||
user_ids: userIds
|
user_ids: userIds
|
||||||
});
|
});
|
||||||
show = false;
|
show = false;
|
||||||
|
|
@ -46,16 +68,24 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
type = channel?.type ?? '';
|
if ($user?.role === 'admin') {
|
||||||
|
channelTypes = ['', 'group', 'dm'];
|
||||||
|
} else {
|
||||||
|
channelTypes = ['group', 'dm'];
|
||||||
|
}
|
||||||
|
|
||||||
|
type = channel?.type ?? channelTypes[0];
|
||||||
|
|
||||||
|
if (channel) {
|
||||||
name = channel?.name ?? '';
|
name = channel?.name ?? '';
|
||||||
|
isPrivate = channel?.is_private ?? null;
|
||||||
accessControl = channel.access_control;
|
accessControl = channel.access_control;
|
||||||
userIds = channel?.user_ids ?? [];
|
userIds = channel?.user_ids ?? [];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
if (channel) {
|
|
||||||
init();
|
init();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
resetHandler();
|
resetHandler();
|
||||||
}
|
}
|
||||||
|
|
@ -119,21 +149,51 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if !edit}
|
{#if !edit}
|
||||||
<div class="flex flex-col w-full mt-2">
|
<div class="flex flex-col w-full mt-2 mb-1">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Channel Type')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Channel Type')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
<Tooltip
|
||||||
|
content={type === 'dm'
|
||||||
|
? $i18n.t('A private conversation between you and selected users')
|
||||||
|
: type === 'group'
|
||||||
|
? $i18n.t('A collaboration channel where people join as members')
|
||||||
|
: $i18n.t(
|
||||||
|
'A discussion channel where access is controlled by groups and permissions'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
bind:value={type}
|
bind:value={type}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Channel')}</option>
|
{#each channelTypes as channelType, channelTypeIdx (channelType)}
|
||||||
<option value="dm">{$i18n.t('Direct Message')}</option>
|
<option value={channelType} selected={channelTypeIdx === 0}>
|
||||||
|
{#if channelType === 'group'}
|
||||||
|
{$i18n.t('Group Channel')}
|
||||||
|
{:else if channelType === 'dm'}
|
||||||
|
{$i18n.t('Direct Message')}
|
||||||
|
{:else if channelType === ''}
|
||||||
|
{$i18n.t('Channel')}
|
||||||
|
{/if}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class=" text-gray-300 dark:text-gray-700 text-xs">
|
||||||
|
{#if type === ''}
|
||||||
|
{$i18n.t('Discussion channel where access is based on groups and permissions')}
|
||||||
|
{:else if type === 'group'}
|
||||||
|
{$i18n.t('Collaboration channel where people join as members')}
|
||||||
|
{:else if type === 'dm'}
|
||||||
|
{$i18n.t('Private conversation between selected users')}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full mt-2">
|
<div class="flex flex-col w-full mt-2">
|
||||||
<div class=" mb-1 text-xs text-gray-500">
|
<div class=" mb-1 text-xs text-gray-500">
|
||||||
{$i18n.t('Channel Name')}
|
{$i18n.t('Channel Name')}
|
||||||
|
|
@ -154,17 +214,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100/40 dark:border-gray-700/10 my-2.5 w-full" />
|
{#if type !== 'dm'}
|
||||||
|
<div class="-mx-2 mb-1 mt-2.5 px-2">
|
||||||
<div class="-mx-2">
|
{#if type === ''}
|
||||||
{#if type === 'dm'}
|
|
||||||
<UserListSelector bind:userIds />
|
|
||||||
{:else}
|
|
||||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
|
|
||||||
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
|
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
|
||||||
</div>
|
{:else if type === 'group'}
|
||||||
|
<Visibility
|
||||||
|
state={isPrivate ? 'private' : 'public'}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value === 'private') {
|
||||||
|
isPrivate = true;
|
||||||
|
} else {
|
||||||
|
isPrivate = false;
|
||||||
|
}
|
||||||
|
console.log(value, isPrivate);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if ['dm'].includes(type)}
|
||||||
|
<div class="">
|
||||||
|
<MemberSelector bind:userIds includeGroups={false} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||||
{#if edit}
|
{#if edit}
|
||||||
|
|
|
||||||
|
|
@ -476,6 +476,14 @@
|
||||||
on:mouseenter={() => {
|
on:mouseenter={() => {
|
||||||
ignoreBlur = true;
|
ignoreBlur = true;
|
||||||
}}
|
}}
|
||||||
|
on:click={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
generateTitleHandler();
|
||||||
|
ignoreBlur = false;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Sparkles strokeWidth="2" />
|
<Sparkles strokeWidth="2" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,7 @@
|
||||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
|
<hr class=" border-gray-50 dark:border-gray-850/30 my-2.5 w-full" />
|
||||||
|
|
||||||
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
|
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
|
||||||
<div class="my-1">
|
<div class="my-1">
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@
|
||||||
import { fade, slide } from 'svelte/transition';
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
|
||||||
import { getUsage } from '$lib/apis';
|
import { getUsage } from '$lib/apis';
|
||||||
import { userSignOut } from '$lib/apis/auths';
|
import { getSessionUser, userSignOut } from '$lib/apis/auths';
|
||||||
|
|
||||||
import { showSettings, mobile, showSidebar, showShortcuts, user } from '$lib/stores';
|
import { showSettings, mobile, showSidebar, showShortcuts, user } from '$lib/stores';
|
||||||
|
|
||||||
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||||
import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte';
|
import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte';
|
||||||
|
|
@ -21,16 +23,27 @@
|
||||||
import Code from '$lib/components/icons/Code.svelte';
|
import Code from '$lib/components/icons/Code.svelte';
|
||||||
import UserGroup from '$lib/components/icons/UserGroup.svelte';
|
import UserGroup from '$lib/components/icons/UserGroup.svelte';
|
||||||
import SignOut from '$lib/components/icons/SignOut.svelte';
|
import SignOut from '$lib/components/icons/SignOut.svelte';
|
||||||
|
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
||||||
|
import UserStatusModal from './UserStatusModal.svelte';
|
||||||
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import { updateUserStatus } from '$lib/apis/users';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let role = '';
|
export let role = '';
|
||||||
|
|
||||||
|
export let profile = false;
|
||||||
export let help = false;
|
export let help = false;
|
||||||
|
|
||||||
export let className = 'max-w-[240px]';
|
export let className = 'max-w-[240px]';
|
||||||
|
|
||||||
export let showActiveUsers = true;
|
export let showActiveUsers = true;
|
||||||
|
|
||||||
|
let showUserStatusModal = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let usage = null;
|
let usage = null;
|
||||||
|
|
@ -52,6 +65,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ShortcutsModal bind:show={$showShortcuts} />
|
<ShortcutsModal bind:show={$showShortcuts} />
|
||||||
|
<UserStatusModal
|
||||||
|
bind:show={showUserStatusModal}
|
||||||
|
onSave={async () => {
|
||||||
|
user.set(await getSessionUser(localStorage.token));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<DropdownMenu.Root
|
<DropdownMenu.Root
|
||||||
|
|
@ -72,6 +91,116 @@
|
||||||
align="end"
|
align="end"
|
||||||
transition={(e) => fade(e, { duration: 100 })}
|
transition={(e) => fade(e, { duration: 100 })}
|
||||||
>
|
>
|
||||||
|
{#if profile}
|
||||||
|
<div class=" flex gap-3.5 w-full p-2.5 items-center">
|
||||||
|
<div class=" items-center flex shrink-0">
|
||||||
|
<img
|
||||||
|
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
|
||||||
|
class=" size-10 object-cover rounded-full"
|
||||||
|
alt="profile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex flex-col w-full flex-1">
|
||||||
|
<div class="font-medium line-clamp-1 pr-2">
|
||||||
|
{$user.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex items-center gap-2">
|
||||||
|
{#if $user?.is_active ?? true}
|
||||||
|
<div>
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
/>
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-xs"> {$i18n.t('Active')} </span>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-gray-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-xs"> {$i18n.t('Away')} </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $user?.status_emoji || $user?.status_message}
|
||||||
|
<div class="mx-1">
|
||||||
|
<button
|
||||||
|
class="mb-1 w-full gap-2 px-2.5 py-1.5 rounded-xl bg-gray-50 dark:text-white dark:bg-gray-900/50 text-black transition text-xs flex items-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showUserStatusModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if $user?.status_emoji}
|
||||||
|
<div class=" self-center shrink-0">
|
||||||
|
<Emoji className="size-4" shortCode={$user?.status_emoji} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={$user?.status_message}
|
||||||
|
className=" self-center line-clamp-2 flex-1 text-left"
|
||||||
|
>
|
||||||
|
{$user?.status_message}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="self-start">
|
||||||
|
<Tooltip content={$i18n.t('Clear status')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
const res = await updateUserStatus(localStorage.token, {
|
||||||
|
status_emoji: '',
|
||||||
|
status_message: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Status cleared successfully'));
|
||||||
|
user.set(await getSessionUser(localStorage.token));
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to clear status'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-4 opacity-50" strokeWidth="2" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mx-1">
|
||||||
|
<button
|
||||||
|
class="mb-1 w-full px-3 py-1.5 gap-1 rounded-xl bg-gray-50 dark:text-white dark:bg-gray-900/50 text-black transition text-xs flex items-center justify-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showUserStatusModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center">
|
||||||
|
<FaceSmile className="size-4" strokeWidth="1.5" />
|
||||||
|
</div>
|
||||||
|
<div class=" self-center truncate">{$i18n.t('Update your status')}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr class=" border-gray-50/30 dark:border-gray-800/30 my-1.5 p-0" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
|
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
|
|
@ -149,7 +278,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if help}
|
{#if help}
|
||||||
<hr class=" border-gray-50 dark:border-gray-800 my-1 p-0" />
|
<hr class=" border-gray-50/30 dark:border-gray-800/30 my-1 p-0" />
|
||||||
|
|
||||||
<!-- {$i18n.t('Help')} -->
|
<!-- {$i18n.t('Help')} -->
|
||||||
|
|
||||||
|
|
@ -202,7 +331,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-800 my-1 p-0" />
|
<hr class=" border-gray-50/30 dark:border-gray-800/30 my-1 p-0" />
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||||
|
|
@ -223,7 +352,7 @@
|
||||||
|
|
||||||
{#if showActiveUsers && usage}
|
{#if showActiveUsers && usage}
|
||||||
{#if usage?.user_count}
|
{#if usage?.user_count}
|
||||||
<hr class=" border-gray-50 dark:border-gray-800 my-1 p-0" />
|
<hr class=" border-gray-50/30 dark:border-gray-800/30 my-1 p-0" />
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={usage?.model_ids && usage?.model_ids.length > 0
|
content={usage?.model_ids && usage?.model_ids.length > 0
|
||||||
|
|
|
||||||
157
src/lib/components/layout/Sidebar/UserStatusModal.svelte
Normal file
157
src/lib/components/layout/Sidebar/UserStatusModal.svelte
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext, createEventDispatcher, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { updateUserStatus } from '$lib/apis/users';
|
||||||
|
import { settings, user } from '$lib/stores';
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
|
||||||
|
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
||||||
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let onSave: Function = () => {};
|
||||||
|
|
||||||
|
let emoji = '';
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
loading = true;
|
||||||
|
const res = await updateUserStatus(localStorage.token, {
|
||||||
|
status_emoji: emoji,
|
||||||
|
status_message: message
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
loading = false;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Status updated successfully'));
|
||||||
|
onSave();
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to update status'));
|
||||||
|
}
|
||||||
|
|
||||||
|
show = false;
|
||||||
|
loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
resetHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
emoji = $user?.status_emoji || '';
|
||||||
|
message = $user?.status_message || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHandler = () => {
|
||||||
|
emoji = '';
|
||||||
|
message = '';
|
||||||
|
loading = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
|
||||||
|
<div class=" text-lg font-medium self-center">
|
||||||
|
{$i18n.t('Set your status')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className={'size-5'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<form
|
||||||
|
class="flex flex-col w-full"
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500 mb-1.5">
|
||||||
|
{$i18n.t('Status')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center px-2.5 py-2 gap-3 border border-gray-100/50 dark:border-gray-850/50 rounded-xl"
|
||||||
|
>
|
||||||
|
<EmojiPicker
|
||||||
|
onClose={() => {}}
|
||||||
|
onSubmit={(name) => {
|
||||||
|
console.log(name);
|
||||||
|
emoji = name;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" flex items-center">
|
||||||
|
{#if emoji}
|
||||||
|
<Emoji shortCode={emoji} />
|
||||||
|
{:else}
|
||||||
|
<FaceSmile />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</EmojiPicker>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={message}
|
||||||
|
class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||||
|
placeholder={$i18n.t("What's on your mind?")}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
emoji = '';
|
||||||
|
message = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-950 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||||
|
? ' cursor-not-allowed'
|
||||||
|
: ''}"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="ml-2 self-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
@ -1326,7 +1326,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
</RecordMenu>
|
</RecordMenu>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer flex gap-0.5 rounded-full border border-gray-50 dark:border-gray-850 dark:bg-gray-850 transition shadow-xl"
|
class="cursor-pointer flex gap-0.5 rounded-full border border-gray-50 dark:border-gray-850/30 dark:bg-gray-850 transition shadow-xl"
|
||||||
>
|
>
|
||||||
<Tooltip content={$i18n.t('AI')} placement="top">
|
<Tooltip content={$i18n.t('AI')} placement="top">
|
||||||
{#if editing}
|
{#if editing}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else if show}
|
{:else if show}
|
||||||
<PaneResizer
|
<PaneResizer
|
||||||
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
||||||
id="controls-resizer"
|
id="controls-resizer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@
|
||||||
>
|
>
|
||||||
{#each notes[timeRange] as note, idx (note.id)}
|
{#each notes[timeRange] as note, idx (note.id)}
|
||||||
<div
|
<div
|
||||||
class=" flex space-x-4 cursor-pointer w-full px-4.5 py-4 border border-gray-50 dark:border-gray-850 bg-transparent dark:hover:bg-gray-850 hover:bg-white rounded-2xl transition"
|
class=" flex space-x-4 cursor-pointer w-full px-4.5 py-4 border border-gray-50 dark:border-gray-850/30 bg-transparent dark:hover:bg-gray-850 hover:bg-white rounded-2xl transition"
|
||||||
>
|
>
|
||||||
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
||||||
<a
|
<a
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className="w-full flex-1"
|
className="w-full flex-1"
|
||||||
bind:open={showSystem}
|
bind:open={showSystem}
|
||||||
buttonClassName="w-full rounded-lg text-sm border border-gray-100 dark:border-gray-850 w-full py-1 px-1.5"
|
buttonClassName="w-full rounded-lg text-sm border border-gray-100/30 dark:border-gray-850/30 w-full py-1 px-1.5"
|
||||||
grow={true}
|
grow={true}
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 justify-between items-center">
|
<div class="flex gap-2 justify-between items-center">
|
||||||
|
|
@ -271,7 +271,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pb-3">
|
<div class="pb-3">
|
||||||
<div class="border border-gray-100 dark:border-gray-850 w-full px-3 py-2.5 rounded-xl">
|
<div
|
||||||
|
class="border border-gray-100/30 dark:border-gray-850/30 w-full px-3 py-2.5 rounded-xl"
|
||||||
|
>
|
||||||
<div class="py-0.5">
|
<div class="py-0.5">
|
||||||
<!-- $i18n.t('a user') -->
|
<!-- $i18n.t('a user') -->
|
||||||
<!-- $i18n.t('an assistant') -->
|
<!-- $i18n.t('an assistant') -->
|
||||||
|
|
@ -322,7 +324,7 @@
|
||||||
<div class="flex items-center justify-between gap-2 w-full sm:w-auto">
|
<div class="flex items-center justify-between gap-2 w-full sm:w-auto">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class=" bg-transparent border border-gray-100 dark:border-gray-850 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-full"
|
class=" bg-transparent border border-gray-100/30 dark:border-gray-850/30 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-full"
|
||||||
bind:value={selectedModelId}
|
bind:value={selectedModelId}
|
||||||
>
|
>
|
||||||
{#each $models as model}
|
{#each $models as model}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
<textarea
|
<textarea
|
||||||
id="text-completion-textarea"
|
id="text-completion-textarea"
|
||||||
bind:this={textCompletionAreaElement}
|
bind:this={textCompletionAreaElement}
|
||||||
class="w-full h-full p-3 bg-transparent border border-gray-100 dark:border-gray-850 outline-hidden resize-none rounded-lg text-sm"
|
class="w-full h-full p-3 bg-transparent border border-gray-100/30 dark:border-gray-850/30 outline-hidden resize-none rounded-lg text-sm"
|
||||||
bind:value={text}
|
bind:value={text}
|
||||||
placeholder={$i18n.t("You're a helpful assistant.")}
|
placeholder={$i18n.t("You're a helpful assistant.")}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
|
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
|
||||||
>
|
>
|
||||||
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
|
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
|
|
||||||
<AccessControl
|
<AccessControl
|
||||||
bind:accessControl
|
bind:accessControl
|
||||||
accessRoles={['read', 'write']}
|
accessRoles={['read', 'write']}
|
||||||
|
|
@ -120,7 +119,6 @@
|
||||||
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end mt-2">
|
<div class="flex justify-end mt-2">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue