Bladeren bron

Merge branch 'develop' into bug/#1911-current-page-menu

Nam Nguyen 7 maanden geleden
bovenliggende
commit
15a258fa26

+ 5 - 3
.github/workflows/frontend.yml

@@ -66,11 +66,12 @@ jobs:
         if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         run: npm ci
 
-      - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+      - name: Test Gui
+        if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         run: npm test
 
       - name: Code coverage
-        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-fe-build.outputs.cache-hit != 'true' && github.event.pull_request.base.repo.full_name == github.event.pull_request.head.repo.full_name
         uses: artiomtr/jest-coverage-report-action@v2
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -128,6 +129,7 @@ jobs:
           echo "HASH=$(cat hash.txt)" >> $GITHUB_OUTPUT
           rm hash.txt
         shell: bash
+
       - name: Restore cached core frontend build
         id: cache-gui-core-fe-build
         uses: actions/cache@v4
@@ -157,7 +159,7 @@ jobs:
         run: npm test
 
       - name: Code coverage
-        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-core-fe-build.outputs.cache-hit != 'true'
+        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-core-fe-build.outputs.cache-hit != 'true' && github.event.pull_request.base.repo.full_name == github.event.pull_request.head.repo.full_name
         uses: artiomtr/jest-coverage-report-action@v2
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}

+ 1 - 1
.github/workflows/overall-tests.yml

@@ -20,7 +20,7 @@ jobs:
   coverage:
     timeout-minutes: 50
     runs-on: ubuntu-latest
-    if: ${{ github.event_name == 'pull_request' }}
+    if: github.event_name == 'pull_request' && github.event.pull_request.base.repo.full_name == github.event.pull_request.head.repo.full_name
     steps:
       - uses: actions/checkout@v4
 

+ 5 - 4
doc/gui/examples/controls/chat_calculator.py

@@ -16,19 +16,20 @@
 # Human-computer dialog UI based on the chat control.
 # -----------------------------------------------------------------------------------------
 from math import cos, pi, sin, sqrt, tan  # noqa: F401
+from typing import Optional
 
 from taipy.gui import Gui
 
 # The user interacts with the Python interpreter
 users = ["human", "Result"]
-messages: list[tuple[str, str, str]] = []
+messages: list[tuple[str, str, str, Optional[str]]] = []
 
 
 def evaluate(state, var_name: str, payload: dict):
     # Retrieve the callback parameters
-    (_, _, expression, sender_id) = payload.get("args", [])
+    (_, _, expression, sender_id, image_url) = payload.get("args", [])
     # Add the input content as a sent message
-    messages.append((f"{len(messages)}", expression, sender_id))
+    messages.append((f"{len(messages)}", expression, sender_id, image_url))
     # Default message used if evaluation fails
     result = "Invalid expression"
     try:
@@ -37,7 +38,7 @@ def evaluate(state, var_name: str, payload: dict):
     except Exception:
         pass
     # Add the result as an incoming message
-    messages.append((f"{len(messages)}", result, users[1]))
+    messages.append((f"{len(messages)}", result, users[1], None))
     state.messages = messages
 
 

+ 4 - 4
doc/gui/examples/controls/chat_discuss.py

@@ -19,14 +19,14 @@
 # incognito windows so a given user's context is not reused.
 # -----------------------------------------------------------------------------------------
 from os import path
-from typing import Union
+from typing import Optional, Union
 
 from taipy.gui import Gui, Icon
 from taipy.gui.gui_actions import navigate, notify
 
 username = ""
 users: list[Union[str, Icon]] = []
-messages: list[tuple[str, str, str]] = []
+messages: list[tuple[str, str, str, Optional[str]]] = []
 
 Gui.add_shared_variables("messages", "users")
 
@@ -62,8 +62,8 @@ def register(state):
 
 
 def send(state, _: str, payload: dict):
