New websocket component: DashSocketIO

Hello everyone :wave:

I have just created a new dash component dash-socketio, to go hand-in-hand with flask-socketio.

It allows to do some pretty cool things with Dash like notifying the user of progress while a callback is running, or streaming outputs while a callback is still running.

See below a GIF of usage.py in the repo. This is a toy example with the above 2 usecases implemented.

dash-socketio-usage

Hopefully this can be useful to some of the community!

15 Likes

Thank you @RenaudLN . Iā€™ve been searching for a way to stream LLM answers a couple of weeks ago. This looks like the perfect solution :pray:

3 Likes

@RenaudLN,

Do you have some examples of how to do the above?

Is it all one callback where the info is just added via streaming?

Could this be used to help with streaming chunks of files back to a download component?

1 Like

Do you mind comparing / contrasting when one might use this library vs. Websocket (dash-extensions.com)? Thanks!

1 Like

Hey @jinnyzor, yes this is all one callback, check out usage.py in the repo :slight_smile:

EDIT: one main callback plus some clientside callbacks to handle whatever is sent from the server via websocket.

1 Like

Hey @dash-beginner for the usecases above I needed a way to send to a websocket from a Dash callback, which I couldnā€™t find a way to make work with the dash-extensions websocket.

The dash-socketio component also allows to leverage flask-socketio which means you donā€™t need a separate server like quart or starlette to handle the websocket flows like in the dash-extensions examples.

1 Like

Let me see if I can configure something where the download component content can be streamed via this process. :slight_smile:

Ok, here is an example base on @RenaudLN, this will allow you to stream a file in a response:

import io
import time
import uuid
from dash_socketio import DashSocketIO
import dash_mantine_components as dmc
from dash import Dash, Input, Output, State, callback, clientside_callback, html, no_update, dcc
from flask_socketio import SocketIO, emit
import base64

app = Dash(__name__)
app.server.secret_key = "Test!"

socketio = SocketIO(app.server)

app.layout = dmc.NotificationsProvider(
    [
        dmc.Title("Hello Socket.IO!", mb="xl"),
        dmc.Stack(
            [
                dmc.Textarea(id="dummy", minRows=5, placeholder="Ask LoremLM something..."),
                html.Div(dmc.Button("Ask LoremLM", id="btn", mb="md", disabled=True)),
            ]
        ),
        dmc.Text(id="results", style={"maxWidth": "60ch"}),
        html.Button(id="trigger_download", style={"display": "none"}),
        html.Div(id="notification_wrapper"),
        DashSocketIO(id='socketio', eventNames=["notification", "stream"]),
        dcc.Download(id='download'),
        dcc.Store(id='download_data', storage_type='memory', data='')
    ],
    position="bottom-right",
)


@socketio.on("connect")
def on_connect():
    print("Client connected")

@socketio.on("disconnect")
def on_disconnect():
    print("Client disconnected")

def notify(socket_id, message, color=None):
    emit(
        "notification",
        dmc.Notification(
            message=message,
            action="show",
            id=uuid.uuid4().hex,
            color=color,
        ).to_plotly_json(),
        namespace="/",
        to=socket_id,
    )

paragraph = """Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Integer augue eros, tincidunt vitae eros eu, faucibus tempus risus.
Donec ullamcorper velit in arcu fermentum faucibus.
Etiam finibus tortor ac vestibulum dictum. Vestibulum ultricies risus eu lacus luctus pretium.
Duis congue et nisl eu fringilla. Mauris lorem metus, varius eget ex eget, ultrices suscipit est.
Integer nunc risus, auctor posuere vehicula id, rutrum et urna.
Pellentesque gravida, orci id pharetra tempus, nulla neque sagittis elit, condimentum tempor mi velit et urna.
Fusce faucibus ac libero facilisis commodo. Quisque condimentum suscipit mi.
Vivamus augue neque, commodo sagittis mollis sed, mollis in sapien.
Integer cursus et magna nec cursus.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
"""

