From 05f602bf977551d80aa51036294fc320dc0c54ad Mon Sep 17 00:00:00 2001 From: Danilo Alves Date: Sun, 22 Sep 2024 21:32:17 -0300 Subject: [PATCH 1/8] Add Dataframe class --- backend/chainlit/__init__.py | 1 + backend/chainlit/element.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) 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/element.py b/backend/chainlit/element.py index 56384581ec..7cb54a5960 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,13 @@ def __post_init__(self) -> None: super().__post_init__() +@dataclass +class Dataframe(Element): + type: ClassVar[ElementType] = "dataframe" + + size: ElementSize = "large" + + @dataclass class Component(Element): """Useful to send a custom component to the UI.""" From 54e3636fec33490b7d73d8815a810abf3d93e5e2 Mon Sep 17 00:00:00 2001 From: Danilo Alves Date: Sun, 22 Sep 2024 21:59:25 -0300 Subject: [PATCH 2/8] Add Dataframe component --- .../components/atoms/elements/Dataframe.tsx | 52 +++++++++++++++++++ .../src/components/atoms/elements/Element.tsx | 3 ++ .../atoms/elements/InlinedDataframeList.tsx | 29 +++++++++++ .../atoms/elements/InlinedElements.tsx | 4 ++ .../src/components/atoms/elements/index.ts | 2 + libs/react-client/src/types/element.ts | 8 ++- 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/atoms/elements/Dataframe.tsx create mode 100644 frontend/src/components/atoms/elements/InlinedDataframeList.tsx diff --git a/frontend/src/components/atoms/elements/Dataframe.tsx b/frontend/src/components/atoms/elements/Dataframe.tsx new file mode 100644 index 0000000000..a7f367580b --- /dev/null +++ b/frontend/src/components/atoms/elements/Dataframe.tsx @@ -0,0 +1,52 @@ +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); + + if (!data) { + return
Loading...
; + } + + const { index, columns, data: rowData } = JSON.parse(data); + + const gridColumns = columns.map((col: string) => ({ + field: col, + minWidth: 150 + })); + + const 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'> {} From d60cf4ed127e02b7bec438a9470274bf6a5e4203 Mon Sep 17 00:00:00 2001 From: Danilo Alves Date: Mon, 23 Sep 2024 10:04:28 -0300 Subject: [PATCH 3/8] Add @mui/x-data-grid package --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 89 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) 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: {} From 3b0c70c905b43e112a4e68fbc078921b40b143a0 Mon Sep 17 00:00:00 2001 From: Danilo Alves Date: Wed, 25 Sep 2024 18:45:34 -0300 Subject: [PATCH 4/8] Refactor Dataframe element to handle DataFrame serialization internally - Updated Dataframe class to accept pandas DataFrame directly. - Automatically serialize DataFrame to JSON with orient="split" in __post_init__. - Simplified interface, removing the need for users to manually serialize data. --- backend/chainlit/element.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/chainlit/element.py b/backend/chainlit/element.py index 7cb54a5960..4c26464a59 100644 --- a/backend/chainlit/element.py +++ b/backend/chainlit/element.py @@ -369,9 +369,21 @@ def __post_init__(self) -> None: @dataclass class Dataframe(Element): - type: ClassVar[ElementType] = "dataframe" + """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 From b3ac869a8eac60051a2f9beb55f2b194adf289cb Mon Sep 17 00:00:00 2001 From: Mathijs de Bruin Date: Mon, 14 Oct 2024 11:36:40 +0100 Subject: [PATCH 5/8] Add pandas-stubs for better type checking and ensure DataFrame validation --- backend/poetry.lock | 28 +++++++++++++++++++++++++++- backend/pyproject.toml | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 4c717509ad..e0e43db156 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -3397,6 +3397,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" @@ -5127,6 +5142,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" @@ -5540,4 +5566,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0.0" -content-hash = "ef9341345f921f6b78cccbcf94fd539d9d0814428d40de0ade9a244362616e04" +content-hash = "5301bcf8467af9756c9c113423aedb8399f96b0e7b8b0c014062a254ba8f9124" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 64c1b61e63..6ea9873ebd 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -93,6 +93,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" From 428d15b788089e7f4976defc276210209a3b9996 Mon Sep 17 00:00:00 2001 From: Danilo Alves Date: Thu, 26 Sep 2024 10:32:49 -0300 Subject: [PATCH 6/8] Implement loading state in Dataframe component - Added loading state to DataGrid using the loading prop. - Refactored data parsing logic to handle loading more cleanly. --- .../components/atoms/elements/Dataframe.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/atoms/elements/Dataframe.tsx b/frontend/src/components/atoms/elements/Dataframe.tsx index a7f367580b..adb0a79d20 100644 --- a/frontend/src/components/atoms/elements/Dataframe.tsx +++ b/frontend/src/components/atoms/elements/Dataframe.tsx @@ -11,30 +11,36 @@ interface Props { const DataframeElement = ({ element }: Props) => { const { data } = useFetch(element.url || null); - if (!data) { - return
Loading...
; - } - - const { index, columns, data: rowData } = JSON.parse(data); - - const gridColumns = columns.map((col: string) => ({ - field: col, - minWidth: 150 - })); - - const gridRows = rowData.map((row: (string | number)[], idx: number) => { - const rowObj: any = { id: index[idx] }; - columns.forEach((col: string, colIdx: number) => { - rowObj[col] = row[colIdx]; + // 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 rowObj; - }); + } return ( Date: Sun, 13 Oct 2024 10:55:18 -0300 Subject: [PATCH 7/8] Add tests for DataFrame rendering and interaction - Created sample DataFrame with 15 rows (more than 10) in main.py to test pagination - Added Cypress test to validate Dataframe rendering, sorting, pagination, and rows per page functionality --- cypress/e2e/dataframe/main.py | 68 ++++++++++++++++++++++++++++++++ cypress/e2e/dataframe/spec.cy.ts | 41 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 cypress/e2e/dataframe/main.py create mode 100644 cypress/e2e/dataframe/spec.cy.ts 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'); + }); +}); From a7e2a74835ba0a497a31a98288c6d0c649ba05f3 Mon Sep 17 00:00:00 2001 From: Mathijs de Bruin Date: Mon, 14 Oct 2024 11:41:36 +0100 Subject: [PATCH 8/8] Add DataFrame as explicit test requirement. --- backend/poetry.lock | 2 +- backend/pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index e0e43db156..4c01054bad 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -5566,4 +5566,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0.0" -content-hash = "5301bcf8467af9756c9c113423aedb8399f96b0e7b8b0c014062a254ba8f9124" +content-hash = "ee4ff898ea398259dab39d12d9746307dd13c1ab931b1ebc0006a32bea2b8b9e" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6ea9873ebd..1b095cc49c 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" [tool.poetry.group.dev.dependencies] black = "^24.8.0" @@ -120,6 +121,7 @@ ignore_missing_imports = true + [tool.poetry.group.custom-data] optional = true