-    (_, _, message, sender_id) = payload.get("args", [])
-    messages.append((f"{len(messages)}", message, sender_id))
+    (_, _, message, sender_id, image_url) = payload.get("args", [])
+    messages.append((f"{len(messages)}", message, sender_id, image_url))
     state.messages = messages
 
 

+ 46 - 0
doc/gui/examples/controls/chat_messages.py

@@ -0,0 +1,46 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from taipy.gui import Gui, Icon
+
+msgs = [
+    ["1", "msg 1", "Alice", None],
+    ["2", "msg From Another unknown User", "Charles", None],
+    ["3", "This from the sender User", "taipy", "./sample.jpeg"],
+    ["4", "And from another known one", "Alice", None],
+]
+users = [
+    ["Alice", Icon("./alice-avatar.png", "Alice avatar")],
+    ["Charles", Icon("./charles-avatar.png", "Charles avatar")],
+    ["taipy", Icon("./beatrix-avatar.png", "Beatrix avatar")],
+]
+
+
+def on_action(state, var_name: str, payload: dict):
+    args = payload.get("args", [])
+    msgs.append([f"{len(msgs) +1 }", args[2], args[3], args[4]])
+    state.msgs = msgs
+
+
+Gui(
+    """
+<|toggle|theme|>
+# Test Chat
+<|1 1 1|layout|
+<|{msgs}|chat|users={users}|show_sender={True}|>
+
+<|part|>
+
+<|{msgs}|chat|users={users}|show_sender={True}|not with_input|>
+|>
+
+""",
+).run()

+ 21 - 0
doc/gui/examples/controls/column_name_styling.py

@@ -0,0 +1,21 @@
+# Example for column name styling for header
+
+import pandas as pd
+
+from taipy.gui import Gui, Markdown
+
+# Sample data in DataFrame format
+df = pd.DataFrame({
+    "Name": ["Alice", "Bob", "Charlie"],
+    "Age": [25, 30, 35],
+    "Job or Occupation": ["Engineer", "Doctor", "Artist"]
+})
+
+
+# Page content with table and header styling
+page = Markdown("""
+<|table|data={df}|columns={columns}|>
+""", style={".taipy-table-name": {"color": "blue"}, ".taipy-table-job-or-occupation": {"color": "green"}})
+
+if __name__ == "__main__":
+    Gui(page).run(title="Column Name Styling Example")

BIN
doc/gui/examples/controls/sample.jpeg


+ 6 - 1
frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

@@ -31,6 +31,7 @@ import AddIcon from "@mui/icons-material/Add";
 import DataSaverOn from "@mui/icons-material/DataSaverOn";
 import DataSaverOff from "@mui/icons-material/DataSaverOff";
 import Download from "@mui/icons-material/Download";
+import { generateHeaderClassName } from "./tableUtils";
 
 import {
     createRequestInfiniteTableUpdateAction,
@@ -604,7 +605,11 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                                         <TableCell
                                             key={`head${columns[col].dfid}`}
                                             sortDirection={orderBy === columns[col].dfid && order}
-                                            sx={columns[col].width ? { width: columns[col].width } : undefined}
+                                            sx={columns[col].width ? { width: columns[col].width } : {}}
+                                            className={col === "EDIT_COL"
+                                                ? getSuffixedClassNames(className, "-action")
+                                                : getSuffixedClassNames(className, generateHeaderClassName(columns[col].dfid))
+                                            }
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [

+ 72 - 4
frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx

@@ -12,7 +12,7 @@
  */
 
 import React from "react";
-import { render, waitFor } from "@testing-library/react";
+import { render, waitFor, fireEvent } from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
 
@@ -108,7 +108,7 @@ describe("Chat Component", () => {
             context: undefined,
             payload: {
                 action: undefined,
-                args: ["Enter", "varName", "new message", "taipy"],
+                args: ["Enter", "varName", "new message", "taipy",null],
             },
         });
     });
@@ -123,15 +123,83 @@ describe("Chat Component", () => {
         const elt = getByLabelText("message (taipy)");
         await userEvent.click(elt);
         await userEvent.keyboard("new message");
-        await userEvent.click(getByRole("button"))
+        await userEvent.click(getByRole("button",{ name: /send message/i }))
         expect(dispatch).toHaveBeenCalledWith({
             type: "SEND_ACTION_ACTION",
             name: "",
             context: undefined,
             payload: {
                 action: undefined,
-                args: ["click", "varName", "new message", "taipy"],
+                args: ["click", "varName", "new message", "taipy",null],
             },
         });
     });
+    it("handle image upload",async()=>{
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const { getByLabelText,getByText,getByAltText,queryByText,getByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Chat messages={messages} updateVarName="varName" defaultKey={valueKey} mode="raw"/>
+            </TaipyContext.Provider>
+        );
+        const file = new File(['(⌐□_□)'], 'test.png', { type: 'image/png' });
+        URL.createObjectURL = jest.fn(() => 'mocked-url');
+        URL.revokeObjectURL = jest.fn();
+
+        const attachButton = getByLabelText('upload image');
+        expect(attachButton).toBeInTheDocument();
+
+
+        const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
+        expect(fileInput).toBeInTheDocument();
+        fireEvent.change(fileInput, { target: { files: [file] } });
+
+        await waitFor(() => {
+            const chipWithImage = getByText('test.png');
+            expect(chipWithImage).toBeInTheDocument();
+            const previewImg = getByAltText('Image preview');
+            expect(previewImg).toBeInTheDocument();
+            expect(previewImg).toHaveAttribute('src', 'mocked-url');
+          });
+
+          const elt = getByLabelText("message (taipy)");
+          await userEvent.click(elt);
+          await userEvent.keyboard("Test message with image");
+          await userEvent.click(getByRole("button",{ name: /send message/i }))
+
+          expect(dispatch).toHaveBeenNthCalledWith(2,
+            expect.objectContaining({
+              type: 'SEND_ACTION_ACTION',
+              payload: expect.objectContaining({
+                args: ['click', 'varName', 'Test message with image', 'taipy', 'mocked-url']
+              })
+            })
+          );
+          await waitFor(() => {
+            const chipWithImage = queryByText('test.png');
+            expect(chipWithImage).not.toBeInTheDocument();
+          });
+          jest.restoreAllMocks()
+    })
+    it("Not upload image over a file size limit",async()=>{
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const { getByText,getByAltText } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Chat messages={messages} updateVarName="varName" maxFileSize={0} defaultKey={valueKey} mode="raw"/>
+            </TaipyContext.Provider>
+        );
+        const file = new File(['(⌐□_□)'], 'test.png', { type: 'image/png' });
+        URL.createObjectURL = jest.fn(() => 'mocked-url');
+
+        const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
+        expect(fileInput).toBeInTheDocument();
+        fireEvent.change(fileInput, { target: { files: [file] } });
+
+        await waitFor(() => {
+            expect(() =>getByText('test.png')).toThrow()
+            expect(()=>getByAltText('Image preview')).toThrow();
+          });
+          jest.restoreAllMocks()
+    })
 });

+ 92 - 7
frontend/taipy-gui/src/components/Taipy/Chat.tsx

@@ -35,6 +35,7 @@ import Popper from "@mui/material/Popper";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
 import Send from "@mui/icons-material/Send";
+import AttachFile from "@mui/icons-material/AttachFile";
 import ArrowDownward from "@mui/icons-material/ArrowDownward";
 import ArrowUpward from "@mui/icons-material/ArrowUpward";
 
@@ -47,11 +48,13 @@ import { emptyArray, getInitials } from "../../utils";
 import { RowType, TableValueType } from "./tableUtils";
 import { Stack } from "@mui/material";
 import { getComponentClassName } from "./TaipyStyle";