def split_base64_to_chunks(base64_string, num_chunks):
    # Calculate the chunk size
    chunk_size = len(base64_string) // (num_chunks-1)

    # Split the bytes into chunks
    chunks = [base64_string[i * chunk_size: (i + 1) * chunk_size] for i in range(num_chunks)]

    return chunks

app.clientside_callback(
    """(n, data) => {console.log(data); return [{base64: true, content: data, filename: 'test.txt'}, '']}""",
    Output('download', 'data'),
    Output('download_data', 'data', allow_duplicate=True),
    Input("trigger_download", "n_clicks"),
    State('download_data', 'data'),
    prevent_initial_call=True
)

@callback(
    Output("trigger_download", "n_clicks"),
    Output("notification_wrapper", "children", allow_duplicate=True),
    Output("results", "children"),
    Input("btn", "n_clicks"),
    State("socketio", "socketId"),
    State("trigger_download", "n_clicks"),
    running=[[Output("results", "children"), "", None]],
    prevent_initial_call=True,
)
def display_status(n_clicks, socket_id, clicks):
    if not n_clicks or not socket_id:
        return no_update, []
    notify(socket_id, "Processing download...")
    time.sleep(1)
    notify(socket_id, "Downloading...")

    stream = io.BytesIO(paragraph.encode("utf-8"))

    # Read the stream content into a byte array
    stream.seek(0)  # Reset the stream position
    content_bytes = stream.read()
    base64_string = base64.b64encode(content_bytes).decode("utf-8")
    # Split into 10 chunks
    num_chunks = 10
    chunk_list = split_base64_to_chunks(base64_string, num_chunks)

    # Print each chunk
    for i, chunk in enumerate(chunk_list):
        print(f"Chunk {i + 1}: {chunk}")
        emit("stream", chunk, namespace="/", to=socket_id)
        notify(socket_id, f'{(i + 1)/num_chunks*100}% downloaded')
        time.sleep(1)

    notify(socket_id, "Done!", color="green")

    return (clicks or 0) + 1, [], paragraph

clientside_callback(
    """connected => !connected""",
    Output("btn", "disabled"),
    Input("socketio", "connected"),
)

clientside_callback(
    """(notification) => {
        if (!notification) return dash_clientside.no_update
        return notification
    }""",
    Output("notification_wrapper", "children", allow_duplicate=True),
    Input("socketio", "data-notification"),
    prevent_initial_call=True,
)

clientside_callback(
    """(newData, prevData) => (prevData || '') + newData""",
    Output("download_data", "data", allow_duplicate=True),
    Input("socketio", "data-stream"),
    State("download_data", "data"),
    prevent_initial_call=True,
)


if __name__ == '__main__':
    app.run_server(debug=True)

7 Likes

Thatā€™s pretty cool. I just tried it in my app and seems to work well.

I wondered if it also supports the broadcasting feature from flask_socketIo.
I can see that multiple clients, browsers in my case, connect to the socket in the console, but when emiting a message with broadcast=True I wonā€™t see it on the other clients.

Basically, I used the example from jinnyzor, just added the broadcasting.

Update:
Just found the answer. The clients need to join the same room and then emit the message not to the socketId but the room. Which is pretty nice. It is another way to keep some configurations in sync across user sessions.

Broadcast should work fine too. Here is an example

dash-socketio-chat

And the code

import json
from dash_socketio import DashSocketIO
import dash_mantine_components as dmc
from dash import Dash, Input, Output, State, callback, clientside_callback, html, no_update
from flask_socketio import SocketIO, emit
from plotly.io.json import to_json_plotly

app = Dash(__name__)
app.server.secret_key = "Test!"

socketio = SocketIO(app.server)

app.layout = dmc.NotificationsProvider(
    [
        dmc.Title("Hello Socket.IO!", mb="xl"),
        dmc.Stack(
            [
                dmc.Textarea(id="msg", minRows=5, placeholder="Write something in the chat"),
                html.Div(dmc.Button("Send", id="send", mb="md", disabled=True)),
            ],
            spacing="xs",
        ),
        dmc.Stack([], id="chat-messages", style={"maxWidth": "60ch"}),
        DashSocketIO(id="socketio", eventNames=["chatMsg"]),
    ],
    position="bottom-right",
)


