mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-25 03:35:48 +00:00
Compare commits
1 Commits
checkmark_
...
updates
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
171deb495e |
@@ -1,75 +0,0 @@
|
||||
"""add user files
|
||||
|
||||
Revision ID: 9aadf32dfeb4
|
||||
Revises: f1ca58b2f2ec
|
||||
Create Date: 2025-01-26 16:08:21.551022
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import datetime
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "9aadf32dfeb4"
|
||||
down_revision = "f1ca58b2f2ec"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create user_folder table without parent_id
|
||||
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("name", sa.String(length=255), nullable=True),
|
||||
sa.Column("description", 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),
|
||||
)
|
||||
|
||||
# Create user_file table with folder_id instead of parent_folder_id
|
||||
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(
|
||||
"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,
|
||||
),
|
||||
)
|
||||
|
||||
# Create persona__user_file table
|
||||
op.create_table(
|
||||
"persona__user_file",
|
||||
sa.Column(
|
||||
"persona_id", sa.Integer(), sa.ForeignKey("persona.id"), primary_key=True
|
||||
),
|
||||
sa.Column(
|
||||
"user_file_id",
|
||||
sa.Integer(),
|
||||
sa.ForeignKey("user_file.id"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop the persona__user_file table
|
||||
op.drop_table("persona__user_file")
|
||||
# Drop the user_file table
|
||||
op.drop_table("user_file")
|
||||
# Drop the user_folder table
|
||||
op.drop_table("user_folder")
|
||||
@@ -205,11 +205,6 @@ 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
|
||||
@@ -1563,12 +1558,6 @@ class Persona(Base):
|
||||
secondary="persona__user_group",
|
||||
viewonly=True,
|
||||
)
|
||||
# Relationship to UserFile
|
||||
user_files: Mapped[list["UserFile"]] = relationship(
|
||||
"UserFile",
|
||||
secondary="persona__user_file",
|
||||
back_populates="assistants",
|
||||
)
|
||||
labels: Mapped[list["PersonaLabel"]] = relationship(
|
||||
"PersonaLabel",
|
||||
secondary=Persona__PersonaLabel.__table__,
|
||||
@@ -1585,15 +1574,6 @@ class Persona(Base):
|
||||
)
|
||||
|
||||
|
||||
class Persona__UserFile(Base):
|
||||
__tablename__ = "persona__user_file"
|
||||
|
||||
persona_id: Mapped[int] = mapped_column(ForeignKey("persona.id"), primary_key=True)
|
||||
user_file_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("user_file.id"), primary_key=True
|
||||
)
|
||||
|
||||
|
||||
class PersonaLabel(Base):
|
||||
__tablename__ = "persona_label"
|
||||
|
||||
@@ -2053,51 +2033,6 @@ class InputPrompt__User(Base):
|
||||
disabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
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)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
description: Mapped[str] = mapped_column(nullable=False)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
default=datetime.datetime.utcnow
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship(back_populates="folders")
|
||||
files: Mapped[list["UserFile"]] = 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)
|
||||
assistants: Mapped[list["Persona"]] = relationship(
|
||||
"Persona",
|
||||
secondary=Persona__UserFile.__table__,
|
||||
back_populates="user_files",
|
||||
)
|
||||
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
|
||||
)
|
||||
user: Mapped["User"] = relationship(back_populates="files")
|
||||
folder: Mapped["UserFolder"] = relationship(back_populates="files")
|
||||
|
||||
|
||||
"""
|
||||
Multi-tenancy related tables
|
||||
"""
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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,
|
||||
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
|
||||
@@ -594,7 +594,6 @@ class VespaIndex(DocumentIndex):
|
||||
primary_index=index_name == self.index_name,
|
||||
)
|
||||
large_chunks_enabled = multipass_config.enable_large_chunks
|
||||
|
||||
enriched_doc_infos = VespaIndex.enrich_basic_chunk_info(
|
||||
index_name=index_name,
|
||||
http_client=http_client,
|
||||
@@ -663,7 +662,6 @@ class VespaIndex(DocumentIndex):
|
||||
tenant_id=tenant_id,
|
||||
large_chunks_enabled=large_chunks_enabled,
|
||||
)
|
||||
|
||||
for doc_chunk_ids_batch in batch_generator(
|
||||
chunks_to_delete, BATCH_SIZE
|
||||
):
|
||||
|
||||
@@ -97,7 +97,6 @@ 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
|
||||
@@ -287,7 +286,6 @@ def get_application() -> FastAPI:
|
||||
include_router_with_global_prefix_prepended(application, input_prompt_router)
|
||||
include_router_with_global_prefix_prepended(application, admin_input_prompt_router)
|
||||
include_router_with_global_prefix_prepended(application, cc_pair_router)
|
||||
include_router_with_global_prefix_prepended(application, user_documents_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)
|
||||
|
||||
@@ -380,7 +380,12 @@ def check_drive_tokens(
|
||||
return AuthStatus(authenticated=True)
|
||||
|
||||
|
||||
def upload_files(files: list[UploadFile], db_session: Session) -> FileUploadResponse:
|
||||
@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:
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="File name cannot be empty")
|
||||
@@ -441,15 +446,6 @@ def upload_files(files: list[UploadFile], db_session: Session) -> FileUploadResp
|
||||
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)
|
||||
|
||||
|
||||
@router.get("/admin/connector")
|
||||
def get_connectors_by_credential(
|
||||
_: User = Depends(current_curator_or_admin_user),
|
||||
@@ -933,21 +929,81 @@ def connector_run_once(
|
||||
connector_id = run_info.connector_id
|
||||
specified_credential_ids = run_info.credential_ids
|
||||
|
||||
if not specified_credential_ids:
|
||||
try:
|
||||
possible_credential_ids = get_connector_credential_ids(
|
||||
run_info.connector_id, db_session
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="No credentials specified for indexing"
|
||||
status_code=404,
|
||||
detail=f"Connector by id {connector_id} does not exist.",
|
||||
)
|
||||
|
||||
try:
|
||||
num_triggers = trigger_indexing_for_cc_pair(
|
||||
specified_credential_ids,
|
||||
connector_id,
|
||||
run_info.from_beginning,
|
||||
tenant_id,
|
||||
db_session,
|
||||
if not specified_credential_ids:
|
||||
credential_ids = possible_credential_ids
|
||||
else:
|
||||
if set(specified_credential_ids).issubset(set(possible_credential_ids)):
|
||||
credential_ids = specified_credential_ids
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Not all specified credentials are associated with connector",
|
||||
)
|
||||
|
||||
if not credential_ids:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Connector has no valid credentials, cannot create index attempts.",
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# Prevents index attempts for cc pairs that already have an index attempt currently running
|
||||
skipped_credentials = [
|
||||
credential_id
|
||||
for credential_id in credential_ids
|
||||
if get_index_attempts_for_cc_pair(
|
||||
cc_pair_identifier=ConnectorCredentialPairIdentifier(
|
||||
connector_id=run_info.connector_id,
|
||||
credential_id=credential_id,
|
||||
),
|
||||
only_current=True,
|
||||
db_session=db_session,
|
||||
disinclude_finished=True,
|
||||
)
|
||||
]
|
||||
|
||||
connector_credential_pairs = [
|
||||
get_connector_credential_pair(
|
||||
db_session=db_session,
|
||||
connector_id=connector_id,
|
||||
credential_id=credential_id,
|
||||
)
|
||||
for credential_id in credential_ids
|
||||
if credential_id not in skipped_credentials
|
||||
]
|
||||
|
||||
num_triggers = 0
|
||||
for cc_pair in connector_credential_pairs:
|
||||
if cc_pair is not None:
|
||||
indexing_mode = IndexingMode.UPDATE
|
||||
if run_info.from_beginning:
|
||||
indexing_mode = IndexingMode.REINDEX
|
||||
|
||||
mark_ccpair_with_indexing_trigger(cc_pair.id, indexing_mode, db_session)
|
||||
num_triggers += 1
|
||||
|
||||
logger.info(
|
||||
f"connector_run_once - marking cc_pair with indexing trigger: "
|
||||
f"connector={run_info.connector_id} "
|
||||
f"cc_pair={cc_pair.id} "
|
||||
f"indexing_trigger={indexing_mode}"
|
||||
)
|
||||
|
||||
# run the beat task to pick up the triggers immediately
|
||||
primary_app.send_task(
|
||||
OnyxCeleryTask.CHECK_FOR_INDEXING,
|
||||
priority=OnyxCeleryPriority.HIGH,
|
||||
kwargs={"tenant_id": tenant_id},
|
||||
)
|
||||
|
||||
msg = f"Marked {num_triggers} index attempts with indexing triggers."
|
||||
return StatusResponse(
|
||||
@@ -1119,82 +1175,3 @@ def get_basic_connector_indexing_status(
|
||||
for cc_pair in cc_pairs
|
||||
if cc_pair.connector.source != DocumentSource.INGESTION_API
|
||||
]
|
||||
|
||||
|
||||
def trigger_indexing_for_cc_pair(
|
||||
specified_credential_ids: list[int],
|
||||
connector_id: int,
|
||||
from_beginning: bool,
|
||||
tenant_id: str,
|
||||
db_session: Session,
|
||||
) -> int:
|
||||
try:
|
||||
possible_credential_ids = get_connector_credential_ids(connector_id, db_session)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Connector by id {connector_id} does not exist: {str(e)}")
|
||||
|
||||
if not specified_credential_ids:
|
||||
credential_ids = possible_credential_ids
|
||||
else:
|
||||
if set(specified_credential_ids).issubset(set(possible_credential_ids)):
|
||||
credential_ids = specified_credential_ids
|
||||
else:
|
||||
raise ValueError(
|
||||
"Not all specified credentials are associated with connector"
|
||||
)
|
||||
|
||||
if not credential_ids:
|
||||
raise ValueError(
|
||||
"Connector has no valid credentials, cannot create index attempts."
|
||||
)
|
||||
|
||||
# Prevents index attempts for cc pairs that already have an index attempt currently running
|
||||
skipped_credentials = [
|
||||
credential_id
|
||||
for credential_id in credential_ids
|
||||
if get_index_attempts_for_cc_pair(
|
||||
cc_pair_identifier=ConnectorCredentialPairIdentifier(
|
||||
connector_id=connector_id,
|
||||
credential_id=credential_id,
|
||||
),
|
||||
only_current=True,
|
||||
db_session=db_session,
|
||||
disinclude_finished=True,
|
||||
)
|
||||
]
|
||||
|
||||
connector_credential_pairs = [
|
||||
get_connector_credential_pair(
|
||||
db_session=db_session,
|
||||
connector_id=connector_id,
|
||||
credential_id=credential_id,
|
||||
)
|
||||
for credential_id in credential_ids
|
||||
if credential_id not in skipped_credentials
|
||||
]
|
||||
|
||||
num_triggers = 0
|
||||
for cc_pair in connector_credential_pairs:
|
||||
if cc_pair is not None:
|
||||
indexing_mode = IndexingMode.UPDATE
|
||||
if from_beginning:
|
||||
indexing_mode = IndexingMode.REINDEX
|
||||
|
||||
mark_ccpair_with_indexing_trigger(cc_pair.id, indexing_mode, db_session)
|
||||
num_triggers += 1
|
||||
|
||||
logger.info(
|
||||
f"connector_run_once - marking cc_pair with indexing trigger: "
|
||||
f"connector={connector_id} "
|
||||
f"cc_pair={cc_pair.id} "
|
||||
f"indexing_trigger={indexing_mode}"
|
||||
)
|
||||
|
||||
# run the beat task to pick up the triggers immediately
|
||||
primary_app.send_task(
|
||||
OnyxCeleryTask.CHECK_FOR_INDEXING,
|
||||
priority=OnyxCeleryPriority.HIGH,
|
||||
kwargs={"tenant_id": tenant_id},
|
||||
)
|
||||
|
||||
return num_triggers
|
||||
|
||||
@@ -411,7 +411,7 @@ class FileUploadResponse(BaseModel):
|
||||
|
||||
|
||||
class ObjectCreationIdResponse(BaseModel):
|
||||
id: int
|
||||
id: int | str
|
||||
credential: CredentialSnapshot | None = None
|
||||
|
||||
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
import time
|
||||
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.configs.constants import DocumentSource
|
||||
from onyx.connectors.models import InputType
|
||||
from onyx.db.connector import create_connector
|
||||
from onyx.db.connector_credential_pair import add_credential_to_connector
|
||||
from onyx.db.credentials import create_credential
|
||||
from onyx.db.engine import get_session
|
||||
from onyx.db.enums import AccessType
|
||||
from onyx.db.models import User
|
||||
from onyx.db.models import UserFile
|
||||
from onyx.db.models import UserFolder
|
||||
from onyx.db.user_documents import create_user_files
|
||||
from onyx.server.documents.models import ConnectorBase
|
||||
from onyx.server.documents.models import CredentialBase
|
||||
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 FolderResponse
|
||||
from onyx.server.user_documents.models import MessageResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class FolderCreationRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
@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,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
)
|
||||
db_session.add(new_folder)
|
||||
db_session.commit()
|
||||
return FolderDetailResponse(
|
||||
id=new_folder.id,
|
||||
name=new_folder.name,
|
||||
description=new_folder.description,
|
||||
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),
|
||||
) -> 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")
|
||||
|
||||
return FolderDetailResponse(
|
||||
id=folder.id,
|
||||
name=folder.name,
|
||||
files=[FileResponse.from_model(file) for file in folder.files],
|
||||
)
|
||||
|
||||
|
||||
@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_paths
|
||||
)
|
||||
for path in file_upload_response.file_paths:
|
||||
connector_base = ConnectorBase(
|
||||
name=f"UserFile-{int(time.time())}",
|
||||
source=DocumentSource.FILE,
|
||||
input_type=InputType.LOAD_STATE,
|
||||
connector_specific_config={
|
||||
"file_locations": [path],
|
||||
},
|
||||
refresh_freq=None,
|
||||
prune_freq=None,
|
||||
indexing_start=None,
|
||||
)
|
||||
connector = create_connector(
|
||||
db_session=db_session,
|
||||
connector_data=connector_base,
|
||||
)
|
||||
|
||||
credential_info = CredentialBase(
|
||||
credential_json={},
|
||||
admin_public=True,
|
||||
source=DocumentSource.FILE,
|
||||
curator_public=True,
|
||||
groups=[],
|
||||
name=f"UserFileCredential-{int(time.time())}",
|
||||
)
|
||||
credential = create_credential(credential_info, user, db_session)
|
||||
|
||||
add_credential_to_connector(
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
connector_id=connector.id,
|
||||
credential_id=credential.id,
|
||||
cc_pair_name=f"UserFileCCPair-{int(time.time())}",
|
||||
access_type=AccessType.PUBLIC,
|
||||
auto_sync_options=None,
|
||||
groups=[],
|
||||
)
|
||||
|
||||
# TODO: functional document indexing
|
||||
# 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,
|
||||
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")
|
||||
|
||||
|
||||
@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_folder_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.folder_id = request.new_folder_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)
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
description: str
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: UserFolder) -> "FolderResponse":
|
||||
return cls(id=model.id, name=model.name, description=model.description)
|
||||
|
||||
|
||||
class FileResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
document_id: str
|
||||
folder_id: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: UserFile) -> "FileResponse":
|
||||
return cls(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
folder_id=model.folder_id,
|
||||
document_id=model.document_id,
|
||||
)
|
||||
|
||||
|
||||
class FolderDetailResponse(FolderResponse):
|
||||
files: List[FileResponse]
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class FileSystemResponse(BaseModel):
|
||||
folders: list[FolderResponse]
|
||||
files: list[FileResponse]
|
||||
@@ -80,7 +80,6 @@ import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { DeletePersonaButton } from "./[id]/DeletePersonaButton";
|
||||
import Title from "@/components/ui/title";
|
||||
import { FilePickerModal } from "@/app/my-documents/components/FilePicker";
|
||||
|
||||
function findSearchTool(tools: ToolSnapshot[]) {
|
||||
return tools.find((tool) => tool.in_code_tool_id === "SearchTool");
|
||||
@@ -145,7 +144,6 @@ export function AssistantEditor({
|
||||
"#6FFFFF",
|
||||
];
|
||||
|
||||
const [filePickerModalOpen, setFilePickerModalOpen] = useState(false);
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
// state to persist across formik reformatting
|
||||
@@ -351,19 +349,6 @@ export function AssistantEditor({
|
||||
<BackButton />
|
||||
</div>
|
||||
)}
|
||||
{filePickerModalOpen && (
|
||||
<FilePickerModal
|
||||
isOpen={filePickerModalOpen}
|
||||
onClose={() => {
|
||||
setFilePickerModalOpen(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
setFilePickerModalOpen(false);
|
||||
}}
|
||||
title="Add Documents to your Assistant"
|
||||
buttonContent="Add to Assistant"
|
||||
/>
|
||||
)}
|
||||
{labelToDelete && (
|
||||
<DeleteEntityModal
|
||||
entityType="label"
|
||||
@@ -761,23 +746,6 @@ export function AssistantEditor({
|
||||
|
||||
<div className="w-full max-w-4xl">
|
||||
<div className="flex flex-col">
|
||||
<Separator />
|
||||
<div className="flex gap-x-2 py-2 flex justify-start">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="block font-medium text-sm">
|
||||
My Documents
|
||||
</p>
|
||||
<Button
|
||||
className="!p-.5 text-xs"
|
||||
type="button"
|
||||
onClick={() => setFilePickerModalOpen(true)}
|
||||
>
|
||||
Attach Files and Folders
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{searchTool && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
@@ -113,7 +113,6 @@ import {
|
||||
import AssistantModal from "../assistants/mine/AssistantModal";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
import { UserSettingsModal } from "./modal/UserSettingsModal";
|
||||
import { FilePickerModal } from "../my-documents/components/FilePicker";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@@ -188,8 +187,6 @@ export function ChatPage({
|
||||
const settings = useContext(SettingsContext);
|
||||
const enterpriseSettings = settings?.enterpriseSettings;
|
||||
|
||||
const [viewingFilePicker, setViewingFilePicker] = useState(false);
|
||||
const [toggleDocSelection, setToggleDocSelection] = useState(false);
|
||||
const [documentSidebarToggled, setDocumentSidebarToggled] = useState(false);
|
||||
|
||||
const [userSettingsToggled, setUserSettingsToggled] = useState(false);
|
||||
@@ -296,19 +293,22 @@ export function ChatPage({
|
||||
);
|
||||
};
|
||||
|
||||
const llmOverrideManager = useLlmOverride(
|
||||
llmProviders,
|
||||
user?.preferences.default_model,
|
||||
selectedChatSession
|
||||
);
|
||||
|
||||
const [alternativeAssistant, setAlternativeAssistant] =
|
||||
useState<Persona | null>(null);
|
||||
|
||||
const [presentingDocument, setPresentingDocument] =
|
||||
useState<OnyxDocument | null>(null);
|
||||
|
||||
const { recentAssistants, refreshRecentAssistants } = useAssistants();
|
||||
const { recentAssistants, refreshRecentAssistants, assistants } =
|
||||
useAssistants();
|
||||
|
||||
const llmOverrideManager = useLlmOverride(
|
||||
llmProviders,
|
||||
user?.preferences.default_model,
|
||||
selectedChatSession,
|
||||
undefined,
|
||||
assistants
|
||||
);
|
||||
|
||||
const liveAssistant: Persona | undefined =
|
||||
alternativeAssistant ||
|
||||
@@ -339,7 +339,7 @@ export function ChatPage({
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [liveAssistant, user?.preferences.default_model]);
|
||||
}, [liveAssistant, user?.preferences.default_model, selectedChatSession]);
|
||||
|
||||
const stopGenerating = () => {
|
||||
const currentSession = currentSessionId();
|
||||
@@ -2071,17 +2071,6 @@ export function ChatPage({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{toggleDocSelection && (
|
||||
<FilePickerModal
|
||||
buttonContent="Set as Context"
|
||||
title="User Documents"
|
||||
isOpen={true}
|
||||
onClose={() => setToggleDocSelection(false)}
|
||||
onSave={() => {
|
||||
setToggleDocSelection(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{retrievalEnabled && documentSidebarToggled && settings?.isMobile && (
|
||||
<div className="md:hidden">
|
||||
@@ -2764,9 +2753,6 @@ export function ChatPage({
|
||||
</div>
|
||||
)}
|
||||
<ChatInputBar
|
||||
toggleDocSelection={() => {
|
||||
setToggleDocSelection(true);
|
||||
}}
|
||||
toggleDocumentSidebar={toggleDocumentSidebar}
|
||||
availableSources={sources}
|
||||
availableDocumentSets={documentSets}
|
||||
|
||||
@@ -86,7 +86,6 @@ export const SourceChip = ({
|
||||
);
|
||||
|
||||
interface ChatInputBarProps {
|
||||
toggleDocSelection: () => void;
|
||||
removeDocs: () => void;
|
||||
showConfigureAPIKey: () => void;
|
||||
selectedDocuments: OnyxDocument[];
|
||||
@@ -113,7 +112,6 @@ interface ChatInputBarProps {
|
||||
}
|
||||
|
||||
export function ChatInputBar({
|
||||
toggleDocSelection,
|
||||
retrievalEnabled,
|
||||
removeDocs,
|
||||
toggleDocumentSidebar,
|
||||
@@ -662,19 +660,18 @@ export function ChatInputBar({
|
||||
name="File"
|
||||
Icon={FiPlusCircle}
|
||||
onClick={() => {
|
||||
toggleDocSelection();
|
||||
// const input = document.createElement("input");
|
||||
// input.type = "file";
|
||||
// input.multiple = true;
|
||||
// input.onchange = (event: any) => {
|
||||
// const files = Array.from(
|
||||
// event?.target?.files || []
|
||||
// ) as File[];
|
||||
// if (files.length > 0) {
|
||||
// handleFileUpload(files);
|
||||
// }
|
||||
// };
|
||||
// input.click();
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.onchange = (event: any) => {
|
||||
const files = Array.from(
|
||||
event?.target?.files || []
|
||||
) as File[];
|
||||
if (files.length > 0) {
|
||||
handleFileUpload(files);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
tooltipContent={"Upload files"}
|
||||
/>
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { MoveFileModal } from "./MoveFileModal";
|
||||
import { FileItem, FolderItem } from "./MyDocumenItem";
|
||||
|
||||
interface FolderType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface FileType extends FolderType {
|
||||
document_id: string;
|
||||
folder_id: number | null;
|
||||
}
|
||||
|
||||
interface FolderContentsProps {
|
||||
pageLimit: number;
|
||||
currentPage: number;
|
||||
contents: {
|
||||
folders: FolderType[];
|
||||
files: FileType[];
|
||||
};
|
||||
onFolderClick: (folderId: number) => void;
|
||||
currentFolder: number | null;
|
||||
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;
|
||||
folders: FolderType[];
|
||||
}
|
||||
|
||||
export function FolderContents({
|
||||
pageLimit,
|
||||
currentPage,
|
||||
setPresentingDocument,
|
||||
contents,
|
||||
onFolderClick,
|
||||
currentFolder,
|
||||
onDeleteItem,
|
||||
onDownloadItem,
|
||||
onMoveItem,
|
||||
onRenameItem,
|
||||
folders,
|
||||
}: 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") {
|
||||
onMoveItem(item.id, targetFolderId, item.isFolder);
|
||||
}
|
||||
};
|
||||
|
||||
const startIndex = pageLimit * (currentPage - 1);
|
||||
const endIndex = startIndex + pageLimit;
|
||||
const itemsToDisplay = [...contents.folders, ...contents.files].slice(
|
||||
startIndex,
|
||||
endIndex
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex-grow" onDragOver={(e) => e.preventDefault()}>
|
||||
{itemsToDisplay.map((item) => {
|
||||
if ("document_id" in item) {
|
||||
return (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
file={{
|
||||
name: item.name,
|
||||
id: item.id,
|
||||
document_id: item.document_id as string,
|
||||
}}
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onDownloadItem={onDownloadItem}
|
||||
onMoveItem={(id) => {
|
||||
setItemToMove({ id, name: item.name, isFolder: false });
|
||||
setIsMoveModalOpen(true);
|
||||
}}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
handleRename={handleRename}
|
||||
onDragStart={handleDragStart}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FolderItem
|
||||
key={item.id}
|
||||
folder={item}
|
||||
onFolderClick={onFolderClick}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onMoveItem={(id) => {
|
||||
setItemToMove({ id, name: item.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>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,574 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Search, Grid, List, Plus, RefreshCw, Upload } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { FolderActions } from "./FolderActions";
|
||||
import { FolderContents } from "./FolderContents";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface FolderResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FileResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
document_id: string;
|
||||
folder_id: number | null;
|
||||
}
|
||||
|
||||
interface FolderContentsResponse {
|
||||
folders: FolderResponse[];
|
||||
files: FileResponse[];
|
||||
}
|
||||
|
||||
const IconButton: React.FC<{
|
||||
icon: React.ComponentType;
|
||||
onClick: () => void;
|
||||
active: boolean;
|
||||
}> = ({ icon: Icon, onClick, active }) => (
|
||||
<button
|
||||
className={`p-2 flex-none h-10 w-10 flex items-center justify-center rounded ${
|
||||
active ? "bg-gray-200" : "hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
);
|
||||
|
||||
const CreateFolderPopover: React.FC<{
|
||||
onCreateFolder: (name: string, description: string) => void;
|
||||
}> = ({ onCreateFolder }) => {
|
||||
const [folderName, setFolderName] = useState("");
|
||||
const [folderDescription, setFolderDescription] = useState("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (folderName.trim()) {
|
||||
onCreateFolder(folderName.trim(), folderDescription.trim());
|
||||
setFolderName("");
|
||||
setFolderDescription("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="inline-flex items-center justify-center relative shrink-0 h-9 px-4 py-2 rounded-lg min-w-[5rem] active:scale-[0.985] whitespace-nowrap pl-2 pr-3 gap-1">
|
||||
<Plus className="h-5 w-5" />
|
||||
Create Folder
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="w-full space-y-2">
|
||||
<Label htmlFor="folderName">Folder Name</Label>
|
||||
<Input
|
||||
className="w-full"
|
||||
id="folderName"
|
||||
value={folderName}
|
||||
onChange={(e) => setFolderName(e.target.value)}
|
||||
placeholder="Enter folder name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-2">
|
||||
<Label htmlFor="folderDescription">Description (optional)</Label>
|
||||
<Input
|
||||
className="w-full"
|
||||
id="folderDescription"
|
||||
value={folderDescription}
|
||||
onChange={(e) => setFolderDescription(e.target.value)}
|
||||
placeholder="Enter folder description"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">Create Folder</Button>
|
||||
</form>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default function MyDocuments() {
|
||||
const [currentFolder, setCurrentFolder] = useState<number | null>(null);
|
||||
const [folderContents, setFolderContents] =
|
||||
useState<FolderContentsResponse | null>(null);
|
||||
const [folders, setFolders] = useState<FolderResponse[]>([]);
|
||||
|
||||
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 [view, setView] = useState<"grid" | "list">("grid");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const folderIdFromParams = parseInt(searchParams.get("folder") || "0", 10);
|
||||
|
||||
const fetchFolders = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/user/folder");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch folders");
|
||||
}
|
||||
const data = await response.json();
|
||||
setFolders(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching folders:", error);
|
||||
setPopup({
|
||||
message: "Failed to fetch folders",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchFolderContents = useCallback(
|
||||
async (folderId: number | null) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/user/file-system?page=${page}&folder_id=${folderId || ""}`
|
||||
);
|
||||
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",
|
||||
});
|
||||
}
|
||||
},
|
||||
[page]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFolders();
|
||||
}, [fetchFolders]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentFolder(folderIdFromParams || null);
|
||||
fetchFolderContents(folderIdFromParams || null);
|
||||
}, [folderIdFromParams, fetchFolderContents]);
|
||||
|
||||
const refreshFolderContents = useCallback(() => {
|
||||
fetchFolderContents(currentFolder);
|
||||
}, [fetchFolderContents, currentFolder]);
|
||||
|
||||
const handleFolderClick = (id: number) => {
|
||||
router.push(`/my-documents?folder=${id}`);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleCreateFolder = useCallback(
|
||||
async (name: string, description: string) => {
|
||||
try {
|
||||
const response = await fetch("/api/user/folder", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, description }),
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchFolders();
|
||||
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",
|
||||
});
|
||||
}
|
||||
},
|
||||
[fetchFolders, refreshFolderContents, setPopup]
|
||||
);
|
||||
|
||||
const handleUploadFiles = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files) {
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append("files", files[i]);
|
||||
}
|
||||
formData.append(
|
||||
"folder_id",
|
||||
currentFolder ? currentFolder.toString() : ""
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/user/file/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (response.ok) {
|
||||
refreshFolderContents();
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
setPage(1);
|
||||
}
|
||||
},
|
||||
[currentFolder, refreshFolderContents, setPopup, setPage]
|
||||
);
|
||||
|
||||
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) {
|
||||
if (isFolder) {
|
||||
fetchFolders();
|
||||
}
|
||||
refreshFolderContents();
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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_folder_id: destinationFolderId,
|
||||
[isFolder ? "folder_id" : "file_id"]: itemId,
|
||||
}),
|
||||
});
|
||||
if (response.ok) {
|
||||
refreshFolderContents();
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
if (isFolder) {
|
||||
fetchFolders();
|
||||
}
|
||||
refreshFolderContents();
|
||||
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="min-h-full w-full min-w-0 flex-1">
|
||||
<header className="flex bg-background w-full items-center justify-between gap-4 pl-11 pr-3 pt-2 md:pl-8 -translate-y-px">
|
||||
<h1 className=" flex items-center gap-1.5 text-lg font-medium leading-tight tracking-tight max-md:hidden">
|
||||
<Grid className="h-5 w-5" />
|
||||
My Documents
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="inline-flex items-center justify-center relative shrink-0 h-9 px-4 py-2 rounded-lg min-w-[5rem] active:scale-[0.985] whitespace-nowrap pl-2 pr-3 gap-1"
|
||||
onClick={refreshFolderContents}
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Refresh
|
||||
</Button>
|
||||
<CreateFolderPopover onCreateFolder={handleCreateFolder} />
|
||||
<label className="inline-flex items-center justify-center relative shrink-0 h-9 px-4 py-2 rounded-lg min-w-[5rem] active:scale-[0.985] whitespace-nowrap pl-2 pr-3 gap-1 cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90">
|
||||
<Upload className="h-5 w-5" />
|
||||
Upload Files
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleUploadFiles}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<main className="mx-auto mt-4 w-full max-w-7xl flex-1 px-4 pb-20 md:pl-8 lg:mt-6 md:pr-8 2xl:pr-14">
|
||||
<div className=" top-3 z-[5] flex gap-4 bg-gradient-to-b via-50% max-lg:flex-col lg:sticky lg:items-center">
|
||||
<div className="w-full md:max-w-96">
|
||||
<div className="bg-background-000 border border-border-200 hover:border-border-100 transition-colors placeholder:text-text-500 focus:border-accent-secondary-100 focus-within:!border-accent-secondary-100 focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 h-11 px-3 rounded-[0.6rem] w-full inline-flex cursor-text items-stretch gap-2">
|
||||
<div className="flex items-center">
|
||||
<Search className="h-4 w-4 text-text-400" />
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search documents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full placeholder:text-text-500 m-0 bg-transparent p-0 focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 items-center gap-3 md:flex lg:justify-end">
|
||||
<div className="flex items-center gap-0.5 max-md:mb-3">
|
||||
<IconButton
|
||||
icon={List}
|
||||
onClick={() => setView("list")}
|
||||
active={view === "list"}
|
||||
/>
|
||||
<IconButton
|
||||
icon={Grid}
|
||||
onClick={() => setView("grid")}
|
||||
active={view === "grid"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{presentingDocument && (
|
||||
<TextView
|
||||
presentingDocument={presentingDocument}
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
{popup}
|
||||
<div className="flex-grow">
|
||||
{folderContents ? (
|
||||
folderContents.folders.length > 0 ||
|
||||
folderContents.files.length > 0 ? (
|
||||
<div
|
||||
className={`mt-4 grid gap-3 md:mt-8 ${
|
||||
view === "grid" ? "md:grid-cols-2" : ""
|
||||
} md:gap-6`}
|
||||
>
|
||||
{folderContents.folders.map((folder) => (
|
||||
<a
|
||||
key={folder.id}
|
||||
className={`from-[#F9F8F4]/80 to-[#F7F6F0] border-0.5 border-border hover:from-[#F9F8F4] hover:to-[#F7F6F0] hover:border-border-200 text-md group relative flex cursor-pointer ${
|
||||
view === "list" ? "flex-row items-center" : "flex-col"
|
||||
} overflow-x-hidden text-ellipsis rounded-xl bg-gradient-to-b py-4 pl-5 pr-4 transition-all ease-in-out hover:shadow-sm active:scale-[0.98]`}
|
||||
href={`/my-documents?folder=${folder.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleFolderClick(folder.id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex ${
|
||||
view === "list" ? "flex-row items-center" : "flex-col"
|
||||
} flex-1`}
|
||||
>
|
||||
<div className="font-tiempos flex items-center">
|
||||
<Grid className="h-5 w-5 mr-2 text-yellow-500" />
|
||||
<span className="text-truncate inline-block max-w-md">
|
||||
{folder.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`text-text-400 ${
|
||||
view === "list" ? "ml-4" : "mt-1"
|
||||
} line-clamp-2 text-xs`}
|
||||
>
|
||||
{folder.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-text-500 mt-3 flex justify-between text-xs">
|
||||
|
||||
<span>
|
||||
Updated <span data-state="closed">5 months ago</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
{folderContents.files.map((file) => (
|
||||
<a
|
||||
key={file.id}
|
||||
className={`from-background-100 to-background-100/30 border-0.5 border-border-300 hover:from-background-000 hover:to-background-000/80 hover:border-border-200 text-md group relative flex cursor-pointer ${
|
||||
view === "list" ? "flex-row items-center" : "flex-col"
|
||||
} overflow-x-hidden text-ellipsis rounded-xl bg-gradient-to-b py-4 pl-5 pr-4 transition-all ease-in-out hover:shadow-sm active:scale-[0.98]`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPresentingDocument({
|
||||
document_id: file.document_id,
|
||||
semantic_identifier: file.name,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex ${
|
||||
view === "list" ? "flex-row items-center" : "flex-col"
|
||||
} flex-1`}
|
||||
>
|
||||
<div className="font-tiempos flex items-center">
|
||||
<List className="h-5 w-5 mr-2 text-blue-500" />
|
||||
<span className="text-truncate inline-block max-w-md">
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`text-text-300 ${
|
||||
view === "list" ? "ml-4" : "mt-1"
|
||||
} line-clamp-2 text-xs`}
|
||||
>
|
||||
Document ID: {file.document_id}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-text-500 mt-3 flex justify-between text-xs">
|
||||
|
||||
<span>
|
||||
Updated <span data-state="closed">5 months ago</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>No content in this folder</p>
|
||||
)
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
<div className="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
currentPage={page}
|
||||
totalPages={Math.ceil(
|
||||
((folderContents?.files?.length || 0) +
|
||||
(folderContents?.folders?.length || 0)) /
|
||||
pageLimit
|
||||
)}
|
||||
onPageChange={(newPage) => {
|
||||
setPage(newPage);
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Title from "@/components/ui/title";
|
||||
import SidebarWrapper from "../assistants/SidebarWrapper";
|
||||
import MyDocuments from "./MyDocuments";
|
||||
|
||||
export default function WrappedUserDocuments({
|
||||
initiallyToggled,
|
||||
}: {
|
||||
initiallyToggled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper size="lg" initiallyToggled={initiallyToggled}>
|
||||
<div className="mx-auto max-w-4xl w-full">
|
||||
<MyDocuments />
|
||||
</div>
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { FolderNode } from "./types";
|
||||
interface BreadcrumbProps {
|
||||
currentFolder: FolderNode | null;
|
||||
setCurrentFolder: React.Dispatch<React.SetStateAction<FolderNode | null>>;
|
||||
rootFolder: FolderNode;
|
||||
}
|
||||
|
||||
export const Breadcrumb: React.FC<BreadcrumbProps> = ({
|
||||
currentFolder,
|
||||
setCurrentFolder,
|
||||
rootFolder,
|
||||
}) => {
|
||||
const breadcrumbs = [];
|
||||
let folder: FolderNode | null = currentFolder;
|
||||
|
||||
while (folder) {
|
||||
breadcrumbs.unshift(folder);
|
||||
folder = folder.parent_id
|
||||
? findFolderById(rootFolder, folder.parent_id)
|
||||
: null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm">
|
||||
<span
|
||||
className="cursor-pointer hover:underline"
|
||||
onClick={() => setCurrentFolder(rootFolder)}
|
||||
>
|
||||
Root
|
||||
</span>
|
||||
{breadcrumbs.map((folder, index) => (
|
||||
<React.Fragment key={folder.id}>
|
||||
<ChevronRight className="mx-1 h-4 w-4 text-gray-400" />
|
||||
<span
|
||||
className="cursor-pointer hover:underline"
|
||||
onClick={() => setCurrentFolder(folder)}
|
||||
>
|
||||
{folder.name}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function findFolderById(root: FolderNode, id: number): FolderNode | null {
|
||||
if (root.id === id) return root;
|
||||
for (const child of root.children) {
|
||||
const found = findFolderById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
import { File as FileIcon } from "lucide-react";
|
||||
import { UserFile } from "./types";
|
||||
|
||||
interface FileListItemProps {
|
||||
file: UserFile;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
view: "grid" | "list";
|
||||
}
|
||||
|
||||
export const FileListItem: React.FC<FileListItemProps> = ({
|
||||
file,
|
||||
isSelected,
|
||||
onSelect,
|
||||
view,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`p-2 s${
|
||||
view === "grid"
|
||||
? "flex flex-col items-center"
|
||||
: "flex items-center hover:bg-gray-100 rounded cursor-pointer"
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div
|
||||
className={`flex w-full items-center ${
|
||||
view === "grid" ? "flex-col" : ""
|
||||
}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className={view === "grid" ? "ml-4 mb-2" : "mr-2"}
|
||||
/>
|
||||
<FileIcon
|
||||
className={`${
|
||||
view === "grid" ? "h-12 w-12 mb-2" : "h-5 w-5 mr-2"
|
||||
} text-gray-500`}
|
||||
/>
|
||||
<span
|
||||
className={`max-w-full text-sm truncate ${
|
||||
view === "grid" ? "text-center" : ""
|
||||
}`}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,339 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Grid, List, UploadIcon } from "lucide-react";
|
||||
import { FolderTreeItem } from "./FolderTreeItem";
|
||||
import { FileListItem } from "./FileListItem";
|
||||
import { Breadcrumb } from "./Breadcrumb";
|
||||
import { SelectedItemsList } from "./SelectedItemsList";
|
||||
import {
|
||||
FolderNode,
|
||||
UserFolder,
|
||||
UserFile,
|
||||
FilePickerModalProps,
|
||||
} from "./types";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const ListIcon = () => <List className="h-4 w-4" />;
|
||||
const GridIcon = () => <Grid className="h-4 w-4" />;
|
||||
|
||||
const IconButton: React.FC<{
|
||||
icon: React.ComponentType;
|
||||
onClick: () => void;
|
||||
active: boolean;
|
||||
}> = ({ icon: Icon, onClick, active }) => (
|
||||
<button
|
||||
className={`p-2 flex-none h-10 w-10 flex items-center justify-center rounded ${
|
||||
active ? "bg-gray-200" : "hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
);
|
||||
|
||||
function buildTree(folders: UserFolder[], files: UserFile[]): FolderNode {
|
||||
const folderMap: { [key: number]: FolderNode } = {};
|
||||
const rootNode: FolderNode = {
|
||||
id: 0,
|
||||
name: "Root",
|
||||
parent_id: null,
|
||||
children: [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
folders.forEach((folder) => {
|
||||
folderMap[folder.id] = { ...folder, children: [], files: [] };
|
||||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.parent_folder_id === null) {
|
||||
rootNode.files.push(file);
|
||||
} else if (folderMap[file.parent_folder_id]) {
|
||||
folderMap[file.parent_folder_id].files.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (folder.parent_id === null) {
|
||||
rootNode.children.push(folderMap[folder.id]);
|
||||
} else if (folderMap[folder.parent_id]) {
|
||||
folderMap[folder.parent_id].children.push(folderMap[folder.id]);
|
||||
}
|
||||
});
|
||||
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
export const FilePickerModal: React.FC<FilePickerModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
title,
|
||||
buttonContent,
|
||||
}) => {
|
||||
const [allFolders, setAllFolders] = useState<UserFolder[]>([]);
|
||||
const [allFiles, setAllFiles] = useState<UserFile[]>([]);
|
||||
const [fileSystem, setFileSystem] = useState<FolderNode | null>(null);
|
||||
const [currentFolder, setCurrentFolder] = useState<FolderNode | null>(null);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
||||
const [links, setLinks] = useState<string[]>([]);
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<{
|
||||
files: number[];
|
||||
folders: number[];
|
||||
}>({ files: [], folders: [] });
|
||||
const [view, setView] = useState<"grid" | "list">("list");
|
||||
|
||||
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_folder_id: f.parent_folder_id,
|
||||
}));
|
||||
|
||||
setAllFolders(folders);
|
||||
setAllFiles(files);
|
||||
|
||||
const tree = buildTree(folders, files);
|
||||
setFileSystem(tree);
|
||||
setCurrentFolder(tree);
|
||||
};
|
||||
if (isOpen) {
|
||||
loadFileSystem();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(selectedItems);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleRemoveSelectedItem = (type: "file" | "folder", id: number) => {
|
||||
setSelectedItems((prev) => ({
|
||||
...prev,
|
||||
[type === "file" ? "files" : "folders"]: prev[
|
||||
type === "file" ? "files" : "folders"
|
||||
].filter((itemId) => itemId !== id),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveUploadedFile = (name: string) => {
|
||||
setUploadedFiles((prev) => prev.filter((file) => file.name !== name));
|
||||
};
|
||||
|
||||
const handleFolderClick = (folder: FolderNode) => {
|
||||
setCurrentFolder(folder);
|
||||
};
|
||||
|
||||
const handleFileSelect = (fileId: number) => {
|
||||
setSelectedItems((prev) => ({
|
||||
...prev,
|
||||
files: prev.files.includes(fileId)
|
||||
? prev.files.filter((id) => id !== fileId)
|
||||
: [...prev.files, fileId],
|
||||
}));
|
||||
};
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files) {
|
||||
setUploadedFiles((prev) => [...prev, ...Array.from(files)]);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateTokens = () => {
|
||||
// This is a placeholder calculation. Replace with actual token calculation logic.
|
||||
return selectedItems.files.length * 10 + selectedItems.folders.length * 50;
|
||||
};
|
||||
|
||||
if (!fileSystem || !currentFolder) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideDividerForTitle
|
||||
onOutsideClick={onClose}
|
||||
className="max-w-4xl flex flex-col w-full !overflow-hidden h-[70vh]"
|
||||
title={title}
|
||||
>
|
||||
<div className="flex w-full items-center flex-col h-full">
|
||||
<div className="grid h-full grid-cols-2 overflow-y-hidden w-full">
|
||||
<div className="w-full pb-4 border-r overflow-y-auto">
|
||||
<div className="mb-4 flex gap-x-2 w-full">
|
||||
<div className="w-full relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search files and folders..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:border-transparent"
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
className="h-5 w-5 text-text-dark"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-2 flex space-x-2">
|
||||
<IconButton
|
||||
icon={ListIcon}
|
||||
onClick={() => setView("list")}
|
||||
active={view === "list"}
|
||||
/>
|
||||
<IconButton
|
||||
icon={GridIcon}
|
||||
onClick={() => setView("grid")}
|
||||
active={view === "grid"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<div
|
||||
className={`${view === "grid" ? "grid grid-cols-4 gap-4" : ""}`}
|
||||
>
|
||||
{currentFolder.children.map((folder) => (
|
||||
<div
|
||||
key={folder.id}
|
||||
className={` ${
|
||||
view === "grid"
|
||||
? "flex flex-col items-center"
|
||||
: "flex items-center"
|
||||
}`}
|
||||
onClick={() => handleFolderClick(folder)}
|
||||
>
|
||||
<FolderTreeItem
|
||||
node={folder}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
setCurrentFolder={setCurrentFolder}
|
||||
depth={0}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentFolder.files.map((file) => (
|
||||
<FileListItem
|
||||
key={file.id}
|
||||
file={file}
|
||||
isSelected={selectedItems.files.includes(file.id)}
|
||||
onSelect={() => handleFileSelect(file.id)}
|
||||
view={view}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* NOTE: update */}
|
||||
<div className="w-full px-4 pb-4 m-2 flex flex-col h-[450px] ">
|
||||
<div className="shrink flex h-full overflow-y-auto mb-1 ">
|
||||
<SelectedItemsList
|
||||
links={links}
|
||||
selectedItems={selectedItems}
|
||||
allFolders={allFolders}
|
||||
allFiles={allFiles}
|
||||
uploadedFiles={uploadedFiles}
|
||||
onRemove={handleRemoveSelectedItem}
|
||||
onRemoveUploadedFile={handleRemoveUploadedFile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="p-4 flex-none border rounded-lg bg-neutral-50">
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="cursor-pointer flex items-center justify-center space-x-2"
|
||||
>
|
||||
<UploadIcon className="w-5 h-5 text-gray-600" />
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Upload files
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm text-text-subtle">
|
||||
Add links to the context
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
className="flex gap-x-4 mt-2"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="w-full gap-x-2 flex">
|
||||
<input
|
||||
type="url"
|
||||
placeholder="Enter URL"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
onChange={(e) => {
|
||||
// Handle URL input change
|
||||
console.log(e.target.value);
|
||||
// You might want to add state to store this value
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
const input = e.currentTarget.form?.querySelector(
|
||||
'input[type="url"]'
|
||||
) as HTMLInputElement;
|
||||
if (input && input.value) {
|
||||
setLinks((prevLinks) => [...prevLinks, input.value]);
|
||||
input.value = "";
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4 flex-col w-full flex border-t mt-auto items-center justify-between">
|
||||
<div className="mb-4 font-medium text-lg text-text-dark">
|
||||
Total tokens: {calculateTokens()}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
className="text-lg"
|
||||
size="lg"
|
||||
onClick={handleSave}
|
||||
variant="default"
|
||||
>
|
||||
{buttonContent}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Folder as FolderIcon } from "lucide-react";
|
||||
import { FolderNode } from "./types";
|
||||
|
||||
interface FolderTreeItemProps {
|
||||
node: FolderNode;
|
||||
selectedItems: { files: number[]; folders: number[] };
|
||||
setSelectedItems: React.Dispatch<
|
||||
React.SetStateAction<{ files: number[]; folders: number[] }>
|
||||
>;
|
||||
setCurrentFolder: React.Dispatch<React.SetStateAction<FolderNode | null>>;
|
||||
depth: number;
|
||||
view: "grid" | "list";
|
||||
}
|
||||
|
||||
export const FolderTreeItem: React.FC<FolderTreeItemProps> = ({
|
||||
node,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
setCurrentFolder,
|
||||
depth,
|
||||
view,
|
||||
}) => {
|
||||
const isFolderSelected = selectedItems.folders.includes(node.id);
|
||||
|
||||
const handleFolderSelect = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItems((prev) => ({
|
||||
...prev,
|
||||
folders: isFolderSelected
|
||||
? prev.folders.filter((id) => id !== node.id)
|
||||
: [...prev.folders, node.id],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
className="from-[#F9F8F2] border border-border w-full to-[#F9F8F2]/30 border-0.5 border-border-300 hover:from-[#F9F8F2] hover:to-[#F9F8F2]/80 hover:border-border-200 text-md group relative flex cursor-pointer flex-col overflow-x-hidden text-ellipsis rounded-xl bg-gradient-to-b py-3 pl-5 pr-4 transition-all ease-in-out hover:shadow-sm "
|
||||
onClick={() => setCurrentFolder(node)}
|
||||
>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex">
|
||||
<span className="text-truncate text-text-dark inline-block max-w-md">
|
||||
{node.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-text-500 mt-1 line-clamp-2 text-xs">
|
||||
This folder contains 1000 files and describes the state of the company
|
||||
{/* Add folder description or other details here */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-text-500 mt-1 flex justify-between text-xs">
|
||||
|
||||
<span>
|
||||
Updated <span data-state="closed">47 minutes ago</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
/* Original implementation commented out
|
||||
<div
|
||||
className={` p-2 w-full ${
|
||||
view === "grid"
|
||||
? "flex flex-col rounded items-center"
|
||||
: "flex items-center hover:bg-gray-100 rounded-gl cursor-pointer"
|
||||
}`}
|
||||
onClick={() => setCurrentFolder(node)}
|
||||
>
|
||||
<div
|
||||
className={`flex overflow-hidden w-full items-center ${
|
||||
view === "grid" ? "flex-col" : ""
|
||||
}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isFolderSelected}
|
||||
onCheckedChange={() => {}}
|
||||
onClick={handleFolderSelect}
|
||||
className={view === "grid" ? "my-1" : "mr-2"}
|
||||
/>
|
||||
<FolderIcon
|
||||
className={`${
|
||||
view === "grid" ? "h-12 w-12 mb-2" : "h-5 w-5 mr-2"
|
||||
} text-blue-500`}
|
||||
/>
|
||||
<span
|
||||
className={`max-w-full text-sm truncate ${
|
||||
view === "grid" ? "text-center" : ""
|
||||
}`}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
*/
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X } from "lucide-react";
|
||||
import { UserFolder, UserFile } from "./types";
|
||||
|
||||
interface SelectedItemsListProps {
|
||||
uploadedFiles: File[];
|
||||
selectedItems: { files: number[]; folders: number[] };
|
||||
allFolders: UserFolder[];
|
||||
allFiles: UserFile[];
|
||||
onRemove: (type: "file" | "folder", id: number) => void;
|
||||
onRemoveUploadedFile: (name: string) => void;
|
||||
links: string[];
|
||||
}
|
||||
|
||||
export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
|
||||
links,
|
||||
uploadedFiles,
|
||||
selectedItems,
|
||||
allFolders,
|
||||
allFiles,
|
||||
onRemove,
|
||||
onRemoveUploadedFile,
|
||||
}) => {
|
||||
const selectedFolders = allFolders.filter((folder) =>
|
||||
selectedItems.folders.includes(folder.id)
|
||||
);
|
||||
const selectedFiles = allFiles.filter((file) =>
|
||||
selectedItems.files.includes(file.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<h3 className="font-semibold mb-2">Selected Items</h3>
|
||||
<div className="w-full overflow-y-auto border-t border-t-text-subtle flex-grow">
|
||||
<div className="space-y-2">
|
||||
{links.map((link: string) => (
|
||||
<div
|
||||
key={link}
|
||||
className="flex w-full items-center justify-between bg-gray-100 p-1.5 rounded"
|
||||
>
|
||||
<span className="text-sm">{link}</span>
|
||||
<Button variant="ghost" size="sm">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{uploadedFiles.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
className="flex w-full items-center justify-between bg-gray-100 p-1.5 rounded"
|
||||
>
|
||||
<span className="text-sm">
|
||||
{file.name}{" "}
|
||||
<span className="text-xs w-full truncate text-gray-500">
|
||||
(uploaded)
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveUploadedFile(file.name)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{selectedFolders.map((folder) => (
|
||||
<div
|
||||
key={folder.id}
|
||||
className="flex items-center justify-between bg-gray-100 p-2 rounded"
|
||||
>
|
||||
<span className="text-sm">{folder.name}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemove("folder", folder.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{selectedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between bg-gray-100 p-2 rounded"
|
||||
>
|
||||
<span className="w-full truncate text-sm">{file.name}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemove("file", file.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
export interface UserFolder {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_id: number | null;
|
||||
}
|
||||
|
||||
export interface UserFile {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_folder_id: number | null;
|
||||
}
|
||||
|
||||
export interface FolderNode extends UserFolder {
|
||||
children: FolderNode[];
|
||||
files: UserFile[];
|
||||
}
|
||||
|
||||
export interface FilePickerModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (selectedItems: { files: number[]; folders: number[] }) => void;
|
||||
title: string;
|
||||
buttonContent: string;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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,
|
||||
folders,
|
||||
inputPrompts,
|
||||
openedFolders,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
folders,
|
||||
toggledSidebar: false,
|
||||
inputPrompts,
|
||||
openedFolders,
|
||||
}}
|
||||
>
|
||||
<WrappedDocuments initiallyToggled={toggleSidebar} />
|
||||
</ChatProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -116,7 +116,7 @@ export function Modal({
|
||||
{icon && icon({ size: 30 })}
|
||||
</h2>
|
||||
</div>
|
||||
{!hideDividerForTitle ? <Separator /> : <div className="my-4" />}
|
||||
{!hideDividerForTitle && <Separator />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Download, XIcon, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { MinimalMarkdown } from "./MinimalMarkdown";
|
||||
|
||||
interface TextViewProps {
|
||||
presentingDocument: MinimalOnyxDocument;
|
||||
presentingDocument: OnyxDocument;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,13 @@ const SelectItem = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!hideCheck && (
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
)}
|
||||
{!selected && Icon && (
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Icon className="h-4 w-4" />
|
||||
|
||||
@@ -13,12 +13,16 @@ import { errorHandlingFetcher } from "./fetcher";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
import { Filters, SourceMetadata } from "./search/interfaces";
|
||||
import { destructureValue, structureValue } from "./llm/utils";
|
||||
import {
|
||||
destructureValue,
|
||||
getLLMProviderOverrideForPersona,
|
||||
structureValue,
|
||||
} from "./llm/utils";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { AllUsersResponse } from "./types";
|
||||
import { Credential } from "./connectors/credentials";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { PersonaLabel } from "@/app/admin/assistants/interfaces";
|
||||
import { Persona, PersonaLabel } from "@/app/admin/assistants/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { isAnthropic } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { getSourceMetadata } from "./sources";
|
||||
@@ -367,7 +371,8 @@ export function useLlmOverride(
|
||||
llmProviders: LLMProviderDescriptor[],
|
||||
globalModel?: string | null,
|
||||
currentChatSession?: ChatSession,
|
||||
defaultTemperature?: number
|
||||
defaultTemperature?: number,
|
||||
assistants?: Persona[]
|
||||
): LlmOverrideManager {
|
||||
const getValidLlmOverride = (
|
||||
overrideModel: string | null | undefined
|
||||
@@ -424,6 +429,32 @@ export function useLlmOverride(
|
||||
defaultTemperature !== undefined ? defaultTemperature : 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPersona = assistants?.find(
|
||||
(a) => a.id === currentChatSession?.persona_id
|
||||
);
|
||||
const personaDefault = currentPersona
|
||||
? getLLMProviderOverrideForPersona(currentPersona, llmProviders)
|
||||
: undefined;
|
||||
|
||||
if (personaDefault) {
|
||||
updateLLMOverride(personaDefault);
|
||||
} else {
|
||||
updateLLMOverride(globalDefault);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentChatSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentChatSession?.current_alternate_model) {
|
||||
setLlmOverride(
|
||||
getValidLlmOverride(currentChatSession.current_alternate_model)
|
||||
);
|
||||
} else {
|
||||
setLlmOverride(globalDefault);
|
||||
}
|
||||
}, [currentChatSession]);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalDefault(getValidLlmOverride(globalModel));
|
||||
}, [globalModel, llmProviders]);
|
||||
|
||||
@@ -44,15 +44,13 @@ export interface Quote {
|
||||
export interface QuotesInfoPacket {
|
||||
quotes: Quote[];
|
||||
}
|
||||
export interface MinimalOnyxDocument {
|
||||
document_id: string;
|
||||
semantic_identifier: string | null;
|
||||
}
|
||||
|
||||
export interface OnyxDocument extends MinimalOnyxDocument {
|
||||
export interface OnyxDocument {
|
||||
document_id: string;
|
||||
link: string;
|
||||
source_type: ValidSources;
|
||||
blurb: string;
|
||||
semantic_identifier: string | null;
|
||||
boost: number;
|
||||
hidden: boolean;
|
||||
score: number;
|
||||
|
||||
Reference in New Issue
Block a user