From 6bf5847b1804ee2d4c75aab74130dfa2f3b02566 Mon Sep 17 00:00:00 2001 From: Danylo Boiko <55975773+danylo-boiko@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:12:53 +0200 Subject: [PATCH] toolkit: add file content viewer (#825) * Add icons * Run format-web for MessageRow * Add handlers * Fetch conv files * Fetch agent files * Add unit tests * Generate web client * Add API calls * Add content for FileViewer * Add padding settings for modals * Refactor styles * Minor clean up * Run format-web * Merge AgentFileFull and ConversationFileFull * Add error message --- src/backend/crud/file.py | 9 ++- src/backend/routers/agent.py | 57 ++++++++++++- src/backend/routers/conversation.py | 45 ++++++++++- src/backend/schemas/file.py | 11 ++- src/backend/services/file.py | 69 ++++++++++------ .../tests/unit/routers/test_conversation.py | 51 ++++++++++++ .../src/cohere-client/client.ts | 20 +++++ .../cohere-client/generated/schemas.gen.ts | 36 +++++++++ .../cohere-client/generated/services.gen.ts | 78 ++++++++++++++++++ .../src/cohere-client/generated/types.gen.ts | 49 ++++++++++++ .../Conversation/ConversationPanel.tsx | 80 +++++++++++++++---- .../src/components/UI/FileViewer.tsx | 37 +++++++++ .../src/components/UI/Modal.tsx | 5 +- .../src/components/UI/Tooltip.tsx | 2 +- .../src/context/ModalContext.tsx | 22 +++-- .../assistants_web/src/hooks/use-files.ts | 23 ++++++ 16 files changed, 538 insertions(+), 56 deletions(-) create mode 100644 src/interfaces/assistants_web/src/components/UI/FileViewer.tsx diff --git a/src/backend/crud/file.py b/src/backend/crud/file.py index ab9e114112..51788e8c0e 100644 --- a/src/backend/crud/file.py +++ b/src/backend/crud/file.py @@ -35,7 +35,7 @@ def batch_create_files(db: Session, files: list[File]) -> list[File]: @validate_transaction -def get_file(db: Session, file_id: str, user_id: str) -> File: +def get_file(db: Session, file_id: str, user_id: str | None = None) -> File: """ Get a file by ID. @@ -47,7 +47,12 @@ def get_file(db: Session, file_id: str, user_id: str) -> File: Returns: File: File with the given ID. """ - return db.query(File).filter(File.id == file_id, File.user_id == user_id).first() + filters = [File.id == file_id] + + if user_id: + filters.append(File.user_id == user_id) + + return db.query(File).filter(*filters).first() @validate_transaction diff --git a/src/backend/routers/agent.py b/src/backend/routers/agent.py index 0be7784d8f..65c8d19efa 100644 --- a/src/backend/routers/agent.py +++ b/src/backend/routers/agent.py @@ -8,6 +8,7 @@ from backend.config.routers import RouterName from backend.crud import agent as agent_crud from backend.crud import agent_tool_metadata as agent_tool_metadata_crud +from backend.crud import file as file_crud from backend.crud import snapshot as snapshot_crud from backend.database_models.agent import Agent as AgentModel from backend.database_models.agent_tool_metadata import ( @@ -34,7 +35,11 @@ ) from backend.schemas.context import Context from backend.schemas.deployment import Deployment as DeploymentSchema -from backend.schemas.file import DeleteAgentFileResponse, UploadAgentFileResponse +from backend.schemas.file import ( + DeleteAgentFileResponse, + FileMetadata, + UploadAgentFileResponse, +) from backend.services.agent import ( raise_db_error, validate_agent_exists, @@ -583,6 +588,54 @@ async def batch_upload_file( return uploaded_files +@router.get("/{agent_id}/files/{file_id}", response_model=FileMetadata) +async def get_agent_file( + agent_id: str, + file_id: str, + session: DBSessionDep, + ctx: Context = Depends(get_context), +) -> FileMetadata: + """ + Get an agent file by ID. + + Args: + agent_id (str): Agent ID. + file_id (str): File ID. + session (DBSessionDep): Database session. + ctx (Context): Context object. + + Returns: + FileMetadata: File with the given ID. + + Raises: + HTTPException: If the agent or file with the given ID is not found, or if the file does not belong to the agent. + """ + user_id = ctx.get_user_id() + + if file_id not in get_file_service().get_file_ids_by_agent_id(session, user_id, agent_id, ctx): + raise HTTPException( + status_code=404, + detail=f"File with ID: {file_id} does not belong to the agent with ID: {agent_id}." + ) + + file = file_crud.get_file(session, file_id) + + if not file: + raise HTTPException( + status_code=404, + detail=f"File with ID: {file_id} not found.", + ) + + return FileMetadata( + id=file.id, + file_name=file.file_name, + file_content=file.file_content, + file_size=file.file_size, + created_at=file.created_at, + updated_at=file.updated_at, + ) + + @router.delete("/{agent_id}/files/{file_id}") async def delete_agent_file( agent_id: str, @@ -605,7 +658,7 @@ async def delete_agent_file( HTTPException: If the agent with the given ID is not found. """ user_id = ctx.get_user_id() - _ = validate_agent_exists(session, agent_id) + _ = validate_agent_exists(session, agent_id, user_id) validate_file(session, file_id, user_id) # Delete the File DB object diff --git a/src/backend/routers/conversation.py b/src/backend/routers/conversation.py index 2909788a12..537bbe798f 100644 --- a/src/backend/routers/conversation.py +++ b/src/backend/routers/conversation.py @@ -24,6 +24,7 @@ ) from backend.schemas.file import ( DeleteConversationFileResponse, + FileMetadata, ListConversationFile, UploadConversationFileResponse, ) @@ -461,6 +462,47 @@ async def list_files( return files_with_conversation_id +@router.get("/{conversation_id}/files/{file_id}", response_model=FileMetadata) +async def get_file( + conversation_id: str, file_id: str, session: DBSessionDep, ctx: Context = Depends(get_context) +) -> FileMetadata: + """ + Get a conversation file by ID. + + Args: + conversation_id (str): Conversation ID. + file_id (str): File ID. + session (DBSessionDep): Database session. + ctx (Context): Context object. + + Returns: + FileMetadata: File with the given ID. + + Raises: + HTTPException: If the conversation or file with the given ID is not found, or if the file does not belong to the conversation. + """ + user_id = ctx.get_user_id() + + conversation = validate_conversation(session, conversation_id, user_id) + + if file_id not in conversation.file_ids: + raise HTTPException( + status_code=404, + detail=f"File with ID: {file_id} does not belong to the conversation with ID: {conversation.id}." + ) + + file = validate_file(session, file_id, user_id) + + return FileMetadata( + id=file.id, + file_name=file.file_name, + file_content=file.file_content, + file_size=file.file_size, + created_at=file.created_at, + updated_at=file.updated_at, + ) + + @router.delete("/{conversation_id}/files/{file_id}") async def delete_file( conversation_id: str, @@ -484,8 +526,7 @@ async def delete_file( """ user_id = ctx.get_user_id() _ = validate_conversation(session, conversation_id, user_id) - validate_file(session, file_id, user_id ) - + validate_file(session, file_id, user_id) # Delete the File DB object get_file_service().delete_conversation_file_by_id( session, conversation_id, file_id, user_id, ctx diff --git a/src/backend/schemas/file.py b/src/backend/schemas/file.py index 8839bfebfb..affd632be6 100644 --- a/src/backend/schemas/file.py +++ b/src/backend/schemas/file.py @@ -30,7 +30,6 @@ class ConversationFilePublic(BaseModel): file_size: int = Field(default=0, ge=0) - class AgentFilePublic(BaseModel): id: str created_at: datetime.datetime @@ -39,6 +38,16 @@ class AgentFilePublic(BaseModel): file_name: str file_size: int = Field(default=0, ge=0) + +class FileMetadata(BaseModel): + id: str + file_name: str + file_content: str + file_size: int = Field(default=0, ge=0) + created_at: datetime.datetime + updated_at: datetime.datetime + + class ListConversationFile(ConversationFilePublic): pass diff --git a/src/backend/services/file.py b/src/backend/services/file.py index 3c4a702dbb..cca8067561 100644 --- a/src/backend/services/file.py +++ b/src/backend/services/file.py @@ -119,49 +119,66 @@ async def create_agent_files( return uploaded_files - def get_files_by_agent_id( + def get_file_ids_by_agent_id( self, session: DBSessionDep, user_id: str, agent_id: str, ctx: Context - ) -> list[File]: + ) -> list[str]: """ - Get files by agent ID + Get file IDs associated with a specific agent ID Args: session (DBSessionDep): The database session user_id (str): The user ID agent_id (str): The agent ID + ctx (Context): Context object Returns: - list[File]: The files that were created + list[str]: IDs of files that were created """ from backend.config.tools import Tool from backend.tools.files import FileToolsArtifactTypes agent = validate_agent_exists(session, agent_id, user_id) - files = [] - agent_tool_metadata = agent.tools_metadata - if agent_tool_metadata is not None and len(agent_tool_metadata) > 0: - artifacts = next( - ( - tool_metadata.artifacts - for tool_metadata in agent_tool_metadata - if tool_metadata.tool_name == Tool.Read_File.value.ID - or tool_metadata.tool_name == Tool.Search_File.value.ID - ), - [], # Default value if the generator is empty - ) + if not agent.tools_metadata: + return [] + + artifacts = next( + ( + tool_metadata.artifacts + for tool_metadata in agent.tools_metadata + if tool_metadata.tool_name == Tool.Read_File.value.ID + or tool_metadata.tool_name == Tool.Search_File.value.ID + ), + [], # Default value if the generator is empty + ) - file_ids = list( - { - artifact.get("id") - for artifact in artifacts - if artifact.get("type") == FileToolsArtifactTypes.local_file - } - ) + return [ + artifact.get("id") + for artifact in artifacts + if artifact.get("type") == FileToolsArtifactTypes.local_file + ] - files = file_crud.get_files_by_ids(session, file_ids, user_id) + def get_files_by_agent_id( + self, session: DBSessionDep, user_id: str, agent_id: str, ctx: Context + ) -> list[File]: + """ + Get files by agent ID - return files + Args: + session (DBSessionDep): The database session + user_id (str): The user ID + agent_id (str): The agent ID + ctx (Context): Context object + + Returns: + list[File]: The files that were created + """ + file_ids = self.get_file_ids_by_agent_id(session, user_id, agent_id, ctx) + + if not file_ids: + return [] + + return file_crud.get_files_by_ids(session, file_ids, user_id) def get_files_by_conversation_id( self, session: DBSessionDep, user_id: str, conversation_id: str, ctx: Context @@ -312,6 +329,8 @@ def validate_file( detail=f"File with ID: {file_id} not found.", ) + return file + async def insert_files_in_db( session: DBSessionDep, diff --git a/src/backend/tests/unit/routers/test_conversation.py b/src/backend/tests/unit/routers/test_conversation.py index 28b4a917f8..915ff5df16 100644 --- a/src/backend/tests/unit/routers/test_conversation.py +++ b/src/backend/tests/unit/routers/test_conversation.py @@ -583,6 +583,57 @@ def test_list_files_missing_user_id( assert response.json() == {"detail": "User-Id required in request headers."} +def test_get_file( + session_client: TestClient, session: Session, user: User +) -> None: + conversation = get_factory("Conversation", session).create(user_id=user.id) + response = session_client.post( + "/v1/conversations/batch_upload_file", + headers={"User-Id": conversation.user_id}, + files=[ + ("files", ("Mariana_Trench.pdf", open("src/backend/tests/unit/test_data/Mariana_Trench.pdf", "rb"))) + ], + data={"conversation_id": conversation.id}, + ) + assert response.status_code == 200 + uploaded_file = response.json()[0] + + response = session_client.get( + f"/v1/conversations/{conversation.id}/files/{uploaded_file['id']}", + headers={"User-Id": conversation.user_id}, + ) + + assert response.status_code == 200 + response_file = response.json() + assert response_file["id"] == uploaded_file["id"] + assert response_file["file_name"] == uploaded_file["file_name"] + + +def test_fail_get_file_nonexistent_conversation( + session_client: TestClient, session: Session, user: User +) -> None: + response = session_client.get( + "/v1/conversations/123/files/456", + headers={"User-Id": user.id}, + ) + + assert response.status_code == 404 + assert response.json() == {"detail": "Conversation with ID: 123 not found."} + + +def test_fail_get_file_nonbelong_file( + session_client: TestClient, session: Session, user: User +) -> None: + conversation = get_factory("Conversation", session).create(user_id=user.id) + response = session_client.get( + f"/v1/conversations/{conversation.id}/files/123", + headers={"User-Id": conversation.user_id}, + ) + + assert response.status_code == 404 + assert response.json() == {"detail": f"File with ID: 123 does not belong to the conversation with ID: {conversation.id}."} + + def test_batch_upload_file_existing_conversation( session_client: TestClient, session: Session, user ) -> None: diff --git a/src/interfaces/assistants_web/src/cohere-client/client.ts b/src/interfaces/assistants_web/src/cohere-client/client.ts index 43d182f69c..2f45f1f9de 100644 --- a/src/interfaces/assistants_web/src/cohere-client/client.ts +++ b/src/interfaces/assistants_web/src/cohere-client/client.ts @@ -53,6 +53,19 @@ export class CohereClient { }); } + public getConversationFile({ + conversationId, + fileId, + }: { + conversationId: string; + fileId: string; + }) { + return this.cohereService.default.getFileV1ConversationsConversationIdFilesFileIdGet({ + conversationId, + fileId, + }); + } + public batchUploadConversationFile( formData: Body_batch_upload_file_v1_conversations_batch_upload_file_post ) { @@ -61,6 +74,13 @@ export class CohereClient { }); } + public getAgentFile({ agentId, fileId }: { agentId: string; fileId: string }) { + return this.cohereService.default.getAgentFileV1AgentsAgentIdFilesFileIdGet({ + agentId, + fileId, + }); + } + public batchUploadAgentFile(formData: Body_batch_upload_file_v1_agents_batch_upload_file_post) { return this.cohereService.default.batchUploadFileV1AgentsBatchUploadFilePost({ formData, diff --git a/src/interfaces/assistants_web/src/cohere-client/generated/schemas.gen.ts b/src/interfaces/assistants_web/src/cohere-client/generated/schemas.gen.ts index dbe174354b..d517eda38d 100644 --- a/src/interfaces/assistants_web/src/cohere-client/generated/schemas.gen.ts +++ b/src/interfaces/assistants_web/src/cohere-client/generated/schemas.gen.ts @@ -1516,6 +1516,42 @@ export const $Email = { title: 'Email', } as const; +export const $FileMetadata = { + properties: { + id: { + type: 'string', + title: 'Id', + }, + file_name: { + type: 'string', + title: 'File Name', + }, + file_content: { + type: 'string', + title: 'File Content', + }, + file_size: { + type: 'integer', + minimum: 0, + title: 'File Size', + default: 0, + }, + created_at: { + type: 'string', + format: 'date-time', + title: 'Created At', + }, + updated_at: { + type: 'string', + format: 'date-time', + title: 'Updated At', + }, + }, + type: 'object', + required: ['id', 'file_name', 'file_content', 'created_at', 'updated_at'], + title: 'FileMetadata', +} as const; + export const $GenerateTitleResponse = { properties: { title: { diff --git a/src/interfaces/assistants_web/src/cohere-client/generated/services.gen.ts b/src/interfaces/assistants_web/src/cohere-client/generated/services.gen.ts index 2dec2e21ed..a84f49c81e 100644 --- a/src/interfaces/assistants_web/src/cohere-client/generated/services.gen.ts +++ b/src/interfaces/assistants_web/src/cohere-client/generated/services.gen.ts @@ -63,10 +63,14 @@ import type { GetAgentByIdV1AgentsAgentIdGetResponse, GetAgentDeploymentV1AgentsAgentIdDeploymentsGetData, GetAgentDeploymentV1AgentsAgentIdDeploymentsGetResponse, + GetAgentFileV1AgentsAgentIdFilesFileIdGetData, + GetAgentFileV1AgentsAgentIdFilesFileIdGetResponse, GetConversationV1ConversationsConversationIdGetData, GetConversationV1ConversationsConversationIdGetResponse, GetDeploymentV1DeploymentsDeploymentIdGetData, GetDeploymentV1DeploymentsDeploymentIdGetResponse, + GetFileV1ConversationsConversationIdFilesFileIdGetData, + GetFileV1ConversationsConversationIdFilesFileIdGetResponse, GetGroupScimV2GroupsGroupIdGetData, GetGroupScimV2GroupsGroupIdGetResponse, GetGroupsScimV2GroupsGetData, @@ -868,6 +872,43 @@ export class DefaultService { }); } + /** + * Get File + * Get a conversation file by ID. + * + * Args: + * conversation_id (str): Conversation ID. + * file_id (str): File ID. + * session (DBSessionDep): Database session. + * ctx (Context): Context object. + * + * Returns: + * FileMetadata: File with the given ID. + * + * Raises: + * HTTPException: If the conversation or file with the given ID is not found, or if the file does not belong to the conversation. + * @param data The data for the request. + * @param data.conversationId + * @param data.fileId + * @returns FileMetadata Successful Response + * @throws ApiError + */ + public getFileV1ConversationsConversationIdFilesFileIdGet( + data: GetFileV1ConversationsConversationIdFilesFileIdGetData + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/v1/conversations/{conversation_id}/files/{file_id}', + path: { + conversation_id: data.conversationId, + file_id: data.fileId, + }, + errors: { + 422: 'Validation Error', + }, + }); + } + /** * Delete File * Delete a file by ID. @@ -1597,6 +1638,43 @@ export class DefaultService { }); } + /** + * Get Agent File + * Get an agent file by ID. + * + * Args: + * agent_id (str): Agent ID. + * file_id (str): File ID. + * session (DBSessionDep): Database session. + * ctx (Context): Context object. + * + * Returns: + * FileMetadata: File with the given ID. + * + * Raises: + * HTTPException: If the agent or file with the given ID is not found, or if the file does not belong to the agent. + * @param data The data for the request. + * @param data.agentId + * @param data.fileId + * @returns FileMetadata Successful Response + * @throws ApiError + */ + public getAgentFileV1AgentsAgentIdFilesFileIdGet( + data: GetAgentFileV1AgentsAgentIdFilesFileIdGetData + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/v1/agents/{agent_id}/files/{file_id}', + path: { + agent_id: data.agentId, + file_id: data.fileId, + }, + errors: { + 422: 'Validation Error', + }, + }); + } + /** * Delete Agent File * Delete an agent file by ID. diff --git a/src/interfaces/assistants_web/src/cohere-client/generated/types.gen.ts b/src/interfaces/assistants_web/src/cohere-client/generated/types.gen.ts index 46e38e6459..860fee09a0 100644 --- a/src/interfaces/assistants_web/src/cohere-client/generated/types.gen.ts +++ b/src/interfaces/assistants_web/src/cohere-client/generated/types.gen.ts @@ -296,6 +296,15 @@ export type Email = { type: string; }; +export type FileMetadata = { + id: string; + file_name: string; + file_content: string; + file_size?: number; + created_at: string; + updated_at: string; +}; + export type GenerateTitleResponse = { title: string; error?: string | null; @@ -910,6 +919,13 @@ export type ListFilesV1ConversationsConversationIdFilesGetData = { export type ListFilesV1ConversationsConversationIdFilesGetResponse = Array; +export type GetFileV1ConversationsConversationIdFilesFileIdGetData = { + conversationId: string; + fileId: string; +}; + +export type GetFileV1ConversationsConversationIdFilesFileIdGetResponse = FileMetadata; + export type DeleteFileV1ConversationsConversationIdFilesFileIdDeleteData = { conversationId: string; fileId: string; @@ -1059,6 +1075,13 @@ export type BatchUploadFileV1AgentsBatchUploadFilePostData = { export type BatchUploadFileV1AgentsBatchUploadFilePostResponse = Array; +export type GetAgentFileV1AgentsAgentIdFilesFileIdGetData = { + agentId: string; + fileId: string; +}; + +export type GetAgentFileV1AgentsAgentIdFilesFileIdGetResponse = FileMetadata; + export type DeleteAgentFileV1AgentsAgentIdFilesFileIdDeleteData = { agentId: string; fileId: string; @@ -1536,6 +1559,19 @@ export type $OpenApiTs = { }; }; '/v1/conversations/{conversation_id}/files/{file_id}': { + get: { + req: GetFileV1ConversationsConversationIdFilesFileIdGetData; + res: { + /** + * Successful Response + */ + 200: FileMetadata; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; delete: { req: DeleteFileV1ConversationsConversationIdFilesFileIdDeleteData; res: { @@ -1847,6 +1883,19 @@ export type $OpenApiTs = { }; }; '/v1/agents/{agent_id}/files/{file_id}': { + get: { + req: GetAgentFileV1AgentsAgentIdFilesFileIdGetData; + res: { + /** + * Successful Response + */ + 200: FileMetadata; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; delete: { req: DeleteAgentFileV1AgentsAgentIdFilesFileIdDeleteData; res: { diff --git a/src/interfaces/assistants_web/src/components/Conversation/ConversationPanel.tsx b/src/interfaces/assistants_web/src/components/Conversation/ConversationPanel.tsx index ac3dd76fdd..3bc3a0dd54 100644 --- a/src/interfaces/assistants_web/src/components/Conversation/ConversationPanel.tsx +++ b/src/interfaces/assistants_web/src/components/Conversation/ConversationPanel.tsx @@ -5,7 +5,9 @@ import { uniqBy } from 'lodash'; import { useMemo, useState } from 'react'; import { Banner, Button, Icon, IconButton, Text, Tooltip } from '@/components/UI'; +import { FileViewer } from '@/components/UI/FileViewer'; import { TOOL_GOOGLE_DRIVE_ID, TOOL_READ_DOCUMENT_ID, TOOL_SEARCH_FILE_ID } from '@/constants'; +import { useContextStore } from '@/context'; import { useAgent, useBrandedColors, @@ -26,6 +28,7 @@ export const ConversationPanel: React.FC = () => { const { agentId, conversationId } = useChatRoutes(); const { data: agent } = useAgent({ agentId }); const { theme } = useBrandedColors(agentId); + const { open } = useContextStore(); const { params: { fileIds }, @@ -69,6 +72,27 @@ export const ConversationPanel: React.FC = () => { ...agentToolMetadataArtifacts.folders, ]; + const handleOpenFile = ({ + fileId, + agentId, + conversationId, + url, + }: { + fileId: string; + agentId?: string; + conversationId?: string; + url?: string; + }) => { + if (url) { + window.open(url, '_blank'); + } else { + open({ + content: , + dialogPaddingClassName: 'p-5', + }); + } + }; + const handleDeleteFile = async (fileId: string) => { if (isDeletingFile || !conversationId) return; @@ -158,13 +182,26 @@ export const ConversationPanel: React.FC = () => {
    {agentKnowledgeFiles.map((file) => ( -
  1. - + + + {file.name} + + + handleOpenFile({ fileId: file.id, agentId: agent!.id, url: file.url }) + } /> - {file.name}
  2. ))}
