Переглянути джерело

Add examples for the chat control (#1631)

- Examples for the chat control
- Example for the table control
- Remove fallback hover text in Chat component
- Hide JsonAdapter class
- Minor fixes in reference and elements docs
Fabien Lelaquais 9 місяців тому
батько
коміт
1ad66d6af0

+ 0 - 3
doc/gui/examples/charts/advanced-python-lib.py

@@ -43,9 +43,6 @@ figure.add_trace(
 figure.update_layout(title="Different Probability Distributions")
 
 page = """
-# Plotly Python
-<|toggle|theme|>
-
 <|chart|figure={figure}|>
 """
 

BIN
doc/gui/examples/controls/alice-avatar.png


BIN
doc/gui/examples/controls/beatrix-avatar.png


BIN
doc/gui/examples/controls/charles-avatar.png


+ 48 - 0
doc/gui/examples/controls/chat-calculator.py

@@ -0,0 +1,48 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# Human-computer dialog UI based on the chat control.
+# -----------------------------------------------------------------------------------------
+from math import cos, pi, sin, sqrt, tan  # noqa: F401
+
+from taipy.gui import Gui
+
+# The user interacts with the Python interpreter
+users = ["human", "Result"]
+messages: list[tuple[str, str, str]] = []
+
+
+def evaluate(state, var_name: str, payload: dict):
+    # Retrieve the callback parameters
+    (_, _, expression, sender_id) = payload.get("args", [])
+    # Add the input content as a sent message
+    messages.append((f"{len(messages)}", expression, sender_id))
+    # Default message used if evaluation fails
+    result = "Invalid expression"
+    try:
+        # Evaluate the expression and store the result
+        result = f"= {eval(expression)}"
+    except Exception:
+        pass
+    # Add the result as an incoming message
+    messages.append((f"{len(messages)}", result, users[1]))
+    state.messages = messages
+
+
+page = """
+<|{messages}|chat|users={users}|sender_id={users[0]}|on_action=evaluate|>
+"""
+
+Gui(page).run()

+ 84 - 0
doc/gui/examples/controls/chat-discuss.py

@@ -0,0 +1,84 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# A chatting application based on the chat control.
+# In order to see the users' avatars, the image files must be stored next to this script.
+# If you want to test this application locally, you need to use several browsers and/or
+# incognito windows so a given user's context is not reused.
+# -----------------------------------------------------------------------------------------
+from os import path
+
+from taipy.gui import Gui, Icon
+from taipy.gui.gui_actions import navigate, notify
+
+username = ""
+users: list[str|Icon] = []
+messages: list[tuple[str, str, str]] = []
+
+Gui.add_shared_variables("messages", "users")
+
+
+def on_init(state):
+    # Copy the global variables users and messages to this user's state
+    state.users = users
+    state.messages = messages
+
+
+def on_navigate(state, path: str):
+    # Navigate to the 'register' page if the user is not registered
+    if path == "discuss" and state.username == "":
+        return "register"
+    return path
+
+
+def register(state):
+    # Check that the user is not already registered
+    for user in users:
+        if state.username == user or (isinstance(user, (list, tuple)) and state.username == user[0]):
+            notify(state, "error", "User already registered.")
+            return
+    # Use the avatar image if we can find it
+    avatar_image_file = f"{state.username.lower()}-avatar.png"
+    if path.isfile(avatar_image_file):
+        users.append((state.username, Icon(avatar_image_file, state.username)))
+    else:
+        users.append(state.username)
+    # Because users is a shared variable, this propagates to every client
+    state.users = users
+    navigate(state, "discuss")
+
+
+def send(state, _: str, payload: dict):
+    (_, _, message, sender_id) = payload.get("args", [])
+    messages.append((f"{len(messages)}", message, sender_id))
+    state.messages = messages
+
+
+register_page = """
+Please enter your user name:
+
+<|{username}|input|>
+
+<|Submit|button|on_action=register|>
+"""
+
+discuss_page = """
+<|### Let's discuss, {username}|text|mode=markdown|>
+
+<|{messages}|chat|users={users}|sender_id={username}|on_action=send|>
+"""
+
+pages = {"register": register_page, "discuss": discuss_page}
+gui = Gui(pages=pages).run()

+ 40 - 0
doc/gui/examples/controls/table-formatting.py

@@ -0,0 +1,40 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+import datetime
+
+from taipy.gui import Gui
+
+stock = {
+    "date": [datetime.datetime(year=2000, month=12, day=d) for d in range(20, 30)],
+    "price": [119.88, 112.657, 164.5, 105.42, 188.36, 103.9, 143.97, 160.11, 136.3, 174.06],
+    "change": [7.814, -5.952, 0.01, 8.781, 7.335, 6.623, -6.635, -6.9, 0.327, -0.089],
+    "volume": [773, 2622, 2751, 1108, 7400, 3772, 9398, 4444, 9264, 1108],
+}
+
+columns = {
+    "date" : {"title": "Data", "format": "MMM d"},
+    "price" : {"title": "Price", "format": "$%.02f"},
+    "change" : {"title": "% change", "format": "%.01f"},
+    "volume" : {"title": "Volume"}
+}
+
+page = """
+# Formatting cells in a table
+
+<|{stock}|table|columns={columns}|>
+"""
+
+Gui(page).run()

+ 0 - 3
doc/gui/examples/controls/text-format.py

@@ -18,9 +18,6 @@ from taipy.gui import Gui
 pi = 3.14159265358979
 
 page = """
-# Text - Formatting
-<|toggle|theme|>
-
 π≈<|{pi}|text|format=%.3f|>
 """
 

+ 0 - 3
doc/gui/examples/controls/text-md.py

@@ -26,9 +26,6 @@ then you can create line skips.
 """ # noqa W291
 
 page = """
-# Text - Markdown
-<|toggle|theme|>
-
 <|{markdown}|text|mode=markdown|>
 """
 

+ 0 - 3
doc/gui/examples/controls/text-pre.py

@@ -24,9 +24,6 @@ if __name__ == "__main__":
 """
 
 page = """
-# Text - pre
-<|toggle|theme|>
-
 <|{code}|text|mode=pre|>
 """
 

+ 0 - 3
doc/gui/examples/controls/text-simple.py

@@ -18,9 +18,6 @@ from taipy.gui import Gui
 name = "Taipy"
 
 page = """
-# Text - simple
-<|toggle|theme|>
-
 <|Hello {name}!|>
 """
 

+ 1 - 3
frontend/taipy-gui/src/components/Taipy/Chat.tsx

@@ -167,7 +167,6 @@ const Chat = (props: ChatProps) => {
 
     const [rows, setRows] = useState<RowType[]>([]);
     const page = useRef<key2Rows>({ key: defaultKey });
-    const [rowCount, setRowCount] = useState(0);
     const [columns, setColumns] = useState<Array<string>>([]);
     const scrollDivRef = useRef<HTMLDivElement>(null);
     const anchorDivRef = useRef<HTMLElement>(null);
@@ -291,7 +290,6 @@ const Chat = (props: ChatProps) => {
     useEffect(() => {
         if (!refresh && props.messages && page.current.key && props.messages[page.current.key] !== undefined) {
             const newValue = props.messages[page.current.key];
-            setRowCount(newValue.rowcount);
             const nr = newValue.data as RowType[];
             if (Array.isArray(nr) && nr.length > newValue.start && nr[newValue.start]) {
                 setRows((old) => {
@@ -341,7 +339,7 @@ const Chat = (props: ChatProps) => {
     );
 
     return (
-        <Tooltip title={hover || "" || `rowCount: ${rowCount}`}>
+        <Tooltip title={hover || ""}>
             <Paper className={className} sx={boxSx} id={id}>
                 <Grid container rowSpacing={2} sx={gridSx} ref={scrollDivRef}>
                     {rows.length && !rows[0] ? (

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

@@ -27,6 +27,7 @@ from ..utils.singleton import _Singleton
 
 
 class JsonAdapter(ABC):
+    """NOT DOCUMENTED"""
     def register(self):
         _TaipyJsonAdapter().register(self)
 

+ 8 - 5
taipy/gui/gui_actions.py

@@ -28,7 +28,7 @@ def download(
         name: File name for the content on the client browser (defaults to content name).
         on_action: Callback function (or callback name) to call when the download ends. See below.
 
-    ## Notes:
+    <h4>Notes:</h4>
 
     - *content*: this parameter can hold several values depending on your use case:
         - a string: the value must be an existing path name to the file that gets downloaded or
@@ -122,9 +122,12 @@ def hold_control(
             chooses to cancel.<br/>
             If empty or None, no cancel action is provided to the user.<br/>
             The signature of this function is:
-            - state (State^): The user state;
+
+            - state (`State^`): The user state;
             - id (str): the id of the button that triggered the callback. That will always be
               "UIBlocker" since it is created and managed internally;
+
+            If this parameter is None, no "Cancel" button is displayed.
         message: The message to show. The default value is the string "Work in Progress...".
     """
     if state and isinstance(state._gui, Gui):
@@ -212,8 +215,8 @@ def get_state_id(state: State) -> t.Optional[str]:
         state (State^): The current user state as received in any callback.
 
     Returns:
-        A string that uniquely identifies the state.<br/>
-        If None, then **state** was not handled by a `Gui^` instance.
+        A string that uniquely identifies the state. If this value None, it indicates that *state* is not
+        handled by a `Gui^` instance.
     """
     if state and isinstance(state._gui, Gui):
         return state._gui._get_client_id()
@@ -273,7 +276,7 @@ def invoke_callback(
     """Invoke a user callback for a given state.
 
     Calling this function is equivalent to calling
-    *gui*.[Gui.]invoke_callback(state_id, callback, args, module_context)^`.
+    *gui*.`(Gui.)invoke_callback(state_id, callback, args, module_context)^`.
 
     Arguments:
         gui (Gui^): The current Gui instance.

+ 48 - 44
taipy/gui/viselements.json

@@ -23,7 +23,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the text is processed:<ul><li>&quot;raw&quot;: synonym for setting the *raw* property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown."
+                        "doc": "Define the way the text is processed:\n<ul><li>&quot;raw&quot;: synonym for setting the *raw* property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown."
                     },
                     {
                         "name": "format",
@@ -108,7 +108,7 @@
                         "name": "lines_shown",
                         "type": "int",
                         "default_value": "5",
-                        "doc": "The height of the displayed element if multiline is True."
+                        "doc": "The number of lines shown in the input control, when multiline is True."
                     },
                     {
                         "name": "type",
@@ -224,12 +224,12 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "\"300px\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this slider, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str",
-                        "doc": "The height, in CSS units, of this element.<br/>It defaults to the <i>width</i> value when using the vertical orientation."
+                        "doc": "The height of this slider, in CSS units.<br/>It defaults to the value of <i>width</i> when using the vertical orientation."
                     },
                     {
                         "name": "orientation",
@@ -272,7 +272,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the toggle is displayed:<ul><li>&quot;theme&quot;: synonym for setting the *theme* property to True</li></ul>"
+                        "doc": "Define the way the toggle is displayed:\n<ul><li>&quot;theme&quot;: synonym for setting the *theme* property to True</li></ul>"
                     }
                 ]
             }
@@ -602,12 +602,12 @@
                         "name": "width",
                         "type": "str|int|float",
                         "default_value": "\"100%\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this chart, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this element."
+                        "doc": "The height of this chart, in CSS units."
                     },
                     {
                         "name": "template",
@@ -685,7 +685,7 @@
                     {
                         "name": "width[<i>column_name</i>]",
                         "type": "str",
-                        "doc": "The width, in CSS units, of the indicated column."
+                        "doc": "The width of the indicated column, in CSS units."
                     },
                     {
                         "name": "selected",
@@ -751,13 +751,13 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "\"100%\"",
-                        "doc": "The width, in CSS units, of this table control."
+                        "doc": "The width of this table control, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str",
                         "default_value": "\"80vh\"",
-                        "doc": "The height, in CSS units, of this table control."
+                        "doc": "The height of this table control, in CSS units."
                     },
                     {
                         "name": "filter",
@@ -932,7 +932,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the selector is displayed:<ul><li>&quot;radio&quot;: list of radio buttons</li><li>&quot;check&quot;: list of check buttons</li><li>any other value: selector as usual."
+                        "doc": "Define the way the selector is displayed:\n<ul><li>&quot;radio&quot;: list of radio buttons</li><li>&quot;check&quot;: list of check buttons</li><li>any other value: selector as usual."
                     },
                     {
                         "name": "dropdown",
@@ -956,12 +956,12 @@
                         "name": "width",
                         "type": "str|int",
                         "default_value": "\"360px\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this selector, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str|int",
-                        "doc": "The height, in CSS units, of this element."
+                        "doc": "The height of this selector, in CSS units."
                     }
                 ]
             }
@@ -1137,12 +1137,12 @@
                         "name": "width",
                         "type": "str|int|float",
                         "default_value": "\"300px\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this image control, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this element."
+                        "doc": "The height of this image control, in CSS units."
                     }
                 ]
             }
@@ -1230,13 +1230,13 @@
                         "name": "width",
                         "type": "str|number",
                         "default_value": "None",
-                        "doc": "The width, in CSS units, of the metric."
+                        "doc": "The width of the metric control, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str|number",
                         "default_value": "None",
-                        "doc": "The height, in CSS units, of the metric."
+                        "doc": "The height of the metric control, in CSS units."
                     },
                     {
                         "name": "template",
@@ -1333,13 +1333,13 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "None",
-                        "doc": "The width, in CSS units, of the indicator (used when orientation is horizontal)."
+                        "doc": "The width of the indicator, in CSS units (used when orientation is horizontal)."
                     },
                     {
                         "name": "height",
                         "type": "str",
                         "default_value": "None",
-                        "doc": "The height, in CSS units, of the indicator (used when orientation is vertical)."
+                        "doc": "The height of the indicator, in CSS units (used when orientation is vertical)."
                     },
                     {
                         "name": "hover_text",
@@ -1387,13 +1387,13 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "\"15vw\"",
-                        "doc": "The width, in CSS units, of the menu when unfolded.<br/>Note that when running on a mobile device, the property <i>width[active]</i> is used instead."
+                        "doc": "The width of the menu when unfolded, in CSS units.<br/>Note that when running on a mobile device, the property <i>width[active]</i> is used instead."
                     },
                     {
                         "name": "width[mobile]",
                         "type": "str",
                         "default_value": "\"85vw\"",
-                        "doc": "The width, in CSS units, of the menu when unfolded, on a mobile device."
+                        "doc": "The width of the menu when unfolded, in CSS units, when running on a mobile device."
                     },
                     {
                         "name": "on_action",
@@ -1473,7 +1473,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of the function that is triggered when the dialog button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:<ul><li>The first element is the username</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul><br/>When the button is pressed, and if this property is not set, Taipy will try to find a callback function called <i>on_login()</i> and invoke it with the parameters listed above.",
+                        "doc": "The name of the function that is triggered when the dialog button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:\n<ul><li>The first element is the username</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul><br/>When the button is pressed, and if this property is not set, Taipy will try to find a callback function called <i>on_login()</i> and invoke it with the parameters listed above.",
                         "signature": [
                             [
                                 "state",
@@ -1510,17 +1510,29 @@
                         "default_property": true,
                         "required": true,
                         "type": "dynamic(list[str])",
-                        "doc": "The list of messages. Each element is a list composed of an id, a message and an 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, and a user identifier."
                     },
                     {
                         "name": "users",
                         "type": "dynamic(list[str|Icon])",
                         "doc": "The list of users. See the <a href=\"../../binding/#list-of-values\">section on List of Values</a> for details."
                     },
+                    {
+                        "name": "sender_id",
+                        "type": "str",
+                        "default_value": "\"taipy\"",
+                        "doc": "The user identifier, as indicated in the <i>users</i> list, associated with all messages sent from the input."
+                    },
+                    {
+                        "name": "with_input",
+                        "type": "dynamic(bool)",
+                        "default_value": "True",
+                        "doc": "If False, the input field is not rendered."
+                    },
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of a function that is triggered when the user enters a new message.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the messages variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args (list): A list composed of a reason (click or Enter), variable name, message, sender id.</li></ul></li></ul>.",
+                        "doc": "The name of a function that is triggered when the user enters a new message.<br/>All the parameters of that function are optional:\n<ul>\n<li><i>state</i> (<code>State^</code>): the state instance.</li>\n<li><i>var_name</i> (str): the name of the variable bound to the <i>messages</i> property.</li>\n<li><i>payload</i> (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li><i>action</i>: the name of the action that triggered this callback.</li>\n<li><i>args</i> (list): A list composed of a reason (\"click\" or \"Enter\"), the variable name, message, the user identifier of the sender.</li></ul></li></ul>",
                         "signature": [
                             [
                                 "state",
@@ -1537,27 +1549,15 @@
                         ]
                     },
                     {
-                        "name": "with_input",
-                        "type": "dynamic(bool)",
-                        "default_value": "True",
-                        "doc": "If True, the input field is visible."
-                    },
-                    {
-                        "name": "sender_id",
-                        "type": "str",
-                        "default_value": "\"taipy\"",
-                        "doc": "The user id associated with the message sent from the input"
+                        "name": "page_size",
+                        "type": "int",
+                        "default_value": "50",
+                        "doc": "The number of messages retrieved from the application and sent to the frontend. Larger values imply more potential latency."
                     },
                     {
                         "name": "height",
                         "type": "str|int|float",
-                        "doc": "The maximum height, in CSS units, of this element."
-                    },
-                    {
-                        "name": "page_size",
-                        "type": "int",
-                        "default_value": "50",
-                        "doc": "The number of rows retrieved on the frontend."
+                        "doc": "The maximum height of this chat control, in CSS units."
                     }
                 ]
             }
@@ -1590,7 +1590,7 @@
                     {
                         "name": "row_height",
                         "type": "str",
-                        "doc": "The height, in CSS units, of each row."
+                        "doc": "The height of each row of this tree, in CSS units."
                     }
                 ]
             }
@@ -1708,12 +1708,12 @@
                     {
                         "name": "width",
                         "type": "str|int|float",
-                        "doc": "The width, in CSS units, of this dialog.<br/>(CSS property)"
+                        "doc": "The width of this dialog, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this dialog.<br/>(CSS property)"
+                        "doc": "The height of this dialog, in CSS units."
                     }
                 ]
             }
@@ -1965,21 +1965,25 @@
                     {
                         "name": "id",
                         "type": "str",
+                        "default_value": "None",
                         "doc": "The identifier that is assigned to the rendered HTML component."
                     },
                     {
                         "name": "properties",
                         "type": "dict[str, any]",
+                        "default_value": "None",
                         "doc": "Bound to a dictionary that contains additional properties for this element."
                     },
                     {
                         "name": "class_name",
                         "type": "dynamic(str)",
+                        "default_value": "None",
                         "doc": "The list of CSS class names that are associated with the generated HTML Element.<br/>These class names are added to the default <code>taipy-&lt;element_type&gt;</code> class name."
                     },
                     {
                         "name": "hover_text",
                         "type": "dynamic(str)",
+                        "default_value": "None",
                         "doc": "The information that is displayed when the user hovers over this element."
                     }
                 ]