def jsonify_data(data):
    return json.loads(to_json_plotly(data))


@callback(
    Output("msg", "value"),
    Input("send", "n_clicks"),
    State("msg", "value"),
    State("socketio", "socketId"),
    running=[[Output("send", "loading"), True, False]],
    prevent_initial_call=True,
)
def display_status(send, msg, socket_id):
    if not send or not msg:
        return no_update

    emit(
        "chatMsg",
        jsonify_data(dmc.Paper(
            [
                dmc.Text(f"{socket_id} says", size="xs", color="dimmed", mb=4),
                dmc.Text(msg),
            ],
            p="0.5rem 1rem",
            withBorder=True,
        )),
        namespace="/",
        broadcast=True,
        # NOTE: Here you could also use skip_sid=socket_id if you only wanted to broadcast
        # to others and handle the message to oneself differently.
    )
    return ""

clientside_callback(
    """connected => !connected""",
    Output("send", "disabled"),
    Input("socketio", "connected"),
)

clientside_callback(
    """(msg, children) => {
        if (!msg) return dash_clientside.no_update
        return [...children, msg]
    }""",
    Output("chat-messages", "children"),
    Input("socketio", "data-chatMsg"),
    State("chat-messages", "children"),
    prevent_initial_call=True,
)


if __name__ == '__main__':
    app.run_server(debug=True)

Note that youā€™d have to be careful in production that several machines may be serving requests so the broadcast might not reach every single user. Those are complexities inherent to websockets though so Iā€™d recommend reading more about them and how to make them work in prod :slight_smile:

Thank you. I will look more into it. Luckily, we are not at that stage yet :smiley: But this makes sending and updating messages easier.

Also, we have some use cases for it, where we want to keep some synchronisation between tabs/browsers for the same user.

Let me know if you need any help with the repo. I can step in and help you to finalise things if you like.

1 Like

Happy to take in some PRs :slight_smile:

1 Like

Hey @RenaudLN ,

Great project once I dug into researching socket.io on behalf of this post I quickly could see many places in my own projects where this could be used. Felt like Zelda music opening a chest and unlocking a new useful tool for the arsenal. I havenā€™t had much time building with it, but for the 3 hours i put into it I wanted to build upon your message system you posted in the form.

Basically, one issue I had with your code was on reloading of the app, the messages would be cleared. Also both instances had to be open at the same time to see and retrieve messages sent. So the improvement I wanted to add was to build the code so on loading the url you could see previous messages sent prior and on url refresh past messages stay visible. This is what I came up with:

import json
from dash_socketio import DashSocketIO
import dash_mantine_components as dmc
from dash import Dash, Input, Output, State, callback, clientside_callback, html, no_update, callback_context, dcc
from flask_socketio import SocketIO, emit
from plotly.io.json import to_json_plotly
import dash
import datetime
import base64


app = Dash(__name__)
app.server.secret_key = "Test!"

socketio = SocketIO(app.server)

# In-memory storage for messages
messages_store = []

app.layout = dmc.NotificationsProvider(
    [
        dmc.Title("Hello History Log!", mb="xl"),
        dmc.Stack(
            [
                dmc.Textarea(id="msg", minRows=5, placeholder="Write something in the chat"),
                html.Div(dmc.Button("Send", id="send", mb="md")),
            ],
            spacing="xs",
        ),
        dmc.Stack([], id="chat-messages", style={"maxWidth": "60ch"}),
        DashSocketIO(id="socketio", eventNames=["chatMsg", "initialChatHistory"]),
    ],
    position="bottom-right",
)


def jsonify_data(data):
    return json.loads(to_json_plotly(data))


