mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
Compare commits
12 commits
a7993f6f4e
...
3ed1df2e53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed1df2e53 | ||
|
|
68219d84a9 | ||
|
|
6068e23590 | ||
|
|
d7467a86e2 | ||
|
|
d098c57d4d | ||
|
|
693636d971 | ||
|
|
a6ef82c5ed | ||
|
|
79cfe29bb2 | ||
|
|
d1d42128e5 | ||
|
|
2bccf8350d | ||
|
|
c15201620d | ||
|
|
f31ca75892 |
15 changed files with 577 additions and 155 deletions
|
|
@ -1306,7 +1306,7 @@ USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
|
||||||
|
|
||||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = (
|
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = (
|
||||||
os.environ.get(
|
os.environ.get(
|
||||||
"USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING", "False"
|
"USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING", "False"
|
||||||
).lower()
|
).lower()
|
||||||
== "true"
|
== "true"
|
||||||
)
|
)
|
||||||
|
|
@ -1345,7 +1345,7 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
|
||||||
|
|
||||||
|
|
||||||
USER_PERMISSIONS_NOTES_ALLOW_SHARING = (
|
USER_PERMISSIONS_NOTES_ALLOW_SHARING = (
|
||||||
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower()
|
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_SHARING", "False").lower()
|
||||||
== "true"
|
== "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Add channel file table
|
||||||
|
|
||||||
|
Revision ID: 6283dc0e4d8d
|
||||||
|
Revises: 3e0e00844bb0
|
||||||
|
Create Date: 2025-12-10 15:11:39.424601
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = "6283dc0e4d8d"
|
||||||
|
down_revision: Union[str, None] = "3e0e00844bb0"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"channel_file",
|
||||||
|
sa.Column("id", sa.Text(), primary_key=True),
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"channel_id",
|
||||||
|
sa.Text(),
|
||||||
|
sa.ForeignKey("channel.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"file_id",
|
||||||
|
sa.Text(),
|
||||||
|
sa.ForeignKey("file.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=False),
|
||||||
|
# indexes
|
||||||
|
sa.Index("ix_channel_file_channel_id", "channel_id"),
|
||||||
|
sa.Index("ix_channel_file_file_id", "file_id"),
|
||||||
|
sa.Index("ix_channel_file_user_id", "user_id"),
|
||||||
|
# unique constraints
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"channel_id", "file_id", name="uq_channel_file_channel_file"
|
||||||
|
), # prevent duplicate entries
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("channel_file")
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Update channel file and knowledge table
|
||||||
|
|
||||||
|
Revision ID: 81cc2ce44d79
|
||||||
|
Revises: 6283dc0e4d8d
|
||||||
|
Create Date: 2025-12-10 16:07:58.001282
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = "81cc2ce44d79"
|
||||||
|
down_revision: Union[str, None] = "6283dc0e4d8d"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add message_id column to channel_file table
|
||||||
|
with op.batch_alter_table("channel_file", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column(
|
||||||
|
"message_id",
|
||||||
|
sa.Text(),
|
||||||
|
sa.ForeignKey(
|
||||||
|
"message.id", ondelete="CASCADE", name="fk_channel_file_message_id"
|
||||||
|
),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add data column to knowledge table
|
||||||
|
with op.batch_alter_table("knowledge", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("data", sa.JSON(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove message_id column from channel_file table
|
||||||
|
with op.batch_alter_table("channel_file", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("message_id")
|
||||||
|
|
||||||
|
# Remove data column from knowledge table
|
||||||
|
with op.batch_alter_table("knowledge", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("data")
|
||||||
|
|
@ -10,7 +10,18 @@ from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, case, cast
|
from sqlalchemy import (
|
||||||
|
BigInteger,
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
ForeignKey,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
JSON,
|
||||||
|
UniqueConstraint,
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -137,6 +148,41 @@ class ChannelMemberModel(BaseModel):
|
||||||
updated_at: Optional[int] = None # timestamp in epoch (time_ns)
|
updated_at: Optional[int] = None # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelFile(Base):
|
||||||
|
__tablename__ = "channel_file"
|
||||||
|
|
||||||
|
id = Column(Text, unique=True, primary_key=True)
|
||||||
|
user_id = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
channel_id = Column(
|
||||||
|
Text, ForeignKey("channel.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
message_id = Column(
|
||||||
|
Text, ForeignKey("message.id", ondelete="CASCADE"), nullable=True
|
||||||
|
)
|
||||||
|
file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
|
||||||
|
created_at = Column(BigInteger, nullable=False)
|
||||||
|
updated_at = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("channel_id", "file_id", name="uq_channel_file_channel_file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelFileModel(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: str
|
||||||
|
|
||||||
|
channel_id: str
|
||||||
|
file_id: str
|
||||||
|
user_id: str
|
||||||
|
|
||||||
|
created_at: int # timestamp in epoch (time_ns)
|
||||||
|
updated_at: int # timestamp in epoch (time_ns)
|
||||||
|
|
||||||
|
|
||||||
class ChannelWebhook(Base):
|
class ChannelWebhook(Base):
|
||||||
__tablename__ = "channel_webhook"
|
__tablename__ = "channel_webhook"
|
||||||
|
|
||||||
|
|
@ -642,6 +688,135 @@ class ChannelTable:
|
||||||
channel = db.query(Channel).filter(Channel.id == id).first()
|
channel = db.query(Channel).filter(Channel.id == id).first()
|
||||||
return ChannelModel.model_validate(channel) if channel else None
|
return ChannelModel.model_validate(channel) if channel else None
|
||||||
|
|
||||||
|
def get_channels_by_file_id(self, file_id: str) -> list[ChannelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
channel_files = (
|
||||||
|
db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||||
|
)
|
||||||
|
channel_ids = [cf.channel_id for cf in channel_files]
|
||||||
|
channels = db.query(Channel).filter(Channel.id.in_(channel_ids)).all()
|
||||||
|
return [ChannelModel.model_validate(channel) for channel in channels]
|
||||||
|
|
||||||
|
def get_channels_by_file_id_and_user_id(
|
||||||
|
self, file_id: str, user_id: str
|
||||||
|
) -> list[ChannelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
# 1. Determine which channels have this file
|
||||||
|
channel_file_rows = (
|
||||||
|
db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||||
|
)
|
||||||
|
channel_ids = [row.channel_id for row in channel_file_rows]
|
||||||
|
|
||||||
|
if not channel_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 2. Load all channel rows that still exist
|
||||||
|
channels = (
|
||||||
|
db.query(Channel)
|
||||||
|
.filter(
|
||||||
|
Channel.id.in_(channel_ids),
|
||||||
|
Channel.deleted_at.is_(None),
|
||||||
|
Channel.archived_at.is_(None),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if not channels:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Preload user's group membership
|
||||||
|
user_group_ids = [g.id for g in Groups.get_groups_by_member_id(user_id)]
|
||||||
|
|
||||||
|
allowed_channels = []
|
||||||
|
|
||||||
|
for channel in channels:
|
||||||
|
# --- Case A: group or dm => user must be an active member ---
|
||||||
|
if channel.type in ["group", "dm"]:
|
||||||
|
membership = (
|
||||||
|
db.query(ChannelMember)
|
||||||
|
.filter(
|
||||||
|
ChannelMember.channel_id == channel.id,
|
||||||
|
ChannelMember.user_id == user_id,
|
||||||
|
ChannelMember.is_active.is_(True),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if membership:
|
||||||
|
allowed_channels.append(ChannelModel.model_validate(channel))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Case B: standard channel => rely on ACL permissions ---
|
||||||
|
query = db.query(Channel).filter(Channel.id == channel.id)
|
||||||
|
|
||||||
|
query = self._has_permission(
|
||||||
|
db,
|
||||||
|
query,
|
||||||
|
{"user_id": user_id, "group_ids": user_group_ids},
|
||||||
|
permission="read",
|
||||||
|
)
|
||||||
|
|
||||||
|
allowed = query.first()
|
||||||
|
if allowed:
|
||||||
|
allowed_channels.append(ChannelModel.model_validate(allowed))
|
||||||
|
|
||||||
|
return allowed_channels
|
||||||
|
|
||||||
|
def get_channel_by_id_and_user_id(
|
||||||
|
self, id: str, user_id: str
|
||||||
|
) -> Optional[ChannelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
# Fetch the channel
|
||||||
|
channel: Channel = (
|
||||||
|
db.query(Channel)
|
||||||
|
.filter(
|
||||||
|
Channel.id == id,
|
||||||
|
Channel.deleted_at.is_(None),
|
||||||
|
Channel.archived_at.is_(None),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not channel:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If the channel is a group or dm, read access requires membership (active)
|
||||||
|
if channel.type in ["group", "dm"]:
|
||||||
|
membership = (
|
||||||
|
db.query(ChannelMember)
|
||||||
|
.filter(
|
||||||
|
ChannelMember.channel_id == id,
|
||||||
|
ChannelMember.user_id == user_id,
|
||||||
|
ChannelMember.is_active.is_(True),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if membership:
|
||||||
|
return ChannelModel.model_validate(channel)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# For channels that are NOT group/dm, fall back to ACL-based read access
|
||||||
|
query = db.query(Channel).filter(Channel.id == id)
|
||||||
|
|
||||||
|
# Determine user groups
|
||||||
|
user_group_ids = [
|
||||||
|
group.id for group in Groups.get_groups_by_member_id(user_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Apply ACL rules
|
||||||
|
query = self._has_permission(
|
||||||
|
db,
|
||||||
|
query,
|
||||||
|
{"user_id": user_id, "group_ids": user_group_ids},
|
||||||
|
permission="read",
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_allowed = query.first()
|
||||||
|
return (
|
||||||
|
ChannelModel.model_validate(channel_allowed)
|
||||||
|
if channel_allowed
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def update_channel_by_id(
|
def update_channel_by_id(
|
||||||
self, id: str, form_data: ChannelForm
|
self, id: str, form_data: ChannelForm
|
||||||
) -> Optional[ChannelModel]:
|
) -> Optional[ChannelModel]:
|
||||||
|
|
@ -663,6 +838,65 @@ class ChannelTable:
|
||||||
db.commit()
|
db.commit()
|
||||||
return ChannelModel.model_validate(channel) if channel else None
|
return ChannelModel.model_validate(channel) if channel else None
|
||||||
|
|
||||||
|
def add_file_to_channel_by_id(
|
||||||
|
self, channel_id: str, file_id: str, user_id: str
|
||||||
|
) -> Optional[ChannelFileModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
channel_file = ChannelFileModel(
|
||||||
|
**{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"channel_id": channel_id,
|
||||||
|
"file_id": file_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = ChannelFile(**channel_file.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
if result:
|
||||||
|
return ChannelFileModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_file_message_id_in_channel_by_id(
|
||||||
|
self, channel_id: str, file_id: str, message_id: str
|
||||||
|
) -> bool:
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
channel_file = (
|
||||||
|
db.query(ChannelFile)
|
||||||
|
.filter_by(channel_id=channel_id, file_id=file_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not channel_file:
|
||||||
|
return False
|
||||||
|
|
||||||
|
channel_file.message_id = message_id
|
||||||
|
channel_file.updated_at = int(time.time())
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_file_from_channel_by_id(self, channel_id: str, file_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
db.query(ChannelFile).filter_by(
|
||||||
|
channel_id=channel_id, file_id=file_id
|
||||||
|
).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_channel_by_id(self, id: str):
|
def delete_channel_by_id(self, id: str):
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
db.query(Channel).filter(Channel.id == id).delete()
|
db.query(Channel).filter(Channel.id == id).delete()
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,21 @@ class KnowledgeTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_knowledge_by_id_and_user_id(
|
||||||
|
self, id: str, user_id: str
|
||||||
|
) -> Optional[KnowledgeModel]:
|
||||||
|
knowledge = self.get_knowledge_by_id(id)
|
||||||
|
if not knowledge:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if knowledge.user_id == user_id:
|
||||||
|
return knowledge
|
||||||
|
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
|
if has_access(user_id, "write", knowledge.access_control, user_group_ids):
|
||||||
|
return knowledge
|
||||||
|
return None
|
||||||
|
|
||||||
def get_knowledges_by_file_id(self, file_id: str) -> list[KnowledgeModel]:
|
def get_knowledges_by_file_id(self, file_id: str) -> list[KnowledgeModel]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,9 @@ class NoteTable:
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
Note.title.ilike(f"%{query_key}%"),
|
Note.title.ilike(f"%{query_key}%"),
|
||||||
Note.data["content"]["md"].ilike(f"%{query_key}%"),
|
cast(Note.data["content"]["md"], Text).ilike(
|
||||||
|
f"%{query_key}%"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1093,6 +1093,15 @@ async def post_new_message(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message, channel = await new_message_handler(request, id, form_data, user)
|
message, channel = await new_message_handler(request, id, form_data, user)
|
||||||
|
try:
|
||||||
|
if files := message.data.get("files", []):
|
||||||
|
for file in files:
|
||||||
|
Channels.set_file_message_id_in_channel_by_id(
|
||||||
|
channel.id, file.get("id", ""), message.id
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(e)
|
||||||
|
|
||||||
active_user_ids = get_user_ids_from_room(f"channel:{channel.id}")
|
active_user_ids = get_user_ids_from_room(f"channel:{channel.id}")
|
||||||
|
|
||||||
async def background_handler():
|
async def background_handler():
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
|
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
|
||||||
|
|
||||||
|
from open_webui.models.channels import Channels
|
||||||
from open_webui.models.users import Users
|
from open_webui.models.users import Users
|
||||||
from open_webui.models.files import (
|
from open_webui.models.files import (
|
||||||
FileForm,
|
FileForm,
|
||||||
|
|
@ -91,6 +92,10 @@ def has_access_to_file(
|
||||||
if knowledge_base.id == knowledge_base_id:
|
if knowledge_base.id == knowledge_base_id:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
channels = Channels.get_channels_by_file_id_and_user_id(file_id, user.id)
|
||||||
|
if access_type == "read" and channels:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -138,6 +143,7 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us
|
||||||
f"File type {file.content_type} is not provided, but trying to process anyway"
|
f"File type {file.content_type} is not provided, but trying to process anyway"
|
||||||
)
|
)
|
||||||
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
|
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error processing file: {file_item.id}")
|
log.error(f"Error processing file: {file_item.id}")
|
||||||
Files.update_file_data_by_id(
|
Files.update_file_data_by_id(
|
||||||
|
|
@ -247,6 +253,13 @@ def upload_file_handler(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "channel_id" in file_metadata:
|
||||||
|
channel = Channels.get_channel_by_id_and_user_id(
|
||||||
|
file_metadata["channel_id"], user.id
|
||||||
|
)
|
||||||
|
if channel:
|
||||||
|
Channels.add_file_to_channel_by_id(channel.id, file_item.id, user.id)
|
||||||
|
|
||||||
if process:
|
if process:
|
||||||
if background_tasks and process_in_background:
|
if background_tasks and process_in_background:
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,11 @@ router = APIRouter()
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[KnowledgeUserResponse])
|
class KnowledgeAccessResponse(KnowledgeUserResponse):
|
||||||
|
write_access: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[KnowledgeAccessResponse])
|
||||||
async def get_knowledge(user=Depends(get_verified_user)):
|
async def get_knowledge(user=Depends(get_verified_user)):
|
||||||
# Return knowledge bases with read access
|
# Return knowledge bases with read access
|
||||||
knowledge_bases = []
|
knowledge_bases = []
|
||||||
|
|
@ -51,27 +55,35 @@ async def get_knowledge(user=Depends(get_verified_user)):
|
||||||
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
|
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
KnowledgeUserResponse(
|
KnowledgeAccessResponse(
|
||||||
**knowledge_base.model_dump(),
|
**knowledge_base.model_dump(),
|
||||||
files=Knowledges.get_file_metadatas_by_id(knowledge_base.id),
|
files=Knowledges.get_file_metadatas_by_id(knowledge_base.id),
|
||||||
|
write_access=(
|
||||||
|
user.id == knowledge_base.user_id
|
||||||
|
or has_access(user.id, "write", knowledge_base.access_control)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for knowledge_base in knowledge_bases
|
for knowledge_base in knowledge_bases
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list", response_model=list[KnowledgeUserResponse])
|
@router.get("/list", response_model=list[KnowledgeAccessResponse])
|
||||||
async def get_knowledge_list(user=Depends(get_verified_user)):
|
async def get_knowledge_list(user=Depends(get_verified_user)):
|
||||||
# Return knowledge bases with write access
|
# Return knowledge bases with write access
|
||||||
knowledge_bases = []
|
knowledge_bases = []
|
||||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||||
knowledge_bases = Knowledges.get_knowledge_bases()
|
knowledge_bases = Knowledges.get_knowledge_bases()
|
||||||
else:
|
else:
|
||||||
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "write")
|
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
KnowledgeUserResponse(
|
KnowledgeAccessResponse(
|
||||||
**knowledge_base.model_dump(),
|
**knowledge_base.model_dump(),
|
||||||
files=Knowledges.get_file_metadatas_by_id(knowledge_base.id),
|
files=Knowledges.get_file_metadatas_by_id(knowledge_base.id),
|
||||||
|
write_access=(
|
||||||
|
user.id == knowledge_base.user_id
|
||||||
|
or has_access(user.id, "write", knowledge_base.access_control)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for knowledge_base in knowledge_bases
|
for knowledge_base in knowledge_bases
|
||||||
]
|
]
|
||||||
|
|
@ -187,6 +199,7 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
|
||||||
|
|
||||||
class KnowledgeFilesResponse(KnowledgeResponse):
|
class KnowledgeFilesResponse(KnowledgeResponse):
|
||||||
files: list[FileMetadataResponse]
|
files: list[FileMetadataResponse]
|
||||||
|
write_access: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
|
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
|
||||||
|
|
@ -203,6 +216,10 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
return KnowledgeFilesResponse(
|
return KnowledgeFilesResponse(
|
||||||
**knowledge.model_dump(),
|
**knowledge.model_dump(),
|
||||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||||
|
write_access=(
|
||||||
|
user.id == knowledge.user_id
|
||||||
|
or has_access(user.id, "write", knowledge.access_control)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -363,11 +380,6 @@ def add_file_to_knowledge_by_id(
|
||||||
detail=ERROR_MESSAGES.FILE_NOT_PROCESSED,
|
detail=ERROR_MESSAGES.FILE_NOT_PROCESSED,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add file to knowledge base
|
|
||||||
Knowledges.add_file_to_knowledge_by_id(
|
|
||||||
knowledge_id=id, file_id=form_data.file_id, user_id=user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add content to the vector database
|
# Add content to the vector database
|
||||||
try:
|
try:
|
||||||
process_file(
|
process_file(
|
||||||
|
|
@ -375,6 +387,11 @@ def add_file_to_knowledge_by_id(
|
||||||
ProcessFileForm(file_id=form_data.file_id, collection_name=id),
|
ProcessFileForm(file_id=form_data.file_id, collection_name=id),
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add file to knowledge base
|
||||||
|
Knowledges.add_file_to_knowledge_by_id(
|
||||||
|
knowledge_id=id, file_id=form_data.file_id, user_id=user.id
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,7 @@
|
||||||
bind:chatInputElement
|
bind:chatInputElement
|
||||||
bind:replyToMessage
|
bind:replyToMessage
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
|
{channel}
|
||||||
userSuggestions={true}
|
userSuggestions={true}
|
||||||
channelSuggestions={true}
|
channelSuggestions={true}
|
||||||
disabled={!channel?.write_access}
|
disabled={!channel?.write_access}
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,10 @@
|
||||||
import XMark from '../icons/XMark.svelte';
|
import XMark from '../icons/XMark.svelte';
|
||||||
|
|
||||||
export let placeholder = $i18n.t('Type here...');
|
export let placeholder = $i18n.t('Type here...');
|
||||||
|
export let chatInputElement;
|
||||||
|
|
||||||
export let id = null;
|
export let id = null;
|
||||||
export let chatInputElement;
|
export let channel = null;
|
||||||
|
|
||||||
export let typingUsers = [];
|
export let typingUsers = [];
|
||||||
export let inputLoading = false;
|
export let inputLoading = false;
|
||||||
|
|
@ -459,15 +460,16 @@
|
||||||
try {
|
try {
|
||||||
// During the file upload, file content is automatically extracted.
|
// During the file upload, file content is automatically extracted.
|
||||||
// If the file is an audio file, provide the language for STT.
|
// If the file is an audio file, provide the language for STT.
|
||||||
let metadata = null;
|
let metadata = {
|
||||||
if (
|
channel_id: channel.id,
|
||||||
(file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
// If the file is an audio file, provide the language for STT.
|
||||||
|
...((file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
||||||
$settings?.audio?.stt?.language
|
$settings?.audio?.stt?.language
|
||||||
) {
|
? {
|
||||||
metadata = {
|
language: $settings?.audio?.stt?.language
|
||||||
language: $settings?.audio?.stt?.language
|
}
|
||||||
};
|
: {})
|
||||||
}
|
};
|
||||||
|
|
||||||
const uploadedFile = await uploadFile(localStorage.token, file, metadata, process);
|
const uploadedFile = await uploadFile(localStorage.token, file, metadata, process);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@
|
||||||
>
|
>
|
||||||
<Plus className="size-3" strokeWidth="2.5" />
|
<Plus className="size-3" strokeWidth="2.5" />
|
||||||
|
|
||||||
<div class=" md:ml-1 text-xs">{$i18n.t('New Note')}</div>
|
<div class=" ml-1 text-xs">{$i18n.t('New Note')}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -196,39 +196,35 @@
|
||||||
<!-- The Aleph dreams itself into being, and the void learns its own name -->
|
<!-- The Aleph dreams itself into being, and the void learns its own name -->
|
||||||
<div class=" my-2 px-3 grid grid-cols-1 lg:grid-cols-2 gap-2">
|
<div class=" my-2 px-3 grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||||
{#each filteredItems as item}
|
{#each filteredItems as item}
|
||||||
<Tooltip content={item?.description ?? item.name}>
|
<button
|
||||||
<button
|
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
|
||||||
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
|
on:click={() => {
|
||||||
on:click={() => {
|
if (item?.meta?.document) {
|
||||||
if (item?.meta?.document) {
|
toast.error(
|
||||||
toast.error(
|
$i18n.t(
|
||||||
$i18n.t(
|
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
||||||
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
)
|
||||||
)
|
);
|
||||||
);
|
} else {
|
||||||
} else {
|
goto(`/workspace/knowledge/${item.id}`);
|
||||||
goto(`/workspace/knowledge/${item.id}`);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<div class=" w-full">
|
||||||
<div class=" w-full">
|
<div class=" self-center flex-1 justify-between">
|
||||||
<div class=" self-center flex-1">
|
<div class="flex items-center justify-between -my-1 h-8">
|
||||||
<div class="flex items-center justify-between -my-1">
|
<div class=" flex gap-2 items-center justify-between w-full">
|
||||||
<div class=" flex gap-2 items-center">
|
<div>
|
||||||
<div>
|
<Badge type="success" content={$i18n.t('Collection')} />
|
||||||
{#if item?.meta?.document}
|
|
||||||
<Badge type="muted" content={$i18n.t('Document')} />
|
|
||||||
{:else}
|
|
||||||
<Badge type="success" content={$i18n.t('Collection')} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-xs text-gray-500 line-clamp-1">
|
|
||||||
{$i18n.t('Updated')}
|
|
||||||
{dayjs(item.updated_at * 1000).fromNow()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !item?.write_access}
|
||||||
|
<div>
|
||||||
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if item?.write_access}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class=" flex self-center">
|
<div class=" flex self-center">
|
||||||
<ItemMenu
|
<ItemMenu
|
||||||
|
|
@ -239,33 +235,42 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" flex items-center gap-1 justify-between px-1.5">
|
<div class=" flex items-center gap-1 justify-between px-1.5">
|
||||||
|
<Tooltip content={item?.description ?? item.name}>
|
||||||
<div class=" flex items-center gap-2">
|
<div class=" flex items-center gap-2">
|
||||||
<div class=" text-sm font-medium line-clamp-1 capitalize">{item.name}</div>
|
<div class=" text-sm font-medium line-clamp-1 capitalize">{item.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-xs text-gray-500">
|
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
|
||||||
<Tooltip
|
<div class=" text-xs text-gray-500 line-clamp-1">
|
||||||
content={item?.user?.email ?? $i18n.t('Deleted User')}
|
{$i18n.t('Updated')}
|
||||||
className="flex shrink-0"
|
{dayjs(item.updated_at * 1000).fromNow()}
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
{$i18n.t('By {{name}}', {
|
|
||||||
name: capitalizeFirstLetter(
|
|
||||||
item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
<Tooltip
|
||||||
|
content={item?.user?.email ?? $i18n.t('Deleted User')}
|
||||||
|
className="flex shrink-0"
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
{$i18n.t('By {{name}}', {
|
||||||
|
name: capitalizeFirstLetter(
|
||||||
|
item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</Tooltip>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -206,16 +206,16 @@
|
||||||
|
|
||||||
fileItems = [...(fileItems ?? []), fileItem];
|
fileItems = [...(fileItems ?? []), fileItem];
|
||||||
try {
|
try {
|
||||||
// If the file is an audio file, provide the language for STT.
|
let metadata = {
|
||||||
let metadata = null;
|
knowledge_id: knowledge.id,
|
||||||
if (
|
// If the file is an audio file, provide the language for STT.
|
||||||
(file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
...((file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
||||||
$settings?.audio?.stt?.language
|
$settings?.audio?.stt?.language
|
||||||
) {
|
? {
|
||||||
metadata = {
|
language: $settings?.audio?.stt?.language
|
||||||
language: $settings?.audio?.stt?.language
|
}
|
||||||
};
|
: {})
|
||||||
}
|
};
|
||||||
|
|
||||||
const uploadedFile = await uploadFile(localStorage.token, file, metadata).catch((e) => {
|
const uploadedFile = await uploadFile(localStorage.token, file, metadata).catch((e) => {
|
||||||
toast.error(`${e}`);
|
toast.error(`${e}`);
|
||||||
|
|
@ -429,7 +429,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
knowledge = res;
|
fileItems = [];
|
||||||
toast.success($i18n.t('Knowledge reset successfully.'));
|
toast.success($i18n.t('Knowledge reset successfully.'));
|
||||||
|
|
||||||
// Upload directory
|
// Upload directory
|
||||||
|
|
@ -441,16 +441,14 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFileHandler = async (fileId) => {
|
const addFileHandler = async (fileId) => {
|
||||||
const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
|
const res = await addFileToKnowledgeById(localStorage.token, id, fileId).catch((e) => {
|
||||||
(e) => {
|
toast.error(`${e}`);
|
||||||
toast.error(`${e}`);
|
return null;
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updatedKnowledge) {
|
if (res) {
|
||||||
knowledge = updatedKnowledge;
|
|
||||||
toast.success($i18n.t('File added successfully.'));
|
toast.success($i18n.t('File added successfully.'));
|
||||||
|
init();
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t('Failed to add file.'));
|
toast.error($i18n.t('Failed to add file.'));
|
||||||
fileItems = fileItems.filter((file) => file.id !== fileId);
|
fileItems = fileItems.filter((file) => file.id !== fileId);
|
||||||
|
|
@ -462,13 +460,12 @@
|
||||||
console.log('Starting file deletion process for:', fileId);
|
console.log('Starting file deletion process for:', fileId);
|
||||||
|
|
||||||
// Remove from knowledge base only
|
// Remove from knowledge base only
|
||||||
const updatedKnowledge = await removeFileFromKnowledgeById(localStorage.token, id, fileId);
|
const res = await removeFileFromKnowledgeById(localStorage.token, id, fileId);
|
||||||
|
console.log('Knowledge base updated:', res);
|
||||||
|
|
||||||
console.log('Knowledge base updated:', updatedKnowledge);
|
if (res) {
|
||||||
|
|
||||||
if (updatedKnowledge) {
|
|
||||||
knowledge = updatedKnowledge;
|
|
||||||
toast.success($i18n.t('File removed successfully.'));
|
toast.success($i18n.t('File removed successfully.'));
|
||||||
|
await init();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error in deleteFileHandler:', e);
|
console.error('Error in deleteFileHandler:', e);
|
||||||
|
|
@ -569,6 +566,11 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragged = false;
|
dragged = false;
|
||||||
|
|
||||||
|
if (!knowledge?.write_access) {
|
||||||
|
toast.error($i18n.t('You do not have permission to upload files to this knowledge base.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleUploadingFileFolder = (items) => {
|
const handleUploadingFileFolder = (items) => {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.isFile) {
|
if (item.isFile) {
|
||||||
|
|
@ -750,6 +752,7 @@
|
||||||
class="text-left w-full font-medium text-lg font-primary bg-transparent outline-hidden flex-1"
|
class="text-left w-full font-medium text-lg font-primary bg-transparent outline-hidden flex-1"
|
||||||
bind:value={knowledge.name}
|
bind:value={knowledge.name}
|
||||||
placeholder={$i18n.t('Knowledge Name')}
|
placeholder={$i18n.t('Knowledge Name')}
|
||||||
|
disabled={!knowledge?.write_access}
|
||||||
on:input={() => {
|
on:input={() => {
|
||||||
changeDebounceHandler();
|
changeDebounceHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -766,21 +769,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="self-center shrink-0">
|
{#if knowledge?.write_access}
|
||||||
<button
|
<div class="self-center shrink-0">
|
||||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
<button
|
||||||
type="button"
|
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||||
on:click={() => {
|
type="button"
|
||||||
showAccessControlModal = true;
|
on:click={() => {
|
||||||
}}
|
showAccessControlModal = true;
|
||||||
>
|
}}
|
||||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
>
|
||||||
|
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||||
|
|
||||||
<div class="text-sm font-medium shrink-0">
|
<div class="text-sm font-medium shrink-0">
|
||||||
{$i18n.t('Access')}
|
{$i18n.t('Access')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-xs shrink-0 text-gray-500">
|
||||||
|
{$i18n.t('Read Only')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
|
|
@ -789,6 +798,7 @@
|
||||||
class="text-left text-xs w-full text-gray-500 bg-transparent outline-hidden"
|
class="text-left text-xs w-full text-gray-500 bg-transparent outline-hidden"
|
||||||
bind:value={knowledge.description}
|
bind:value={knowledge.description}
|
||||||
placeholder={$i18n.t('Knowledge Description')}
|
placeholder={$i18n.t('Knowledge Description')}
|
||||||
|
disabled={!knowledge?.write_access}
|
||||||
on:input={() => {
|
on:input={() => {
|
||||||
changeDebounceHandler();
|
changeDebounceHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -815,22 +825,24 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
{#if knowledge?.write_access}
|
||||||
<AddContentMenu
|
<div>
|
||||||
on:upload={(e) => {
|
<AddContentMenu
|
||||||
if (e.detail.type === 'directory') {
|
on:upload={(e) => {
|
||||||
uploadDirectoryHandler();
|
if (e.detail.type === 'directory') {
|
||||||
} else if (e.detail.type === 'text') {
|
uploadDirectoryHandler();
|
||||||
showAddTextContentModal = true;
|
} else if (e.detail.type === 'text') {
|
||||||
} else {
|
showAddTextContentModal = true;
|
||||||
document.getElementById('files-input').click();
|
} else {
|
||||||
}
|
document.getElementById('files-input').click();
|
||||||
}}
|
}
|
||||||
on:sync={(e) => {
|
}}
|
||||||
showSyncConfirmModal = true;
|
on:sync={(e) => {
|
||||||
}}
|
showSyncConfirmModal = true;
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -899,6 +911,7 @@
|
||||||
<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
|
<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
|
||||||
<Files
|
<Files
|
||||||
files={fileItems}
|
files={fileItems}
|
||||||
|
{knowledge}
|
||||||
{selectedFileId}
|
{selectedFileId}
|
||||||
onClick={(fileId) => {
|
onClick={(fileId) => {
|
||||||
selectedFileId = fileId;
|
selectedFileId = fileId;
|
||||||
|
|
@ -962,28 +975,31 @@
|
||||||
{selectedFile?.meta?.name}
|
{selectedFile?.meta?.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{#if knowledge?.write_access}
|
||||||
<button
|
<div>
|
||||||
class="flex self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
<button
|
||||||
disabled={isSaving}
|
class="flex self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
on:click={() => {
|
disabled={isSaving}
|
||||||
updateFileContentHandler();
|
on:click={() => {
|
||||||
}}
|
updateFileContentHandler();
|
||||||
>
|
}}
|
||||||
{$i18n.t('Save')}
|
>
|
||||||
{#if isSaving}
|
{$i18n.t('Save')}
|
||||||
<div class="ml-2 self-center">
|
{#if isSaving}
|
||||||
<Spinner />
|
<div class="ml-2 self-center">
|
||||||
</div>
|
<Spinner />
|
||||||
{/if}
|
</div>
|
||||||
</button>
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#key selectedFile.id}
|
{#key selectedFile.id}
|
||||||
<textarea
|
<textarea
|
||||||
class="w-full h-full text-sm outline-none resize-none px-3 py-2"
|
class="w-full h-full text-sm outline-none resize-none px-3 py-2"
|
||||||
bind:value={selectedFileContent}
|
bind:value={selectedFileContent}
|
||||||
|
disabled={!knowledge?.write_access}
|
||||||
placeholder={$i18n.t('Add content here')}
|
placeholder={$i18n.t('Add content here')}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
|
||||||
|
export let knowledge = null;
|
||||||
export let selectedFileId = null;
|
export let selectedFileId = null;
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
|
|
@ -50,7 +51,9 @@
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
{file?.name ?? file?.meta?.name}
|
{file?.name ?? file?.meta?.name}
|
||||||
<span class="text-xs text-gray-500">{formatFileSize(file?.meta?.size)}</span>
|
{#if file?.meta?.size}
|
||||||
|
<span class="text-xs text-gray-500">{formatFileSize(file?.meta?.size)}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,19 +80,21 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex items-center">
|
{#if knowledge?.write_access}
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<div class="flex items-center">
|
||||||
<button
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
class="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
<button
|
||||||
type="button"
|
class="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||||
on:click={() => {
|
type="button"
|
||||||
onDelete(file?.id ?? file?.tempId);
|
on:click={() => {
|
||||||
}}
|
onDelete(file?.id ?? file?.tempId);
|
||||||
>
|
}}
|
||||||
<XMark />
|
>
|
||||||
</button>
|
<XMark />
|
||||||
</Tooltip>
|
</button>
|
||||||
</div>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue