Skip to content

Commit

Permalink
Document the chat control (Avaiga#1070)
Browse files Browse the repository at this point in the history
- Documentation and examples for the chat control.
- Reorganize Release Notes for Taipy 4.0 (3.0 moved to Legacy).
- Section on shared variables
- Release Notes for pull requests Avaiga#1092 and #1598.
- Fix generation of crossrefs to Reference Manuals
  • Loading branch information
FabienLelaquais authored Aug 5, 2024
1 parent 6d8a151 commit 98f3b71
Show file tree
Hide file tree
Showing 36 changed files with 766 additions and 390 deletions.
2 changes: 1 addition & 1 deletion docs/manuals/userman/auth/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Taipy Enterprise edition supports three authentication protocols:
with the arguments that were provided to `(auth.)login()^`.


Beside their specific parameters, all authenticators have two parameters that you
Besides their specific parameters, all authenticators have two parameters that you
can provide in the `Authenticator.__init__^`(`Authenticator` constructor):

- *secret_key*: a string that is used to encrypt the user credentials. Because
Expand Down
26 changes: 26 additions & 0 deletions docs/manuals/userman/gui/binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,32 @@ is defined.
Similarly, *state["\_\_main\_\_"].x* always refers to the global *x* variable, defined in the
main module.

# Sharing variables across clients

Your application can share a variable across all the connected users (identified as a new
connection to the application).<br/>
The `State^` object that all callbacks receive holds the value of all the bound variables for a
specific client. If you change that value, it does not impact the other clients' pages.

However, variables declared in the main module of the Python script can be *shared* among all the
connected users by declaring them as *shared variables*.<br/>
You can call the function `Gui.add_shared_variable(<variable-name>)^` to achieve that. When you do
this, modifying the value of `state.<variable-name>` is automatically propagated to every client.

Another way to achieve the same result is to use:
```
gui.broadcast_change("<variable-name>", new_value)
```
instead of
```
state.<variable-name> = new_value
```

Using the `Gui.broadcast_change()^` function propagates a new value for a variable to the states of
every connected user.

The function `Gui.broadcast_changes()^` acts in a similar manner, for multiple variables and values.

# List of values

Some controls (such as [selector](viselements/generic/selector.md) or
Expand Down
14 changes: 10 additions & 4 deletions docs/manuals/userman/gui/styling/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ There are two ways you can apply a stylesheet to your application:

- Global style sheet.<br/>
The *css_file* parameter of the `Gui.__init__^`(`Gui` constructor) lets you specify a CSS file
that your application will use for every page. The default value for this parameter is a file
located next to your main Python script, with the same name except for the extension that must be
'.css'.
that your application will use for every page.<br/>
The default value for this parameter is a file located next to your main Python script, with the
same name except for the extension that must be '.css'.<br/>
If such a file does not exist, Taipy GUI will search for a file called `taipy.css` and load it
if it exists. This mechanism allows sharing the same stylesheet across all Taipy GUI applications
whose main script sits in the same directory.<br/>
If you need to apply styles for a specific application but want to benefit from the shared styles
defined in `taipy.css`, you can add the directive `@import url("taipy.css");` to your
application-specific CSS file.

- Page-specific style.<br/>
The method `Gui.add_page()^` has a *style* parameter that can be set to CSS content.
This additional style is applied to the page and **only** this page.

Beside explicit style sheets, you can also modify the global theme, as
Besides explicit style sheets, you can also modify the global theme, as
described in the [section on Themes](#themes).

# Applying style
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
308 changes: 306 additions & 2 deletions docs/manuals/userman/gui/viselements/generic/chat.md_template
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
A control that provides the user interface for chatting.

The control represents the chat area that can be used in a messaging application that allows users
to send and receive messages.

The control has two main components:

- A list of messages.<br/>
For any given user connected to the application, messages this user sends appear in a bubble
aligned to the right. Other messages are aligned to the left and show the identifier of the
sender and the sender's avatar image if one was defined.
- A message input box.<br/>
Located at the bottom of the message list, this input field allows users to type and send
messages.

# Details

TODO
Three mandatory properties can configure the `chat` control:

- [*users*](#p-users) defines the list of users that can send or receive messages using the
control.<br/>
This property value can be as simple as a list of user identifiers or a list of `Icon^` objects
that can associate an avatar image with a user.
- [*sender_id*](#p-sender_id) indicates the identifier of user that is sending messages.<br/>
This identifier must appear in the [*users*](#p-users) list.<br/>
Messages whose sender identifier (see below) is equal to this property's value appear on the right
side of the message list; the other messages appear on the left side of the control.
- [*messages*](#p-messages) is a list of message records.<br/>
Each message record must a tuple or a list that must have three elements:

- Element 0: an identifier for the message.<br/>
That can be any string that uniquely identifies a specific message.
- Element 1: the message content.<br/>
The string that is represented in a message bubble.
- Element 2: the identifier of the sender.<br/>
This identifier must appear in the [*users*](#p-users) list.

# Styling

Expand All @@ -11,4 +42,277 @@ name to select the date selectors on your page and apply style.

# Usage

TODO
## Human-to-machine dialog {data-source="gui:doc/examples/controls/chat-calculator.py"}

A simple use case where the `chat` control is relevant is if you want to create an application where
a user can type and send text messages that are processed by some engine, whose role is to generate
messages back for the user to read.<br/>
Typical examples of such interactive applications are based on
[Large Language Models](https://en.wikipedia.org/wiki/Large_language_model) (LLMs) that can process
incoming messages written in natural language, interpret the text, and generate responses. Such
applications are known as digital assistants. There are several famous examples of these, such as
[ChatGPT](https://openai.com/index/chatgpt/),
[Microsoft Copilot](https://www.microsoft.com/fr-fr/microsoft-copilot), and
[Google Gemini](https://gemini.google.com/), among others.

In this example, we will use the `chat` control in a similar manner, in a simpler context: a
calculator.<br/>
The end user is invited to provide a mathematical expression. The application computes the
evaluation of that expression (presumed to be a valid Python expression) and displays the result.

Let's start by defining the main variables that the `chat` control relies on:
```python
users = ["human", "Result"]
messages: list[tuple[str, str, str]] = []
```

*users* holds the identifier of the two interlocutors for this application: "human" (*users[0]*) is
to identifier the end user, "Result" is the identifier we use to represent the computer: this string
is displayed above messages generated by the application.<br/>
*messages* stores all the created messages.

The control definition is the following:
!!! taipy-element
default={messages}
users={users}
sender_id={users[0]}
on_action=evaluate

*messages* and *users* are bound to their respective properties and the [*sender_id*](#p-sender_id)
property is set to the first user identifier in *users*.

The *on_action* property is set to the *evaluate()* callback function whose code is the following:
```python linenums="1"
def evaluate(state, var_name: str, payload: dict):
(_, _, expression, sender_id) = payload.get("args", [])
messages.append((f"{len(messages)}", expression, sender_id))
result = "Invalid expression"
try:
result = f"= {eval(expression)}"
except Exception:
pass
messages.append((f"{len(messages)}", result, users[1]))
state.messages = messages
```

Line 2 retrieves the callback parameters from the payload.<br/>
The variable *expression* saves the expression provided by the user and *sender_id* keeps the
identifier of the user that sent this message.

Line 3 adds a new message to the `chat` control message list, copying the expression text and
indicating who the sender is, so the message appears on the right side of the control.<br/>
Note how we generate the message identifier: each added message enlarges the message list, so we
simply use the length of the message list as the message identifier. This value increments every
time a new message gets in.

Line 6 evaluates the expression. If this call raises an exception, the application sets the response
message to a string indicating an error has occurred.<br/>
When the evaluation succeeds, we create a response made of an equal sign followed by the result.

Finally, on line 9, we store the evaluation's result in a new message, indicating it was sent by the
application (so it appears on the left side of the control).

Line 10 updates the user's *state* so the control is updated.

After the user has submitted a few expressions to evaluate, the control displays something like
this:
<figure>
<img src="../chat-calculator-d.png" class="visible-dark" />
<img src="../chat-calculator-l.png" class="visible-light"/>
<figcaption>A Taipy calculator</figcaption>
</figure>

## Multi-user chatting {data-source="gui:doc/examples/controls/chat-discuss.py"}

You can also use the `chat` control in another situation: when you want to develop an application
that allows several users to share the same chatting space.<br/>
Here is how you can create such an application using Taipy GUI.

<h4>General setup</h4>

The application has two pages:

- The first page (called "register") allows users to register for the chatting application.
- The second page (called "discuss") shows all the messages exchanged in a `chat` control. You can
consider this page as showing a *channel* in a chat application: several users can send and read
messages in a community.

Let's describe this first page.<br/>
The definition of the page is the following:
```python
username = ""

register_page = """
Please enter your user name:

<|{username}|input|>

<|Submit|button|on_action=register|>
"""
```

It holds an input field bound to the variable *username*.<br/>

Here is how this page looks like when the user Alice has entered her name:
<figure>
<img src="../chat-discuss-register-a-d.png" class="visible-dark" />
<img src="../chat-discuss-register-a-l.png" class="visible-light"/>
<figcaption>Alice is about to register</figcaption>
</figure>

The control's `on_action` callback property is set so that the callback function *register()* is
invoked when the "Submit" button is pressed.<br/>
Here is the code of this function:
```python linenums="1"
users: list[str|Icon] = []

def register(state):
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
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)
state.users = users
navigate(state, "discuss")
```

In lines 4 to 7, we test whether that user name is one of the names stored in the *users* list and
reject the registration if it is.

In lines 8 to 12, we add this user name to the *users* list.<br/>
If we can find an avatar image for that user (a file called "&lt;username>-avatar.png" located next
to the Python script), then we create an `Icon^` instance to represent the user avatar.

Line 13 updates the variable *users* for the user's state.<br/>
And on line 14, we navigate to the "discuss" page so the user can read and send messages.

Here is the code that defines the "discuss" page:
```python
messages: list[tuple[str, str, str]] = []

discuss_page = """
<|### Let's discuss, {username}|text|mode=markdown|>

<|{messages}|chat|users={users}|sender_id={username}|on_action=send|>
"""
```

This page creates a `chat` control that binds its default property ([*messages*](#p-messages_id)) to
the variable *messages* that is initialized to an empty list.<br/>
The variables *users* and *username* are bound as expected.

Note that because this application has two pages, you must create the `Gui^` instance with the
*pages* parameter set to a dictionary that holds those pages.<br/>
Here is the code that initializes and runs the Taipy GUI application:
```python
pages = {"register": register_page, "discuss": discuss_page}
gui = Gui(pages=pages).run()
```

After the registration, this is the page that Alice will see:
<figure>
<img src="../chat-discuss-discuss-a-1-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-a-1-l.png" class="visible-light"/>
<figcaption>The chatting area for Alice</figcaption>
</figure>

We now need to implement the callback function *send()*, which is set to the *on_action* property
of the chat control.<br/>
Here is the code for this function:
```python linenums="1"
def send(state, _: str, payload: dict):
(_, _, message, sender_id) = payload.get("args", [])
messages.append((f"{len(messages)}", message, sender_id))
state.messages = messages
```

Like in the calculator example above, we retrieve the callback parameters on line 2 and
add a new message (on line 3) using the message index as its identifier.<br/>
The state's variable *messages* is updated on line 4.

<h4>Multi-user considerations</h4>

We want several users to connect to this application and share the messages and the user list
we created.<br/>
To make that happen, you can use the function `Gui.add_shared_variable()^` to ensure that both
*messages* and *users* have the same value for every connected user.<br/>
Let's do that for our use case:
```python
Gui.add_shared_variables("messages", "users")
```
After that function is invoked, every time any user modifies *messages* or *users*, the impact is
propagated to all the connected users.

Let's pretend that a new user, Beatrix, connects to the application.<br/>
Here is the registration page as this user sees it:
<figure>
<img src="../chat-discuss-register-b-d.png" class="visible-dark" />
<img src="../chat-discuss-register-b-l.png" class="visible-light"/>
<figcaption>Beatrix registers</figcaption>
</figure>

When Beatrix presses the "Submit" button, the *register()* callback function is invoked.<br/>
Because *users* is a *shared variable*, every client, including Alice, is aware of this new
registration: their state's *users* variable is updated accordingly and automatically.

Imagine that Alice enters the following message:
<figure>
<img src="../chat-discuss-discuss-a-1-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-a-1-l.png" class="visible-light"/>
<figcaption>Alice sends a message</figcaption>
</figure>

When Alices pressed the send button, she can see the updated `chat` control:
<figure>
<img src="../chat-discuss-discuss-a-2-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-a-2-l.png" class="visible-light"/>
<figcaption>Alice has sent a message</figcaption>
</figure>
The message sent appears in a right-aligned bubble, indicating that the current user, Alice, was
the one who sent this message.

Because both *users* and *messages* are shared among all connected clients, Beatrix will see
the following page:
<figure>
<img src="../chat-discuss-discuss-b-1-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-b-1-l.png" class="visible-light"/>
<figcaption>Beatrix can see Alice's message</figcaption>
</figure>
As expected, messages coming from Alice appear on the left side of Beatrix's page.<br/>
Also, because Alice has an avatar image, this image appears next to the messages she sent.

Here are the pages that Alice and Beatrix can see when they start sending messages to each other:
<figure>
<img src="../chat-discuss-discuss-a-3-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-a-3-l.png" class="visible-light"/>
<figcaption>What Alice sees</figcaption>
</figure>
<figure>
<img src="../chat-discuss-discuss-b-2-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-b-2-l.png" class="visible-light"/>
<figcaption>What Beatrix sees</figcaption>
</figure>

Now, let's imagine that another user, Charles, joins the discussion. The three connected users can
send messages to the group, sharing the bound variables *users* and *messages* so that their `chat`
controls all represent the same content, with a different layout depending on who sent messages.

<figure>
<img src="../chat-discuss-discuss-a-4-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-a-4-l.png" class="visible-light"/>
<figcaption>Alice's screen</figcaption>
</figure>
<figure>
<img src="../chat-discuss-discuss-b-3-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-b-3-l.png" class="visible-light"/>
<figcaption>Beatrix's screen</figcaption>
</figure>
<figure>
<img src="../chat-discuss-discuss-c-1-d.png" class="visible-dark" />
<img src="../chat-discuss-discuss-c-1-l.png" class="visible-light"/>
<figcaption>Charles's screen</figcaption>
</figure>
Loading

0 comments on commit 98f3b71

Please sign in to comment.