@callback(
    Output("msg", "value"),
    Input("send", "n_clicks"),
    State("msg", "value"),
    State("socketio", "socketId"),
    running=[[Output("send", "loading"), True, False]],
    prevent_initial_call=True,
)
def display_status(send, msg, socket_id):
    if not send or not msg:
        return no_update

    message_component = dmc.Paper(
        [
            dmc.Text(f"{socket_id} says", size="xs", color="dimmed", mb=4),
            dmc.Text(msg),
            dmc.Text(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), size="xs", color="dimmed", mt=4),
        ],
        p="0.5rem 1rem",
        withBorder=True,
    )

    # Convert to JSON-like structure
    message_data = jsonify_data(message_component)

    # Store the message
    messages_store.append(message_data)

    emit(
        "chatMsg",
        message_data,
        namespace="/",
        broadcast=True,
    )
    return ""


@socketio.on('connect', namespace='/')
def handle_connect():
    # Send the entire chat history as one event
    emit('initialChatHistory', messages_store)


clientside_callback(
    """connected => !connected""",
    Output("send", "disabled"),
    Input("socketio", "connected"),
)



@callback(
    Output("chat-messages", "children"),
    [Input("socketio", "data-chatMsg"),
     Input("socketio", "data-initialChatHistory")],
    State("chat-messages", "children"),
    prevent_initial_call=True,
)
def update_chat_messages(new_msg, initial_history, children):
    ctx = callback_context
    trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]

    if trigger_id == "socketio":
        if new_msg:
            # Append the new message to the chat
            return children + [jsonify_data(new_msg)]
        elif initial_history:
            # Replace the chat with the initial chat history
            # Ensure initial_history is properly formatted to match the expected structure in the frontend
            all_messages = []
            for msg in initial_history:
                all_messages.append(jsonify_data(msg))
            return all_messages

    return dash.no_update

if __name__ == '__main__':
    app.run_server(debug=True, port=8059)

Excited to see this project expand and continue.
Thanks for building this to fit plotly/dash :beers: Cheers mate

1 Like

Hey @PipInstallPython, glad you like the component and its potential!

I have also worked on improving the toy chat example posted previously. I created an example live chat application with chat rooms. It leverages Google Firestore for both the database and message queue layers. You can find the example code under this link.

(Note that youā€™ll need to install the firestore emulator to run this locally)

And hereā€™s a demo GIF for it :slight_smile:

dash-firestore-chat-demo

2 Likes

2024-ezgif.com-optimize
Going into a new direction, not sure if this is the ideal setup but I wanted to see if I could get Socket.IO to work with leaflet.

Was able to get it working this is my code and findings:

import json
from dash_socketio import DashSocketIO
import dash_mantine_components as dmc
from dash import Dash, Input, Output, State, callback, clientside_callback, html, no_update, dcc, callback_context
from flask_socketio import SocketIO, emit
from plotly.io.json import to_json_plotly
import dash_leaflet as dl
import random

app = Dash(__name__)
app.server.secret_key = "Test!"

socketio = SocketIO(app.server)

app.layout = dmc.NotificationsProvider(
    [
        dmc.Title("Hello Leaflet Socket.IO!", mb="xl"),
        dmc.Stack(
            [
                dl.Map(
                    [
                        dl.TileLayer(id='errorLayer'),
                        dl.LayerGroup(id='errorLayerGroup'),
                        dl.LayerGroup(id='errorLayerGroup2'),
                        dl.Marker(position=[56, 10], children=dl.Tooltip("This is a tooltip")),
                    ],
                    center=[56, 10],
                    zoom=6,
                    style={"width": "100%", "height": "50vh"},
                    id="map",
                ),

                # dmc.Textarea(id="msg", minRows=5, placeholder="Write something in the chat"),
                html.Div(dmc.Button("Send", id="send", mb="md", disabled=True)),
            ],
            spacing="xs",
        ),
        # dmc.Stack([], id="chat-messages", style={"maxWidth": "60ch"}),
        DashSocketIO(id="socketio", eventNames=["chatMsg"]),
    ],
    position="bottom-right",
)


def jsonify_data(data):
    return json.loads(to_json_plotly(data))


