Compare commits

..

1 Commits

Author SHA1 Message Date
pablodanswer
171deb495e fix model defaults 2025-01-27 21:22:45 -08:00
33 changed files with 148 additions and 2870 deletions

View File

@@ -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")

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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
):

View File

@@ -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)

View File

@@ -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

View File

@@ -411,7 +411,7 @@ class FileUploadResponse(BaseModel):
class ObjectCreationIdResponse(BaseModel):
id: int
id: int | str
credential: CredentialSnapshot | None = None

View File

@@ -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)

View 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]

View File

@@ -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 />

View File

@@ -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}

View File

@@ -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"}
/>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 &quot;{fileName}&quot;
</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>
);
}

View File

@@ -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>
);
}

View File

@@ -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">
&nbsp;
<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">
&nbsp;
<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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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">
&nbsp;
<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>
*/
}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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,
};
};

View File

@@ -116,7 +116,7 @@ export function Modal({
{icon && icon({ size: 30 })}
</h2>
</div>
{!hideDividerForTitle ? <Separator /> : <div className="my-4" />}
{!hideDividerForTitle && <Separator />}
</>
)}
</div>

View File

@@ -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;
}

View File

@@ -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" />

View File

@@ -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]);

View File

@@ -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;