+import { noDisplayStyle } from "./utils";
 
 const Markdown = lazy(() => import("react-markdown"));
 
 interface ChatProps extends TaipyActiveProps {
     messages?: TableValueType;
+    maxFileSize?: number;
     withInput?: boolean;
     users?: LoVElt[];
     defaultUsers?: string;
@@ -132,7 +135,7 @@ const defaultBoxSx = {
 } as SxProps<Theme>;
 const noAnchorSx = { overflowAnchor: "none", "& *": { overflowAnchor: "none" } } as SxProps<Theme>;
 const anchorSx = { overflowAnchor: "auto", height: "1px", width: "100%" } as SxProps<Theme>;
-
+const imageSx = {width:3/5, height:"auto"}
 interface key2Rows {
     key: string;
 }
@@ -140,6 +143,7 @@ interface key2Rows {
 interface ChatRowProps {
     senderId: string;
     message: string;
+    image?: string;
     name: string;
     className?: string;
     getAvatar: (id: string, sender: boolean) => ReactNode;
@@ -149,7 +153,7 @@ interface ChatRowProps {
 }
 
 const ChatRow = (props: ChatRowProps) => {
-    const { senderId, message, name, className, getAvatar, index, showSender, mode } = props;
+    const { senderId, message, image, name, className, getAvatar, index, showSender, mode } = props;
     const sender = senderId == name;
     const avatar = getAvatar(name, sender);
 
@@ -162,8 +166,18 @@ const ChatRow = (props: ChatRowProps) => {
             justifyContent={sender ? "flex-end" : undefined}
         >
             <Grid sx={sender ? senderMsgSx : undefined}>
+            {image?(
+                <Grid container justifyContent={sender ? "flex-end" : undefined}>
+                <Box
+                                component="img"
+                                sx={imageSx}
+                                alt="Uploaded image"
+                                src={image}
+                            />
+                </Grid>
+                            ):null}
                 {(!sender || showSender) && avatar ? (
-                    <Stack direction="row" gap={1}>
+                    <Stack direction="row" gap={1} justifyContent={sender ? "flex-end" : undefined}>
                         {!sender ? <Box sx={avatarColSx}>{avatar}</Box> : null}
                         <Stack>
                             <Box sx={sender ? rightNameSx : leftNameSx}>{name}</Box>
@@ -213,6 +227,7 @@ const Chat = (props: ChatProps) => {
         onAction,
         withInput = true,
         defaultKey = "",
+        maxFileSize= 1 * 1024 * 1024, // 1 MB
         pageSize = 50,
         showSender = false,
     } = props;
@@ -227,6 +242,10 @@ const Chat = (props: ChatProps) => {
     const isAnchorDivVisible = useElementVisible(anchorDivRef);
     const [showMessage, setShowMessage] = useState(false);
     const [anchorPopup, setAnchorPopup] = useState<HTMLDivElement | null>(null);
+    const [selectedFile, setSelectedFile] = useState<File | null>(null);
+    const [imagePreview, setImagePreview] = useState<string | null>(null);
+    const [objectURLs, setObjectURLs] = useState<string[]>([]);
+    const fileInputRef = useRef<HTMLInputElement>(null);
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
@@ -256,14 +275,16 @@ const Chat = (props: ChatProps) => {
                 const elt = evt.currentTarget.querySelector("input");
                 if (elt?.value) {
                     dispatch(
-                        createSendActionNameAction(id, module, onAction, evt.key, updateVarName, elt?.value, senderId)
+                        createSendActionNameAction(id, module, onAction, evt.key, updateVarName, elt?.value,senderId, imagePreview)
                     );
                     elt.value = "";
+                    setSelectedFile(null);
+                    setImagePreview(null);
                 }
                 evt.preventDefault();
             }
         },
-        [updateVarName, onAction, senderId, id, dispatch, module]
+        [imagePreview, updateVarName, onAction, senderId, id, dispatch, module]
     );
 
     const handleClick = useCallback(
@@ -271,15 +292,39 @@ const Chat = (props: ChatProps) => {
             const elt = evt.currentTarget.parentElement?.parentElement?.querySelector("input");
             if (elt?.value) {
                 dispatch(
-                    createSendActionNameAction(id, module, onAction, "click", updateVarName, elt?.value, senderId)
+                    createSendActionNameAction(id, module, onAction, "click", updateVarName, elt?.value,senderId,imagePreview)
                 );
                 elt.value = "";
+                setSelectedFile(null);
+                setImagePreview(null);
             }
             evt.preventDefault();
         },
-        [updateVarName, onAction, senderId, id, dispatch, module]
+        [imagePreview,updateVarName, onAction, senderId, id, dispatch, module]
     );
 
+    const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+            const file = event.target.files ? event.target.files[0] : null;
+            if (file) {
+                if (file.type.startsWith("image/") && file.size <= maxFileSize) {
+                    setSelectedFile(file);
+                    const newImagePreview = URL.createObjectURL(file);
+                    setImagePreview(newImagePreview);
+                    setObjectURLs((prevURLs) => [...prevURLs, newImagePreview]);
+                } else {
+                    setSelectedFile(null);
+                    setImagePreview(null);
+                }
+            }
+        };
+
+    const handleAttachClick = useCallback(() => {
+        if (fileInputRef.current) {
+            fileInputRef.current.click();
+        }
+    }, [fileInputRef]);
+
+
     const avatars = useMemo(() => {
         return users.reduce((pv, elt) => {
             if (elt.id) {
@@ -390,6 +435,14 @@ const Chat = (props: ChatProps) => {
         loadMoreItems(0);
     }, [loadMoreItems]);
 
+    useEffect(() => {
+        return () => {
+            for (const objectURL of objectURLs) {
+                URL.revokeObjectURL(objectURL);
+            }
+        };
+    }, [objectURLs]);
+
     const loadOlder = useCallback(
         (evt: MouseEvent<HTMLElement>) => {
             const { start } = evt.currentTarget.dataset;
@@ -424,6 +477,7 @@ const Chat = (props: ChatProps) => {
                                 senderId={senderId}
                                 message={`${row[columns[1]]}`}
                                 name={columns[2] ? `${row[columns[2]]}` : "Unknown"}
+                                image={columns[3] && columns[3] != "_tp_index" && row[columns[3]] ? `${row[columns[3]]}` : undefined}
                                 className={className}
                                 getAvatar={getAvatar}
                                 index={idx}
@@ -443,6 +497,25 @@ const Chat = (props: ChatProps) => {
                     />
                 </Popper>
                 {withInput ? (
+                    <>
+                    {imagePreview && selectedFile && (
+                            <Box mb={1}>
+                                <Chip
+                                    label={selectedFile.name}
+                                    avatar={<Avatar alt="Image preview" src={imagePreview}/>}
+                                    onDelete={() => setSelectedFile(null)}
+                                    variant="outlined"
+                                />
+                            </Box>
+                        )}
+                    <input
+                            type="file"
+                            ref={fileInputRef}
+                            style={noDisplayStyle}
+                            onChange={(e) => handleFileSelect(e)}
+                            accept="image/*"
+                        />
+
                     <TextField
                         margin="dense"
                         fullWidth
@@ -452,6 +525,17 @@ const Chat = (props: ChatProps) => {
                         onKeyDown={handleAction}
                         slotProps={{
                             input: {
+                                startAdornment: (<InputAdornment position="start">
+                                    <IconButton
+                                            aria-label="upload image"
+                                            onClick={handleAttachClick}
+                                            edge="start"
+                                            disabled={!active}
+                                        >
+                                       <AttachFile color={disableColor("primary", !active)} />
+                                    </IconButton>
+
+                                </InputAdornment>),
                                 endAdornment: (
                                     <InputAdornment position="end">
                                         <IconButton
@@ -468,6 +552,7 @@ const Chat = (props: ChatProps) => {
                         }}
                         sx={inputSx}
                     />
+                    </>
                 ) : null}
                 {props.children}
             </Paper>

+ 5 - 0
frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

@@ -41,6 +41,7 @@ import TableSortLabel from "@mui/material/TableSortLabel";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
 import { visuallyHidden } from "@mui/utils";
+import { generateHeaderClassName } from "./tableUtils";
 
 import { createRequestTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
 import { emptyArray } from "../../utils";
@@ -528,6 +529,10 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                                     ? { minWidth: `${100 / nbWidth}%` }
                                                     : undefined
                                             }
+                                            className={col === "EDIT_COL"
+                                                ? getSuffixedClassNames(className, "-action")
+                                                : getSuffixedClassNames(className, generateHeaderClassName(columns[col].dfid))
+                                            }
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [

+ 28 - 0
frontend/taipy-gui/src/components/Taipy/tableUtils.spec.ts

@@ -0,0 +1,28 @@
+import { generateHeaderClassName } from "./tableUtils";
+
+describe("generateHeaderClassName", () => {
+    it("should generate a CSS class name with a hyphen prefix and convert to lowercase", () => {
+        const result = generateHeaderClassName("ColumnName");
+        expect(result).toBe("-columnname");
+    });
+
+    it("should replace spaces and special characters with hyphens", () => {
+        const result = generateHeaderClassName("Column Name@123!");
+        expect(result).toBe("-column-name-123-");
+    });
+
+    it("should remove multiple hyphens in a row", () => {
+        const result = generateHeaderClassName("Column--Name");
+        expect(result).toBe("-column-name");
+    });
+
+    it("should handle empty strings and return an empty string", () => {
+        const result = generateHeaderClassName("");
+        expect(result).toBe("");
+    });
+
+    it("should return empty string for the undefined", () => {
+        const result = generateHeaderClassName(undefined);
+        expect(result).toBe("");
+    });
+});

+ 16 - 0
frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

@@ -47,6 +47,22 @@ import { TaipyActiveProps, TaipyMultiSelectProps, getSuffixedClassNames } from "
 /**
  * A column description as received by the backend.
  */
+
+/**
+ * Generates a  CSS class name for a table header.
+ * @param columnName - The name of the column.
+ * @returns for CSS class name.
+ */
+
+export const generateHeaderClassName = (columnName: string | undefined): string => {
+    // logic for the css header classname
+    if (!columnName){
+        // return an empty string if columname is undefined or empty
+        return "";
+    }
+    return '-' + columnName.replace(/\W+/g, '-').replace(/-+/g, '-').toLowerCase();
+};
+
 export interface ColumnDesc {
     /** The unique column identifier. */
     dfid: string;

+ 1 - 0
taipy/gui/_renderers/factory.py

@@ -99,6 +99,7 @@ class _Factory:
                 ("sender_id",),
                 ("height",),
                 ("page_size", PropertyType.number, 50),
+                ("max_file_size", PropertyType.number, 1 * 1024 * 1024),
                 ("show_sender", PropertyType.boolean, False),
                 ("mode",),
             ]

+ 8 - 1
taipy/gui/viselements.json

@@ -1701,7 +1701,14 @@
                         "default_property": true,
                         "required": true,
                         "type": "dynamic(list[str])",
-                        "doc": "The list of messages. Each item of this list must consist of a list of three strings: a message identifier, a message content, and a user identifier."
+                        "doc": "The list of messages. Each item of this list must consist of a list of three strings: a message identifier, a message content, a user identifier, and an image URL."
+                    },
+                    {
+                        "name": "max_file_size",
+                        "type": "int",
+                        "default_value": "1 * 1024 * 1024 (1MB)",
+                        "doc": "The maximum file size can be uploaded to a chat message."
+
                     },
                     {
                         "name": "users",