# ****************************************************
# Setup the map markers Icons
# ****************************************************
sensor_green_dot = dict(
    iconUrl='assets/imgs/sensor_green_dot.png',
    iconSize=[25, 25],
)

sensor_yellow_dot = dict(
    iconUrl='assets/imgs/sensor_yellow_dot.png',
    iconSize=[25, 25],
)

sensor_red_dot = dict(
    iconUrl='assets/imgs/sensor_red_dot.png',
    iconSize=[25, 25],
)

@callback(
    Output("errorLayerGroup", "children"),
    Input("send", "n_clicks"),
    State("errorLayerGroup", "children"),
    State("socketio", "socketId"),
    running=[[Output("send", "loading"), True, False]],
    prevent_initial_call=True,
)
def display_status(send, errorLayerGroup, socket_id):
    if not send:
        return no_update

    sensors = [sensor_green_dot, sensor_yellow_dot, sensor_red_dot]

    markers = [dl.Marker(
        position=[56 + random.uniform(-0.5, 0.5), 10 + random.uniform(-0.5, 0.5)],
            icon=random.choice(sensors)
        )
        for _ in range(5)
    ]

    emit('chatMsg', jsonify_data(
        markers), namespace='/', broadcast=True,)

    return ''


clientside_callback(
    """connected => !connected""",
    Output("send", "disabled"),
    Input("socketio", "connected"),
)

clientside_callback(
    """(errorLayerGroup, children) => {
        if (!errorLayerGroup) return dash_clientside.no_update
        return errorLayerGroup
    }""",
    Output("errorLayerGroup2", "children"),
    Input("socketio", "data-chatMsg"),
    State("errorLayerGroup", "children"),
    prevent_initial_call=True,
)

if __name__ == '__main__':
    app.run_server(debug=True, port=7059)

Iā€™m using images for icons located in assets/imgs download these 3 images and add them to that folder and all should run fine.

Not sure exactly the use case of such an app built with leaflet and socket.io curious, figure in some niche map develop might be useful. Would love to see some real use case developed out of it.

4 Likes

Hey @RenaudLN, great work on getting a place to save the data for long term use.

Messages kinda looking a little boring thoughā€¦ What if we added pictures, video, code blocks and plotly graphs to the chat log?

Another sleepless night of coding development later BAM:

20242-ezgif.com-optimize

Note: Code has some bugs for example you cant post two pictures or graphs at the same time. You need to switch ChipGroup to another value, return then it works.

Would appreciate another set of eyes to look through this snippet of code, this way of developments new to me so I donā€™t think its polished but its a fair amount of work worth sharing.

pip install dash dash-ace dash-mantine-components dash-socketio flask-socketio dash-iconify numpy pandas

import json
from dash_socketio import DashSocketIO
import dash_mantine_components as dmc
from dash import Dash, Input, Output, State, callback, clientside_callback, html, no_update, callback_context, dcc
from flask_socketio import SocketIO, emit
from plotly.io.json import to_json_plotly
import dash
import datetime
import base64
from dash_iconify import DashIconify
from dash.exceptions import PreventUpdate
import dash_ace as da
import plotly.express as px
import numpy as np
import pandas as pd


app = Dash(__name__, suppress_callback_exceptions=True)
app.server.secret_key = "Test!"

socketio = SocketIO(app.server)

# In-memory storage for messages
messages_store = []

app.layout = dmc.NotificationsProvider(
    [
        # Hidden Components
        dcc.Upload(
                    id='upload-image',
                    children=html.Div([
                        'Drag and Drop or ',
                        html.A('Select Files')
                    ]),
                    style={
                        'width': '100%',
                        'height': '60px',
                        'lineHeight': '60px',
                        'borderWidth': '1px',
                        'borderStyle': 'dashed',
                        'borderRadius': '5px',
                        'textAlign': 'center',
                        'margin': '10px',
                        'display': 'none'
                    },
                    # Allow multiple files to be uploaded
                    multiple=True,

                ),
        html.Div(id="notifications-container"),


        dmc.Title("Hello History Log!", mb="xl"),
        dmc.ChipGroup(
            [dmc.Chip(x, value=x) for x in ["text", "img", "code", 'graph']],
            value="text",
            id="chip_group",
        ),

        dmc.Stack(
            [
                dmc.Textarea(id="msg", minRows=5, placeholder="Write something in the chat"),
                html.Div(dmc.Button("Send", id="send", mb="md")),
            ],
            id="chat-input",
            spacing="xs",
        ),

        dmc.Stack([], id="chat-messages", style={"maxWidth": "60ch"}),
        DashSocketIO(id="socketio", eventNames=["chatMsg", "initialChatHistory"]),
        dmc.Affix(
            dmc.Button("History Modal"), id='history_modal_btn', position={"bottom": 20, "right": 20}
        )
    ],
    position="bottom-right",
)


