New websocket component: DashSocketIO

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