mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-03 06:35:49 +00:00
Compare commits
9 Commits
v2.11.2
...
my_documen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da86610022 | ||
|
|
0027759dbf | ||
|
|
595ef152d2 | ||
|
|
083d669d1b | ||
|
|
3ac31136b2 | ||
|
|
a73a438d95 | ||
|
|
c0770481e8 | ||
|
|
c27d13c07f | ||
|
|
ab34c4e772 |
129
backend/alembic/versions/25d86cbfce78_add_my_documents.py
Normal file
129
backend/alembic/versions/25d86cbfce78_add_my_documents.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import datetime
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "25d86cbfce78"
|
||||
down_revision = "c0aab6edb6dd"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create user_folder table with additional 'display_priority' field
|
||||
op.create_table(
|
||||
"user_folder",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||
sa.Column("user_id", sa.UUID(), sa.ForeignKey("user.id"), nullable=True),
|
||||
sa.Column(
|
||||
"parent_id", sa.Integer(), sa.ForeignKey("user_folder.id"), nullable=True
|
||||
),
|
||||
sa.Column("name", sa.String(length=255), nullable=True),
|
||||
sa.Column("display_priority", sa.Integer(), nullable=True, default=0),
|
||||
sa.Column("created_at", sa.DateTime(), default=datetime.datetime.utcnow),
|
||||
)
|
||||
|
||||
# Migrate data from chat_folder to user_folder
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO user_folder (id, user_id, name, display_priority, created_at)
|
||||
SELECT id, user_id, name, display_priority, CURRENT_TIMESTAMP FROM chat_folder
|
||||
"""
|
||||
)
|
||||
|
||||
# Update chat_session table to reference user_folder instead of chat_folder
|
||||
op.drop_constraint(
|
||||
"chat_session_chat_folder_fk", "chat_session", type_="foreignkey"
|
||||
)
|
||||
op.alter_column(
|
||||
"chat_session",
|
||||
"folder_id",
|
||||
existing_type=sa.Integer(),
|
||||
nullable=True,
|
||||
existing_nullable=True,
|
||||
existing_server_default=None,
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_chat_session_folder_id_user_folder",
|
||||
"chat_session",
|
||||
"user_folder",
|
||||
["folder_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
# Drop the chat_folder table
|
||||
op.drop_table("chat_folder")
|
||||
|
||||
# Create user_file table
|
||||
op.create_table(
|
||||
"user_file",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||
sa.Column("user_id", sa.UUID(), sa.ForeignKey("user.id"), nullable=True),
|
||||
sa.Column(
|
||||
"parent_folder_id",
|
||||
sa.Integer(),
|
||||
sa.ForeignKey("user_folder.id"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("file_type", sa.String(), nullable=True),
|
||||
sa.Column("file_id", sa.String(length=255), nullable=False),
|
||||
sa.Column("document_id", sa.String(length=255), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(),
|
||||
default=datetime.datetime.utcnow,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Recreate chat_folder table
|
||||
op.create_table(
|
||||
"chat_folder",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.UUID(),
|
||||
sa.ForeignKey("user.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("name", sa.String(length=255), nullable=True),
|
||||
sa.Column("display_priority", sa.Integer(), nullable=True, default=0),
|
||||
)
|
||||
|
||||
# Migrate data back from user_folder to chat_folder
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO chat_folder (id, user_id, name, display_priority)
|
||||
SELECT id, user_id, name, display_priority FROM user_folder
|
||||
WHERE id IN (SELECT DISTINCT folder_id FROM chat_session WHERE folder_id IS NOT NULL)
|
||||
"""
|
||||
)
|
||||
|
||||
# Update chat_session table to reference chat_folder again
|
||||
op.drop_constraint(
|
||||
"fk_chat_session_folder_id_user_folder", "chat_session", type_="foreignkey"
|
||||
)
|
||||
op.alter_column(
|
||||
"chat_session",
|
||||
"folder_id",
|
||||
existing_type=sa.Integer(),
|
||||
nullable=True,
|
||||
existing_nullable=True,
|
||||
existing_server_default=None,
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"chat_session_chat_folder_fk",
|
||||
"chat_session",
|
||||
"chat_folder",
|
||||
["folder_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
# Drop the user_file table
|
||||
op.drop_table("user_file")
|
||||
# Drop the user_folder table
|
||||
op.drop_table("user_folder")
|
||||
@@ -963,7 +963,7 @@ def connector_indexing_task_wrapper(
|
||||
|
||||
|
||||
def connector_indexing_task(
|
||||
index_attempt_id: int,
|
||||
index_attempt_id: int | None,
|
||||
cc_pair_id: int,
|
||||
search_settings_id: int,
|
||||
tenant_id: str | None,
|
||||
|
||||
@@ -221,6 +221,7 @@ class FileOrigin(str, Enum):
|
||||
CHAT_IMAGE_GEN = "chat_image_gen"
|
||||
CONNECTOR = "connector"
|
||||
GENERATED_REPORT = "generated_report"
|
||||
MY_DOCUMENTS = "my_documents"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class OnyxConfluence(Confluence):
|
||||
try:
|
||||
response = self.get(url, params=params)
|
||||
except HTTPError as e:
|
||||
if e.response.status_code == 403:
|
||||
if e.response is not None and e.response.status_code == 403:
|
||||
raise ApiPermissionError(
|
||||
"The calling user does not have permission", reason=e
|
||||
)
|
||||
|
||||
@@ -96,6 +96,8 @@ class Tag(BaseModel):
|
||||
class BaseFilters(BaseModel):
|
||||
source_type: list[DocumentSource] | None = None
|
||||
document_set: list[str] | None = None
|
||||
user_folders: list[str] | None = None
|
||||
document_ids: list[str] | None = None
|
||||
time_cutoff: datetime | None = None
|
||||
tags: list[Tag] | None = None
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.db.chat import delete_chat_session
|
||||
from onyx.db.models import ChatFolder
|
||||
from onyx.db.models import ChatSession
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def get_user_folders(
|
||||
user_id: UUID | None,
|
||||
db_session: Session,
|
||||
) -> list[ChatFolder]:
|
||||
return db_session.query(ChatFolder).filter(ChatFolder.user_id == user_id).all()
|
||||
|
||||
|
||||
def update_folder_display_priority(
|
||||
user_id: UUID | None,
|
||||
display_priority_map: dict[int, int],
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
folders = get_user_folders(user_id=user_id, db_session=db_session)
|
||||
folder_ids = {folder.id for folder in folders}
|
||||
if folder_ids != set(display_priority_map.keys()):
|
||||
raise ValueError("Invalid Folder IDs provided")
|
||||
|
||||
for folder in folders:
|
||||
folder.display_priority = display_priority_map[folder.id]
|
||||
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def get_folder_by_id(
|
||||
user_id: UUID | None,
|
||||
folder_id: int,
|
||||
db_session: Session,
|
||||
) -> ChatFolder:
|
||||
folder = (
|
||||
db_session.query(ChatFolder).filter(ChatFolder.id == folder_id).one_or_none()
|
||||
)
|
||||
if not folder:
|
||||
raise ValueError("Folder by specified id does not exist")
|
||||
|
||||
if folder.user_id != user_id:
|
||||
raise PermissionError(f"Folder does not belong to user: {user_id}")
|
||||
|
||||
return folder
|
||||
|
||||
|
||||
def create_folder(
|
||||
user_id: UUID | None, folder_name: str | None, db_session: Session
|
||||
) -> int:
|
||||
new_folder = ChatFolder(
|
||||
user_id=user_id,
|
||||
name=folder_name,
|
||||
)
|
||||
db_session.add(new_folder)
|
||||
db_session.commit()
|
||||
|
||||
return new_folder.id
|
||||
|
||||
|
||||
def rename_folder(
|
||||
user_id: UUID | None, folder_id: int, folder_name: str | None, db_session: Session
|
||||
) -> None:
|
||||
folder = get_folder_by_id(
|
||||
user_id=user_id, folder_id=folder_id, db_session=db_session
|
||||
)
|
||||
|
||||
folder.name = folder_name
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def add_chat_to_folder(
|
||||
user_id: UUID | None, folder_id: int, chat_session: ChatSession, db_session: Session
|
||||
) -> None:
|
||||
folder = get_folder_by_id(
|
||||
user_id=user_id, folder_id=folder_id, db_session=db_session
|
||||
)
|
||||
|
||||
chat_session.folder_id = folder.id
|
||||
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def remove_chat_from_folder(
|
||||
user_id: UUID | None, folder_id: int, chat_session: ChatSession, db_session: Session
|
||||
) -> None:
|
||||
folder = get_folder_by_id(
|
||||
user_id=user_id, folder_id=folder_id, db_session=db_session
|
||||
)
|
||||
|
||||
if chat_session.folder_id != folder.id:
|
||||
raise ValueError("The chat session is not in the specified folder.")
|
||||
|
||||
if folder.user_id != user_id:
|
||||
raise ValueError(
|
||||
f"Tried to remove a chat session from a folder that does not below to "
|
||||
f"this user, user id: {user_id}"
|
||||
)
|
||||
|
||||
chat_session.folder_id = None
|
||||
if chat_session in folder.chat_sessions:
|
||||
folder.chat_sessions.remove(chat_session)
|
||||
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def delete_folder(
|
||||
user_id: UUID | None,
|
||||
folder_id: int,
|
||||
including_chats: bool,
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
folder = get_folder_by_id(
|
||||
user_id=user_id, folder_id=folder_id, db_session=db_session
|
||||
)
|
||||
|
||||
# Assuming there will not be a massive number of chats in any given folder
|
||||
if including_chats:
|
||||
for chat_session in folder.chat_sessions:
|
||||
delete_chat_session(
|
||||
user_id=user_id,
|
||||
chat_session_id=chat_session.id,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
db_session.delete(folder)
|
||||
db_session.commit()
|
||||
@@ -129,6 +129,56 @@ Auth/Authz (users, permissions, access) Tables
|
||||
"""
|
||||
|
||||
|
||||
class UserFolder(Base):
|
||||
__tablename__ = "user_folder"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), nullable=False)
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("user_folder.id"), nullable=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
default=datetime.datetime.utcnow
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship(back_populates="folders")
|
||||
parent: Mapped["UserFolder"] = relationship(
|
||||
remote_side=[id], back_populates="children"
|
||||
)
|
||||
children: Mapped[list["UserFolder"]] = relationship(back_populates="parent")
|
||||
files: Mapped[list["UserFile"]] = relationship(back_populates="folder")
|
||||
chat_sessions: Mapped[list["ChatSession"]] = relationship(back_populates="folder")
|
||||
|
||||
|
||||
class UserDocument(str, Enum):
|
||||
CHAT = "chat"
|
||||
RECENT = "recent"
|
||||
FILE = "file"
|
||||
|
||||
|
||||
class UserFile(Base):
|
||||
__tablename__ = "user_file"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int | None] = mapped_column(ForeignKey("user.id"), nullable=False)
|
||||
parent_folder_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("user_folder.id"), nullable=True
|
||||
)
|
||||
|
||||
file_id: Mapped[str] = mapped_column(nullable=False)
|
||||
document_id: Mapped[str] = mapped_column(nullable=False)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
default=datetime.datetime.utcnow
|
||||
)
|
||||
ccpair_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("connector_credential_pair.id"), nullable=False
|
||||
)
|
||||
user: Mapped["User"] = relationship(back_populates="files")
|
||||
folder: Mapped["UserFolder"] = relationship(back_populates="files")
|
||||
|
||||
|
||||
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
|
||||
# even an almost empty token from keycloak will not fit the default 1024 bytes
|
||||
access_token: Mapped[str] = mapped_column(Text, nullable=False) # type: ignore
|
||||
@@ -178,9 +228,6 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
chat_sessions: Mapped[list["ChatSession"]] = relationship(
|
||||
"ChatSession", back_populates="user"
|
||||
)
|
||||
chat_folders: Mapped[list["ChatFolder"]] = relationship(
|
||||
"ChatFolder", back_populates="user"
|
||||
)
|
||||
|
||||
prompts: Mapped[list["Prompt"]] = relationship("Prompt", back_populates="user")
|
||||
|
||||
@@ -198,6 +245,11 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
primaryjoin="User.id == foreign(ConnectorCredentialPair.creator_id)",
|
||||
)
|
||||
|
||||
folders: Mapped[list["UserFolder"]] = relationship(
|
||||
"UserFolder", back_populates="user"
|
||||
)
|
||||
files: Mapped[list["UserFile"]] = relationship("UserFile", back_populates="user")
|
||||
|
||||
|
||||
class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base):
|
||||
pass
|
||||
@@ -998,7 +1050,7 @@ class ChatSession(Base):
|
||||
default=ChatSessionSharedStatus.PRIVATE,
|
||||
)
|
||||
folder_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("chat_folder.id"), nullable=True
|
||||
ForeignKey("user_folder.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
current_alternate_model: Mapped[str | None] = mapped_column(String, default=None)
|
||||
@@ -1028,8 +1080,8 @@ class ChatSession(Base):
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
user: Mapped[User] = relationship("User", back_populates="chat_sessions")
|
||||
folder: Mapped["ChatFolder"] = relationship(
|
||||
"ChatFolder", back_populates="chat_sessions"
|
||||
folder: Mapped["UserFolder"] = relationship(
|
||||
"UserFolder", back_populates="chat_sessions"
|
||||
)
|
||||
messages: Mapped[list["ChatMessage"]] = relationship(
|
||||
"ChatMessage", back_populates="chat_session", cascade="all, delete-orphan"
|
||||
@@ -1117,33 +1169,6 @@ class ChatMessage(Base):
|
||||
)
|
||||
|
||||
|
||||
class ChatFolder(Base):
|
||||
"""For organizing chat sessions"""
|
||||
|
||||
__tablename__ = "chat_folder"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
# Only null if auth is off
|
||||
user_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey("user.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
name: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
display_priority: Mapped[int] = mapped_column(Integer, nullable=True, default=0)
|
||||
|
||||
user: Mapped[User] = relationship("User", back_populates="chat_folders")
|
||||
chat_sessions: Mapped[list["ChatSession"]] = relationship(
|
||||
"ChatSession", back_populates="folder"
|
||||
)
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if not isinstance(other, ChatFolder):
|
||||
return NotImplemented
|
||||
if self.display_priority == other.display_priority:
|
||||
# Bigger ID (created later) show earlier
|
||||
return self.id > other.id
|
||||
return self.display_priority < other.display_priority
|
||||
|
||||
|
||||
"""
|
||||
Feedback, Logging, Metrics Tables
|
||||
"""
|
||||
|
||||
36
backend/onyx/db/my_documents.py
Normal file
36
backend/onyx/db/my_documents.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.db.models import User
|
||||
from onyx.db.models import UserFile
|
||||
from onyx.server.documents.connector import upload_files
|
||||
from onyx.server.documents.models import FileUploadResponse
|
||||
|
||||
CHAT_FOLDER_ID = -1
|
||||
RECENT_DOCUMENTS_FOLDER_ID = -2
|
||||
|
||||
|
||||
def create_user_files(
|
||||
files: List[UploadFile],
|
||||
folder_id: int | None,
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
) -> FileUploadResponse:
|
||||
upload_response = upload_files(files, db_session)
|
||||
for file_path, file in zip(upload_response.file_paths, files):
|
||||
new_file = UserFile(
|
||||
user_id=user.id if user else None,
|
||||
parent_folder_id=folder_id,
|
||||
file_id=file_path,
|
||||
document_id=file_path, # We'll use the same ID for now
|
||||
name=file.filename,
|
||||
)
|
||||
db_session.add(new_file)
|
||||
|
||||
db_session.commit()
|
||||
return upload_response
|
||||
|
||||
|
||||
# def trigger_document_indexing(db_session: Session, user_id: int) -> None:
|
||||
29
backend/onyx/db/user_documents.py
Normal file
29
backend/onyx/db/user_documents.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.db.models import User
|
||||
from onyx.db.models import UserFile
|
||||
from onyx.server.documents.connector import upload_files
|
||||
from onyx.server.documents.models import FileUploadResponse
|
||||
|
||||
|
||||
def create_user_files(
|
||||
files: List[UploadFile],
|
||||
folder_id: int | None,
|
||||
user: User,
|
||||
db_session: Session,
|
||||
) -> FileUploadResponse:
|
||||
upload_response = upload_files(files, db_session)
|
||||
for file_path, file in zip(upload_response.file_paths, files):
|
||||
new_file = UserFile(
|
||||
user_id=user.id if user else None,
|
||||
parent_folder_id=folder_id if folder_id != -1 else None,
|
||||
file_id=file_path,
|
||||
document_id=file_path,
|
||||
name=file.filename,
|
||||
)
|
||||
db_session.add(new_file)
|
||||
db_session.commit()
|
||||
return upload_response
|
||||
0
backend/onyx/db/user_file.py
Normal file
0
backend/onyx/db/user_file.py
Normal file
@@ -112,6 +112,11 @@ schema DANSWER_CHUNK_NAME {
|
||||
rank: filter
|
||||
attribute: fast-search
|
||||
}
|
||||
field user_folders type weightedset<string> {
|
||||
indexing: summary | attribute
|
||||
rank: filter
|
||||
attribute: fast-search
|
||||
}
|
||||
}
|
||||
|
||||
# If using different tokenization settings, the fieldset has to be removed, and the field must
|
||||
|
||||
@@ -313,6 +313,7 @@ class VespaIndex(DocumentIndex):
|
||||
with updating the associated permissions. Assumes that a document will not be split into
|
||||
multiple chunk batches calling this function multiple times, otherwise only the last set of
|
||||
chunks will be kept"""
|
||||
|
||||
# IMPORTANT: This must be done one index at a time, do not use secondary index here
|
||||
cleaned_chunks = [clean_chunk_id_copy(chunk) for chunk in chunks]
|
||||
|
||||
@@ -706,6 +707,8 @@ class VespaIndex(DocumentIndex):
|
||||
offset: int = 0,
|
||||
title_content_ratio: float | None = TITLE_CONTENT_RATIO,
|
||||
) -> list[InferenceChunkUncleaned]:
|
||||
print("filters", filters)
|
||||
print("filters.user_folders", filters.__dict__)
|
||||
vespa_where_clauses = build_vespa_filters(filters)
|
||||
# Needs to be at least as much as the value set in Vespa schema config
|
||||
target_hits = max(10 * num_to_retrieve, 1000)
|
||||
|
||||
@@ -9,11 +9,13 @@ from onyx.document_index.vespa_constants import ACCESS_CONTROL_LIST
|
||||
from onyx.document_index.vespa_constants import CHUNK_ID
|
||||
from onyx.document_index.vespa_constants import DOC_UPDATED_AT
|
||||
from onyx.document_index.vespa_constants import DOCUMENT_ID
|
||||
from onyx.document_index.vespa_constants import DOCUMENT_IDS
|
||||
from onyx.document_index.vespa_constants import DOCUMENT_SETS
|
||||
from onyx.document_index.vespa_constants import HIDDEN
|
||||
from onyx.document_index.vespa_constants import METADATA_LIST
|
||||
from onyx.document_index.vespa_constants import SOURCE_TYPE
|
||||
from onyx.document_index.vespa_constants import TENANT_ID
|
||||
from onyx.document_index.vespa_constants import USER_FOLDERS
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
@@ -77,10 +79,15 @@ def build_vespa_filters(
|
||||
tags = filters.tags
|
||||
if tags:
|
||||
tag_attributes = [tag.tag_key + INDEX_SEPARATOR + tag.tag_value for tag in tags]
|
||||
|
||||
filter_str += _build_or_filters(METADATA_LIST, tag_attributes)
|
||||
|
||||
filter_str += _build_or_filters(DOCUMENT_SETS, filters.document_set)
|
||||
|
||||
filter_str += _build_or_filters(USER_FOLDERS, filters.user_folders)
|
||||
|
||||
filter_str += _build_or_filters(DOCUMENT_IDS, filters.document_ids)
|
||||
|
||||
filter_str += _build_time_filter(filters.time_cutoff)
|
||||
|
||||
if remove_trailing_and and filter_str.endswith(" and "):
|
||||
|
||||
@@ -64,6 +64,8 @@ EMBEDDINGS = "embeddings"
|
||||
TITLE_EMBEDDING = "title_embedding"
|
||||
ACCESS_CONTROL_LIST = "access_control_list"
|
||||
DOCUMENT_SETS = "document_sets"
|
||||
USER_FOLDERS = "user_folders"
|
||||
DOCUMENT_IDS = "document_ids"
|
||||
LARGE_CHUNK_REFERENCE_IDS = "large_chunk_reference_ids"
|
||||
METADATA = "metadata"
|
||||
METADATA_LIST = "metadata_list"
|
||||
|
||||
@@ -53,7 +53,6 @@ from onyx.server.documents.document import router as document_router
|
||||
from onyx.server.documents.indexing import router as indexing_router
|
||||
from onyx.server.documents.standard_oauth import router as oauth_router
|
||||
from onyx.server.features.document_set.api import router as document_set_router
|
||||
from onyx.server.features.folder.api import router as folder_router
|
||||
from onyx.server.features.notifications.api import router as notification_router
|
||||
from onyx.server.features.persona.api import admin_router as admin_persona_router
|
||||
from onyx.server.features.persona.api import basic_router as persona_router
|
||||
@@ -91,6 +90,7 @@ from onyx.server.settings.api import basic_router as settings_router
|
||||
from onyx.server.token_rate_limits.api import (
|
||||
router as token_rate_limit_settings_router,
|
||||
)
|
||||
from onyx.server.user_documents.api import router as user_documents_router
|
||||
from onyx.server.utils import BasicAuthenticationError
|
||||
from onyx.setup import setup_multitenant_onyx
|
||||
from onyx.setup import setup_onyx
|
||||
@@ -270,8 +270,6 @@ def get_application() -> FastAPI:
|
||||
include_router_with_global_prefix_prepended(application, user_router)
|
||||
include_router_with_global_prefix_prepended(application, credential_router)
|
||||
include_router_with_global_prefix_prepended(application, cc_pair_router)
|
||||
include_router_with_global_prefix_prepended(application, folder_router)
|
||||
include_router_with_global_prefix_prepended(application, document_set_router)
|
||||
include_router_with_global_prefix_prepended(application, search_settings_router)
|
||||
include_router_with_global_prefix_prepended(
|
||||
application, slack_bot_management_router
|
||||
@@ -286,15 +284,18 @@ def get_application() -> FastAPI:
|
||||
include_router_with_global_prefix_prepended(application, onyx_api_router)
|
||||
include_router_with_global_prefix_prepended(application, gpts_router)
|
||||
include_router_with_global_prefix_prepended(application, settings_router)
|
||||
include_router_with_global_prefix_prepended(application, user_documents_router)
|
||||
|
||||
include_router_with_global_prefix_prepended(application, settings_admin_router)
|
||||
include_router_with_global_prefix_prepended(application, llm_admin_router)
|
||||
include_router_with_global_prefix_prepended(application, llm_router)
|
||||
include_router_with_global_prefix_prepended(application, embedding_admin_router)
|
||||
include_router_with_global_prefix_prepended(application, embedding_router)
|
||||
include_router_with_global_prefix_prepended(application, document_set_router)
|
||||
include_router_with_global_prefix_prepended(application, indexing_router)
|
||||
include_router_with_global_prefix_prepended(
|
||||
application, token_rate_limit_settings_router
|
||||
)
|
||||
include_router_with_global_prefix_prepended(application, indexing_router)
|
||||
include_router_with_global_prefix_prepended(
|
||||
application, get_full_openai_assistants_api_router()
|
||||
)
|
||||
|
||||
9
backend/onyx/seeding/my_documents.yaml
Normal file
9
backend/onyx/seeding/my_documents.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# This file is used to seed the default chat folder for a user
|
||||
|
||||
default_folder:
|
||||
name: "Chat"
|
||||
description: "This is the default chat folder for a user"
|
||||
|
||||
|
||||
name: "Recent Documents"
|
||||
description: "This is the default folder for users to store their recent documents"
|
||||
@@ -375,10 +375,8 @@ def check_drive_tokens(
|
||||
return AuthStatus(authenticated=True)
|
||||
|
||||
|
||||
@router.post("/admin/connector/file/upload")
|
||||
def upload_files(
|
||||
files: list[UploadFile],
|
||||
_: User = Depends(current_curator_or_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FileUploadResponse:
|
||||
for file in files:
|
||||
@@ -408,6 +406,15 @@ def upload_files(
|
||||
return FileUploadResponse(file_paths=deduped_file_paths)
|
||||
|
||||
|
||||
@router.post("/admin/connector/file/upload")
|
||||
def upload_files_api(
|
||||
files: list[UploadFile],
|
||||
_: User = Depends(current_curator_or_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FileUploadResponse:
|
||||
return upload_files(files, db_session)
|
||||
|
||||
|
||||
# Retrieves most recent failure cases for connectors that are currently failing
|
||||
@router.get("/admin/connector/failed-indexing-status")
|
||||
def get_currently_failed_indexing_status(
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.auth.users import current_user
|
||||
from onyx.db.chat import get_chat_session_by_id
|
||||
from onyx.db.engine import get_session
|
||||
from onyx.db.folder import add_chat_to_folder
|
||||
from onyx.db.folder import create_folder
|
||||
from onyx.db.folder import delete_folder
|
||||
from onyx.db.folder import get_user_folders
|
||||
from onyx.db.folder import remove_chat_from_folder
|
||||
from onyx.db.folder import rename_folder
|
||||
from onyx.db.folder import update_folder_display_priority
|
||||
from onyx.db.models import User
|
||||
from onyx.server.features.folder.models import DeleteFolderOptions
|
||||
from onyx.server.features.folder.models import FolderChatSessionRequest
|
||||
from onyx.server.features.folder.models import FolderCreationRequest
|
||||
from onyx.server.features.folder.models import FolderResponse
|
||||
from onyx.server.features.folder.models import FolderUpdateRequest
|
||||
from onyx.server.features.folder.models import GetUserFoldersResponse
|
||||
from onyx.server.models import DisplayPriorityRequest
|
||||
from onyx.server.query_and_chat.models import ChatSessionDetails
|
||||
|
||||
router = APIRouter(prefix="/folder")
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_folders(
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> GetUserFoldersResponse:
|
||||
folders = get_user_folders(
|
||||
user_id=user.id if user else None,
|
||||
db_session=db_session,
|
||||
)
|
||||
folders.sort()
|
||||
return GetUserFoldersResponse(
|
||||
folders=[
|
||||
FolderResponse(
|
||||
folder_id=folder.id,
|
||||
folder_name=folder.name,
|
||||
display_priority=folder.display_priority,
|
||||
chat_sessions=[
|
||||
ChatSessionDetails(
|
||||
id=chat_session.id,
|
||||
name=chat_session.description,
|
||||
persona_id=chat_session.persona_id,
|
||||
time_created=chat_session.time_created.isoformat(),
|
||||
shared_status=chat_session.shared_status,
|
||||
folder_id=folder.id,
|
||||
)
|
||||
for chat_session in folder.chat_sessions
|
||||
if not chat_session.deleted
|
||||
],
|
||||
)
|
||||
for folder in folders
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.put("/reorder")
|
||||
def put_folder_display_priority(
|
||||
display_priority_request: DisplayPriorityRequest,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
update_folder_display_priority(
|
||||
user_id=user.id if user else None,
|
||||
display_priority_map=display_priority_request.display_priority_map,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_folder_endpoint(
|
||||
request: FolderCreationRequest,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> int:
|
||||
return create_folder(
|
||||
user_id=user.id if user else None,
|
||||
folder_name=request.folder_name,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{folder_id}")
|
||||
def patch_folder_endpoint(
|
||||
request: FolderUpdateRequest,
|
||||
folder_id: int = Path(..., description="The ID of the folder to rename"),
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
try:
|
||||
rename_folder(
|
||||
user_id=user.id if user else None,
|
||||
folder_id=folder_id,
|
||||
folder_name=request.folder_name,
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{folder_id}")
|
||||
def delete_folder_endpoint(
|
||||
request: DeleteFolderOptions,
|
||||
folder_id: int = Path(..., description="The ID of the folder to delete"),
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
user_id = user.id if user else None
|
||||
try:
|
||||
delete_folder(
|
||||
user_id=user_id,
|
||||
folder_id=folder_id,
|
||||
including_chats=request.including_chats,
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{folder_id}/add-chat-session")
|
||||
def add_chat_to_folder_endpoint(
|
||||
request: FolderChatSessionRequest,
|
||||
folder_id: int = Path(
|
||||
..., description="The ID of the folder in which to add the chat session"
|
||||
),
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
user_id = user.id if user else None
|
||||
try:
|
||||
chat_session = get_chat_session_by_id(
|
||||
chat_session_id=request.chat_session_id,
|
||||
user_id=user_id,
|
||||
db_session=db_session,
|
||||
)
|
||||
add_chat_to_folder(
|
||||
user_id=user.id if user else None,
|
||||
folder_id=folder_id,
|
||||
chat_session=chat_session,
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{folder_id}/remove-chat-session/")
|
||||
def remove_chat_from_folder_endpoint(
|
||||
request: FolderChatSessionRequest,
|
||||
folder_id: int = Path(
|
||||
..., description="The ID of the folder from which to remove the chat session"
|
||||
),
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
user_id = user.id if user else None
|
||||
try:
|
||||
chat_session = get_chat_session_by_id(
|
||||
chat_session_id=request.chat_session_id,
|
||||
user_id=user_id,
|
||||
db_session=db_session,
|
||||
)
|
||||
remove_chat_from_folder(
|
||||
user_id=user_id,
|
||||
folder_id=folder_id,
|
||||
chat_session=chat_session,
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -1,32 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from onyx.server.query_and_chat.models import ChatSessionDetails
|
||||
|
||||
|
||||
class FolderResponse(BaseModel):
|
||||
folder_id: int
|
||||
folder_name: str | None
|
||||
display_priority: int
|
||||
chat_sessions: list[ChatSessionDetails]
|
||||
|
||||
|
||||
class GetUserFoldersResponse(BaseModel):
|
||||
folders: list[FolderResponse]
|
||||
|
||||
|
||||
class FolderCreationRequest(BaseModel):
|
||||
folder_name: str | None = None
|
||||
|
||||
|
||||
class FolderUpdateRequest(BaseModel):
|
||||
folder_name: str | None = None
|
||||
|
||||
|
||||
class FolderChatSessionRequest(BaseModel):
|
||||
chat_session_id: UUID
|
||||
|
||||
|
||||
class DeleteFolderOptions(BaseModel):
|
||||
including_chats: bool = False
|
||||
@@ -53,6 +53,8 @@ from onyx.db.engine import get_session_with_tenant
|
||||
from onyx.db.feedback import create_chat_message_feedback
|
||||
from onyx.db.feedback import create_doc_retrieval_feedback
|
||||
from onyx.db.models import User
|
||||
from onyx.db.my_documents import create_user_files
|
||||
from onyx.db.my_documents import RECENT_DOCUMENTS_FOLDER_ID
|
||||
from onyx.db.persona import get_persona_by_id
|
||||
from onyx.document_index.document_index_utils import get_both_index_names
|
||||
from onyx.document_index.factory import get_default_document_index
|
||||
@@ -620,7 +622,7 @@ def convert_to_jpeg(file: UploadFile) -> Tuple[io.BytesIO, str]:
|
||||
def upload_files_for_chat(
|
||||
files: list[UploadFile],
|
||||
db_session: Session = Depends(get_session),
|
||||
_: User | None = Depends(current_user),
|
||||
user: User | None = Depends(current_user),
|
||||
) -> dict[str, list[FileDescriptor]]:
|
||||
image_content_types = {"image/jpeg", "image/png", "image/webp"}
|
||||
csv_content_types = {"text/csv"}
|
||||
@@ -678,6 +680,9 @@ def upload_files_for_chat(
|
||||
detail="File size must be less than 20MB",
|
||||
)
|
||||
|
||||
# Create the user files in the recent documents folder
|
||||
create_user_files(files, RECENT_DOCUMENTS_FOLDER_ID, user, db_session)
|
||||
|
||||
file_store = get_default_file_store(db_session)
|
||||
|
||||
file_info: list[tuple[str, str | None, ChatFileType]] = []
|
||||
|
||||
288
backend/onyx/server/user_documents/api.py
Normal file
288
backend/onyx/server/user_documents/api.py
Normal file
@@ -0,0 +1,288 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import File
|
||||
from fastapi import Form
|
||||
from fastapi import HTTPException
|
||||
from fastapi import UploadFile
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.auth.users import current_user
|
||||
from onyx.db.engine import get_session
|
||||
from onyx.db.models import User
|
||||
from onyx.db.models import UserFile
|
||||
from onyx.db.models import UserFolder
|
||||
from onyx.db.my_documents import create_user_files
|
||||
from onyx.server.documents.models import FileUploadResponse
|
||||
from onyx.server.user_documents.models import FileResponse
|
||||
from onyx.server.user_documents.models import FileSystemResponse
|
||||
from onyx.server.user_documents.models import FolderDetailResponse
|
||||
from onyx.server.user_documents.models import FolderFullDetailResponse
|
||||
from onyx.server.user_documents.models import FolderResponse
|
||||
from onyx.server.user_documents.models import MessageResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class FolderCreationRequest(BaseModel):
|
||||
name: str
|
||||
parent_id: int | None = None
|
||||
|
||||
|
||||
@router.post("/user/folder")
|
||||
def create_folder(
|
||||
request: FolderCreationRequest,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FolderDetailResponse:
|
||||
new_folder = UserFolder(
|
||||
user_id=user.id if user else None,
|
||||
parent_id=None if request.parent_id == -1 else request.parent_id,
|
||||
name=request.name,
|
||||
)
|
||||
db_session.add(new_folder)
|
||||
db_session.commit()
|
||||
return FolderDetailResponse(
|
||||
id=new_folder.id,
|
||||
name=new_folder.name,
|
||||
parent_id=new_folder.parent_id,
|
||||
children=[],
|
||||
files=[],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/user/folder",
|
||||
)
|
||||
def get_folders(
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> List[FolderResponse]:
|
||||
user_id = user.id if user else None
|
||||
folders = db_session.query(UserFolder).filter(UserFolder.user_id == user_id).all()
|
||||
return [FolderResponse.from_model(folder) for folder in folders]
|
||||
|
||||
|
||||
@router.get("/user/folder/{folder_id}")
|
||||
def get_folder(
|
||||
folder_id: int,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FolderFullDetailResponse:
|
||||
user_id = user.id if user else None
|
||||
if folder_id == -1:
|
||||
children = (
|
||||
db_session.query(UserFolder)
|
||||
.filter(UserFolder.user_id == user_id, UserFolder.parent_id.is_(None))
|
||||
.all()
|
||||
)
|
||||
files = (
|
||||
db_session.query(UserFile)
|
||||
.filter(UserFile.user_id == user_id, UserFile.parent_folder_id.is_(None))
|
||||
.all()
|
||||
)
|
||||
return FolderFullDetailResponse(
|
||||
name="Default Folder",
|
||||
parent_id=None,
|
||||
id=-1,
|
||||
children=[FolderResponse.from_model(child).dict() for child in children],
|
||||
files=[FileResponse.from_model(file).dict() for file in files],
|
||||
parents=[],
|
||||
)
|
||||
else:
|
||||
folder = (
|
||||
db_session.query(UserFolder)
|
||||
.filter(UserFolder.id == folder_id, UserFolder.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
|
||||
parents: List[FolderResponse] = []
|
||||
current_folder = folder
|
||||
while current_folder.parent_id is not None:
|
||||
parent = (
|
||||
db_session.query(UserFolder)
|
||||
.filter(UserFolder.id == current_folder.parent_id)
|
||||
.first()
|
||||
)
|
||||
if parent:
|
||||
parents.insert(
|
||||
0,
|
||||
FolderResponse.from_model(parent).dict(),
|
||||
)
|
||||
current_folder = parent
|
||||
else:
|
||||
break
|
||||
return FolderFullDetailResponse(
|
||||
name=folder.name,
|
||||
parent_id=folder.parent_id,
|
||||
id=folder.id,
|
||||
children=[
|
||||
FolderResponse.from_model(child).dict() for child in folder.children
|
||||
],
|
||||
files=[FileResponse.from_model(file).dict() for file in folder.files],
|
||||
parents=parents,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/user/file/upload")
|
||||
def upload_user_files(
|
||||
files: List[UploadFile] = File(...),
|
||||
folder_id: int | None = Form(None),
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FileUploadResponse:
|
||||
file_upload_response = FileUploadResponse(
|
||||
file_paths=create_user_files(files, folder_id, user, db_session).file_id
|
||||
)
|
||||
# trigger_document_indexing(db_session, user.id)
|
||||
return file_upload_response
|
||||
|
||||
|
||||
@router.put("/user/folder/{folder_id}")
|
||||
def update_folder(
|
||||
folder_id: int,
|
||||
name: str,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FolderDetailResponse:
|
||||
user_id = user.id if user else None
|
||||
folder = (
|
||||
db_session.query(UserFolder)
|
||||
.filter(UserFolder.id == folder_id, UserFolder.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
folder.name = name
|
||||
db_session.commit()
|
||||
return FolderDetailResponse(
|
||||
id=folder.id,
|
||||
name=folder.name,
|
||||
parent_id=folder.parent_id,
|
||||
children=[FolderResponse.from_model(child) for child in folder.children],
|
||||
files=[FileResponse.from_model(file) for file in folder.files],
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/user/folder/{folder_id}")
|
||||
def delete_folder(
|
||||
folder_id: int,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> MessageResponse:
|
||||
user_id = user.id if user else None
|
||||
folder = (
|
||||
db_session.query(UserFolder)
|
||||
.filter(UserFolder.id == folder_id, UserFolder.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
db_session.delete(folder)
|
||||
db_session.commit()
|
||||
return MessageResponse(message="Folder deleted successfully")
|
||||
|
||||
|
||||
class FolderMoveRequest(BaseModel):
|
||||
folder_id: int
|
||||
new_parent_id: int | None
|
||||
|
||||
|
||||
@router.put("/user/folder/{folder_id}/move")
|
||||
def move_folder(
|
||||
request: FolderMoveRequest,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FolderResponse:
|
||||
user_id = user.id if user else None
|
||||
folder = (
|
||||
db_session.query(UserFolder)
|
||||
.filter(UserFolder.id == request.folder_id, UserFolder.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
folder.parent_id = request.new_parent_id
|
||||
db_session.commit()
|
||||
return FolderResponse.from_model(folder)
|
||||
|
||||
|
||||
@router.delete("/user/file/{file_id}")
|
||||
def delete_file(
|
||||
file_id: int,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> MessageResponse:
|
||||
user_id = user.id if user else None
|
||||
file = (
|
||||
db_session.query(UserFile)
|
||||
.filter(UserFile.id == file_id, UserFile.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not file:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
db_session.delete(file)
|
||||
db_session.commit()
|
||||
return MessageResponse(message="File deleted successfully")
|
||||
|
||||
|
||||
class FileMoveRequest(BaseModel):
|
||||
file_id: int
|
||||
new_parent_id: int | None
|
||||
|
||||
|
||||
@router.put("/user/file/{file_id}/move")
|
||||
def move_file(
|
||||
request: FileMoveRequest,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FileResponse:
|
||||
user_id = user.id if user else None
|
||||
file = (
|
||||
db_session.query(UserFile)
|
||||
.filter(UserFile.id == request.file_id, UserFile.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not file:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
file.parent_folder_id = request.new_parent_id
|
||||
db_session.commit()
|
||||
return FileResponse.from_model(file)
|
||||
|
||||
|
||||
@router.get("/user/file-system")
|
||||
def get_file_system(
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FileSystemResponse:
|
||||
user_id = user.id if user else None
|
||||
folders = db_session.query(UserFolder).filter(UserFolder.user_id == user_id).all()
|
||||
files = db_session.query(UserFile).filter(UserFile.user_id == user_id).all()
|
||||
return FileSystemResponse(
|
||||
folders=[FolderResponse.from_model(folder) for folder in folders],
|
||||
files=[FileResponse.from_model(file) for file in files],
|
||||
)
|
||||
|
||||
|
||||
@router.put("/user/file/{file_id}/rename")
|
||||
def rename_file(
|
||||
file_id: int,
|
||||
name: str,
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> FileResponse:
|
||||
user_id = user.id if user else None
|
||||
file = (
|
||||
db_session.query(UserFile)
|
||||
.filter(UserFile.id == file_id, UserFile.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not file:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
file.name = name
|
||||
db_session.commit()
|
||||
return FileResponse.from_model(file)
|
||||
54
backend/onyx/server/user_documents/models.py
Normal file
54
backend/onyx/server/user_documents/models.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from onyx.db.models import UserFile
|
||||
from onyx.db.models import UserFolder
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class FolderResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
parent_id: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: UserFolder) -> "FolderResponse":
|
||||
return cls(id=model.id, name=model.name, parent_id=model.parent_id)
|
||||
|
||||
|
||||
class FolderDetailResponse(FolderResponse):
|
||||
children: List[FolderResponse]
|
||||
files: List[dict]
|
||||
|
||||
|
||||
class FolderFullDetailResponse(FolderDetailResponse):
|
||||
parents: List[FolderResponse]
|
||||
|
||||
|
||||
class FileResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
document_id: str
|
||||
parent_folder_id: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: UserFile) -> "FileResponse":
|
||||
return cls(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
parent_folder_id=model.parent_folder_id,
|
||||
document_id=model.document_id,
|
||||
)
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class FileSystemResponse(BaseModel):
|
||||
folders: list[FolderResponse]
|
||||
files: list[FileResponse]
|
||||
72
web/package-lock.json
generated
72
web/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
@@ -56,6 +57,7 @@
|
||||
"react-icons": "^4.8.0",
|
||||
"react-loader-spinner": "^5.4.5",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-select": "^5.8.0",
|
||||
"recharts": "^2.13.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@@ -1194,6 +1196,21 @@
|
||||
"@floating-ui/utils": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
@@ -1229,20 +1246,6 @@
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react/node_modules/@floating-ui/react": {
|
||||
"version": "0.26.27",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.27.tgz",
|
||||
"integrity": "sha512-jLP72x0Kr2CgY6eTYi/ra3VA9LOkTo4C+DUTrbFgFOExKy3omYVmwMjNKqxAHdsnyLS96BIDLcO2SlnsNf8KUQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react/node_modules/@react-aria/focus": {
|
||||
"version": "3.18.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
|
||||
@@ -2592,6 +2595,17 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/instrumentation": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.19.1.tgz",
|
||||
@@ -14171,6 +14185,27 @@
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-popper": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
|
||||
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-fast-compare": "^3.0.1",
|
||||
"warning": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.0.0",
|
||||
"react": "^16.8.0 || ^17 || ^18",
|
||||
"react-dom": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-popper/node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
|
||||
@@ -16206,6 +16241,15 @@
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
@@ -58,6 +59,7 @@
|
||||
"react-icons": "^4.8.0",
|
||||
"react-loader-spinner": "^5.4.5",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-select": "^5.8.0",
|
||||
"recharts": "^2.13.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
import Cookies from "js-cookie";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import {
|
||||
@@ -35,7 +33,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
size = "sm",
|
||||
children,
|
||||
}: SidebarWrapperProps<T>) {
|
||||
const { chatSessions, folders, openedFolders } = useChatContext();
|
||||
const { chatSessions } = useChatContext();
|
||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||
const [showDocSidebar, setShowDocSidebar] = useState(false); // State to track if sidebar is open
|
||||
// Used to maintain a "time out" for history sidebar so our existing refs can have time to process change
|
||||
@@ -122,8 +120,6 @@ export default function SidebarWrapper<T extends object>({
|
||||
toggled={toggledSidebar}
|
||||
existingChats={chatSessions}
|
||||
currentChatSession={null}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,6 @@ export default async function GalleryPage(props: {
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
availableSources,
|
||||
@@ -46,8 +44,6 @@ export default async function GalleryPage(props: {
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
|
||||
@@ -24,8 +24,6 @@ export default async function GalleryPage(props: {
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
availableSources,
|
||||
@@ -47,8 +45,6 @@ export default async function GalleryPage(props: {
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
|
||||
@@ -110,7 +110,7 @@ import AssistantBanner from "../../components/assistants/AssistantBanner";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
import AssistantSelector from "@/components/chat_search/AssistantSelector";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { createPostponedAbortSignal } from "next/dist/server/app-render/dynamic-rendering";
|
||||
import { FilePicker, UserFile, UserFolder } from "../my-documents/FilePicker";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@@ -134,8 +134,6 @@ export function ChatPage({
|
||||
tags,
|
||||
documentSets,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
refreshChatSessions,
|
||||
@@ -200,6 +198,11 @@ export function ChatPage({
|
||||
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
|
||||
);
|
||||
|
||||
const [myDocumentsToggled, setMyDocumentsToggled] = useState(false);
|
||||
|
||||
const toggleMyDocuments = () => {
|
||||
setMyDocumentsToggled(!myDocumentsToggled);
|
||||
};
|
||||
// Effect to handle sendOnLoad
|
||||
useEffect(() => {
|
||||
if (sendOnLoad) {
|
||||
@@ -1928,6 +1931,36 @@ export function ChatPage({
|
||||
}
|
||||
};
|
||||
|
||||
const [allFolders, setAllFolders] = useState<UserFolder[]>([]);
|
||||
const [allFiles, setAllFiles] = useState<UserFile[]>([]);
|
||||
useEffect(() => {
|
||||
const loadFileSystem = async () => {
|
||||
const res = await fetch("/api/user/file-system");
|
||||
const data = await res.json();
|
||||
const folders = data.folders.map((f: any) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
parent_id: f.parent_id,
|
||||
}));
|
||||
const files = data.files.map((f: any) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
parent_id: f.parent_id,
|
||||
}));
|
||||
|
||||
setAllFolders(folders);
|
||||
setAllFiles(files);
|
||||
};
|
||||
loadFileSystem();
|
||||
}, []);
|
||||
|
||||
const [folders, setFolders] = useState<UserFolder[]>([]);
|
||||
const [userFiles, setUserFiles] = useState<UserFile[]>([]);
|
||||
|
||||
const removeFolder = (folderId: number) => {
|
||||
setFolders((prev) => prev.filter((f) => f.id !== folderId));
|
||||
};
|
||||
|
||||
interface RegenerationRequest {
|
||||
messageId: number;
|
||||
parentMessage: Message;
|
||||
@@ -1969,6 +2002,21 @@ export function ChatPage({
|
||||
{popup}
|
||||
|
||||
<ChatPopup />
|
||||
{myDocumentsToggled && (
|
||||
<FilePicker
|
||||
allFolders={allFolders}
|
||||
setSelectedFolders={(folders) => setFolders(folders)}
|
||||
setUserFiles={(userFiles) => {
|
||||
setUserFiles(userFiles);
|
||||
}}
|
||||
allFiles={allFiles}
|
||||
isOpen={myDocumentsToggled}
|
||||
userFiles={userFiles}
|
||||
selectedFolders={folders}
|
||||
onClose={() => setMyDocumentsToggled(false)}
|
||||
onSave={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeleteAllModal && (
|
||||
<DeleteEntityModal
|
||||
@@ -2143,17 +2191,13 @@ export function ChatPage({
|
||||
<div className="w-full relative">
|
||||
<HistorySidebar
|
||||
explicitlyUntoggle={explicitlyUntoggle}
|
||||
stopGenerating={stopGenerating}
|
||||
reset={() => setMessage("")}
|
||||
page="chat"
|
||||
ref={innerSidebarElementRef}
|
||||
toggleSidebar={toggleSidebar}
|
||||
toggled={toggledSidebar}
|
||||
backgroundToggled={toggledSidebar || showHistorySidebar}
|
||||
toggled={toggledSidebar && !settings?.isMobile}
|
||||
existingChats={chatSessions}
|
||||
currentChatSession={selectedChatSession}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
removeToggle={removeToggle}
|
||||
showShareModal={showShareModal}
|
||||
showDeleteModal={showDeleteModal}
|
||||
@@ -2761,7 +2805,22 @@ export function ChatPage({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChatInputBar
|
||||
removeUserFile={(userFileId) => {
|
||||
setUserFiles(
|
||||
userFiles.filter(
|
||||
(file) => file.id !== userFileId
|
||||
)
|
||||
);
|
||||
}}
|
||||
removeFilters={() => {
|
||||
setFiltersToggled(false);
|
||||
}}
|
||||
userFiles={userFiles}
|
||||
folders={folders}
|
||||
removeFolder={removeFolder}
|
||||
toggleMyDocuments={toggleMyDocuments}
|
||||
removeDocs={() => {
|
||||
clearSelectedDocuments();
|
||||
}}
|
||||
@@ -2857,6 +2916,7 @@ export function ChatPage({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FixedLogo backgroundToggled={toggledSidebar || showHistorySidebar} />
|
||||
</div>
|
||||
{/* Right Sidebar - DocumentSidebar */}
|
||||
|
||||
@@ -79,9 +79,7 @@ export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
|
||||
|
||||
const currentDocuments = selectedMessage?.documents || null;
|
||||
const dedupedDocuments = removeDuplicateDocs(currentDocuments || []);
|
||||
|
||||
const tokenLimitReached = selectedDocumentTokens > maxTokens - 75;
|
||||
console.log("SELECTED MESSAGE is", selectedMessage);
|
||||
|
||||
const hasSelectedDocuments = selectedDocumentIds.length > 0;
|
||||
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Folder } from "./interfaces";
|
||||
import { ChatSessionDisplay } from "../sessionSidebar/ChatSessionDisplay"; // Ensure this is correctly imported
|
||||
import {
|
||||
FiChevronDown,
|
||||
FiChevronRight,
|
||||
FiFolder,
|
||||
FiEdit2,
|
||||
FiCheck,
|
||||
FiX,
|
||||
FiTrash, // Import the trash icon
|
||||
} from "react-icons/fi";
|
||||
import { BasicSelectable } from "@/components/BasicClickable";
|
||||
import {
|
||||
addChatToFolder,
|
||||
deleteFolder,
|
||||
updateFolderName,
|
||||
} from "./FolderManagement";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
|
||||
import Cookies from "js-cookie";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
const FolderItem = ({
|
||||
folder,
|
||||
currentChatId,
|
||||
isInitiallyExpanded,
|
||||
initiallySelected,
|
||||
showShareModal,
|
||||
showDeleteModal,
|
||||
}: {
|
||||
folder: Folder;
|
||||
currentChatId?: string;
|
||||
isInitiallyExpanded: boolean;
|
||||
initiallySelected: boolean;
|
||||
|
||||
showShareModal: ((chatSession: ChatSession) => void) | undefined;
|
||||
showDeleteModal: ((chatSession: ChatSession) => void) | undefined;
|
||||
}) => {
|
||||
const { refreshChatSessions } = useChatContext();
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(initiallySelected);
|
||||
const [editedFolderName, setEditedFolderName] = useState<string>(
|
||||
folder.folder_name
|
||||
);
|
||||
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||||
const { setPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
|
||||
const toggleFolderExpansion = () => {
|
||||
if (!isEditing) {
|
||||
const newIsExpanded = !isExpanded;
|
||||
setIsExpanded(newIsExpanded);
|
||||
// Update the cookie with the new state
|
||||
const openedFoldersCookieVal = Cookies.get("openedFolders");
|
||||
const openedFolders = openedFoldersCookieVal
|
||||
? JSON.parse(openedFoldersCookieVal)
|
||||
: {};
|
||||
if (newIsExpanded) {
|
||||
openedFolders[folder.folder_id] = true;
|
||||
} else {
|
||||
setShowDeleteConfirm(false);
|
||||
|
||||
delete openedFolders[folder.folder_id];
|
||||
}
|
||||
Cookies.set("openedFolders", JSON.stringify(openedFolders));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditFolderName = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation(); // Prevent the event from bubbling up to the toggle expansion
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleFolderNameChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setEditedFolderName(event.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
saveFolderName();
|
||||
}
|
||||
};
|
||||
|
||||
const saveFolderName = async (continueEditing?: boolean) => {
|
||||
try {
|
||||
await updateFolderName(folder.folder_id, editedFolderName);
|
||||
if (!continueEditing) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
router.refresh(); // Refresh values to update the sidebar
|
||||
} catch (error) {
|
||||
setPopup({ message: "Failed to save folder name", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||
const deleteConfirmRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleDeleteClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await deleteFolder(folder.folder_id);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
setPopup({ message: "Failed to delete folder", type: "error" });
|
||||
} finally {
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
deleteConfirmRef.current &&
|
||||
!deleteConfirmRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (initiallySelected && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [initiallySelected]);
|
||||
|
||||
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const chatSessionId = event.dataTransfer.getData(CHAT_SESSION_ID_KEY);
|
||||
try {
|
||||
await addChatToFolder(folder.folder_id, chatSessionId);
|
||||
await refreshChatSessions();
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
setPopup({
|
||||
message: "Failed to add chat session to folder",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const folders = folder.chat_sessions.sort((a, b) => {
|
||||
return a.time_created.localeCompare(b.time_created);
|
||||
});
|
||||
|
||||
// Determine whether to show the trash can icon
|
||||
const showTrashIcon = (isHovering && !isEditing) || showDeleteConfirm;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={folder.folder_id}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
className={`transition duration-300 ease-in-out rounded-md ${
|
||||
isDragOver ? "bg-hover" : ""
|
||||
}`}
|
||||
>
|
||||
<BasicSelectable fullWidth selected={false}>
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<div onClick={toggleFolderExpansion} className="cursor-pointer">
|
||||
<div className="text-sm text-text-600 flex items-center justify-start w-full">
|
||||
<div className="mr-2">
|
||||
{isExpanded ? (
|
||||
<FiChevronDown size={16} />
|
||||
) : (
|
||||
<FiChevronRight size={16} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<FiFolder size={16} className="mr-2" />
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={editedFolderName}
|
||||
onChange={handleFolderNameChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => saveFolderName(true)}
|
||||
className="text-sm px-1 flex-1 min-w-0 -my-px mr-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 break-all min-w-0">
|
||||
{editedFolderName || folder.folder_name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex ml-auto my-auto">
|
||||
<div
|
||||
onClick={handleEditFolderName}
|
||||
className={`hover:bg-black/10 p-1 -m-1 rounded ${
|
||||
isHovering && !isEditing
|
||||
? ""
|
||||
: "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<FiEdit2 size={16} />
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Popover
|
||||
open={showDeleteConfirm}
|
||||
onOpenChange={setShowDeleteConfirm}
|
||||
content={
|
||||
<div
|
||||
onClick={handleDeleteClick}
|
||||
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2 ${
|
||||
showTrashIcon ? "" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<FiTrash size={16} />
|
||||
</div>
|
||||
}
|
||||
popover={
|
||||
<div className="p-2 w-[225px] bg-background-100 rounded shadow-lg">
|
||||
<p className="text-sm mb-2">
|
||||
Are you sure you want to delete folder{" "}
|
||||
<i>{folder.folder_name}</i>?
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={confirmDelete}
|
||||
className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs mr-2"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelDelete}
|
||||
className="bg-gray-300 hover:bg-gray-200 px-2 py-1 rounded text-xs"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
side="top"
|
||||
align="center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="flex ml-auto my-auto">
|
||||
<div
|
||||
onClick={() => saveFolderName()}
|
||||
className="hover:bg-black/10 p-1 -m-1 rounded"
|
||||
>
|
||||
<FiCheck size={16} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsEditing(false)}
|
||||
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
|
||||
>
|
||||
<FiX size={16} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicSelectable>
|
||||
|
||||
{/* Expanded Folder Content */}
|
||||
{isExpanded && folders && (
|
||||
<div className={"ml-2 pl-2 border-l border-border"}>
|
||||
{folders.map((chatSession) => (
|
||||
<ChatSessionDisplay
|
||||
key={chatSession.id}
|
||||
chatSession={chatSession}
|
||||
isSelected={chatSession.id === currentChatId}
|
||||
skipGradient={isDragOver}
|
||||
showShareModal={showShareModal}
|
||||
showDeleteModal={showDeleteModal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FolderList = ({
|
||||
folders,
|
||||
currentChatId,
|
||||
openedFolders,
|
||||
newFolderId,
|
||||
showShareModal,
|
||||
showDeleteModal,
|
||||
}: {
|
||||
folders: Folder[];
|
||||
currentChatId?: string;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
newFolderId: number | null;
|
||||
showShareModal: ((chatSession: ChatSession) => void) | undefined;
|
||||
showDeleteModal: ((chatSession: ChatSession) => void) | undefined;
|
||||
}) => {
|
||||
if (folders.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-1 mb-1 overflow-visible">
|
||||
{folders.map((folder) => (
|
||||
<FolderItem
|
||||
key={folder.folder_id}
|
||||
folder={folder}
|
||||
currentChatId={currentChatId}
|
||||
initiallySelected={newFolderId == folder.folder_id}
|
||||
isInitiallyExpanded={
|
||||
openedFolders ? openedFolders[folder.folder_id] || false : false
|
||||
}
|
||||
showShareModal={showShareModal}
|
||||
showDeleteModal={showDeleteModal}
|
||||
/>
|
||||
))}
|
||||
{folders.length == 1 && folders[0].chat_sessions.length == 0 && (
|
||||
<p className="text-sm font-normal text-subtle mt-2">
|
||||
{" "}
|
||||
Drag a chat into a folder to save for later{" "}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
// Function to create a new folder
|
||||
export async function createFolder(folderName: string): Promise<number> {
|
||||
const response = await fetch("/api/folder", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ folder_name: folderName }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create folder");
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
// Function to add a chat session to a folder
|
||||
export async function addChatToFolder(
|
||||
folderId: number,
|
||||
chatSessionId: string
|
||||
): Promise<void> {
|
||||
const response = await fetch(`/api/folder/${folderId}/add-chat-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ chat_session_id: chatSessionId }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to add chat to folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to remove a chat session from a folder
|
||||
export async function removeChatFromFolder(
|
||||
folderId: number,
|
||||
chatSessionId: string
|
||||
): Promise<void> {
|
||||
const response = await fetch(`/api/folder/${folderId}/remove-chat-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ chat_session_id: chatSessionId }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to remove chat from folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to delete a folder
|
||||
export async function deleteFolder(folderId: number): Promise<void> {
|
||||
const response = await fetch(`/api/folder/${folderId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update a folder's name
|
||||
export async function updateFolderName(
|
||||
folderId: number,
|
||||
newName: string
|
||||
): Promise<void> {
|
||||
const response = await fetch(`/api/folder/${folderId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ folder_name: newName }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update folder name");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ChatSession } from "../interfaces";
|
||||
|
||||
export interface Folder {
|
||||
folder_id: number;
|
||||
folder_name: string;
|
||||
display_priority: number;
|
||||
chat_sessions: ChatSession[];
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi";
|
||||
import {
|
||||
FiPlusCircle,
|
||||
FiPlus,
|
||||
FiInfo,
|
||||
FiX,
|
||||
FiSearch,
|
||||
FiFolder,
|
||||
} from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
|
||||
@@ -30,12 +37,21 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { ChatState } from "../types";
|
||||
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { XIcon } from "lucide-react";
|
||||
import AnimatedToggle from "@/components/search/SearchBar";
|
||||
import { Popup } from "@/components/admin/connectors/Popup";
|
||||
import { AssistantsTab } from "../modal/configuration/AssistantsTab";
|
||||
import { IconType } from "react-icons";
|
||||
import { LlmTab } from "../modal/configuration/LlmTab";
|
||||
import { FolderIcon, XIcon } from "lucide-react";
|
||||
import FiltersDisplay from "./FilterDisplay";
|
||||
import { UserFolder, UserFile } from "@/app/my-documents/FilePicker";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
interface ChatInputBarProps {
|
||||
removeFilters: () => void;
|
||||
|
||||
removeDocs: () => void;
|
||||
openModelSettings: () => void;
|
||||
showDocs: () => void;
|
||||
@@ -46,6 +62,8 @@ interface ChatInputBarProps {
|
||||
stopGenerating: () => void;
|
||||
onSubmit: () => void;
|
||||
filterManager: FilterManager;
|
||||
folders: UserFolder[];
|
||||
removeFolder: (folderId: number) => void;
|
||||
chatState: ChatState;
|
||||
alternativeAssistant: Persona | null;
|
||||
// assistants
|
||||
@@ -57,12 +75,20 @@ interface ChatInputBarProps {
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
toggleFilters?: () => void;
|
||||
toggleMyDocuments: () => void;
|
||||
userFiles: UserFile[];
|
||||
removeUserFile: (fileId: number) => void;
|
||||
}
|
||||
|
||||
export function ChatInputBar({
|
||||
removeUserFile,
|
||||
removeFilters,
|
||||
removeDocs,
|
||||
removeFolder,
|
||||
folders,
|
||||
openModelSettings,
|
||||
showDocs,
|
||||
userFiles,
|
||||
showConfigureAPIKey,
|
||||
selectedDocuments,
|
||||
message,
|
||||
@@ -82,7 +108,9 @@ export function ChatInputBar({
|
||||
textAreaRef,
|
||||
alternativeAssistant,
|
||||
toggleFilters,
|
||||
toggleMyDocuments,
|
||||
}: ChatInputBarProps) {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (textarea) {
|
||||
@@ -322,9 +350,54 @@ export function ChatInputBar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(selectedDocuments.length > 0 || files.length > 0) && (
|
||||
{(selectedDocuments.length > 0 ||
|
||||
files.length > 0 ||
|
||||
folders.length > 0 ||
|
||||
userFiles.length > 0) && (
|
||||
<div className="flex gap-x-2 px-2 pt-2">
|
||||
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
||||
{userFiles.map((file) => (
|
||||
<div className="flex-none" key={file.id}>
|
||||
<button className="flex-none relative overflow-visible flex items-center gap-x-2 h-10 px-3 rounded-lg bg-background-150 hover:bg-background-200 transition-colors duration-300 cursor-pointer max-w-[150px]">
|
||||
<FileIcon size={20} />
|
||||
<span className="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{file.name}
|
||||
</span>
|
||||
<XIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeUserFile(file.id);
|
||||
}}
|
||||
size={16}
|
||||
className="flex-none text-text-400 hover:text-text-600 ml-auto"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{folders.map((folder) => (
|
||||
<button
|
||||
key={folder.id}
|
||||
onClick={() =>
|
||||
router.push(`/my-documents?path=${folder.id}`)
|
||||
}
|
||||
className="flex-none relative overflow-visible flex items-center gap-x-2 h-10 px-3 rounded-lg bg-background-150 hover:bg-background-200 transition-colors duration-300 cursor-pointer max-w-[150px]"
|
||||
>
|
||||
<FolderIcon size={20} />
|
||||
<span className="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{folder.name}
|
||||
</span>
|
||||
<XIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFolder(folder.id);
|
||||
// Remove this folder
|
||||
}}
|
||||
size={16}
|
||||
className="text-text-400 hover:text-text-600 ml-auto"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{selectedDocuments.length > 0 && (
|
||||
<button
|
||||
onClick={showDocs}
|
||||
@@ -445,6 +518,14 @@ export function ChatInputBar({
|
||||
input.click();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ChatInputOption
|
||||
flexPriority="stiff"
|
||||
name="Documents"
|
||||
Icon={FiFolder}
|
||||
onClick={toggleMyDocuments}
|
||||
/>
|
||||
|
||||
{toggleFilters && (
|
||||
<ChatInputOption
|
||||
flexPriority="stiff"
|
||||
|
||||
@@ -26,9 +26,7 @@ export default async function Page(props: {
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
toggleSidebar,
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
ccPairs,
|
||||
@@ -50,8 +48,6 @@ export default async function Page(props: {
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { FiEdit, FiFolderPlus } from "react-icons/fi";
|
||||
import { FiBarChart, FiBook, FiEdit } from "react-icons/fi";
|
||||
import React, { ForwardedRef, forwardRef, useContext, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
|
||||
import { Folder } from "../folders/interfaces";
|
||||
import { createFolder } from "../folders/FolderManagement";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
@@ -20,18 +18,14 @@ interface HistorySidebarProps {
|
||||
page: pageType;
|
||||
existingChats?: ChatSession[];
|
||||
currentChatSession?: ChatSession | null | undefined;
|
||||
folders?: Folder[];
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
toggleSidebar?: () => void;
|
||||
toggled?: boolean;
|
||||
removeToggle?: () => void;
|
||||
reset?: () => void;
|
||||
showShareModal?: (chatSession: ChatSession) => void;
|
||||
showDeleteModal?: (chatSession: ChatSession) => void;
|
||||
stopGenerating?: () => void;
|
||||
explicitlyUntoggle: () => void;
|
||||
showDeleteAllModal?: () => void;
|
||||
backgroundToggled?: boolean;
|
||||
}
|
||||
|
||||
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
@@ -42,24 +36,16 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
page,
|
||||
existingChats,
|
||||
currentChatSession,
|
||||
folders,
|
||||
openedFolders,
|
||||
explicitlyUntoggle,
|
||||
toggleSidebar,
|
||||
removeToggle,
|
||||
stopGenerating = () => null,
|
||||
showShareModal,
|
||||
showDeleteModal,
|
||||
showDeleteAllModal,
|
||||
backgroundToggled,
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const router = useRouter();
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
// For determining intial focus state
|
||||
const [newFolderId, setNewFolderId] = useState<number | null>(null);
|
||||
|
||||
const currentChatId = currentChatSession?.id;
|
||||
|
||||
@@ -86,7 +72,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<div
|
||||
ref={ref}
|
||||
className={`
|
||||
@@ -135,26 +120,23 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
<FiEdit className="flex-none text-text-history-sidebar-button" />
|
||||
<p className="my-auto flex items-center text-sm">New Chat</p>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() =>
|
||||
createFolder("New Folder")
|
||||
.then((folderId) => {
|
||||
router.refresh();
|
||||
setNewFolderId(folderId);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to create folder:", error);
|
||||
setPopup({
|
||||
message: `Failed to create folder: ${error.message}`,
|
||||
type: "error",
|
||||
});
|
||||
})
|
||||
}
|
||||
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-history-sidebar-button-hover cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
<Link
|
||||
className=" w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
href={`/my-documents`}
|
||||
onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
if (handleNewChat) {
|
||||
handleNewChat();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiFolderPlus className="my-auto text-text-history-sidebar-button" />
|
||||
<p className="my-auto flex items-center text-sm">New Folder</p>
|
||||
</button>
|
||||
<FiBook className="flex-none text-text-history-sidebar-button" />
|
||||
<p className="my-auto flex items-center text-sm">
|
||||
My Documents
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/assistants/mine"
|
||||
@@ -169,16 +151,12 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
)}
|
||||
<div className="border-b border-divider-history-sidebar-bar pb-4 mx-3" />
|
||||
<PagesTab
|
||||
newFolderId={newFolderId}
|
||||
showDeleteModal={showDeleteModal}
|
||||
showShareModal={showShareModal}
|
||||
closeSidebar={removeToggle}
|
||||
page={page}
|
||||
existingChats={existingChats}
|
||||
currentChatId={currentChatId}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
showDeleteAllModal={showDeleteAllModal}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { groupSessionsByDateRange } from "../lib";
|
||||
import { ChatSessionDisplay } from "./ChatSessionDisplay";
|
||||
import { removeChatFromFolder } from "../folders/FolderManagement";
|
||||
import { FolderList } from "../folders/FolderList";
|
||||
import { Folder } from "../folders/interfaces";
|
||||
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
@@ -16,10 +12,7 @@ export function PagesTab({
|
||||
page,
|
||||
existingChats,
|
||||
currentChatId,
|
||||
folders,
|
||||
openedFolders,
|
||||
closeSidebar,
|
||||
newFolderId,
|
||||
showShareModal,
|
||||
showDeleteModal,
|
||||
showDeleteAllModal,
|
||||
@@ -27,10 +20,7 @@ export function PagesTab({
|
||||
page: pageType;
|
||||
existingChats?: ChatSession[];
|
||||
currentChatId?: string;
|
||||
folders?: Folder[];
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
closeSidebar?: () => void;
|
||||
newFolderId: number | null;
|
||||
showShareModal?: (chatSession: ChatSession) => void;
|
||||
showDeleteModal?: (chatSession: ChatSession) => void;
|
||||
showDeleteAllModal?: () => void;
|
||||
@@ -43,27 +33,6 @@ export function PagesTab({
|
||||
const router = useRouter();
|
||||
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||||
|
||||
const handleDropToRemoveFromFolder = async (
|
||||
event: React.DragEvent<HTMLDivElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false); // Reset drag over state on drop
|
||||
const chatSessionId = event.dataTransfer.getData(CHAT_SESSION_ID_KEY);
|
||||
const folderId = event.dataTransfer.getData(FOLDER_ID_KEY);
|
||||
|
||||
if (folderId) {
|
||||
try {
|
||||
await removeChatFromFolder(parseInt(folderId, 10), chatSessionId);
|
||||
router.refresh(); // Refresh the page to reflect the changes
|
||||
} catch (error) {
|
||||
setPopup({
|
||||
message: "Failed to remove chat from folder",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isHistoryEmpty = !existingChats || existingChats.length === 0;
|
||||
|
||||
return (
|
||||
@@ -73,29 +42,12 @@ export function PagesTab({
|
||||
NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && "pb-20 "
|
||||
}`}
|
||||
>
|
||||
{folders && folders.length > 0 && (
|
||||
<div className="py-2 border-b border-border">
|
||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
|
||||
Chat Folders
|
||||
</div>
|
||||
<FolderList
|
||||
newFolderId={newFolderId}
|
||||
folders={folders}
|
||||
currentChatId={currentChatId}
|
||||
openedFolders={openedFolders}
|
||||
showShareModal={showShareModal}
|
||||
showDeleteModal={showDeleteModal}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={handleDropToRemoveFromFolder}
|
||||
className={`pt-1 transition duration-300 ease-in-out mr-3 ${
|
||||
isDragOver ? "bg-hover" : ""
|
||||
} rounded-md`}
|
||||
|
||||
361
web/src/app/my-documents/FilePicker.tsx
Normal file
361
web/src/app/my-documents/FilePicker.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import {
|
||||
Folder as FolderIcon,
|
||||
File as FileIcon,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface UserFolder {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_id: number | null;
|
||||
}
|
||||
|
||||
export interface UserFile {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_folder_id: number | null;
|
||||
}
|
||||
|
||||
interface FolderNode extends UserFolder {
|
||||
children: FolderNode[];
|
||||
files: UserFile[];
|
||||
}
|
||||
|
||||
interface FilePickerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
allFolders: UserFolder[];
|
||||
setSelectedFolders: (folders: UserFolder[]) => void;
|
||||
onSave: (selectedItems: { files: number[]; folders: number[] }) => void;
|
||||
allFiles: UserFile[];
|
||||
setUserFiles: (files: UserFile[]) => void;
|
||||
selectedFolders: UserFolder[];
|
||||
userFiles: UserFile[];
|
||||
}
|
||||
|
||||
function buildTree(folders: UserFolder[], files: UserFile[]): FolderNode[] {
|
||||
const folderMap: { [key: number]: FolderNode } = {};
|
||||
folders.forEach((folder) => {
|
||||
folderMap[folder.id] = { ...folder, children: [], files: [] };
|
||||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.parent_folder_id !== null && folderMap[file.parent_folder_id]) {
|
||||
folderMap[file.parent_folder_id].files.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
const roots: FolderNode[] = [];
|
||||
|
||||
Object.values(folderMap).forEach((folder) => {
|
||||
if (folder.parent_id === null) {
|
||||
roots.push(folder);
|
||||
} else if (folderMap[folder.parent_id]) {
|
||||
folderMap[folder.parent_id].children.push(folder);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
const FolderTreeItem: React.FC<{
|
||||
node: FolderNode;
|
||||
selectedItems: { files: number[]; folders: number[] };
|
||||
setSelectedItems: React.Dispatch<
|
||||
React.SetStateAction<{ files: number[]; folders: number[] }>
|
||||
>;
|
||||
parentNode?: FolderNode;
|
||||
}> = ({ node, selectedItems, setSelectedItems, parentNode }) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const toggleFolder = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const isFolderSelected = selectedItems.folders.includes(node.id);
|
||||
|
||||
const getAllDescendantIds = (
|
||||
folder: FolderNode
|
||||
): { folderIds: number[]; fileIds: number[] } => {
|
||||
let folderIds: number[] = [];
|
||||
let fileIds: number[] = [];
|
||||
|
||||
const traverse = (node: FolderNode) => {
|
||||
folderIds.push(node.id);
|
||||
fileIds.push(...node.files.map((file) => file.id));
|
||||
node.children.forEach(traverse);
|
||||
};
|
||||
|
||||
traverse(folder);
|
||||
return { folderIds, fileIds };
|
||||
};
|
||||
|
||||
const shouldFolderBeSelected = (
|
||||
folder: FolderNode,
|
||||
newlySelectedItem: number,
|
||||
newlySelectedItemType: "file" | "folder"
|
||||
): boolean => {
|
||||
const allFilesSelected = folder.files.every((file) => {
|
||||
const isSelected =
|
||||
selectedItems.files.includes(file.id) ||
|
||||
(newlySelectedItemType === "file" && newlySelectedItem === file.id);
|
||||
return isSelected;
|
||||
});
|
||||
|
||||
const allChildrenSelected = folder.children.every((child) => {
|
||||
const isSelected =
|
||||
selectedItems.folders.includes(child.id) ||
|
||||
(newlySelectedItemType === "folder" && newlySelectedItem === child.id);
|
||||
return isSelected;
|
||||
});
|
||||
|
||||
const shouldBeSelected = allFilesSelected && allChildrenSelected;
|
||||
return shouldBeSelected;
|
||||
};
|
||||
|
||||
const updateParentFolderSelection = (currentNode: FolderNode) => {
|
||||
if (parentNode) {
|
||||
// const shouldSelect = shouldFolderBeSelected(currentNode);
|
||||
// setSelectedItems((prev) => {
|
||||
// const newFolders = shouldSelect
|
||||
// ? Array.from(new Set([...prev.folders, currentNode.id]))
|
||||
// : prev.folders.filter((id) => id !== currentNode.id);
|
||||
// return { ...prev, folders: newFolders };
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderSelect = () => {
|
||||
setSelectedItems((prev) => {
|
||||
const { folderIds, fileIds } = getAllDescendantIds(node);
|
||||
|
||||
if (isFolderSelected) {
|
||||
const newState = {
|
||||
folders: prev.folders.filter((id) => !folderIds.includes(id)),
|
||||
files: prev.files.filter((id) => !fileIds.includes(id)),
|
||||
};
|
||||
setTimeout(() => updateParentFolderSelection(node), 0);
|
||||
return newState;
|
||||
} else {
|
||||
const newState = {
|
||||
folders: Array.from(new Set([...prev.folders, ...folderIds])),
|
||||
files: Array.from(new Set([...prev.files, ...fileIds])),
|
||||
};
|
||||
setTimeout(() => updateParentFolderSelection(node), 0);
|
||||
return newState;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileSelect = (fileId: number) => {
|
||||
setSelectedItems((prev) => {
|
||||
const newFiles = prev.files.includes(fileId)
|
||||
? prev.files.filter((id) => id !== fileId)
|
||||
: [...prev.files, fileId];
|
||||
|
||||
const newState = { ...prev, files: newFiles };
|
||||
setTimeout(() => {
|
||||
const shouldSelect = shouldFolderBeSelected(node, fileId, "file");
|
||||
|
||||
setSelectedItems((prevState) => {
|
||||
const newFolders = shouldSelect
|
||||
? Array.from(new Set([...prevState.folders, node.id]))
|
||||
: prevState.folders.filter((id) => id !== node.id);
|
||||
updateParentFolderSelection(node);
|
||||
return { ...prevState, folders: newFolders };
|
||||
});
|
||||
}, 100);
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="my-1">
|
||||
<div className="flex items-center">
|
||||
{node.children.length > 0 || node.files.length > 0 ? (
|
||||
<button onClick={toggleFolder} className="mr-1">
|
||||
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
) : (
|
||||
<div className="mr-4" />
|
||||
)}
|
||||
<Checkbox
|
||||
checked={isFolderSelected}
|
||||
onCheckedChange={handleFolderSelect}
|
||||
/>
|
||||
<FolderIcon className="ml-2 mr-1 h-5 w-5 text-text-600" />
|
||||
<span className="ml-1">
|
||||
{node.name}
|
||||
{node.id}
|
||||
</span>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ul className="ml-6">
|
||||
{node.children.map((child) => (
|
||||
<FolderTreeItem
|
||||
key={child.id}
|
||||
node={child}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
parentNode={node}
|
||||
/>
|
||||
))}
|
||||
{node.files.map((file) => (
|
||||
<li key={file.id} className="my-1">
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={selectedItems.files.includes(file.id)}
|
||||
onCheckedChange={() => {
|
||||
handleFileSelect(file.id);
|
||||
}}
|
||||
/>
|
||||
<FileIcon className="ml-2 mr-1 h-5 w-5 text-text-600" />
|
||||
<span className="ml-1">
|
||||
{file.name}
|
||||
{file.id}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilePicker: React.FC<FilePickerProps> = ({
|
||||
isOpen,
|
||||
setSelectedFolders,
|
||||
setUserFiles,
|
||||
allFolders,
|
||||
allFiles,
|
||||
selectedFolders,
|
||||
userFiles,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [fileSystem, setFileSystem] = useState<FolderNode[]>([]);
|
||||
const [selectedItems, setSelectedItems] = useState<{
|
||||
files: number[];
|
||||
folders: number[];
|
||||
}>({
|
||||
files: userFiles.map((file) => file.id),
|
||||
folders: selectedFolders.map((folder) => folder.id),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const loadFileSystem = async () => {
|
||||
const response = await fetch("/api/user/file-system");
|
||||
const data = await response.json();
|
||||
const tree = buildTree(data.folders, data.files);
|
||||
setFileSystem(tree);
|
||||
};
|
||||
loadFileSystem();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const getAllDescendantIds = (
|
||||
folder: FolderNode
|
||||
): { folderIds: number[]; fileIds: number[] } => {
|
||||
let folderIds: number[] = [];
|
||||
let fileIds: number[] = [];
|
||||
|
||||
const traverse = (node: FolderNode) => {
|
||||
folderIds.push(node.id);
|
||||
fileIds.push(...node.files.map((file) => file.id));
|
||||
node.children.forEach(traverse);
|
||||
};
|
||||
|
||||
traverse(folder);
|
||||
return { folderIds, fileIds };
|
||||
};
|
||||
|
||||
const isFullySelected = (node: FolderNode): boolean => {
|
||||
const allDescendants = getAllDescendantIds(node);
|
||||
return (
|
||||
allDescendants.folderIds.every((id: number) =>
|
||||
selectedItems.folders.includes(id)
|
||||
) &&
|
||||
allDescendants.fileIds.every((id: number) =>
|
||||
selectedItems.files.includes(id)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getOptimizedSelection = (
|
||||
nodes: FolderNode[]
|
||||
): { folders: number[]; files: number[] } => {
|
||||
let optimizedFolders: number[] = [];
|
||||
let optimizedFiles: number[] = [];
|
||||
|
||||
const processNode = (node: FolderNode) => {
|
||||
if (isFullySelected(node)) {
|
||||
optimizedFolders.push(node.id);
|
||||
} else {
|
||||
node.children.forEach(processNode);
|
||||
node.files.forEach((file) => {
|
||||
if (selectedItems.files.includes(file.id)) {
|
||||
optimizedFiles.push(file.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
nodes.forEach(processNode);
|
||||
return { folders: optimizedFolders, files: optimizedFiles };
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const optimizedSelection = getOptimizedSelection(fileSystem);
|
||||
|
||||
setSelectedFolders(
|
||||
allFolders.filter((folder) =>
|
||||
optimizedSelection.folders.includes(folder.id)
|
||||
)
|
||||
);
|
||||
|
||||
const selectedFiles = optimizedSelection.files
|
||||
.map((fileId) => allFiles.find((file) => file.id === fileId))
|
||||
.filter((file): file is UserFile => file !== undefined);
|
||||
setUserFiles(selectedFiles);
|
||||
onSave(optimizedSelection);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onOutsideClick={onClose}
|
||||
className="max-w-xl"
|
||||
title="Select Files and Folders"
|
||||
>
|
||||
<div className="p-4 w-full mx-auto">
|
||||
<div className="max-h-96 overflow-y-auto border rounded p-2">
|
||||
<ul className="list-none">
|
||||
{fileSystem.map((node) => (
|
||||
<FolderTreeItem
|
||||
key={node.id}
|
||||
node={node}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button onClick={onClose} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} variant="default">
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
108
web/src/app/my-documents/FolderActions.tsx
Normal file
108
web/src/app/my-documents/FolderActions.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Plus, Upload, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
interface FolderActionsProps {
|
||||
onRefresh: () => void;
|
||||
onCreateFolder: (folderName: string) => void;
|
||||
onUploadFiles: (files: FileList) => void;
|
||||
}
|
||||
|
||||
export function FolderActions({
|
||||
onRefresh,
|
||||
onCreateFolder,
|
||||
onUploadFiles,
|
||||
}: FolderActionsProps) {
|
||||
const [newFolderName, setNewFolderName] = useState("");
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||
|
||||
const handleCreateFolder = () => {
|
||||
if (newFolderName.trim()) {
|
||||
onCreateFolder(newFolderName.trim());
|
||||
setNewFolderName("");
|
||||
setIsCreatingFolder(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files) {
|
||||
onUploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-background-300 hover:bg-background-100"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 text-text-600" />
|
||||
</Button>
|
||||
<Popover open={isCreatingFolder} onOpenChange={setIsCreatingFolder}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-background-300 hover:bg-background-100"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-text-600" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{isCreatingFolder && (
|
||||
<PopoverContent className="w-56 p-3 bg-white shadow-md rounded-md">
|
||||
<div className="space-y-2 flex flex-col">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="New folder name"
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
className="!w-full p-1 flex text-sm border border-background-300 focus:border-background-500 rounded"
|
||||
/>
|
||||
<div className="flex justify-between space-x-2">
|
||||
<Button
|
||||
onClick={handleCreateFolder}
|
||||
size="sm"
|
||||
className="bg-background-800 hover:bg-background-900 text-white text-xs"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsCreatingFolder(false)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border border-background-300 hover:bg-background-100 text-xs"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-background-300 hover:bg-background-100"
|
||||
onClick={() => document.getElementById("file-upload")?.click()}
|
||||
>
|
||||
<Upload className="h-4 w-4 text-text-600" />
|
||||
</Button>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
web/src/app/my-documents/FolderBreadcrumb.tsx
Normal file
42
web/src/app/my-documents/FolderBreadcrumb.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
interface FolderBreadcrumbProps {
|
||||
parents: { name: string; id: number }[];
|
||||
currentFolder: { name: string; id: number };
|
||||
onBreadcrumbClick: (id: number) => void;
|
||||
}
|
||||
|
||||
export function FolderBreadcrumb({
|
||||
parents,
|
||||
onBreadcrumbClick,
|
||||
currentFolder,
|
||||
}: FolderBreadcrumbProps) {
|
||||
return (
|
||||
<div className="flex items-center space-x-2 text-sm text-text-500 mb-4">
|
||||
<span
|
||||
className="cursor-pointer hover:text-text-700"
|
||||
onClick={() => onBreadcrumbClick(-1)}
|
||||
>
|
||||
Root
|
||||
</span>
|
||||
{parents.map((parent, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<span
|
||||
className="cursor-pointer hover:text-text-700"
|
||||
onClick={() => onBreadcrumbClick(parent.id)}
|
||||
>
|
||||
{parent.name}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{currentFolder && currentFolder.id !== -1 && (
|
||||
<>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<span className="text-text-700">{currentFolder.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
143
web/src/app/my-documents/FolderContents.tsx
Normal file
143
web/src/app/my-documents/FolderContents.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { useState } from "react";
|
||||
import { MoveFileModal } from "./MoveFileModal";
|
||||
import { FileItem, FolderItem } from "./MyDocumenItem";
|
||||
|
||||
interface FolderContentsProps {
|
||||
pageLimit: number;
|
||||
currentPage: number;
|
||||
contents: {
|
||||
children: { name: string; id: number }[];
|
||||
files: { name: string; id: number; document_id: string }[];
|
||||
};
|
||||
onFolderClick: (folderId: number) => void;
|
||||
currentFolder: number;
|
||||
onDeleteItem: (itemId: number, isFolder: boolean) => void;
|
||||
onDownloadItem: (documentId: string) => void;
|
||||
onMoveItem: (
|
||||
itemId: number,
|
||||
destinationFolderId: number | null,
|
||||
isFolder: boolean
|
||||
) => void;
|
||||
setPresentingDocument: (
|
||||
document_id: string,
|
||||
semantic_identifier: string
|
||||
) => void;
|
||||
onRenameItem: (itemId: number, newName: string, isFolder: boolean) => void;
|
||||
}
|
||||
|
||||
export function FolderContents({
|
||||
pageLimit,
|
||||
currentPage,
|
||||
setPresentingDocument,
|
||||
contents,
|
||||
onFolderClick,
|
||||
currentFolder,
|
||||
onDeleteItem,
|
||||
onDownloadItem,
|
||||
onMoveItem,
|
||||
onRenameItem,
|
||||
}: FolderContentsProps) {
|
||||
const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
|
||||
const [itemToMove, setItemToMove] = useState<{
|
||||
id: number;
|
||||
name: string;
|
||||
isFolder: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const [editingItem, setEditingItem] = useState<{
|
||||
id: number;
|
||||
name: string;
|
||||
isFolder: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const handleMove = (destinationFolderId: number | null) => {
|
||||
if (itemToMove) {
|
||||
onMoveItem(itemToMove.id, destinationFolderId, itemToMove.isFolder);
|
||||
setIsMoveModalOpen(false);
|
||||
setItemToMove(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = (itemId: number, newName: string, isFolder: boolean) => {
|
||||
onRenameItem(itemId, newName, isFolder);
|
||||
setEditingItem(null);
|
||||
};
|
||||
|
||||
const handleDragStart = (
|
||||
e: React.DragEvent<HTMLDivElement>,
|
||||
item: { id: number; isFolder: boolean; name: string }
|
||||
) => {
|
||||
e.dataTransfer.setData("application/json", JSON.stringify(item));
|
||||
};
|
||||
|
||||
const handleDrop = (
|
||||
e: React.DragEvent<HTMLDivElement>,
|
||||
targetFolderId: number
|
||||
) => {
|
||||
e.preventDefault();
|
||||
const item = JSON.parse(e.dataTransfer.getData("application/json"));
|
||||
if (item && typeof item.id === "number") {
|
||||
// Move the dragged item to the target folder
|
||||
onMoveItem(item.id, targetFolderId, item.isFolder);
|
||||
}
|
||||
};
|
||||
|
||||
// we need the logic to ben let's show all the files firs then folders (ie if we have 4 files and 10 foldres and page size of 3,
|
||||
|
||||
// First index: first 3 files,
|
||||
// nexte index; lat fileand first two folders, etc.
|
||||
|
||||
return (
|
||||
<div className="flex-grow" onDragOver={(e) => e.preventDefault()}>
|
||||
{contents.files
|
||||
.slice(pageLimit * (currentPage - 1), pageLimit * currentPage)
|
||||
.map((file) => (
|
||||
<FileItem
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
key={file.id}
|
||||
file={file}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onDownloadItem={onDownloadItem}
|
||||
onMoveItem={(id) => {
|
||||
setItemToMove({ id, name: file.name, isFolder: false });
|
||||
setIsMoveModalOpen(true);
|
||||
}}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
handleRename={handleRename}
|
||||
onDragStart={handleDragStart}
|
||||
/>
|
||||
))}
|
||||
|
||||
{contents.children
|
||||
.slice(pageLimit * (currentPage - 1), pageLimit * currentPage)
|
||||
.map((folder) => (
|
||||
<FolderItem
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
onFolderClick={onFolderClick}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onMoveItem={(id) => {
|
||||
setItemToMove({ id, name: folder.name, isFolder: true });
|
||||
setIsMoveModalOpen(true);
|
||||
}}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
handleRename={handleRename}
|
||||
onDragStart={handleDragStart}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
))}
|
||||
|
||||
{itemToMove && (
|
||||
<MoveFileModal
|
||||
isOpen={isMoveModalOpen}
|
||||
onClose={() => setIsMoveModalOpen(false)}
|
||||
onMove={handleMove}
|
||||
fileName={itemToMove.name}
|
||||
currentFolderId={currentFolder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
web/src/app/my-documents/FolderTree.tsx
Normal file
47
web/src/app/my-documents/FolderTree.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { Folder as FolderIcon } from "lucide-react";
|
||||
|
||||
interface FolderNode {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_id: number | null;
|
||||
children?: FolderNode[];
|
||||
}
|
||||
|
||||
interface FolderTreeProps {
|
||||
treeData: FolderNode[];
|
||||
onFolderClick: (folderId: number) => void;
|
||||
}
|
||||
|
||||
function renderTree(
|
||||
nodes: FolderNode[],
|
||||
onFolderClick: (folderId: number) => void
|
||||
) {
|
||||
return (
|
||||
<ul className="ml-4 list-none">
|
||||
{nodes.map((node) => (
|
||||
<li key={node.id} className="my-1">
|
||||
<div
|
||||
className="flex items-center cursor-pointer hover:text-text-700"
|
||||
onClick={() => onFolderClick(node.id)}
|
||||
>
|
||||
<FolderIcon className="mr-1 h-4 w-4 text-text-600" />
|
||||
<span>{node.name}</span>
|
||||
</div>
|
||||
{node.children &&
|
||||
node.children.length > 0 &&
|
||||
renderTree(node.children, onFolderClick)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function FolderTree({ treeData, onFolderClick }: FolderTreeProps) {
|
||||
return (
|
||||
<div className="w-64 border-r border-background-300 p-2 overflow-y-auto hidden lg:block">
|
||||
<h2 className="font-bold text-sm mb-2">Folders</h2>
|
||||
{renderTree(treeData, onFolderClick)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
web/src/app/my-documents/MoveFileModal.tsx
Normal file
113
web/src/app/my-documents/MoveFileModal.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Folder } from "lucide-react";
|
||||
|
||||
interface Folder {
|
||||
id: number | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface MoveFileModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onMove: (destinationFolderId: number | null) => void;
|
||||
fileName: string;
|
||||
currentFolderId: number | null;
|
||||
}
|
||||
|
||||
export function MoveFileModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onMove,
|
||||
fileName,
|
||||
currentFolderId,
|
||||
}: MoveFileModalProps) {
|
||||
const [folders, setFolders] = useState<Folder[]>([]);
|
||||
const [selectedFolder, setSelectedFolder] = useState<Folder | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const loadFolders = async () => {
|
||||
const res = await fetch("/api/user/folder");
|
||||
const data = await res.json();
|
||||
setFolders(data);
|
||||
};
|
||||
loadFolders();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-96">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Move "{fileName}"
|
||||
</h2>
|
||||
<div className="mb-4">
|
||||
<span className="font-medium">Choose a folder:</span>
|
||||
<div className="max-h-60 overflow-y-auto mt-2 border rounded">
|
||||
{folders.map((folder) => (
|
||||
<div
|
||||
key={folder.id}
|
||||
className="flex items-center justify-between py-2 px-3 hover:bg-background-100 cursor-pointer"
|
||||
onClick={() => setSelectedFolder(folder)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Folder className="mr-2 h-5 w-5" />
|
||||
<span>{folder.name}</span>
|
||||
{folder.id === currentFolderId && (
|
||||
<span className="text-sm my-auto ml-2 text-text-500">
|
||||
(Current folder)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border ${
|
||||
selectedFolder?.id === folder.id
|
||||
? "bg-blue-600 border-blue-600"
|
||||
: "border-blue-300 border-2"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-3 hover:bg-background-100 cursor-pointer"
|
||||
onClick={() => setSelectedFolder({ id: null, name: "Root" })}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Folder className="mr-2 h-5 w-5" />
|
||||
<span>Root</span>
|
||||
</div>
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border ${
|
||||
selectedFolder?.id === null
|
||||
? "bg-blue-600 border-blue-600"
|
||||
: "border-blue-300 border-2"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
className="px-4 py-2 cursor-pointer text-text-600 hover:bg-background-100 rounded"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 text-white rounded ${
|
||||
selectedFolder
|
||||
? "bg-blue-600 hover:bg-blue-700 cursor-pointer"
|
||||
: "bg-blue-400 cursor-not-allowed"
|
||||
}`}
|
||||
onClick={() => selectedFolder && onMove(selectedFolder.id)}
|
||||
disabled={!selectedFolder}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
343
web/src/app/my-documents/MyDocumenItem.tsx
Normal file
343
web/src/app/my-documents/MyDocumenItem.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
FolderIcon,
|
||||
FileIcon,
|
||||
DownloadIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
InfoIcon,
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
interface FolderItemProps {
|
||||
folder: { name: string; id: number };
|
||||
onFolderClick: (folderId: number) => void;
|
||||
onDeleteItem: (itemId: number, isFolder: boolean) => void;
|
||||
onMoveItem: (folderId: number) => void;
|
||||
editingItem: { id: number; name: string; isFolder: boolean } | null;
|
||||
setEditingItem: React.Dispatch<
|
||||
React.SetStateAction<{ id: number; name: string; isFolder: boolean } | null>
|
||||
>;
|
||||
handleRename: (id: number, newName: string, isFolder: boolean) => void;
|
||||
onDragStart: (
|
||||
e: React.DragEvent<HTMLDivElement>,
|
||||
item: { id: number; isFolder: boolean; name: string }
|
||||
) => void;
|
||||
onDrop: (e: React.DragEvent<HTMLDivElement>, targetFolderId: number) => void;
|
||||
}
|
||||
|
||||
export function FolderItem({
|
||||
folder,
|
||||
onFolderClick,
|
||||
onDeleteItem,
|
||||
onMoveItem,
|
||||
editingItem,
|
||||
setEditingItem,
|
||||
handleRename,
|
||||
onDragStart,
|
||||
onDrop,
|
||||
}: FolderItemProps) {
|
||||
const [showMenu, setShowMenu] = useState<undefined | number>(undefined);
|
||||
const [newName, setNewName] = useState(folder.name);
|
||||
|
||||
const isEditing =
|
||||
editingItem && editingItem.id === folder.id && editingItem.isFolder;
|
||||
|
||||
const folderItemRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
console.log("Context menu clicked");
|
||||
e.preventDefault();
|
||||
const xPos =
|
||||
e.clientX - folderItemRef.current?.getBoundingClientRect().left! - 40;
|
||||
setShowMenu(xPos);
|
||||
};
|
||||
|
||||
const startEditing = () => {
|
||||
setEditingItem({ id: folder.id, name: folder.name, isFolder: true });
|
||||
setNewName(folder.name);
|
||||
setShowMenu(undefined);
|
||||
};
|
||||
|
||||
const submitRename = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleRename(folder.id, newName, true);
|
||||
};
|
||||
|
||||
const cancelEditing = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditingItem(null);
|
||||
setNewName(folder.name);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
setShowMenu(undefined);
|
||||
});
|
||||
return () => {
|
||||
document.removeEventListener("click", () => {});
|
||||
};
|
||||
}, [showMenu]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={folderItemRef}
|
||||
className="flex items-center justify-between p-2 hover:bg-background-100 cursor-pointer relative"
|
||||
onClick={() => !isEditing && onFolderClick(folder.id)}
|
||||
onContextMenu={handleContextMenu}
|
||||
draggable={!isEditing}
|
||||
onDragStart={(e) =>
|
||||
onDragStart(e, { id: folder.id, isFolder: true, name: folder.name })
|
||||
}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => onDrop(e, folder.id)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FolderIcon className="mr-2" />
|
||||
{isEditing ? (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setNewName(e.target.value);
|
||||
}}
|
||||
className="border rounded px-2 py-1 mr-2"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={submitRename}
|
||||
className="text-green-500 hover:text-green-700 mr-2"
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEditing}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span>{folder.name}</span>
|
||||
)}
|
||||
</div>
|
||||
{showMenu && !isEditing && (
|
||||
<div
|
||||
className="absolute bg-white border rounded shadow py-1 right-0 top-full mt-1 z-50"
|
||||
style={{ left: showMenu }}
|
||||
>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEditing();
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItem(folder.id);
|
||||
setShowMenu(undefined);
|
||||
}}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm text-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteItem(folder.id, true);
|
||||
setShowMenu(undefined);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FileItemProps {
|
||||
file: { name: string; id: number; document_id: string };
|
||||
onDeleteItem: (itemId: number, isFolder: boolean) => void;
|
||||
onDownloadItem: (documentId: string) => void;
|
||||
onMoveItem: (fileId: number) => void;
|
||||
editingItem: { id: number; name: string; isFolder: boolean } | null;
|
||||
setEditingItem: React.Dispatch<
|
||||
React.SetStateAction<{ id: number; name: string; isFolder: boolean } | null>
|
||||
>;
|
||||
setPresentingDocument: (
|
||||
document_id: string,
|
||||
semantic_identifier: string
|
||||
) => void;
|
||||
handleRename: (fileId: number, newName: string, isFolder: boolean) => void;
|
||||
onDragStart: (
|
||||
e: React.DragEvent<HTMLDivElement>,
|
||||
item: { id: number; isFolder: boolean; name: string }
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function FileItem({
|
||||
setPresentingDocument,
|
||||
file,
|
||||
onDeleteItem,
|
||||
onDownloadItem,
|
||||
onMoveItem,
|
||||
editingItem,
|
||||
setEditingItem,
|
||||
handleRename,
|
||||
onDragStart,
|
||||
}: FileItemProps) {
|
||||
const [showMenu, setShowMenu] = useState<undefined | number>();
|
||||
const [newFileName, setNewFileName] = useState(file.name);
|
||||
|
||||
const isEditing =
|
||||
editingItem && editingItem.id === file.id && !editingItem.isFolder;
|
||||
|
||||
const fileItemRef = useRef<HTMLDivElement>(null);
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const xPos =
|
||||
e.clientX - fileItemRef.current?.getBoundingClientRect().left! - 40;
|
||||
setShowMenu(xPos);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
if (fileItemRef.current?.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
setShowMenu(undefined);
|
||||
});
|
||||
document.addEventListener("contextmenu", (e) => {
|
||||
if (fileItemRef.current?.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
setShowMenu(undefined);
|
||||
});
|
||||
return () => {
|
||||
document.removeEventListener("click", () => {});
|
||||
document.removeEventListener("contextmenu", () => {});
|
||||
};
|
||||
}, [showMenu]);
|
||||
|
||||
const startEditing = () => {
|
||||
setEditingItem({ id: file.id, name: file.name, isFolder: false });
|
||||
setNewFileName(file.name);
|
||||
setShowMenu(undefined);
|
||||
};
|
||||
|
||||
const submitRename = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleRename(file.id, newFileName, false);
|
||||
};
|
||||
|
||||
const cancelEditing = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditingItem(null);
|
||||
setNewFileName(file.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={fileItemRef}
|
||||
key={file.id}
|
||||
className="flex items-center w-full justify-between p-2 hover:bg-background-100 cursor-pointer relative"
|
||||
onContextMenu={handleContextMenu}
|
||||
draggable={!isEditing}
|
||||
onDragStart={(e) =>
|
||||
onDragStart(e, { id: file.id, isFolder: false, name: file.name })
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => setPresentingDocument(file.document_id, file.name)}
|
||||
className="flex items-center flex-grow"
|
||||
>
|
||||
<FileIcon className="mr-2" />
|
||||
{isEditing ? (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
type="text"
|
||||
value={newFileName}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setNewFileName(e.target.value);
|
||||
}}
|
||||
className="border rounded px-2 py-1 mr-2"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={submitRename}
|
||||
className="text-green-500 hover:text-green-700 mr-2"
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEditing}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="flex text-wrap text-left line-clamp-2">{file.name}</p>
|
||||
)}
|
||||
</button>
|
||||
{showMenu && !isEditing && (
|
||||
<div
|
||||
className="absolute bg-white max-w-40 border rounded shadow py-1 right-0 top-full mt-1 z-50"
|
||||
style={{ left: showMenu }}
|
||||
>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownloadItem(file.document_id);
|
||||
setShowMenu(undefined);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEditing();
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItem(file.id);
|
||||
setShowMenu(undefined);
|
||||
}}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 hover:bg-background-100 text-sm text-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteItem(file.id, false);
|
||||
setShowMenu(undefined);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
347
web/src/app/my-documents/MyDocuments.tsx
Normal file
347
web/src/app/my-documents/MyDocuments.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { FolderActions } from "./FolderActions";
|
||||
import { FolderBreadcrumb } from "./FolderBreadcrumb";
|
||||
import { FolderContents } from "./FolderContents";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
|
||||
interface FolderResponse {
|
||||
children: { name: string; id: number }[];
|
||||
files: { name: string; document_id: string; id: number }[];
|
||||
parents: { name: string; id: number }[];
|
||||
name: string;
|
||||
id: number;
|
||||
document_id: string;
|
||||
}
|
||||
|
||||
export default function MyDocuments() {
|
||||
const [currentFolder, setCurrentFolder] = useState<number>(-1);
|
||||
const [folderContents, setFolderContents] = useState<FolderResponse | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
|
||||
const pageLimit = 10;
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const [presentingDocument, setPresentingDocument] =
|
||||
useState<MinimalOnyxDocument | null>(null);
|
||||
|
||||
const folderIdFromParams = parseInt(searchParams.get("path") || "-1", 10);
|
||||
|
||||
const fetchFolderContents = useCallback(async (folderId: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/user/folder/${folderId}?page=${page}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch folder contents");
|
||||
}
|
||||
const data = await response.json();
|
||||
setFolderContents(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching folder contents:", error);
|
||||
setPopup({
|
||||
message: "Failed to fetch folder contents",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentFolder(folderIdFromParams);
|
||||
fetchFolderContents(folderIdFromParams);
|
||||
}, [searchParams]);
|
||||
|
||||
const refreshFolderContents = useCallback(() => {
|
||||
fetchFolderContents(currentFolder);
|
||||
}, [fetchFolderContents, currentFolder]);
|
||||
|
||||
const handleFolderClick = (id: number) => {
|
||||
router.push(`/my-documents?path=${id}`);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = (folderId: number) => {
|
||||
router.push(`/my-documents?path=${folderId}`);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleCreateFolder = async (folderName: string) => {
|
||||
try {
|
||||
const response = await fetch("/api/user/folder", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: folderName, parent_id: currentFolder }),
|
||||
});
|
||||
if (response.ok) {
|
||||
refreshFolderContents();
|
||||
setPopup({
|
||||
message: "Folder created successfully",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to create folder");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating folder:", error);
|
||||
setPopup({
|
||||
message: "Failed to create folder",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (itemId: number, isFolder: boolean) => {
|
||||
try {
|
||||
const endpoint = isFolder
|
||||
? `/api/user/folder/${itemId}`
|
||||
: `/api/user/file/${itemId}`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchFolderContents(currentFolder);
|
||||
setPopup({
|
||||
message: `${isFolder ? "Folder" : "File"} deleted successfully`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Failed to delete ${isFolder ? "folder" : "file"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting item:", error);
|
||||
setPopup({
|
||||
message: `Failed to delete ${isFolder ? "folder" : "file"}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
refreshFolderContents();
|
||||
};
|
||||
|
||||
const handleUploadFiles = async (files: FileList) => {
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append("files", files[i]);
|
||||
}
|
||||
formData.append(
|
||||
"folder_id",
|
||||
currentFolder.toString() === "-1" ? "" : currentFolder.toString()
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/user/file/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (response.ok) {
|
||||
await fetchFolderContents(currentFolder);
|
||||
setPopup({
|
||||
message: "Files uploaded successfully",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to upload files");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading files:", error);
|
||||
setPopup({
|
||||
message: "Failed to upload files",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshFolderContents();
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleMoveItem = async (
|
||||
itemId: number,
|
||||
destinationFolderId: number | null,
|
||||
isFolder: boolean
|
||||
) => {
|
||||
const endpoint = isFolder
|
||||
? `/api/user/folder/${itemId}/move`
|
||||
: `/api/user/file/${itemId}/move`;
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
new_parent_id: destinationFolderId,
|
||||
[isFolder ? "folder_id" : "file_id"]: itemId,
|
||||
}),
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchFolderContents(currentFolder);
|
||||
setPopup({
|
||||
message: `${isFolder ? "Folder" : "File"} moved successfully`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to move item");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error moving item:", error);
|
||||
setPopup({
|
||||
message: "Failed to move item",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
refreshFolderContents();
|
||||
};
|
||||
|
||||
const handleDownloadItem = async (documentId: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/chat/file/${encodeURIComponent(documentId)}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch file");
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const contentDisposition = response.headers.get("Content-Disposition");
|
||||
const fileName = contentDisposition
|
||||
? contentDisposition.split("filename=")[1]
|
||||
: "document";
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName || "document";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("Error downloading file:", error);
|
||||
setPopup({
|
||||
message: "Failed to download file",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRenameItem = async (
|
||||
itemId: number,
|
||||
newName: string,
|
||||
isFolder: boolean
|
||||
) => {
|
||||
const endpoint = isFolder
|
||||
? `/api/user/folder/${itemId}?name=${encodeURIComponent(newName)}`
|
||||
: `/api/user/file/${itemId}/rename?name=${encodeURIComponent(newName)}`;
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "PUT",
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchFolderContents(currentFolder);
|
||||
setPopup({
|
||||
message: `${isFolder ? "Folder" : "File"} renamed successfully`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to rename item");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error renaming item:", error);
|
||||
setPopup({
|
||||
message: `Failed to rename ${isFolder ? "folder" : "file"}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
{presentingDocument && (
|
||||
<TextView
|
||||
presentingDocument={presentingDocument}
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
{popup}
|
||||
<div className="flex-grow">
|
||||
<FolderBreadcrumb
|
||||
currentFolder={{
|
||||
name: folderContents ? folderContents.name : "",
|
||||
id: currentFolder,
|
||||
}}
|
||||
parents={folderContents?.parents || []}
|
||||
onBreadcrumbClick={handleBreadcrumbClick}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Folder Contents</CardTitle>
|
||||
<FolderActions
|
||||
onRefresh={() => fetchFolderContents(currentFolder)}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
onUploadFiles={handleUploadFiles}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{folderContents ? (
|
||||
folderContents.files.length > 0 ||
|
||||
folderContents.children.length > 0 ? (
|
||||
<FolderContents
|
||||
currentPage={page}
|
||||
pageLimit={pageLimit}
|
||||
setPresentingDocument={(
|
||||
document_id: string,
|
||||
semantic_identifier: string
|
||||
) =>
|
||||
setPresentingDocument({ document_id, semantic_identifier })
|
||||
}
|
||||
contents={folderContents}
|
||||
onFolderClick={handleFolderClick}
|
||||
currentFolder={currentFolder}
|
||||
onDeleteItem={handleDeleteItem}
|
||||
onDownloadItem={handleDownloadItem}
|
||||
onMoveItem={handleMoveItem}
|
||||
onRenameItem={onRenameItem}
|
||||
/>
|
||||
) : (
|
||||
<p>No content in this folder</p>
|
||||
)
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
currentPage={page}
|
||||
totalPages={Math.ceil(
|
||||
((folderContents?.files?.length || 0) +
|
||||
(folderContents?.children?.length || 0)) /
|
||||
pageLimit
|
||||
)}
|
||||
onPageChange={(newPage) => {
|
||||
setPage(newPage);
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
web/src/app/my-documents/WrappedDocuments.tsx
Normal file
20
web/src/app/my-documents/WrappedDocuments.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { AssistantsPageTitle } from "../assistants/AssistantsPageTitle";
|
||||
import SidebarWrapper from "../assistants/SidebarWrapper";
|
||||
import MyDocuments from "./MyDocuments";
|
||||
|
||||
export default function WrappedUserDocuments({
|
||||
initiallyToggled,
|
||||
}: {
|
||||
initiallyToggled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper size="lg" page="chat" initiallyToggled={initiallyToggled}>
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>My Documents</AssistantsPageTitle>
|
||||
<MyDocuments />
|
||||
</div>
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
46
web/src/app/my-documents/page.tsx
Normal file
46
web/src/app/my-documents/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import WrappedDocuments from "./WrappedDocuments";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
|
||||
export default async function GalleryPage(props: {
|
||||
searchParams: Promise<{ [key: string]: string }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const data = await fetchChatData(searchParams);
|
||||
|
||||
if ("redirect" in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const {
|
||||
chatSessions,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
defaultAssistantId,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
>
|
||||
<WrappedDocuments initiallyToggled={toggleSidebar} />
|
||||
</ChatProvider>
|
||||
);
|
||||
}
|
||||
64
web/src/app/my-documents/useDocuments.ts
Normal file
64
web/src/app/my-documents/useDocuments.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// API functions
|
||||
const fetchDocuments = async (): Promise<Document[]> => {
|
||||
const response = await fetch("/api/manage/admin/documents");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch documents");
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const deleteDocument = async (documentId: number): Promise<void> => {
|
||||
const response = await fetch(`/api/manage/admin/documents/${documentId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete document");
|
||||
}
|
||||
};
|
||||
|
||||
export interface Document {
|
||||
id: number;
|
||||
document_id: string;
|
||||
}
|
||||
// Custom hook
|
||||
export const useDocuments = () => {
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadDocuments = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const fetchedDocuments = await fetchDocuments();
|
||||
setDocuments(fetchedDocuments);
|
||||
} catch (err) {
|
||||
setError("Failed to load documents err: " + err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDeleteDocument = async (documentId: number) => {
|
||||
try {
|
||||
await deleteDocument(documentId);
|
||||
await loadDocuments();
|
||||
} catch (err) {
|
||||
setError("Failed to delete document");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadDocuments();
|
||||
}, [loadDocuments]);
|
||||
|
||||
return {
|
||||
documents,
|
||||
isLoading,
|
||||
error,
|
||||
loadDocuments,
|
||||
handleDeleteDocument,
|
||||
};
|
||||
};
|
||||
@@ -7,9 +7,6 @@ const getPaginationOptions = (
|
||||
pageCount: number
|
||||
): number[] => {
|
||||
const paginationOptions = [currentPage];
|
||||
// if (currentPage !== 1) {
|
||||
// paginationOptions.push(currentPage)
|
||||
// }
|
||||
|
||||
let offset = 1;
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Download, XIcon, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { OnyxDocument, MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { MinimalMarkdown } from "./MinimalMarkdown";
|
||||
|
||||
interface TextViewProps {
|
||||
presentingDocument: OnyxDocument;
|
||||
presentingDocument: MinimalOnyxDocument;
|
||||
onClose: () => void;
|
||||
}
|
||||
export default function TextView({
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import {
|
||||
CCPairBasicInfo,
|
||||
DocumentSet,
|
||||
Tag,
|
||||
User,
|
||||
ValidSources,
|
||||
} from "@/lib/types";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
|
||||
interface ChatContextProps {
|
||||
chatSessions: ChatSession[];
|
||||
@@ -21,8 +14,6 @@ interface ChatContextProps {
|
||||
availableDocumentSets: DocumentSet[];
|
||||
availableTags: Tag[];
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
folders: Folder[];
|
||||
openedFolders: Record<string, boolean>;
|
||||
shouldShowWelcomeModal?: boolean;
|
||||
shouldDisplaySourcesIncompleteModal?: boolean;
|
||||
defaultAssistantId?: number;
|
||||
|
||||
@@ -2,8 +2,14 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
isEditing?: boolean;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
({ className, width, style, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
@@ -11,6 +17,15 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width:
|
||||
width ||
|
||||
`${Math.max(
|
||||
1,
|
||||
String(props.value || props.defaultValue || "").length
|
||||
)}ch`,
|
||||
...style,
|
||||
}}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,6 @@ import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import {
|
||||
SIDEBAR_TOGGLED_COOKIE_NAME,
|
||||
@@ -35,8 +34,6 @@ interface FetchChatDataResult {
|
||||
documentSets: DocumentSet[];
|
||||
tags: Tag[];
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
folders: Folder[];
|
||||
openedFolders: Record<string, boolean>;
|
||||
defaultAssistantId?: number;
|
||||
toggleSidebar: boolean;
|
||||
finalDocumentSidebarInitialWidth?: number;
|
||||
@@ -55,7 +52,6 @@ export async function fetchChatData(searchParams: {
|
||||
fetchSS("/chat/get-user-chat-sessions"),
|
||||
fetchSS("/query/valid-tags"),
|
||||
fetchLLMProvidersSS(),
|
||||
fetchSS("/folder"),
|
||||
];
|
||||
|
||||
let results: (
|
||||
@@ -83,7 +79,6 @@ export async function fetchChatData(searchParams: {
|
||||
|
||||
const tagsResponse = results[5] as Response | null;
|
||||
const llmProviders = (results[6] || []) as LLMProviderDescriptor[];
|
||||
const foldersResponse = results[7] as Response | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
|
||||
@@ -176,18 +171,6 @@ export async function fetchChatData(searchParams: {
|
||||
// if no connectors are setup, only show personas that are pure
|
||||
// passthrough and don't do any retrieval
|
||||
|
||||
let folders: Folder[] = [];
|
||||
if (foldersResponse?.ok) {
|
||||
folders = (await foldersResponse.json()).folders as Folder[];
|
||||
} else {
|
||||
console.log(`Failed to fetch folders - ${foldersResponse?.status}`);
|
||||
}
|
||||
|
||||
const openedFoldersCookie = requestCookies.get("openedFolders");
|
||||
const openedFolders = openedFoldersCookie
|
||||
? JSON.parse(openedFoldersCookie.value)
|
||||
: {};
|
||||
|
||||
return {
|
||||
user,
|
||||
chatSessions,
|
||||
@@ -196,8 +179,6 @@ export async function fetchChatData(searchParams: {
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
finalDocumentSidebarInitialWidth,
|
||||
toggleSidebar,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
@@ -36,8 +35,6 @@ interface FetchChatDataResult {
|
||||
assistants?: Persona[];
|
||||
tags?: Tag[];
|
||||
llmProviders?: LLMProviderDescriptor[];
|
||||
folders?: Folder[];
|
||||
openedFolders?: Record<string, boolean>;
|
||||
defaultAssistantId?: number;
|
||||
toggleSidebar?: boolean;
|
||||
finalDocumentSidebarInitialWidth?: number;
|
||||
@@ -52,8 +49,7 @@ type FetchOption =
|
||||
| "documentSets"
|
||||
| "assistants"
|
||||
| "tags"
|
||||
| "llmProviders"
|
||||
| "folders";
|
||||
| "llmProviders";
|
||||
|
||||
/*
|
||||
NOTE: currently unused, but leaving here for future use.
|
||||
@@ -72,7 +68,6 @@ export async function fetchSomeChatData(
|
||||
assistants: fetchAssistantsSS,
|
||||
tags: () => fetchSS("/query/valid-tags"),
|
||||
llmProviders: fetchLLMProvidersSS,
|
||||
folders: () => fetchSS("/folder"),
|
||||
};
|
||||
|
||||
// Always fetch auth type metadata
|
||||
@@ -129,7 +124,7 @@ export async function fetchSomeChatData(
|
||||
case "assistants":
|
||||
const [rawAssistantsList, assistantsFetchError] = result as [
|
||||
Persona[],
|
||||
string | null,
|
||||
string | null
|
||||
];
|
||||
result.assistants = rawAssistantsList
|
||||
.filter((assistant) => assistant.is_visible)
|
||||
@@ -143,11 +138,6 @@ export async function fetchSomeChatData(
|
||||
case "llmProviders":
|
||||
result.llmProviders = result || [];
|
||||
break;
|
||||
case "folders":
|
||||
result.folders = result?.ok
|
||||
? ((await result.json()) as { folders: Folder[] }).folders
|
||||
: [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,13 +175,6 @@ export async function fetchSomeChatData(
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchOptions.includes("folders")) {
|
||||
const openedFoldersCookie = requestCookies.get("openedFolders");
|
||||
result.openedFolders = openedFoldersCookie
|
||||
? JSON.parse(openedFoldersCookie.value)
|
||||
: {};
|
||||
}
|
||||
|
||||
const defaultAssistantIdRaw = searchParams["assistantId"];
|
||||
result.defaultAssistantId = defaultAssistantIdRaw
|
||||
? parseInt(defaultAssistantIdRaw)
|
||||
|
||||
@@ -45,12 +45,15 @@ export interface QuotesInfoPacket {
|
||||
quotes: Quote[];
|
||||
}
|
||||
|
||||
export interface OnyxDocument {
|
||||
export interface MinimalOnyxDocument {
|
||||
document_id: string;
|
||||
semantic_identifier: string | null;
|
||||
}
|
||||
|
||||
export interface OnyxDocument extends MinimalOnyxDocument {
|
||||
link: string;
|
||||
source_type: ValidSources;
|
||||
blurb: string;
|
||||
semantic_identifier: string | null;
|
||||
boost: number;
|
||||
hidden: boolean;
|
||||
score: number;
|
||||
|
||||
Reference in New Issue
Block a user