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:
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