def jsonify_data(data):
    return json.loads(to_json_plotly(data))

@callback(
    Output("chat-input", "children"),
    Input('chip_group', 'value'),
    prevent_initial_call=True
)
def change_input_mech(chip_group):
    if chip_group == 'text':
        return dmc.Stack(
            [
                dmc.Textarea(id="msg", minRows=5, placeholder="Write something in the chat"),
                dcc.Upload(
                    id='upload-image',
                    children=html.Div([
                        'Drag and Drop or ',
                        html.A('Select Files')
                    ]),
                    style={
                        'width': '100%',
                        'height': '60px',
                        'lineHeight': '60px',
                        'borderWidth': '1px',
                        'borderStyle': 'dashed',
                        'borderRadius': '5px',
                        'textAlign': 'center',
                        'margin': '10px',
                        'display': 'none'
                    },
                    # Allow multiple files to be uploaded
                    multiple=True
                ),
                html.Div(dmc.Button("Send", id="send", mb="md")),
            ],
            id="chat-input",
            spacing="xs",
        )
    elif chip_group == 'img':
        return dmc.Stack(
            [
                dmc.Textarea(id="msg", minRows=5, value='Image', style={'display': 'none'}),
                dcc.Upload(
                    id='upload-image',
                    children=html.Div([
                        'Drag and Drop or ',
                        html.A('Select Files')
                    ]),
                    style={
                        'width': '100%',
                        'height': '60px',
                        'lineHeight': '60px',
                        'borderWidth': '1px',
                        'borderStyle': 'dashed',
                        'borderRadius': '5px',
                        'textAlign': 'center',
                        'margin': '10px'
                    },
                    # Allow multiple files to be uploaded
                    multiple=True
                ),
                html.Div(dmc.Button("Send", id="send", mb="md")),
            ],
            id="chat-input",
            spacing="xs",
        )
    elif chip_group == 'code':
        return dmc.Stack(
            [
                da.DashAceEditor(
                    id='msg',
                    value='',
                    theme='monokai',
                    mode='python',
                    tabSize=2,
                    enableBasicAutocompletion=True,
                    enableLiveAutocompletion=True,
                    autocompleter='/autocompleter?prefix=',
                    placeholder='Python code ...',
                    style={'width': '100%', 'height': '200px'},
                ),
                dcc.Upload(
                    id='upload-image',
                    children=html.Div([
                        'Drag and Drop or ',
                        html.A('Select Files')
                    ]),
                    style={
                        'width': '100%',
                        'height': '60px',
                        'lineHeight': '60px',
                        'borderWidth': '1px',
                        'borderStyle': 'dashed',
                        'borderRadius': '5px',
                        'textAlign': 'center',
                        'margin': '10px',
                        'display': 'none'
                    },
                    # Allow multiple files to be uploaded
                    multiple=True
                ),
                html.Div(dmc.Button("Send", id="send", mb="md")),
            ],
            id="chat-input",
            spacing="xs",
        )
    elif chip_group == 'graph':

        return dmc.Stack(
            [
                dmc.Textarea(id="msg", minRows=5, value='graph', style={'display': 'none'}),

                # dcc.Graph(figure=fig),
                dcc.Upload(
                    id='upload-image',
                    children=html.Div([
                        'Drag and Drop or ',
                        html.A('Select Files')
                    ]),
                    style={
                        'width': '100%',
                        'height': '60px',
                        'lineHeight': '60px',
                        'borderWidth': '1px',
                        'borderStyle': 'dashed',
                        'borderRadius': '5px',
                        'textAlign': 'center',
                        'margin': '10px',
                        'display': 'none'
                    },
                    # Allow multiple files to be uploaded
                    multiple=True
                ),
                html.Div(dmc.Button("Send", id="send", mb="md")),
            ],
            id="chat-input",
            spacing="xs",
        )



