diff --git a/backend/chainlit/data/sql_alchemy.py b/backend/chainlit/data/sql_alchemy.py index 50a65d2a41..90d051242b 100644 --- a/backend/chainlit/data/sql_alchemy.py +++ b/backend/chainlit/data/sql_alchemy.py @@ -615,8 +615,23 @@ async def create_element(self, element: "Element"): if not element.mime: element.mime = "application/octet-stream" + # Set inline disposition for browser-renderable elements so they display + # correctly on chat resume (e.g. PDF in iframe, images in tags). + content_disposition = None + _RENDERABLE_MIME_PREFIXES = ("image/", "audio/", "video/") + _RENDERABLE_MIME_TYPES = ("application/pdf",) + if element.mime: + if element.mime in _RENDERABLE_MIME_TYPES or element.mime.startswith( + _RENDERABLE_MIME_PREFIXES + ): + content_disposition = "inline" + uploaded_file = await self.storage_provider.upload_file( - object_key=file_object_key, data=content, mime=element.mime, overwrite=True + object_key=file_object_key, + data=content, + mime=element.mime, + overwrite=True, + content_disposition=content_disposition, ) if not uploaded_file: raise ValueError( diff --git a/backend/tests/data/test_sql_alchemy.py b/backend/tests/data/test_sql_alchemy.py index c509c6a95e..94cd1b4f13 100644 --- a/backend/tests/data/test_sql_alchemy.py +++ b/backend/tests/data/test_sql_alchemy.py @@ -149,6 +149,68 @@ async def test_create_and_get_element( # The 'content' field is not part of the ElementDict, so we remove this assertion +async def test_create_element_pdf_inline_disposition( + mock_chainlit_context, data_layer: SQLAlchemyDataLayer +): + """PDF elements must get content_disposition=inline for Azure Blob iframe rendering.""" + async with mock_chainlit_context: + from chainlit.element import Pdf + + pdf_element = Pdf( + id=str(uuid.uuid4()), + name="test.pdf", + mime="application/pdf", + content=b"%PDF-1.4 fake", + for_id="test_step_id", + ) + await data_layer.create_element(pdf_element) + + upload_call = data_layer.storage_provider.upload_file.call_args + assert upload_call.kwargs.get("content_disposition") == "inline", ( + f"Expected content_disposition=inline for PDF, got {upload_call.kwargs}" + ) + + +async def test_create_element_text_default_disposition( + mock_chainlit_context, data_layer: SQLAlchemyDataLayer +): + """Non-renderable elements must keep content_disposition=None (default).""" + async with mock_chainlit_context: + text_element = Text( + id=str(uuid.uuid4()), + name="test.txt", + mime="text/plain", + content="test content", + for_id="test_step_id", + ) + await data_layer.create_element(text_element) + + upload_call = data_layer.storage_provider.upload_file.call_args + assert upload_call.kwargs.get("content_disposition") is None, ( + f"Expected content_disposition=None for text/plain, got {upload_call.kwargs}" + ) + + +async def test_create_element_image_inline_disposition( + mock_chainlit_context, data_layer: SQLAlchemyDataLayer +): + """Image elements must get content_disposition=inline.""" + async with mock_chainlit_context: + img_element = Text( + id=str(uuid.uuid4()), + name="test.png", + mime="image/png", + content=b"fake png", + for_id="test_step_id", + ) + await data_layer.create_element(img_element) + + upload_call = data_layer.storage_provider.upload_file.call_args + assert upload_call.kwargs.get("content_disposition") == "inline", ( + f"Expected content_disposition=inline for image/png, got {upload_call.kwargs}" + ) + + async def test_get_current_timestamp(data_layer: SQLAlchemyDataLayer): timestamp = await data_layer.get_current_timestamp() assert isinstance(timestamp, str)