@@ -187,11 +224,8 @@ export const ConversationPanel: React.FC = () => { {files && files.length > 0 && (
- {files.map(({ file_name: name, id }) => ( -
+ {files.map(({ id, conversation_id, file_name: name }) => ( +
= () => { /> {name}
- handleDeleteFile(id)} - disabled={isDeletingFile} - iconName="close" - className="invisible group-hover:visible" - /> +
+ + handleOpenFile({ fileId: id, conversationId: conversation_id }) + } + /> + handleDeleteFile(id)} + /> +
))} diff --git a/src/interfaces/assistants_web/src/components/UI/FileViewer.tsx b/src/interfaces/assistants_web/src/components/UI/FileViewer.tsx new file mode 100644 index 0000000000..76df78fd8b --- /dev/null +++ b/src/interfaces/assistants_web/src/components/UI/FileViewer.tsx @@ -0,0 +1,37 @@ +import { Markdown } from '@/components/Markdown'; +import { Icon } from '@/components/UI/Icon'; +import { Spinner } from '@/components/UI/Spinner'; +import { Text } from '@/components/UI/Text'; +import { useFile } from '@/hooks'; + +type Props = { + fileId: string; + agentId?: string; + conversationId?: string; +}; + +export const FileViewer: React.FC = ({ fileId, agentId, conversationId }) => { + const { data: file, isLoading } = useFile({ fileId, agentId, conversationId }); + + if (isLoading) { + return ; + } + + return ( +
+
+
+ +
+ + {file?.file_name ?? 'Failed to load file content'} + +
+ {file && ( +
+ +
+ )} +
+ ); +}; diff --git a/src/interfaces/assistants_web/src/components/UI/Modal.tsx b/src/interfaces/assistants_web/src/components/UI/Modal.tsx index 0271f6c345..b45232d249 100644 --- a/src/interfaces/assistants_web/src/components/UI/Modal.tsx +++ b/src/interfaces/assistants_web/src/components/UI/Modal.tsx @@ -11,6 +11,7 @@ type ModalProps = { title?: string; children?: React.ReactNode; onClose?: VoidFunction; + dialogPaddingClassName?: string; }; /** @@ -21,6 +22,7 @@ export const Modal: React.FC = ({ isOpen, children, onClose = () => {}, + dialogPaddingClassName, }) => { return ( @@ -60,7 +62,8 @@ export const Modal: React.FC = ({ {children && ( = ({ return ( <> {children ? ( -
+
{children}
) : ( diff --git a/src/interfaces/assistants_web/src/context/ModalContext.tsx b/src/interfaces/assistants_web/src/context/ModalContext.tsx index 646cf19103..bf0ed9db8a 100644 --- a/src/interfaces/assistants_web/src/context/ModalContext.tsx +++ b/src/interfaces/assistants_web/src/context/ModalContext.tsx @@ -7,6 +7,7 @@ import { Modal } from '@/components/UI'; interface OpenParams { title?: string; content?: React.ReactNode | React.FC; + dialogPaddingClassName?: string; } export type OpenFunction = (params: OpenParams) => void; @@ -18,6 +19,7 @@ interface Context { open: OpenFunction; close: CloseFunction; content: React.ReactNode | React.FC; + dialogPaddingClassName?: string; } /** @@ -27,18 +29,22 @@ const useModal = (): Context => { const [isOpen, setIsOpen] = useState(false); const [title, setTitle] = useState(undefined); const [content, setContent] = useState(undefined); + const [dialogPaddingClassName, setDialogPaddingClassName] = useState( + undefined + ); - const open = ({ title, content }: OpenParams) => { + const open = ({ title, content, dialogPaddingClassName }: OpenParams) => { setIsOpen(true); setTitle(title); setContent(content); + setDialogPaddingClassName(dialogPaddingClassName); }; const close = () => { setIsOpen(false); }; - return { isOpen, open, close, content, title }; + return { isOpen, open, close, content, title, dialogPaddingClassName }; }; /** @@ -56,15 +62,21 @@ const ModalContext = createContext({ open: () => {}, close: () => {}, content: undefined, + dialogPaddingClassName: undefined, }); const ModalProvider: React.FC = ({ children }) => { - const { isOpen, title, open, close, content } = useModal(); + const { isOpen, title, open, close, content, dialogPaddingClassName } = useModal(); return ( - + <>{children} - + <>{content} diff --git a/src/interfaces/assistants_web/src/hooks/use-files.ts b/src/interfaces/assistants_web/src/hooks/use-files.ts index fcb6122d86..5997b68336 100644 --- a/src/interfaces/assistants_web/src/hooks/use-files.ts +++ b/src/interfaces/assistants_web/src/hooks/use-files.ts @@ -12,6 +12,29 @@ import { useConversationStore, useFilesStore, useParamsStore } from '@/stores'; import { UploadingFile } from '@/stores/slices/filesSlice'; import { fileSizeToBytes, formatFileSize, getFileExtension, mapExtensionToMimeType } from '@/utils'; +export const useFile = ({ + fileId, + agentId, + conversationId, +}: { + fileId: string; + agentId?: string; + conversationId?: string; +}) => { + const cohereClient = useCohereClient(); + return useQuery({ + queryKey: ['file', fileId], + queryFn: async () => { + if ((!agentId && !conversationId) || (agentId && conversationId)) { + throw new Error('Exactly one of agentId or conversationId must be provided'); + } + return agentId + ? await cohereClient.getAgentFile({ agentId: agentId!, fileId }) + : await cohereClient.getConversationFile({ conversationId: conversationId!, fileId }); + }, + }); +}; + export const useListConversationFiles = ( conversationId?: string, options?: { enabled?: boolean }