def parse_contents(contents, filename, date):
    return html.Div([
        html.H5(filename),
        html.H6(datetime.datetime.fromtimestamp(date)),

        # HTML images accept base64 encoded strings in the same format
        # that is supplied by the upload
        html.Img(src=contents, style={'width': '100%'}),
        html.Hr(),
        html.Div('Raw Content'),
        html.Pre(contents[0:200] + '...', style={
            'whiteSpace': 'pre-wrap',
            'wordBreak': 'break-all'
        })
    ])


@callback(
    Output("msg", "value"),
    Output('notifications-container', 'children'),
    Input("send", "n_clicks"),
    Input('chip_group', 'value'),
    State("msg", "value"),
    State('upload-image', 'contents'),
    State('upload-image', 'filename'),
    State('upload-image', 'last_modified'),
    State("socketio", "socketId"),
    # running=[[Output("send", "loading"), True, False]],
    prevent_initial_call=True,
)
def display_status(send, chip_group, msg, msg_contents, name, mod_date, socket_id):

    if not send or not msg:
        return no_update

    message_component = None
    # print('testing display_status')
    # print(chip_group)
    # print(msg)
    # print(send)
    if chip_group == 'text':
        message_component = dmc.MantineProvider(
    theme={"colorScheme": "dark"},
            children=dmc.Paper(
            [
                dmc.Text(f"{socket_id} says", size="xs", color="dimmed", mb=4),
                dmc.Text(msg),
                dmc.Text(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), size="xs", color="dimmed", mt=4),
            ],
            p="0.5rem 1rem",
            withBorder=True,
            shadow="xl",
        ))
        alert_component = dmc.Notification(
            title="New Message Added!",
            id="simple-notify",
            action="show",
            color="orange",
            message="Austin added a Message to the History Log",
            icon=DashIconify(icon="openmoji:mobile-message"),
        )
    elif chip_group == 'img':
        if msg_contents is not None:
            children = [
                parse_contents(c, n, d) for c, n, d in
                zip(msg_contents, name, mod_date)]

            message_component = dmc.MantineProvider(
    theme={"colorScheme": "dark"},
                children=dmc.Paper(
                children,
                p="0.5rem 1rem",
                withBorder=True,
                shadow="xl",
            ))
            alert_component = dmc.Notification(
                title="New Media File Added!",
                id="simple-notify",
                action="show",
                color="white",
                message="Austin added a Media File to the History Log",
                icon=DashIconify(icon="vscode-icons:folder-type-cake-opened"),
            )
    elif chip_group == 'code':
        message_component = dmc.MantineProvider(
    theme={"colorScheme": "dark"},children=dmc.Paper(
            [
                dmc.Text(f"{socket_id} says", size="xs", color="dimmed", mb=4),

                da.DashAceEditor(
                    id='input',
                    value=f'{msg}',
                    theme='monokai',
                    mode='python',
                    tabSize=2,
                    enableBasicAutocompletion=True,
                    enableLiveAutocompletion=True,
                    autocompleter='/autocompleter?prefix=',
                    placeholder='Python code ...',
                    style={'width': '100%', 'height': '200px'},
                    readOnly=True
                ),
                dmc.Text(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), size="xs", color="dimmed", mt=4),
            ],
            p="0.5rem 1rem",
            withBorder=True,
            shadow="xl",
        ))
        alert_component = dmc.Notification(
            title="New Code Added!",
            id="simple-notify",
            action="show",
            color="gray",
            message="Austin added code to the History Log",
            icon=DashIconify(icon="vscode-icons:file-type-befunge"),
        )
    elif chip_group == 'graph':
        # Generate random data for X and Y axes
        np.random.seed(42)  # For reproducibility
        x_values = np.random.randint(1, 10, 5)
        y_values = np.random.randint(1, 10, 5)

        # Create a DataFrame using the random data
        df = pd.DataFrame({
            "X Axis": x_values,
            "Y Axis": y_values
        })

        # Create a Plotly Express figure
        fig = px.scatter(df, x="X Axis", y="Y Axis", title="Simple Scatter Plot with Random Data", color_discrete_sequence=['red'])
        # Optionally, make the axes and gridlines lighter or remove them
        fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray', color='white')  # Set axes color to white
        fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray', color='white')  # Set axes color to white

        # Make the background transparent
        fig.update_layout(
            paper_bgcolor='rgba(0,0,0,0)',
            plot_bgcolor='rgba(0,0,0,0)',
            font_color='white'
        )

        message_component = dmc.MantineProvider(
    theme={"colorScheme": "dark"},children=dmc.Paper(
            [
                dcc.Graph(figure=fig,
    ),
                dmc.Text(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), size="xs", color="dimmed", mt=4),
            ],
            p="0.5rem 1rem",
            withBorder=True,
            shadow="xl",

        ))

        alert_component = dmc.Notification(
        title="New Graph Added!",
        id="simple-notify",
        action="show",
        color="blue",
        message="Austin added a graph to the History Log",
        icon=DashIconify(icon="emojione-v1:bar-chart"),
    )

    # Convert to JSON-like structure
    message_data = jsonify_data(message_component)

    # Store the message
    messages_store.append(message_data)



    emit(
        "chatMsg",
        message_data,
        namespace="/",
        broadcast=True,
    )
    return "", alert_component


