diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py
index 5455fac760..635a2d2cd9 100644
--- a/backend/chainlit/__init__.py
+++ b/backend/chainlit/__init__.py
@@ -23,6 +23,7 @@
from chainlit.element import (
Audio,
Component,
+ Dataframe,
File,
Image,
Pdf,
diff --git a/backend/chainlit/data/sql_alchemy.py b/backend/chainlit/data/sql_alchemy.py
index 79596b7359..bb43ef7c16 100644
--- a/backend/chainlit/data/sql_alchemy.py
+++ b/backend/chainlit/data/sql_alchemy.py
@@ -7,7 +7,6 @@
import aiofiles
import aiohttp
-
from chainlit.data.base import BaseDataLayer
from chainlit.data.storage_clients.base import BaseStorageClient
from chainlit.data.utils import queue_until_user_message
diff --git a/backend/chainlit/element.py b/backend/chainlit/element.py
index 56384581ec..4c26464a59 100644
--- a/backend/chainlit/element.py
+++ b/backend/chainlit/element.py
@@ -31,7 +31,16 @@
}
ElementType = Literal[
- "image", "text", "pdf", "tasklist", "audio", "video", "file", "plotly", "component"
+ "image",
+ "text",
+ "pdf",
+ "tasklist",
+ "audio",
+ "video",
+ "file",
+ "plotly",
+ "dataframe",
+ "component",
]
ElementDisplay = Literal["inline", "side", "page"]
ElementSize = Literal["small", "medium", "large"]
@@ -358,6 +367,25 @@ def __post_init__(self) -> None:
super().__post_init__()
+@dataclass
+class Dataframe(Element):
+ """Useful to send a pandas DataFrame to the UI."""
+
+ type: ClassVar[ElementType] = "dataframe"
+ size: ElementSize = "large"
+ data: Any = None # The type is Any because it is checked in __post_init__.
+
+ def __post_init__(self) -> None:
+ """Ensures the data is a pandas DataFrame and converts it to JSON."""
+ from pandas import DataFrame
+
+ if not isinstance(self.data, DataFrame):
+ raise TypeError("data must be a pandas.DataFrame")
+
+ self.content = self.data.to_json(orient="split", date_format="iso")
+ super().__post_init__()
+
+
@dataclass
class Component(Element):
"""Useful to send a custom component to the UI."""
diff --git a/backend/poetry.lock b/backend/poetry.lock
index 4ce5808c61..141bbb1a45 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -3528,6 +3528,21 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.9.2)"]
+[[package]]
+name = "pandas-stubs"
+version = "2.2.2.240807"
+description = "Type annotations for pandas"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"},
+ {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"},
+]
+
+[package.dependencies]
+numpy = ">=1.23.5"
+types-pytz = ">=2022.1.1"
+
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -5277,6 +5292,17 @@ files = [
{file = "types_aiofiles-23.2.0.20240623-py3-none-any.whl", hash = "sha256:70597b29fc40c8583b6d755814b2cd5fcdb6785622e82d74ef499f9066316e08"},
]
+[[package]]
+name = "types-pytz"
+version = "2024.2.0.20240913"
+description = "Typing stubs for pytz"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "types-pytz-2024.2.0.20240913.tar.gz", hash = "sha256:4433b5df4a6fc587bbed41716d86a5ba5d832b4378e506f40d34bc9c81df2c24"},
+ {file = "types_pytz-2024.2.0.20240913-py3-none-any.whl", hash = "sha256:a1eebf57ebc6e127a99d2fa2ba0a88d2b173784ef9b3defcc2004ab6855a44df"},
+]
+
[[package]]
name = "types-requests"
version = "2.31.0.6"
@@ -5718,4 +5744,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<4.0.0"
-content-hash = "eda02c1c17a62c92406f37a9a5a81cbebf039f63bf3200258122ac564ec47de9"
+content-hash = "198eaaf758a037ee459912d1b3ff1cf7b4618f8f4eb2b6f55bf9bceaf17d0b45"
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 9f22ab9cf8..a4a844cedb 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -80,6 +80,7 @@ slack_bolt = "^1.18.1"
discord = "^2.3.2"
botbuilder-core = "^4.15.0"
aiosqlite = "^0.20.0"
+pandas = "^2.2.2"
moto = "^5.0.14"
[tool.poetry.group.dev.dependencies]
@@ -94,6 +95,7 @@ mypy = "^1.7.1"
types-requests = "^2.31.0.2"
types-aiofiles = "^23.1.0.5"
mypy-boto3-dynamodb = "^1.34.113"
+pandas-stubs = { version = "^2.2.2", python = ">=3.9" }
[tool.mypy]
python_version = "3.9"
@@ -120,7 +122,6 @@ ignore_missing_imports = true
-
[tool.poetry.group.custom-data]
optional = true
diff --git a/backend/tests/data/conftest.py b/backend/tests/data/conftest.py
index e1fe80faf7..e7a98d9081 100644
--- a/backend/tests/data/conftest.py
+++ b/backend/tests/data/conftest.py
@@ -1,7 +1,6 @@
-import pytest
-
from unittest.mock import AsyncMock
+import pytest
from chainlit.data.storage_clients.base import BaseStorageClient
from chainlit.user import User
diff --git a/backend/tests/data/test_sql_alchemy.py b/backend/tests/data/test_sql_alchemy.py
index b94f174512..8597b6ce40 100644
--- a/backend/tests/data/test_sql_alchemy.py
+++ b/backend/tests/data/test_sql_alchemy.py
@@ -2,13 +2,13 @@
from pathlib import Path
import pytest
+from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
+from chainlit.data.storage_clients.base import BaseStorageClient
+from chainlit.element import Text
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from chainlit import User
-from chainlit.data.storage_clients.base import BaseStorageClient
-from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
-from chainlit.element import Text
@pytest.fixture
@@ -23,18 +23,21 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path):
# Ref: https://docs.chainlit.io/data-persistence/custom#sql-alchemy-data-layer
async with engine.begin() as conn:
await conn.execute(
- text("""
+ text(
+ """
CREATE TABLE users (
"id" UUID PRIMARY KEY,
"identifier" TEXT NOT NULL UNIQUE,
"metadata" JSONB NOT NULL,
"createdAt" TEXT
);
- """)
+ """
+ )
)
await conn.execute(
- text("""
+ text(
+ """
CREATE TABLE IF NOT EXISTS threads (
"id" UUID PRIMARY KEY,
"createdAt" TEXT,
@@ -45,11 +48,13 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path):
"metadata" JSONB,
FOREIGN KEY ("userId") REFERENCES users("id") ON DELETE CASCADE
);
- """)
+ """
+ )
)
await conn.execute(
- text("""
+ text(
+ """
CREATE TABLE IF NOT EXISTS steps (
"id" UUID PRIMARY KEY,
"name" TEXT NOT NULL,
@@ -72,11 +77,13 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path):
"language" TEXT,
"indent" INT
);
- """)
+ """
+ )
)
await conn.execute(
- text("""
+ text(
+ """
CREATE TABLE IF NOT EXISTS elements (
"id" UUID PRIMARY KEY,
"threadId" UUID,
@@ -92,11 +99,13 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path):
"forId" UUID,
"mime" TEXT
);
- """)
+ """
+ )
)
await conn.execute(
- text("""
+ text(
+ """
CREATE TABLE IF NOT EXISTS feedbacks (
"id" UUID PRIMARY KEY,
"forId" UUID NOT NULL,
@@ -104,7 +113,8 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path):
"value" INT NOT NULL,
"comment" TEXT
);
- """)
+ """
+ )
)
# Create SQLAlchemyDataLayer instance
diff --git a/cypress/e2e/dataframe/main.py b/cypress/e2e/dataframe/main.py
new file mode 100644
index 0000000000..a42097c491
--- /dev/null
+++ b/cypress/e2e/dataframe/main.py
@@ -0,0 +1,68 @@
+import pandas as pd
+
+import chainlit as cl
+
+
+@cl.on_chat_start
+async def start():
+ # Create a sample DataFrame with more than 10 rows to test pagination functionality
+ data = {
+ "Name": [
+ "Alice",
+ "David",
+ "Charlie",
+ "Bob",
+ "Eva",
+ "Grace",
+ "Hannah",
+ "Jack",
+ "Frank",
+ "Kara",
+ "Liam",
+ "Ivy",
+ "Mia",
+ "Noah",
+ "Olivia",
+ ],
+ "Age": [25, 40, 35, 30, 45, 55, 60, 70, 50, 75, 80, 65, 85, 90, 95],
+ "City": [
+ "New York",
+ "Houston",
+ "Chicago",
+ "Los Angeles",
+ "Phoenix",
+ "San Antonio",
+ "San Diego",
+ "San Jose",
+ "Philadelphia",
+ "Austin",
+ "Fort Worth",
+ "Dallas",
+ "Jacksonville",
+ "Columbus",
+ "Charlotte",
+ ],
+ "Salary": [
+ 70000,
+ 100000,
+ 90000,
+ 80000,
+ 110000,
+ 130000,
+ 140000,
+ 160000,
+ 120000,
+ 170000,
+ 180000,
+ 150000,
+ 190000,
+ 200000,
+ 210000,
+ ],
+ }
+
+ df = pd.DataFrame(data)
+
+ elements = [cl.Dataframe(data=df, display="inline", name="Dataframe")]
+
+ await cl.Message(content="This message has a Dataframe", elements=elements).send()
diff --git a/cypress/e2e/dataframe/spec.cy.ts b/cypress/e2e/dataframe/spec.cy.ts
new file mode 100644
index 0000000000..14c59b611d
--- /dev/null
+++ b/cypress/e2e/dataframe/spec.cy.ts
@@ -0,0 +1,41 @@
+import { runTestServer } from '../../support/testUtils';
+
+describe('dataframe', () => {
+ before(() => {
+ runTestServer();
+ });
+
+ it('should be able to display an inline dataframe', () => {
+ // Check if the DataFrame is rendered within the first step
+ cy.get('.step').should('have.length', 1);
+ cy.get('.step').first().find('.MuiDataGrid-main').should('have.length', 1);
+
+ // Click the sort button in the "Age" column header to sort in ascending order
+ cy.get('.MuiDataGrid-columnHeader[aria-label="Age"]')
+ .find('button')
+ .first()
+ .click({ force: true });
+ // Verify the first row's "Age" cell contains '25' after sorting
+ cy.get('.MuiDataGrid-row')
+ .first()
+ .find('.MuiDataGrid-cell[data-field="Age"] .MuiDataGrid-cellContent')
+ .should('have.text', '25');
+
+ // Click the "Next page" button in the pagination controls
+ cy.get('.MuiTablePagination-actions').find('button').eq(1).click();
+ // Verify that the next page contains exactly 5 rows
+ cy.get('.MuiDataGrid-row').should('have.length', 5);
+
+ // Click the input to open the dropdown
+ cy.get('.MuiTablePagination-select').click();
+ // Select the option with the value '50' from the dropdown list
+ cy.get('ul.MuiMenu-list li').contains('50').click();
+ // Scroll to the bottom of the virtual scroller in the MUI DataGrid
+ cy.get('.MuiDataGrid-virtualScroller').scrollTo('bottom');
+ // Check that tha last name is Olivia
+ cy.get('.MuiDataGrid-row')
+ .last()
+ .find('.MuiDataGrid-cell[data-field="Name"] .MuiDataGrid-cellContent')
+ .should('have.text', 'Olivia');
+ });
+});
diff --git a/frontend/package.json b/frontend/package.json
index 88afc29a05..93a7682195 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,6 +20,7 @@
"@mui/icons-material": "^5.14.9",
"@mui/lab": "^5.0.0-alpha.122",
"@mui/material": "^5.14.10",
+ "@mui/x-data-grid": "^6.20.4",
"formik": "^2.4.3",
"highlight.js": "^11.9.0",
"i18next": "^23.7.16",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index d227207400..06dfed2047 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
'@mui/material':
specifier: ^5.14.10
version: 5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@mui/x-data-grid':
+ specifier: ^6.20.4
+ version: 6.20.4(@mui/material@5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.14.19(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
formik:
specifier: ^2.4.3
version: 2.4.3(react@18.2.0)
@@ -303,6 +306,10 @@ packages:
resolution: {integrity: sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.25.6':
+ resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.22.15':
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
@@ -852,6 +859,14 @@ packages:
'@types/react':
optional: true
+ '@mui/types@7.2.17':
+ resolution: {integrity: sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==}
+ peerDependencies:
+ '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@mui/utils@5.14.19':
resolution: {integrity: sha512-qAHvTXzk7basbyqPvhgWqN6JbmI2wLB/mf97GkSlz5c76MiKYV6Ffjvw9BjKZQ1YRb8rDX9kgdjRezOcoB91oQ==}
engines: {node: '>=12.0.0'}
@@ -862,6 +877,25 @@ packages:
'@types/react':
optional: true
+ '@mui/utils@5.16.6':
+ resolution: {integrity: sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ '@types/react': ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@mui/x-data-grid@6.20.4':
+ resolution: {integrity: sha512-I0JhinVV4e25hD2dB+R6biPBtpGeFrXf8RwlMPQbr9gUggPmPmNtWKo8Kk2PtBBMlGtdMAgHWe7PqhmucUxU1w==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@mui/material': ^5.4.1
+ '@mui/system': ^5.4.1
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1242,6 +1276,9 @@ packages:
'@types/prop-types@15.7.11':
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
+ '@types/prop-types@15.7.13':
+ resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
+
'@types/react-dom@18.2.22':
resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==}
@@ -1548,6 +1585,10 @@ packages:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-alpha@1.0.4:
resolution: {integrity: sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==}
@@ -3095,6 +3136,9 @@ packages:
react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
react-markdown@9.0.1:
resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
peerDependencies:
@@ -3221,6 +3265,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+ reselect@4.1.8:
+ resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -4004,6 +4051,10 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.0
+ '@babel/runtime@7.25.6':
+ dependencies:
+ regenerator-runtime: 0.14.0
+
'@babel/template@7.22.15':
dependencies:
'@babel/code-frame': 7.23.5
@@ -4459,6 +4510,10 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.0
+ '@mui/types@7.2.17(@types/react@18.2.0)':
+ optionalDependencies:
+ '@types/react': 18.2.0
+
'@mui/utils@5.14.19(@types/react@18.2.0)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.23.5
@@ -4469,6 +4524,32 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.0
+ '@mui/utils@5.16.6(@types/react@18.2.0)(react@18.2.0)':
+ dependencies:
+ '@babel/runtime': 7.25.6
+ '@mui/types': 7.2.17(@types/react@18.2.0)
+ '@types/prop-types': 15.7.13
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-is: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.2.0
+
+ '@mui/x-data-grid@6.20.4(@mui/material@5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.14.19(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@babel/runtime': 7.25.6
+ '@mui/material': 5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@mui/system': 5.14.19(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0)
+ '@mui/utils': 5.16.6(@types/react@18.2.0)(react@18.2.0)
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ reselect: 4.1.8
+ transitivePeerDependencies:
+ - '@types/react'
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -4856,6 +4937,8 @@ snapshots:
'@types/prop-types@15.7.11': {}
+ '@types/prop-types@15.7.13': {}
+
'@types/react-dom@18.2.22':
dependencies:
'@types/react': 18.2.0
@@ -5160,6 +5243,8 @@ snapshots:
clsx@2.0.0: {}
+ clsx@2.1.1: {}
+
color-alpha@1.0.4:
dependencies:
color-parse: 1.4.3
@@ -7170,6 +7255,8 @@ snapshots:
react-is@18.2.0: {}
+ react-is@18.3.1: {}
+
react-markdown@9.0.1(@types/react@18.2.0)(react@18.2.0):
dependencies:
'@types/hast': 3.0.4
@@ -7399,6 +7486,8 @@ snapshots:
requires-port@1.0.0: {}
+ reselect@4.1.8: {}
+
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
diff --git a/frontend/src/components/atoms/elements/Dataframe.tsx b/frontend/src/components/atoms/elements/Dataframe.tsx
new file mode 100644
index 0000000000..adb0a79d20
--- /dev/null
+++ b/frontend/src/components/atoms/elements/Dataframe.tsx
@@ -0,0 +1,58 @@
+import { DataGrid } from '@mui/x-data-grid';
+
+import { useFetch } from 'hooks/useFetch';
+
+import { type IDataframeElement } from 'client-types/';
+
+interface Props {
+ element: IDataframeElement;
+}
+
+const DataframeElement = ({ element }: Props) => {
+ const { data } = useFetch(element.url || null);
+
+ // Check if data is still being fetched
+ const isLoading: boolean = !data;
+
+ let gridColumns = [];
+ let gridRows = [];
+
+ // Parse data only if it exists
+ if (!isLoading) {
+ const { index, columns, data: rowData } = JSON.parse(data);
+
+ gridColumns = columns.map((col: string) => ({
+ field: col,
+ minWidth: 150
+ }));
+
+ gridRows = rowData.map((row: (string | number)[], idx: number) => {
+ const rowObj: any = { id: index[idx] };
+ columns.forEach((col: string, colIdx: number) => {
+ rowObj[col] = row[colIdx];
+ });
+ return rowObj;
+ });
+ }
+
+ return (
+
+ );
+};
+
+export { DataframeElement };
diff --git a/frontend/src/components/atoms/elements/Element.tsx b/frontend/src/components/atoms/elements/Element.tsx
index 0ac81cb9cf..70a590310f 100644
--- a/frontend/src/components/atoms/elements/Element.tsx
+++ b/frontend/src/components/atoms/elements/Element.tsx
@@ -1,6 +1,7 @@
import type { IMessageElement } from 'client-types/';
import { AudioElement } from './Audio';
+import { DataframeElement } from './Dataframe';
import { FileElement } from './File';
import { ImageElement } from './Image';
import { PDFElement } from './PDF';
@@ -28,6 +29,8 @@ const Element = ({ element }: ElementProps): JSX.Element | null => {
return ;
case 'plotly':
return ;
+ case 'dataframe':
+ return ;
default:
return null;
}
diff --git a/frontend/src/components/atoms/elements/InlinedDataframeList.tsx b/frontend/src/components/atoms/elements/InlinedDataframeList.tsx
new file mode 100644
index 0000000000..9f9275b433
--- /dev/null
+++ b/frontend/src/components/atoms/elements/InlinedDataframeList.tsx
@@ -0,0 +1,29 @@
+import Stack from '@mui/material/Stack';
+
+import type { IDataframeElement } from 'client-types/';
+
+import { DataframeElement } from './Dataframe';
+
+interface Props {
+ items: IDataframeElement[];
+}
+
+const InlinedDataframeList = ({ items }: Props) => (
+
+ {items.map((element, i) => {
+ return (
+
+
+
+ );
+ })}
+
+);
+
+export { InlinedDataframeList };
diff --git a/frontend/src/components/atoms/elements/InlinedElements.tsx b/frontend/src/components/atoms/elements/InlinedElements.tsx
index a5d06787a0..1969dc6540 100644
--- a/frontend/src/components/atoms/elements/InlinedElements.tsx
+++ b/frontend/src/components/atoms/elements/InlinedElements.tsx
@@ -3,6 +3,7 @@ import Stack from '@mui/material/Stack';
import type { ElementType, IMessageElement } from 'client-types/';
import { InlinedAudioList } from './InlinedAudioList';
+import { InlinedDataframeList } from './InlinedDataframeList';
import { InlinedFileList } from './InlinedFileList';
import { InlinedImageList } from './InlinedImageList';
import { InlinedPDFList } from './InlinedPDFList';
@@ -64,6 +65,9 @@ const InlinedElements = ({ elements }: Props) => {
{elementsByType.plotly?.length ? (
) : null}
+ {elementsByType.dataframe?.length ? (
+
+ ) : null}
);
};
diff --git a/frontend/src/components/atoms/elements/index.ts b/frontend/src/components/atoms/elements/index.ts
index 0a520ff7e3..24b654040b 100644
--- a/frontend/src/components/atoms/elements/index.ts
+++ b/frontend/src/components/atoms/elements/index.ts
@@ -1,4 +1,5 @@
export { AudioElement } from './Audio';
+export { DataframeElement } from './Dataframe';
export { Element } from './Element';
export { ElementSideView } from './ElementSideView';
export { ElementView } from './ElementView';
@@ -12,6 +13,7 @@ export { VideoElement } from './Video';
// Inlined
export { InlinedAudioList } from './InlinedAudioList';
+export { InlinedDataframeList } from './InlinedDataframeList';
export { InlinedElements } from './InlinedElements';
export { InlinedFileList } from './InlinedFileList';
export { InlinedImageList } from './InlinedImageList';
diff --git a/libs/react-client/src/types/element.ts b/libs/react-client/src/types/element.ts
index 1d8e25ccd2..157d540afa 100644
--- a/libs/react-client/src/types/element.ts
+++ b/libs/react-client/src/types/element.ts
@@ -6,7 +6,8 @@ export type IElement =
| IAudioElement
| IVideoElement
| IFileElement
- | IPlotlyElement;
+ | IPlotlyElement
+ | IDataframeElement;
export type IMessageElement =
| IImageElement
@@ -15,7 +16,8 @@ export type IMessageElement =
| IAudioElement
| IVideoElement
| IFileElement
- | IPlotlyElement;
+ | IPlotlyElement
+ | IDataframeElement;
export type ElementType = IElement['type'];
export type IElementSize = 'small' | 'medium' | 'large';
@@ -69,3 +71,5 @@ export interface IFileElement extends TMessageElement<'file'> {
export interface IPlotlyElement extends TMessageElement<'plotly'> {}
export interface ITasklistElement extends TElement<'tasklist'> {}
+
+export interface IDataframeElement extends TMessageElement<'dataframe'> {}