@socketio.on('connect', namespace='/')
def handle_connect():
    # messages_store = messages_store[::-1]
    # Send the entire chat history as one event
    emit('initialChatHistory', messages_store)


clientside_callback(
    """connected => !connected""",
    Output("send", "disabled"),
    Input("socketio", "connected"),
)


@app.callback(
    Output("chat-messages", "children"),
    [Input("socketio", "data-chatMsg"),
     Input("socketio", "data-initialChatHistory")],
    State("chat-messages", "children"),
    prevent_initial_call=True,
)
def update_chat_messages(new_msg, initial_history, children):
    ctx = callback_context
    trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]

    if trigger_id == "socketio":
        if new_msg:
            # Insert the new message at the beginning of the chat
            return [jsonify_data(new_msg)] + children
        elif initial_history:
            # Handle the initial chat history; this assumes initial_history is already in correct order
            all_messages = []
            for msg in initial_history[::-1]:  # Reverse the initial history to display newest first
                all_messages.append(jsonify_data(msg))
            return all_messages

    return dash.no_update

if __name__ == '__main__':
    app.run_server(debug=True, port=8159)

Ways Iā€™m looking to improve this:
On graph select, Iā€™d like to show a codeblock where you can create your own plotly graph and dcc.Graph(figure=fig) to return it to the message log rather than the template graph Iā€™ve included in this code snipit.

Connect it to a Postgres database for local storage rather than cloud storage

Fix bug with not being able to post two pictures or graphs back to back

1 Like

Anyone care to elucidate in which way does this go beyond the official WebSocket support of Dash?

Iā€™m not sure I understand ā€œI needed a way to send to a websocket from a Dash callbackā€ about this, which may explain it all, hence humbly asking.

Hey @matan3, the WebSocket component does not allow for instance to send information to the client while a server callback is running. It also doesnā€™t support things like ā€˜roomsā€™ to send messages from the server to a subset of users.

Also fyi dash-extensions is not officially supported by Dash but is (excellently) maintained by a community member :slightly_smiling_face:

2 Likes

Thanks for letting me know! You mean that in that case messages will get silently dropped rather than queued? which side do we consider the client here? the Dash side or the other side?