From 817faa4aea7fda5943ed4e3819b0d1d4febc2eed Mon Sep 17 00:00:00 2001 From: Xarangi Date: Fri, 15 Nov 2024 12:13:06 +0530 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat(dashboard-basic.py):=20add?= =?UTF-8?q?ed=20features=20to=20log=20agent=20outputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysis_utils/dashboard-basic.py | 1623 ++++++++++++----- 1 file changed, 1203 insertions(+), 420 deletions(-) diff --git a/examples/election/src/election_sim/analysis_utils/dashboard-basic.py b/examples/election/src/election_sim/analysis_utils/dashboard-basic.py index 383dd84..d95382a 100644 --- a/examples/election/src/election_sim/analysis_utils/dashboard-basic.py +++ b/examples/election/src/election_sim/analysis_utils/dashboard-basic.py @@ -1,23 +1,19 @@ import argparse -import math +import base64 import re import dash import dash_cytoscape as cyto import networkx as nx import plotly.graph_objs as go -from dash import Input, Output, dcc, html +from dash import Input, Output, State, dcc, html from plotly.subplots import make_subplots -# Load extra layouts for Cytoscape cyto.load_extra_layouts() def compute_positions(graph): - # Use Kamada-Kawai layout for better distribution pos = nx.kamada_kawai_layout(graph, scale=750) - - # Ensure positions are scaled and rounded to prevent overlaps scaled_pos = {} for node, (x, y) in pos.items(): scaled_pos[node] = {"x": x, "y": y} @@ -25,29 +21,62 @@ def compute_positions(graph): return scaled_pos -# Load and parse the interaction data +# Serialization function to convert complex data structures into JSON-serializable format +def serialize_data(follow_graph, interactions_by_episode, posted_users_by_episode, toots, votes): + return { + "nodes": list(follow_graph.nodes), + "edges": list(follow_graph.edges), + "interactions_by_episode": interactions_by_episode, + "posted_users_by_episode": {k: list(v) for k, v in posted_users_by_episode.items()}, + "toots": toots, + "votes": votes, + } + + +# Deserialization function to convert JSON-serializable data back into original structures +def deserialize_data(serialized): + follow_graph = nx.DiGraph() + follow_graph.add_nodes_from(serialized["nodes"]) + follow_graph.add_edges_from(serialized["edges"]) + + # Convert episode keys back to integers + interactions_by_episode = {int(k): v for k, v in serialized["interactions_by_episode"].items()} + posted_users_by_episode = { + int(k): set(v) for k, v in serialized["posted_users_by_episode"].items() + } + toots = serialized["toots"] + votes = {int(k): v for k, v in serialized["votes"].items()} + return follow_graph, interactions_by_episode, posted_users_by_episode, toots, votes + + +# Load and parse the interaction data from a file path def load_data(filepath): follow_graph = nx.DiGraph() interactions_by_episode = {} posted_users_by_episode = {} - with open(filepath) as file: + toots = {} + current_episode = -1 + interactions_by_episode[current_episode] = [] + posted_users_by_episode[current_episode] = set() + + with open(filepath, encoding="utf-8") as file: lines = file.readlines() - current_episode = None for line in lines: line = line.strip() if "Episode:" in line: match = re.match(r"Episode:\s*(\d+)(.*)", line) if match: - current_episode = int( - match.group(1) - ) # First capturing group is the episode number - remaining_text = match.group( - 2 - ).strip() # Remaining part of the line after the episode number - line = remaining_text # Continue processing the rest of the line as an action + current_episode = int(match.group(1)) + remaining_text = match.group(2).strip() interactions_by_episode[current_episode] = [] posted_users_by_episode[current_episode] = set() + line = remaining_text + else: + continue # Skip malformed episode lines + + if not line: + continue # Skip empty lines if "followed" in line: user = line.split()[0] @@ -55,65 +84,274 @@ def load_data(filepath): follow_graph.add_edge(user, target_user) elif "replied" in line: if current_episode is not None: - action = "replied" - user = line.split()[0] - target_user = line.split()[6] + # Line format: user replied to a toot by target_user with Toot ID:[id], new Toot ID:[new_id] --- [content] + parts = line.split("---") + main_part = parts[0].strip() + content = parts[1].strip() if len(parts) > 1 else "" + + # Extract user, target_user, parent Toot ID, new Toot ID + reply_pattern = r"(\w+) replied to a toot by (\w+) with Toot ID:?[:]? ?(\d+), new Toot ID:?[:]? ?(\d+)" + match = re.match(reply_pattern, main_part) + if match: + user = match.group(1) + target_user = match.group(2) + parent_toot_id = match.group(3) + new_toot_id = match.group(4) + # Store the new Toot with content + toots[new_toot_id] = { + "user": user, + "action": "replied", + "content": content, + "parent_toot_id": parent_toot_id, + } + # Add interaction + interactions_by_episode[current_episode].append( + { + "source": user, + "target": target_user, + "action": "replied", + "episode": current_episode, + "toot_id": new_toot_id, + "parent_toot_id": parent_toot_id, + } + ) + elif "boosted" in line: + if current_episode is not None: + # Line format: user boosted a toot from target_user with Toot ID:[id] + boosted_pattern = r"(\w+) boosted a toot from (\w+) with Toot ID:?[:]? ?(\d+)" + match = re.match(boosted_pattern, line) + if match: + user = match.group(1) + target_user = match.group(2) + toot_id = match.group(3) + interactions_by_episode[current_episode].append( + { + "source": user, + "target": target_user, + "action": "boosted", + "episode": current_episode, + "toot_id": toot_id, + } + ) + elif "liked" in line: + if current_episode is not None: + # Line format: user liked a toot from target_user with Toot ID:[id] + liked_pattern = r"(\w+) liked a toot from (\w+) with Toot ID:?[:]? ?(\d+)" + match = re.match(liked_pattern, line) + if match: + user = match.group(1) + target_user = match.group(2) + toot_id = match.group(3) + interactions_by_episode[current_episode].append( + { + "source": user, + "target": target_user, + "action": "liked", + "episode": current_episode, + "toot_id": toot_id, + } + ) + elif "posted" in line: + if current_episode is not None: + # Line format: user posted a toot with Toot ID: [id] --- [content] + parts = line.split("---") + main_part = parts[0].strip() + content = parts[1].strip() if len(parts) > 1 else "" + + # Extract user and Toot ID + post_pattern = r"(\w+) posted a toot with Toot ID:?[:]? ?(\d+)" + match = re.match(post_pattern, main_part) + if match: + user = match.group(1) + toot_id = match.group(2) + # Store the Toot with content + toots[toot_id] = { + "user": user, + "action": "posted", + "content": content, + } + # Add interaction + interactions_by_episode[current_episode].append( + { + "source": user, + "target": user, # For 'post', target is the user themselves + "action": "posted", + "episode": current_episode, + "toot_id": toot_id, + } + ) + # Add to posted_users + posted_users_by_episode[current_episode].add(user) + + return follow_graph, interactions_by_episode, posted_users_by_episode, toots + + +# Load and parse the vote data from a file path +def load_votes(filepath): + votes = {} + with open(filepath) as file: + lines = file.readlines() + current_episode = None + for line in lines: + line = line.strip() + if line.startswith("Episode:"): + current_episode = int(line.split(":")[1].strip()) + votes[current_episode] = {} + elif "votes for" in line: + user, candidate = line.split(" votes for ") + votes[current_episode][user.split()[0]] = candidate + return votes + + +# Load and parse the interaction data from a string (for uploaded files) +def load_data_from_string(file_contents): + follow_graph = nx.DiGraph() + interactions_by_episode = {} + posted_users_by_episode = {} + toots = {} + current_episode = -1 + interactions_by_episode[current_episode] = [] + posted_users_by_episode[current_episode] = set() + + lines = file_contents.splitlines() + for line in lines: + line = line.strip() + + if "Episode:" in line: + match = re.match(r"Episode:\s*(\d+)(.*)", line) + if match: + current_episode = int(match.group(1)) + remaining_text = match.group(2).strip() + interactions_by_episode[current_episode] = [] + posted_users_by_episode[current_episode] = set() + line = remaining_text + else: + continue # Skip malformed episode lines + + if not line: + continue # Skip empty lines + + if "followed" in line: + user = line.split()[0] + target_user = line.split()[-1] + follow_graph.add_edge(user, target_user) + elif "replied" in line: + if current_episode is not None: + # Line format: user replied to a toot by target_user with Toot ID:[id], new Toot ID:[new_id] --- [content] + parts = line.split("---") + main_part = parts[0].strip() + content = parts[1].strip() if len(parts) > 1 else "" + + # Extract user, target_user, parent Toot ID, new Toot ID + reply_pattern = r"(\w+) replied to a toot by (\w+) with Toot ID:?[:]? ?(\d+), new Toot ID:?[:]? ?(\d+)" + match = re.match(reply_pattern, main_part) + if match: + user = match.group(1) + target_user = match.group(2) + parent_toot_id = match.group(3) + new_toot_id = match.group(4) + # Store the new Toot with content + toots[new_toot_id] = { + "user": user, + "action": "replied", + "content": content, + "parent_toot_id": parent_toot_id, + } + # Add interaction interactions_by_episode[current_episode].append( { "source": user, "target": target_user, - "action": action, + "action": "replied", "episode": current_episode, + "toot_id": new_toot_id, + "parent_toot_id": parent_toot_id, } ) - elif "boosted" in line: - if current_episode is not None: - action = "boosted" - user = line.split()[0] - target_user = line.split()[5] + elif "boosted" in line: + if current_episode is not None: + # Line format: user boosted a toot from target_user with Toot ID:[id] + boosted_pattern = r"(\w+) boosted a toot from (\w+) with Toot ID:?[:]? ?(\d+)" + match = re.match(boosted_pattern, line) + if match: + user = match.group(1) + target_user = match.group(2) + toot_id = match.group(3) interactions_by_episode[current_episode].append( { "source": user, "target": target_user, - "action": action, + "action": "boosted", "episode": current_episode, + "toot_id": toot_id, } ) - elif "liked" in line: - if current_episode is not None: - action = "liked" - user = line.split()[0] - target_user = line.split()[5] + elif "liked" in line: + if current_episode is not None: + # Line format: user liked a toot from target_user with Toot ID:[id] + liked_pattern = r"(\w+) liked a toot from (\w+) with Toot ID:?[:]? ?(\d+)" + match = re.match(liked_pattern, line) + if match: + user = match.group(1) + target_user = match.group(2) + toot_id = match.group(3) interactions_by_episode[current_episode].append( { "source": user, "target": target_user, - "action": action, + "action": "liked", "episode": current_episode, + "toot_id": toot_id, } ) - elif "posted" in line: - if current_episode is not None: - user = line.split()[0] + elif "posted" in line: + if current_episode is not None: + # Line format: user posted a toot with Toot ID: [id] --- [content] + parts = line.split("---") + main_part = parts[0].strip() + content = parts[1].strip() if len(parts) > 1 else "" + + # Extract user and Toot ID + post_pattern = r"(\w+) posted a toot with Toot ID:?[:]? ?(\d+)" + match = re.match(post_pattern, main_part) + if match: + user = match.group(1) + toot_id = match.group(2) + # Store the Toot with content + toots[toot_id] = { + "user": user, + "action": "posted", + "content": content, + } + # Add interaction + interactions_by_episode[current_episode].append( + { + "source": user, + "target": user, # For 'post', target is the user themselves + "action": "posted", + "episode": current_episode, + "toot_id": toot_id, + } + ) + # Add to posted_users posted_users_by_episode[current_episode].add(user) - return follow_graph, interactions_by_episode, posted_users_by_episode + return follow_graph, interactions_by_episode, posted_users_by_episode, toots -# Load and parse the vote data -def load_votes(filepath): +# Load and parse the vote data from a string (for uploaded files) +def load_votes_from_string(file_contents): votes = {} - with open(filepath) as file: - lines = file.readlines() - current_episode = None - for line in lines: - line = line.strip() - if line.startswith("Episode:"): - current_episode = int(line.split(":")[1].strip()) - votes[current_episode] = {} - elif "votes for" in line: - user, candidate = line.split(" votes for ") - votes[current_episode][user.split()[0]] = candidate + lines = file_contents.splitlines() + current_episode = None + for line in lines: + line = line.strip() + if line.startswith("Episode:"): + current_episode = int(line.split(":")[1].strip()) + votes[current_episode] = {} + elif "votes for" in line: + user, candidate = line.split(" votes for ") + votes[current_episode][user.split()[0]] = candidate return votes @@ -121,277 +359,768 @@ def load_votes(filepath): if __name__ == "__main__": # Set up argument parsing parser = argparse.ArgumentParser(description="Run the Dash app with specific data files.") - parser.add_argument("interaction_file", type=str, help="The path to the interaction log file.") - parser.add_argument("votes_file", type=str, help="The path to the votes log file.") + parser.add_argument( + "interaction_file", + type=str, + nargs="?", + default=None, + help="The path to the interaction log file.", + ) + parser.add_argument( + "votes_file", type=str, nargs="?", default=None, help="The path to the votes log file." + ) args = parser.parse_args() - # Load the data using the files passed as arguments - follow_graph, interactions_by_episode, posted_users_by_episode = load_data( - args.interaction_file - ) - votes = load_votes(args.votes_file) - # Compute positions - all_positions = compute_positions(follow_graph) - - # Add positions to the layout - layout = {"name": "preset", "positions": all_positions} - - # Prepare Cytoscape elements with all nodes and all edges, classified by episode - elements = [ - { - "data": {"id": node, "label": node}, - "classes": "default_node", - } - for node in follow_graph.nodes - ] + [ - { - "data": { - "source": src, - "target": tgt, - }, - "classes": "follow_edge", - } - for src, tgt in follow_graph.edges - ] - - # Add all interaction edges classified by the episode they belong to - for episode, interactions in interactions_by_episode.items(): - for interaction in interactions: - source = interaction["source"] - target = interaction["target"] - - # Check if both source and target exist in the graph before creating the edge - if source in follow_graph.nodes and target in follow_graph.nodes: - elements.append( - { - "data": { - "source": source, - "target": target, - "label": f"{interaction['action']}", - }, - "classes": f"interaction_edge episode_{episode}", # Classify edge by episode - } - ) + # Initialize variables + if args.interaction_file and args.votes_file: + # Load the data using the files passed as arguments + follow_graph, interactions_by_episode, posted_users_by_episode, toots = load_data( + args.interaction_file + ) + votes = load_votes(args.votes_file) + # Compute positions + all_positions = compute_positions(follow_graph) - app = dash.Dash(__name__) + layout = {"name": "preset", "positions": all_positions} - # Create a list of unique names for the name selector dropdown - unique_names = sorted(follow_graph.nodes) + # Serialize the initial data + serialized_initial_data = serialize_data( + follow_graph, interactions_by_episode, posted_users_by_episode, toots, votes + ) + else: + # No initial data provided + serialized_initial_data = None + + app = dash.Dash(__name__) + # Create the layout with conditional sections app.layout = html.Div( [ - # Line graphs container + # Store component to hold serialized data + dcc.Store(id="data-store", data=serialized_initial_data), + # Upload Screen html.Div( - [ - # Vote distribution line graph - dcc.Graph( - id="vote-distribution-line", - config={"displayModeBar": False}, - style={"height": "170px", "width": "48%", "display": "inline-block"}, - ), - # Interactions count line graph - dcc.Graph( - id="interactions-line-graph", - config={"displayModeBar": False}, - style={"height": "170px", "width": "48%", "display": "inline-block"}, - ), - ], - style={"display": "flex", "justify-content": "space-between", "margin-top": "20px"}, - ), - # Cytoscape graph - html.Div( - [ + id="upload-screen", + children=[ + # Upload Interaction Log html.Div( [ - dcc.Dropdown( - id="name-selector", - options=[{"label": name, "value": name} for name in unique_names], - value=None, - placeholder="Select Name", - clearable=True, + html.Label( + "Upload Interaction Log:", style={ - "padding": "0px", - "font-size": "16px", + "font-size": "18px", "font-weight": "bold", - "width": "150px", - "z-index": "1000", # Ensure it stays on top of the Cytoscape graph + "margin-bottom": "10px", + "color": "#555555", + "text-align": "center", }, ), - dcc.Dropdown( - id="mode-dropdown", - options=[ - {"label": "Universal View", "value": "normal"}, - {"label": "Active View", "value": "focused"}, - ], - value="normal", - clearable=False, + dcc.Upload( + id="upload-app-logger", + children=html.Div( + [ + "Drag and Drop or ", + html.A( + "Select Files", + style={ + "color": "#1a73e8", + "text-decoration": "underline", + }, + ), + ] + ), style={ - "padding": "0px", - "font-size": "16px", + "width": "100%", + "max-width": "400px", + "height": "80px", + "lineHeight": "80px", + "borderWidth": "2px", + "borderStyle": "dashed", + "borderRadius": "10px", + "textAlign": "center", + "background-color": "#f9f9f9", + "cursor": "pointer", + "margin": "0 auto", # Center the upload box + "transition": "border 0.3s ease-in-out", + }, + multiple=False, + ), + ], + style={"width": "100%", "max-width": "500px", "margin-bottom": "30px"}, + ), + # Upload Vote Log + html.Div( + [ + html.Label( + "Upload Vote Log:", + style={ + "font-size": "18px", "font-weight": "bold", - "width": "150px", - "z-index": "1000", # Ensure it stays on top of the Cytoscape graph + "margin-bottom": "10px", + "color": "#555555", + "text-align": "center", + }, + ), + dcc.Upload( + id="upload-vote-logger", + children=html.Div( + [ + "Drag and Drop or ", + html.A( + "Select Files", + style={ + "color": "#1a73e8", + "text-decoration": "underline", + }, + ), + ] + ), + style={ + "width": "100%", + "max-width": "400px", + "height": "80px", + "lineHeight": "80px", + "borderWidth": "2px", + "borderStyle": "dashed", + "borderRadius": "10px", + "textAlign": "center", + "background-color": "#f9f9f9", + "cursor": "pointer", + "margin": "0 auto", # Center the upload box + "transition": "border 0.3s ease-in-out", }, + multiple=False, ), ], + style={"width": "100%", "max-width": "500px", "margin-bottom": "40px"}, + ), + # Submit Button + html.Button( + "Submit", + id="submit-button", + n_clicks=0, style={ - "position": "absolute", - "top": "10px", # Aligns at the top of the graph - "left": "10px", # Aligns on the left - "display": "flex", - "gap": "10px", # Space between the two dropdowns - "z-index": "1000", # Ensure it's above the Cytoscape graph + "width": "200px", + "height": "50px", + "font-size": "18px", + "background-color": "#4CAF50", # Green background + "color": "white", + "border": "none", + "border-radius": "8px", + "cursor": "pointer", + "box-shadow": "0 4px 6px rgba(0, 0, 0, 0.1)", + "transition": "background-color 0.3s ease, transform 0.2s ease", + "margin-bottom": "20px", + "align-self": "center", # Center the button }, ), - # Episode number display (top-right) + # Error Message html.Div( - id="current-episode", + id="upload-error-message", style={ - "position": "absolute", - "top": "10px", - "right": "10px", - "padding": "10px", - "font-size": "20px", - "font-weight": "bold", - "background-color": "#ffcc99", # Optional: add a background color - "z-index": "1000", # Ensure it stays on top of the Cytoscape graph + "color": "red", + "textAlign": "center", + "margin-top": "20px", + "font-size": "16px", }, - children=f"Episode: {min(interactions_by_episode.keys())}", ), - cyto.Cytoscape( - id="cytoscape-graph", - elements=elements, # Start with all nodes and edges - layout=layout, - style={"width": "100%", "height": "620px", "background-color": "#e1e1e1"}, - stylesheet=[ - { - "selector": ".default_node", - "style": { - "background-color": "#fffca0", - "label": "data(label)", - "color": "#000000", - "font-size": "20px", - "text-halign": "center", - "text-valign": "center", - "width": "70px", - "height": "70px", - "border-width": 10, - "border-color": "#000000", - }, - }, - { - "selector": ".follow_edge", - "style": { - "curve-style": "bezier", - "target-arrow-shape": "triangle", - "opacity": 0.8, - "width": 2, - "line-color": "#FFFFFF", - }, - }, - { - "selector": ".interaction_edge", - "style": { - "curve-style": "bezier", - "target-arrow-shape": "triangle", - "opacity": 0.8, - "width": 4, - "line-color": "#000000", - "visibility": "hidden", - }, - }, - { - "selector": ".interaction_edge:hover", - "style": { - "label": "data(label)", - "font-size": "14px", - "color": "#000000", + ], + style={ + "display": "flex", + "flexDirection": "column", + "alignItems": "center", + "justifyContent": "center", + "height": "100vh", + "background-color": "#f0f2f5", # Light gray background for better contrast + "padding": "20px", + }, + ), + # Dashboard + html.Div( + id="dashboard", # Added 'dashboard' id here + children=[ + # Line graphs container + html.Div( + [ + # Vote distribution line graph + dcc.Graph( + id="vote-distribution-line", + config={"displayModeBar": False}, + style={ + "height": "170px", + "width": "48%", + "display": "inline-block", }, - }, - # Edge labels - { - "selector": "edge", - "style": { - "label": "data(label)", - "text-rotation": "autorotate", - "text-margin-y": "-10px", - "font-size": "10px", - "color": "#000000", - "text-background-color": "#FFFFFF", - "text-background-opacity": 0.8, - "text-background-padding": "3px", + ), + # Interactions count line graph + dcc.Graph( + id="interactions-line-graph", + config={"displayModeBar": False}, + style={ + "height": "170px", + "width": "48%", + "display": "inline-block", }, - }, - # Specific styles for "Bill" and "Bradley" nodes - { - "selector": '[id="Bill"]', - "style": { - "background-color": "blue", - "border-color": "#000000", + ), + ], + style={ + "display": "flex", + "justify-content": "space-between", + "margin-top": "20px", + }, + ), + # Main content: Cytoscape graph and interactions window + html.Div( + [ + html.Div( + [ + dcc.Dropdown( + id="name-selector", + options=[], # To be populated by callback + value=None, + placeholder="Select Name", + clearable=True, + style={ + "padding": "10px", + "font-size": "16px", + "font-weight": "bold", + "width": "200px", + "z-index": "1000", # Ensure it stays on top of the Cytoscape graph + }, + ), + dcc.Dropdown( + id="mode-dropdown", + options=[ + {"label": "Universal View", "value": "normal"}, + {"label": "Active View", "value": "focused"}, + ], + value="normal", + clearable=False, + style={ + "padding": "10px", + "font-size": "16px", + "font-weight": "bold", + "width": "200px", + "z-index": "1000", # Ensure it stays on top of the Cytoscape graph + }, + ), + ], + style={ + "position": "absolute", + "top": "10px", # Aligns at the top of the graph + "left": "10px", # Aligns on the left + "display": "flex", + "gap": "10px", # Space between the two dropdowns + "z-index": "1000", # Ensure it's above the Cytoscape graph }, - }, - { - "selector": '[id="Bradley"]', - "style": { - "background-color": "orange", - "border-color": "#000000", + ), + # Episode number display (top-right) + html.Div( + id="current-episode", + style={ + "position": "absolute", + "top": "10px", + "right": "10px", + "padding": "10px", + "font-size": "20px", + "font-weight": "bold", + "background-color": "#ffcc99", # Optional: add a background color + "z-index": "1000", # Ensure it stays on top of the Cytoscape graph }, - }, - # Highlighted Nodes (Added) - { - "selector": ".highlighted", - "style": { - "background-color": "#98FF98", # Mint color - "border-color": "#FF69B4", # Hot pink border for visibility - "border-width": 10, + children="", + ), + # Flex container for Cytoscape and Interactions Window + html.Div( + [ + cyto.Cytoscape( + id="cytoscape-graph", + elements=[], # To be populated by callback + layout={ + "name": "preset", + "positions": {}, + }, # To be updated by callback + style={ + "width": "100%", # Initial width set to 100% + "height": "600px", + "background-color": "#e1e1e1", + "transition": "width 0.5s", # Smooth width transition + }, + stylesheet=[ + { + "selector": ".default_node", + "style": { + "background-color": "#fffca0", + "label": "data(label)", + "color": "#000000", + "font-size": "20px", + "text-halign": "center", + "text-valign": "center", + "width": "70px", + "height": "70px", + "border-width": 6, + "border-color": "#000000", + }, + }, + { + "selector": ".follow_edge", + "style": { + "curve-style": "bezier", + "target-arrow-shape": "triangle", + "opacity": 0.8, + "width": 2, + "line-color": "#FFFFFF", + }, + }, + { + "selector": ".interaction_edge", + "style": { + "curve-style": "bezier", + "target-arrow-shape": "triangle", + "opacity": 0.8, + "width": 4, + "line-color": "#000000", + "visibility": "hidden", + }, + }, + { + "selector": ".interaction_edge:hover", + "style": { + "label": "data(label)", + "font-size": "14px", + "color": "#000000", + }, + }, + # Edge labels + { + "selector": "edge", + "style": { + "label": "data(label)", + "text-rotation": "autorotate", + "text-margin-y": "-10px", + "font-size": "10px", + "color": "#000000", + "text-background-color": "#FFFFFF", + "text-background-opacity": 0.8, + "text-background-padding": "3px", + }, + }, + # Specific styles for "Bill" and "Bradley" nodes + { + "selector": '[id="Bill"]', + "style": { + "background-color": "blue", + "border-color": "#000000", + }, + }, + { + "selector": '[id="Bradley"]', + "style": { + "background-color": "orange", + "border-color": "#000000", + }, + }, + # Highlighted Nodes (Added) + { + "selector": ".highlighted", + "style": { + "background-color": "#98FF98", # Mint color + "border-color": "#FF69B4", # Hot pink border for visibility + "border-width": 4, + }, + }, + ], + ), + # Interactions Window + html.Div( + [ + html.H3("Interactions"), + html.Div( + id="interactions-window", + style={"overflowY": "auto", "height": "580px"}, + ), + ], + style={ + "width": "0%", # Initial width set to 0% + "height": "600px", + "padding": "10px", + "border-left": "1px solid #ccc", + "background-color": "#f9f9f9", + "transition": "width 0.5s", # Smooth width transition + "overflow": "hidden", + }, + id="interactions-container", + ), + ], + style={ + "display": "flex", + "flexDirection": "row", + "height": "600px", + "transition": "all 0.5s ease", # Smooth transition for all properties }, - }, + ), ], + style={ + "position": "relative", + "height": "600px", + "margin-top": "10px", + "margin-bottom": "20px", + }, + ), + # Episode slider + dcc.Slider( + id="episode-slider", + min=0, # To be updated by callback + max=0, # To be updated by callback + value=0, # To be updated by callback + marks={}, # To be updated by callback + step=None, + tooltip={"placement": "bottom", "always_visible": True}, + ), + dcc.Graph( + id="vote-percentages-bar", + config={"displayModeBar": False}, + style={"height": "50px", "margin-top": "20px"}, + ), + # Upload components and Submit button added at the bottom + html.Div( + [ + html.Div( + [ + html.Label("Upload Interaction Log:"), + dcc.Upload( + id="upload-app-logger-dashboard", + children=html.Div([html.A("Select Files")]), + style={ + "width": "60%", + "height": "50px", + "lineHeight": "80px", + "borderWidth": "2px", + "borderStyle": "dashed", + "borderRadius": "10px", + "textAlign": "center", + "background-color": "#f0f0f0", + "cursor": "pointer", + "margin-bottom": "20px", + "padding": "30px", + }, + multiple=False, + ), + ], + className="upload-component", + ), + html.Div( + [ + html.Label("Upload Vote Log:"), + dcc.Upload( + id="upload-vote-logger-dashboard", + children=html.Div([html.A("Select Files")]), + style={ + "width": "60%", + "height": "50px", + "lineHeight": "80px", + "borderWidth": "2px", + "borderStyle": "dashed", + "borderRadius": "10px", + "textAlign": "center", + "background-color": "#f0f0f0", + "cursor": "pointer", + "margin-bottom": "20px", + "padding": "30px", + }, + multiple=False, + ), + ], + className="upload-component", + ), + html.Div( + [ + html.Button( + "Upload Files", + id="upload-button-dashboard", + n_clicks=0, + style={ + "width": "100px", + "height": "50px", + "font-size": "12px", + "padding": "30px", + }, + ), + ], + className="upload-button-container", + ), + ], + style={ + "display": "flex", + "justify-content": "space-around", + # 'flex-wrap': 'wrap', + "gap": "20px", + "margin-top": "20px", + "margin-bottom": "20px", + }, + id="dashboard-upload-section", ), ], - style={ - "position": "relative", - "height": "600px", - "margin-top": "10px", - "margin-bottom": "20px", - }, - ), - # Episode slider - dcc.Slider( - id="episode-slider", - min=min(interactions_by_episode.keys()), - max=max(interactions_by_episode.keys()), - value=min(interactions_by_episode.keys()), - marks={str(episode): str(episode) for episode in interactions_by_episode.keys()}, - step=None, - tooltip={"placement": "bottom", "always_visible": True}, - ), - dcc.Graph( - id="vote-percentages-bar", - config={"displayModeBar": False}, - style={"height": "100px", "margin-top": "20px"}, + style={"display": "none"}, # Initially hidden; shown when data is available ), + # Hidden div for error messages (specific to dashboard uploads) + html.Div(id="error-message", style={"color": "red", "textAlign": "center"}), ] ) @app.callback( [ + Output("upload-screen", "style"), + Output("dashboard", "style"), + Output("dashboard-upload-section", "style"), + Output("name-selector", "options"), + ], + [Input("data-store", "data")], + ) + def toggle_layout(data_store): + if data_store and "nodes" in data_store and len(data_store["nodes"]) > 0: + # Data is available; show dashboard and hide upload screen + return ( + {"display": "none"}, + {"display": "block"}, + {"display": "flex"}, + [{"label": name, "value": name} for name in sorted(data_store["nodes"])], + ) + # No data; show upload screen and hide dashboard + return {"display": "flex"}, {"display": "none"}, {"display": "none"}, [] + + # Combined Callback for Initial and Dashboard Uploads + @app.callback( + [ + Output("data-store", "data"), + Output("upload-error-message", "children"), + Output("error-message", "children"), + ], + [ + Input("submit-button", "n_clicks"), + Input("upload-button-dashboard", "n_clicks"), + ], + [ + State("upload-app-logger", "contents"), + State("upload-vote-logger", "contents"), + State("upload-app-logger", "filename"), + State("upload-vote-logger", "filename"), + State("upload-app-logger-dashboard", "contents"), + State("upload-vote-logger-dashboard", "contents"), + State("upload-app-logger-dashboard", "filename"), + State("upload-vote-logger-dashboard", "filename"), + State("data-store", "data"), + ], + ) + def update_data( + n_clicks_initial, + n_clicks_dashboard, + app_logger_contents_initial, + vote_logger_contents_initial, + app_logger_filename_initial, + vote_logger_filename_initial, + app_logger_contents_dashboard, + vote_logger_contents_dashboard, + app_logger_filename_dashboard, + vote_logger_filename_dashboard, + current_data, + ): + ctx = dash.callback_context + + if not ctx.triggered: + raise dash.exceptions.PreventUpdate + + triggered_id = ctx.triggered[0]["prop_id"].split(".")[0] + + try: + if triggered_id == "submit-button": + # Handle initial upload + if ( + app_logger_contents_initial is not None + and vote_logger_contents_initial is not None + ): + # Process app_logger + content_type, content_string = app_logger_contents_initial.split(",") + decoded = base64.b64decode(content_string) + app_logger_string = decoded.decode("utf-8") + ( + follow_graph_new, + interactions_by_episode_new, + posted_users_by_episode_new, + toots_new, + ) = load_data_from_string(app_logger_string) + + # Process vote_logger + content_type, content_string = vote_logger_contents_initial.split(",") + decoded = base64.b64decode(content_string) + vote_logger_string = decoded.decode("utf-8") + votes_new = load_votes_from_string(vote_logger_string) + + # Serialize the new data + serialized_new_data = serialize_data( + follow_graph_new, + interactions_by_episode_new, + posted_users_by_episode_new, + toots_new, + votes_new, + ) + + return serialized_new_data, "", "" + raise ValueError("Both Interaction Log and Vote Log files are required.") + + if triggered_id == "upload-button-dashboard": + # Handle dashboard upload + if ( + app_logger_contents_dashboard is not None + and vote_logger_contents_dashboard is not None + ): + # Process app_logger + content_type, content_string = app_logger_contents_dashboard.split(",") + decoded = base64.b64decode(content_string) + app_logger_string = decoded.decode("utf-8") + ( + follow_graph_new, + interactions_by_episode_new, + posted_users_by_episode_new, + toots_new, + ) = load_data_from_string(app_logger_string) + + # Process vote_logger + content_type, content_string = vote_logger_contents_dashboard.split(",") + decoded = base64.b64decode(content_string) + vote_logger_string = decoded.decode("utf-8") + votes_new = load_votes_from_string(vote_logger_string) + + # Serialize the new data + serialized_new_data = serialize_data( + follow_graph_new, + interactions_by_episode_new, + posted_users_by_episode_new, + toots_new, + votes_new, + ) + + return serialized_new_data, "", "" + raise ValueError( + "Both Interaction Log and Vote Log files are required for dashboard upload." + ) + + raise dash.exceptions.PreventUpdate + + except Exception as e: + if triggered_id == "submit-button": + return dash.no_update, f"Error uploading initial data: {e!s}", "" + if triggered_id == "upload-button-dashboard": + return dash.no_update, "", f"Error uploading dashboard data: {e!s}" + return dash.no_update, "", "" + + # Callback to update the dashboard based on data-store + @app.callback( + [ + Output("cytoscape-graph", "elements"), Output("cytoscape-graph", "layout"), Output("cytoscape-graph", "stylesheet"), Output("vote-percentages-bar", "figure"), Output("vote-distribution-line", "figure"), Output("interactions-line-graph", "figure"), - Output("current-episode", "children"), # Added Output + Output("current-episode", "children"), + Output("interactions-window", "children"), # Added Output + Output("interactions-container", "style"), # Added Output to control width + Output("cytoscape-graph", "style"), # Added Output to control width + Output("episode-slider", "min"), + Output("episode-slider", "max"), + Output("episode-slider", "value"), + Output("episode-slider", "marks"), + Output("name-selector", "value"), ], [ Input("episode-slider", "value"), Input("mode-dropdown", "value"), Input("name-selector", "value"), # Added Input + Input("data-store", "data"), # Added Input to trigger update on data change ], ) - def update_graph(selected_episode, selected_mode, selected_name): - # Base stylesheet (node styles, follow edges) + def update_graph(selected_episode, selected_mode, selected_name, data_store): + if not data_store: + # If no data is present, return defaults + return ( + [], # elements + {"name": "preset", "positions": {}}, # layout + [], # stylesheet + {}, # vote-percentages-bar + {}, # vote-distribution-line + {}, # interactions-line-graph + "Episode: N/A", # current-episode + [], # interactions-window + { + "width": "0%", # Collapsed width + "height": "600px", + "padding": "10px", + "border-left": "1px solid #ccc", + "background-color": "#f9f9f9", + "transition": "width 0.5s", # Smooth width transition + "overflow": "hidden", + }, # interactions-container + { + "width": "100%", # Full width + "height": "600px", + "background-color": "#e1e1e1", + "transition": "width 0.5s", # Smooth width transition + }, # cytoscape-style + 0, # slider min + 0, # slider max + 0, # slider value + {}, # slider marks + None, # name-selector value + ) + + # Deserialize the data_store. + follow_graph, interactions_by_episode, posted_users_by_episode, toots, votes = ( + deserialize_data(data_store) + ) + + # Compute positions based on the current follow_graph + all_positions = compute_positions(follow_graph) + layout = {"name": "preset", "positions": all_positions} + + # Build Cytoscape elements + elements = [ + { + "data": {"id": node, "label": node}, + "classes": "default_node", + } + for node in follow_graph.nodes + ] + [ + { + "data": { + "source": src, + "target": tgt, + }, + "classes": "follow_edge", + } + for src, tgt in follow_graph.edges + ] + + # Add all interaction edges classified by the episode they belong to + for episode, interactions in interactions_by_episode.items(): + for interaction in interactions: + source = interaction["source"] + target = interaction["target"] + + # Check if both source and target exist in the graph before creating the edge + if source in follow_graph.nodes and target in follow_graph.nodes: + elements.append( + { + "data": { + "source": source, + "target": target, + "label": f"{interaction['action']}", + }, + "classes": f"interaction_edge episode_{episode}", # Classify edge by episode + } + ) + + # Initialize the stylesheet stylesheet = [ { "selector": ".default_node", @@ -404,7 +1133,7 @@ def update_graph(selected_episode, selected_mode, selected_name): "text-valign": "center", "width": "70px", "height": "70px", - "border-width": 10, + "border-width": 6, "border-color": "#000000", }, }, @@ -439,96 +1168,168 @@ def update_graph(selected_episode, selected_mode, selected_name): "color": "#000000", }, }, + # Specific styles for "Bill" and "Bradley" nodes + { + "selector": '[id="Bill"]', + "style": { + "background-color": "blue", + "border-color": "#000000", + }, + }, + { + "selector": '[id="Bradley"]', + "style": { + "background-color": "orange", + "border-color": "#000000", + }, + }, + # Highlighted Nodes + { + "selector": ".highlighted", + "style": { + "background-color": "#98FF98", # Mint color + "border-color": "#FF69B4", # Hot pink border for visibility + "border-width": 4, + }, + }, ] - layout = {} - current_episode_display = f"Episode: {selected_episode}" # Updated episode display - - active_nodes = set() - active_nodes = { - interaction["source"] for interaction in interactions_by_episode[selected_episode] - }.union( - {interaction["target"] for interaction in interactions_by_episode[selected_episode]} - ).union(posted_users_by_episode[selected_episode]) - if selected_mode == "focused": - # Identify active nodes based on interactions and posted users - active_nodes = { - interaction["source"] for interaction in interactions_by_episode[selected_episode] - }.union( - {interaction["target"] for interaction in interactions_by_episode[selected_episode]} - ).union(posted_users_by_episode[selected_episode]) + # Determine the sizing of the interactions window and Cytoscape graph + interactions_content = [] - non_active_nodes = [node for node in follow_graph.nodes if node not in active_nodes] - - # Compute positions for active nodes using NetworkX's Kamada-Kawai layout - active_subgraph = follow_graph.subgraph(active_nodes) - if len(active_nodes) > 0: - active_pos = nx.kamada_kawai_layout(active_subgraph, scale=1500) + if selected_name: + # Get interactions where source is selected_name in selected_episode + interactions = [ + interaction + for interaction in interactions_by_episode.get(selected_episode, []) + if interaction["source"] == selected_name + ] + if interactions: + for interaction in interactions: + action = interaction["action"] + if action in ["liked", "boosted"]: + toot_id = interaction["toot_id"] + content = toots.get(toot_id, {}).get("content", "No content available.") + user = toots.get(toot_id, {}).get("user", "No user available.") + interactions_content.append( + html.Div( + [ + html.H4( + f"{action.capitalize()} a toot (ID: {toot_id}) by {user}" + ), + html.P(content), + ], + style={ + "border": "1px solid #ccc", + "padding": "10px", + "margin-bottom": "10px", + }, + ) + ) + elif action == "replied": + parent_toot_id = interaction.get("parent_toot_id") + reply_toot_id = interaction.get("toot_id") + parent_content = toots.get(parent_toot_id, {}).get( + "content", "No content available." + ) + reply_content = toots.get(reply_toot_id, {}).get( + "content", "No content available." + ) + user = toots.get(reply_toot_id, {}).get("user", "No user available.") + interactions_content.append( + html.Div( + [ + html.H4(f"Replied to toot (ID: {parent_toot_id}) by {user}"), + html.P(parent_content), + html.H5(f"Reply (ID: {reply_toot_id}):"), + html.P(reply_content), + ], + style={ + "border": "1px solid #ccc", + "padding": "10px", + "margin-bottom": "10px", + }, + ) + ) + elif action == "posted": + toot_id = interaction["toot_id"] + content = toots.get(toot_id, {}).get("content", "No content available.") + interactions_content.append( + html.Div( + [ + html.H4(f"Posted a toot (ID: {toot_id})"), + html.P(content), + ], + style={ + "border": "1px solid #ccc", + "padding": "10px", + "margin-bottom": "10px", + }, + ) + ) else: - active_pos = {} - - for node in active_pos: - x, y = active_pos[node] - active_pos[node] = {"x": x, "y": y} - - # Assign peripheral positions to non-active nodes in a circular layout - num_non_active = len(non_active_nodes) - peripheral_radius = 1800 # Distance from the center for peripheral nodes - - angle_step = (2 * math.pi) / max(num_non_active, 1) - positions_non_active = {} - for i, node in enumerate(non_active_nodes): - angle = i * angle_step - x_pos = peripheral_radius * math.cos(angle) - y_pos = peripheral_radius * math.sin(angle) - positions_non_active[node] = {"x": x_pos, "y": y_pos} - - # Combine positions - all_positions = {} - for node, pos in active_pos.items(): - all_positions[node] = pos - for node, pos in positions_non_active.items(): - all_positions[node] = pos - - # Set layout to preset with all positions - layout = {"name": "preset", "positions": all_positions} - - # Style non-active nodes in gray and smaller - for node in non_active_nodes: - stylesheet.append( - { - "selector": f'[id="{node}"]', - "style": { - "background-color": "#d3d3d3", # Gray out inactive nodes - "width": "120px", - "height": "120px", - "font-size": "30px", - "border-width": 10, - }, - } + interactions_content.append( + html.P("No interactions found for this agent in the selected episode.") ) + else: + interactions_content.append(html.P("Select an agent to view their interactions.")) - # Style active nodes to be larger and more visible - for node in active_nodes: - stylesheet.append( - { - "selector": f'[id="{node}"]', - "style": { - "width": "200px", # Increase active node size - "height": "200px", - "border-width": 30, # Thicker border for visibility - "font-size": "60px", # Larger font size - "border-color": "#000000", # High contrast border - }, - } - ) + if selected_name: + interactions_style = { + "width": "30%", # Expanded width + "height": "600px", + "padding": "10px", + "border-left": "1px solid #ccc", + "background-color": "#f9f9f9", + "transition": "width 0.5s", # Smooth width transition + "overflow": "auto", + } + cytoscape_style = { + "width": "70%", # Reduced width + "height": "600px", + "background-color": "#e1e1e1", + "transition": "width 0.5s", # Smooth width transition + } else: - # Normal mode: use preset layout with precomputed positions - all_positions = compute_positions(follow_graph) - layout = {"name": "preset", "positions": all_positions} + interactions_style = { + "width": "0%", # Collapsed width + "height": "600px", + "padding": "10px", + "border-left": "1px solid #ccc", + "background-color": "#f9f9f9", + "transition": "width 0.5s", # Smooth width transition + "overflow": "hidden", + } + cytoscape_style = { + "width": "100%", # Full width + "height": "600px", + "background-color": "#e1e1e1", + "transition": "width 0.5s", # Smooth width transition + } + + # Highlight selected node and the nodes they follow + if selected_name: + # Find the nodes that the selected node follows (outgoing edges) + follows = list(follow_graph.successors(selected_name)) + # Define the selector for the selected node and its followees + if follows: + highlight_selector = f'[id="{selected_name}"], ' + ", ".join( + [f'[id="{follow}"]' for follow in follows] + ) + else: + highlight_selector = f'[id="{selected_name}"]' - # Reapply base styles since we're resetting layout - # No additional styling needed as positions are already spread ou + # Apply the 'highlighted' class to the selected node and its followees + stylesheet.append( + { + "selector": highlight_selector, + "style": { + "background-color": "#98FF98", # Mint color + "border-color": "#FF69B4", # Hot pink border for visibility + "border-width": 4, + }, + } + ) # Show interaction edges for the selected episode for episode in interactions_by_episode.keys(): @@ -541,48 +1342,28 @@ def update_graph(selected_episode, selected_mode, selected_name): ) # Update node border colors based on votes - episode_votes = votes[selected_episode] + episode_votes = votes.get(selected_episode, {}) total_votes = len(episode_votes) vote_counts = {"Bill": 0, "Bradley": 0, "None": 0} - if selected_name: - # Find the nodes that the selected node follows (outgoing edges) - follows = list(follow_graph.successors(selected_name)) - for node in follow_graph.nodes: if node in episode_votes: vote = episode_votes[node] - if vote == "Bill": - color = "#ff7f0e" + color = "#1f77b4" vote_counts["Bill"] += 1 elif vote == "Bradley": - color = "#1f77b4" + color = "#ff7f0e" vote_counts["Bradley"] += 1 else: color = "#000000" vote_counts["None"] += 1 - if selected_name: - if node == selected_name: - backcolor = "#A020F0" - elif node in follows: - backcolor = "#98FF98" - else: - backcolor = "#fffca0" - elif node == "Bill": - backcolor = "orange" - elif node == "Bradley": - backcolor = "#add8e6" - else: - backcolor = "#fffca0" stylesheet.append( { "selector": f'[id="{node}"]', - "style": {"border-color": color, "background-color": backcolor}, + "style": {"border-color": color}, } ) - else: - print(f"{node} not in vote list") # Calculate vote percentages bill_percentage = (vote_counts["Bill"] / total_votes) * 100 if total_votes > 0 else 0 @@ -595,7 +1376,7 @@ def update_graph(selected_episode, selected_mode, selected_name): x=[bill_percentage], y=["Support"], orientation="h", - marker=dict(color="#ff7f0e"), + marker=dict(color="#1f77b4"), text=f"Bill: {bill_percentage:.1f}%", textposition="inside", ) @@ -605,7 +1386,7 @@ def update_graph(selected_episode, selected_mode, selected_name): x=[bradley_percentage], y=["Support"], orientation="h", - marker=dict(color="#1f77b4"), + marker=dict(color="#ff7f0e"), text=f"Bradley: {bradley_percentage:.1f}%", textposition="inside", base=bill_percentage, @@ -616,10 +1397,9 @@ def update_graph(selected_episode, selected_mode, selected_name): yaxis=dict(showticklabels=False), barmode="stack", title=f"Vote Percentages for Episode {selected_episode}", - title_x=0.5, showlegend=False, - height=100, - margin=dict(l=20, r=20, t=40, b=0), + height=50, + margin=dict(l=0, r=0, t=30, b=0), ) # Create the line graph showing vote distribution over time @@ -646,9 +1426,8 @@ def update_graph(selected_episode, selected_mode, selected_name): x=episodes, y=Bill_votes_over_time, mode="lines+markers", - name="for Bill", - line=dict(color="#ff7f0e"), - cliponaxis=False, + name="Bill", + line=dict(color="#1f77b4"), ) ) vote_line_fig.add_trace( @@ -656,26 +1435,19 @@ def update_graph(selected_episode, selected_mode, selected_name): x=episodes, y=Bradley_votes_over_time, mode="lines+markers", - name="for Bradley", - line=dict(color="#1f77b4"), - cliponaxis=False, + name="Bradley", + line=dict(color="#ff7f0e"), ) ) vote_line_fig.update_layout( - title={"text": "Vote Share Over Time", "font": {"size": 14}, "x": 0.43}, - xaxis={ - "title": {"text": "Episode", "font": {"size": 10}}, - "tickfont": {"size": 8}, - "dtick": 8, - "range": [0, len(episodes) - 1], - }, + title={"text": "Vote Distribution Over Time", "font": {"size": 14}}, + xaxis={"title": {"text": "Episode", "font": {"size": 10}}, "tickfont": {"size": 8}}, yaxis={ - "title": {"text": "Vote Percentage", "font": {"size": 12}}, + "title": {"text": "Vote Percentage", "font": {"size": 10}}, "tickfont": {"size": 8}, - "range": [0, 100], }, height=200, - margin=dict(l=0, r=0, t=20, b=10), + margin=dict(l=40, r=40, t=20, b=10), ) # Create the line graph showing interactions over time @@ -684,8 +1456,6 @@ def update_graph(selected_episode, selected_mode, selected_name): total_users = len(follow_graph.nodes) - # Initialize lists to store normalized interactions and active user fractions - active_user_fractions = [] for ep in episodes: @@ -712,7 +1482,9 @@ def update_graph(selected_episode, selected_mode, selected_name): # Append counts to the respective lists for interaction in interaction_types: - interactions_over_time[interaction].append(counts[interaction] / num_active_users) + interactions_over_time[interaction].append( + counts[interaction] / num_active_users if num_active_users > 0 else 0 + ) # Calculate active user fraction active_user_fraction = num_active_users / total_users if total_users > 0 else 0 @@ -729,11 +1501,9 @@ def update_graph(selected_episode, selected_mode, selected_name): name="Likes", line=dict(color="#2ca02c"), # Green marker=dict(symbol="circle", size=6), - cliponaxis=False, ), secondary_y=False, ) - interactions_line_fig.add_trace( go.Scatter( x=episodes, @@ -742,11 +1512,9 @@ def update_graph(selected_episode, selected_mode, selected_name): name="Boosts", line=dict(color="#ff7f0e"), # Orange marker=dict(symbol="square", size=6), - cliponaxis=False, ), secondary_y=False, ) - interactions_line_fig.add_trace( go.Scatter( x=episodes, @@ -755,11 +1523,9 @@ def update_graph(selected_episode, selected_mode, selected_name): name="Replies", line=dict(color="#9467bd"), # Purple marker=dict(symbol="diamond", size=6), - cliponaxis=False, ), secondary_y=False, ) - interactions_line_fig.add_trace( go.Scatter( x=episodes, @@ -768,7 +1534,6 @@ def update_graph(selected_episode, selected_mode, selected_name): name="Posts", line=dict(color="#1f77b4"), # Blue marker=dict(symbol="triangle-up", size=6), - cliponaxis=False, ), secondary_y=False, ) @@ -779,9 +1544,8 @@ def update_graph(selected_episode, selected_mode, selected_name): x=episodes, y=active_user_fractions, mode="lines", - name="Active User
Fraction", + name="Active User Fraction", line=dict(color="gray"), - cliponaxis=False, ), secondary_y=True, ) @@ -804,14 +1568,12 @@ def update_graph(selected_episode, selected_mode, selected_name): secondary_y=True, showgrid=False, # Typically, grid lines are only on the primary y-axis gridcolor="lightgray", - tickfont={"size": 8}, ) interactions_line_fig.update_layout( title={ - "text": "Actions Over Time", + "text": "Interactions Over Time", "font": {"size": 14}, # Reduced title font size to 14 - "x": 0.35, }, xaxis={ "title": { @@ -820,32 +1582,53 @@ def update_graph(selected_episode, selected_mode, selected_name): }, "tickfont": {"size": 8}, # Reduced x-axis tick font size "range": [ - 0, - len(episodes) - 1, - ], # Setting the range from 0 to length of episodes + 1 - "dtick": 8, # Show a tick marker every 5 episodes + min(episodes), + max(episodes) + 1, + ], # Setting the range from min to max episode + "dtick": 1, # Show a tick marker every episode }, yaxis={ "title": { - "text": "#Actions /
#Active Users", - "font": {"size": 12}, # Reduced y-axis label font size to 12 + "text": "Interactions/ Num. Agents", + "font": {"size": 10}, # Reduced y-axis label font size to 12 }, "tickfont": {"size": 8}, # Reduced y-axis tick font size }, height=200, - margin=dict(l=0, r=40, t=20, b=20), - legend=dict(yanchor="top", y=0.99, xanchor="left", x=1.3), + margin=dict(l=40, r=40, t=20, b=10), showlegend=True, ) + # Adjust the x-axis range to include all episodes + interactions_line_fig.update_xaxes(range=[min(episodes), max(episodes) + 1]) + + # Update the name-selector dropdown options + unique_names = sorted(follow_graph.nodes) + name_options = [{"label": name, "value": name} for name in unique_names] + # Set episode slider properties + slider_min = min(episodes) + slider_max = max(episodes) + slider_value = selected_episode if selected_episode in episodes else slider_min + slider_marks = {str(ep): f"{ep}" for ep in sorted(episodes)} + + # Return all outputs, including the interactions window content and styles return ( + elements, # Updated elements layout, stylesheet, fig, vote_line_fig, interactions_line_fig, - current_episode_display, - ) # Added Output + f"Episode: {selected_episode}", # Updated episode display + interactions_content, + interactions_style, + cytoscape_style, + slider_min, + slider_max, + slider_value, + slider_marks, + selected_name, + ) # Run the Dash app app.run_server(debug=True) From d98f79b7e1e452f0fe44fb311d474e5460e540da Mon Sep 17 00:00:00 2001 From: Xarangi Date: Sat, 16 Nov 2024 00:09:47 +0530 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20feat(apps.py):=20log=20all=20ag?= =?UTF-8?q?ent=20interactions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 371 +++++++++--------- pyproject.toml | 3 + src/mastodon_sim/concordia/components/apps.py | 23 +- 3 files changed, 215 insertions(+), 182 deletions(-) diff --git a/poetry.lock b/poetry.lock index 21fea97..5316979 100644 --- a/poetry.lock +++ b/poetry.lock @@ -319,6 +319,17 @@ files = [ [package.dependencies] chardet = ">=3.0.2" +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + [[package]] name = "blurhash" version = "1.1.4" @@ -926,6 +937,88 @@ attrs = ">=23.1.0" commitizen = ">=3.2.2" setuptools = {version = "*", markers = "python_version >= \"3.12\""} +[[package]] +name = "dash" +version = "2.18.2" +description = "A Python framework for building reactive web-apps. Developed by Plotly." +optional = false +python-versions = ">=3.8" +files = [ + {file = "dash-2.18.2-py3-none-any.whl", hash = "sha256:0ce0479d1bc958e934630e2de7023b8a4558f23ce1f9f5a4b34b65eb3903a869"}, + {file = "dash-2.18.2.tar.gz", hash = "sha256:20e8404f73d0fe88ce2eae33c25bbc513cbe52f30d23a401fa5f24dbb44296c8"}, +] + +[package.dependencies] +dash-core-components = "2.0.0" +dash-html-components = "2.0.0" +dash-table = "5.0.0" +Flask = ">=1.0.4,<3.1" +importlib-metadata = "*" +nest-asyncio = "*" +plotly = ">=5.0.0" +requests = "*" +retrying = "*" +setuptools = "*" +typing-extensions = ">=4.1.1" +Werkzeug = "<3.1" + +[package.extras] +celery = ["celery[redis] (>=5.1.2)", "redis (>=3.5.3)"] +ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] +compress = ["flask-compress"] +dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] +diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] +testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] + +[[package]] +name = "dash-core-components" +version = "2.0.0" +description = "Core component suite for Dash" +optional = false +python-versions = "*" +files = [ + {file = "dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346"}, + {file = "dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee"}, +] + +[[package]] +name = "dash-cytoscape" +version = "1.0.2" +description = "A Component Library for Dash aimed at facilitating network visualization in Python, wrapped around Cytoscape.js" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dash_cytoscape-1.0.2.tar.gz", hash = "sha256:a61019d2184d63a2b3b5c06d056d3b867a04223a674cc3c7cf900a561a9a59aa"}, +] + +[package.dependencies] +dash = "*" + +[package.extras] +leaflet = ["dash-leaflet (>=1.0.16rc3)"] + +[[package]] +name = "dash-html-components" +version = "2.0.0" +description = "Vanilla HTML components for Dash" +optional = false +python-versions = "*" +files = [ + {file = "dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63"}, + {file = "dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50"}, +] + +[[package]] +name = "dash-table" +version = "5.0.0" +description = "Dash table" +optional = false +python-versions = "*" +files = [ + {file = "dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9"}, + {file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"}, +] + [[package]] name = "debugpy" version = "1.8.5" @@ -1166,6 +1259,28 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] typing = ["typing-extensions (>=4.12.2)"] +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + [[package]] name = "fonttools" version = "4.53.1" @@ -2152,6 +2267,29 @@ files = [ {file = "immutabledict-4.2.0.tar.gz", hash = "sha256:e003fd81aad2377a5a758bf7e1086cf3b70b63e9a5cc2f46bce8d0a2b4727c5f"}, ] +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -2453,17 +2591,6 @@ files = [ [package.dependencies] jsonpointer = ">=1.9" -[[package]] -name = "jsonpath-python" -version = "1.0.6" -description = "A more powerful JSONPath implementation in modern python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "jsonpath-python-1.0.6.tar.gz", hash = "sha256:dd5be4a72d8a2995c3f583cf82bf3cd1a9544cfdabf2d22595b67aff07349666"}, - {file = "jsonpath_python-1.0.6-py3-none-any.whl", hash = "sha256:1e3b78df579f5efc23565293612decee04214609208a2335884b3ee3f786b575"}, -] - [[package]] name = "jsonpickle" version = "3.3.0" @@ -2719,8 +2846,8 @@ langchain-core = ">=0.2.38,<0.3.0" langchain-text-splitters = ">=0.2.0,<0.3.0" langsmith = ">=0.1.17,<0.2.0" numpy = [ - {version = ">=1,<2", markers = "python_version < \"3.12\""}, {version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""}, + {version = ">=1,<2", markers = "python_version < \"3.12\""}, ] pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -2744,8 +2871,8 @@ jsonpatch = ">=1.33,<2.0" langsmith = ">=0.1.75,<0.2.0" packaging = ">=23.2,<25" pydantic = [ - {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, + {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, ] PyYAML = ">=5.3" tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" @@ -2780,8 +2907,8 @@ files = [ httpx = ">=0.23.0,<1" orjson = ">=3.9.14,<4.0.0" pydantic = [ - {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, + {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, ] requests = ">=2,<3" @@ -3198,27 +3325,6 @@ httpx = ">=0.25,<1" orjson = ">=3.9.10,<3.11" pydantic = ">=2.5.2,<3" -[[package]] -name = "mistralai" -version = "1.0.3" -description = "Python Client SDK for the Mistral AI API." -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "mistralai-1.0.3-py3-none-any.whl", hash = "sha256:64af7c9192e64dc66b2da6d1c4d54a1324a881c21665a2f93d6b35d9de9f87c8"}, - {file = "mistralai-1.0.3.tar.gz", hash = "sha256:84f1a217666c76fec9d477ae266399b813c3ac32a4a348d2ecd5fe1c039b0667"}, -] - -[package.dependencies] -httpx = ">=0.27.0,<0.28.0" -jsonpath-python = ">=1.0.6,<2.0.0" -pydantic = ">=2.8.2,<2.9.0" -python-dateutil = ">=2.9.0.post0,<3.0.0" -typing-inspect = ">=0.9.0,<0.10.0" - -[package.extras] -gcp = ["google-auth (==2.27.0)", "requests (>=2.32.3,<3.0.0)"] - [[package]] name = "mpmath" version = "1.3.0" @@ -4263,28 +4369,6 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pydantic" -version = "2.8.2" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] - -[package.extras] -email = ["email-validator (>=2.0.0)"] - [[package]] name = "pydantic" version = "2.9.1" @@ -4299,113 +4383,15 @@ files = [ [package.dependencies] annotated-types = ">=0.6.0" pydantic-core = "2.23.3" -typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] email = ["email-validator (>=2.0.0)"] timezone = ["tzdata"] -[[package]] -name = "pydantic-core" -version = "2.20.1" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - [[package]] name = "pydantic-core" version = "2.23.3" @@ -5159,6 +5145,20 @@ files = [ decorator = ">=3.4.2" py = ">=1.4.26,<2.0.0" +[[package]] +name = "retrying" +version = "1.3.4" +description = "Retrying" +optional = false +python-versions = "*" +files = [ + {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"}, + {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"}, +] + +[package.dependencies] +six = ">=1.7.0" + [[package]] name = "rich" version = "13.8.0" @@ -6556,21 +6556,6 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "typing-inspect" -version = "0.9.0" -description = "Runtime inspection utilities for typing module." -optional = false -python-versions = "*" -files = [ - {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, - {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, -] - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - [[package]] name = "tzdata" version = "2024.1" @@ -6998,6 +6983,23 @@ files = [ {file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"}, ] +[[package]] +name = "werkzeug" +version = "3.0.6" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, + {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + [[package]] name = "wheel" version = "0.44.0" @@ -7142,7 +7144,26 @@ files = [ idna = ">=2.0" multidict = ">=4.0" +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.0" python-versions = ">=3.11,<4.0" -content-hash = "f4267ae3f69f6c4e43b2e23ea32b26464f817eb8209b462e437e908e9a39b525" +content-hash = "a21e3ec8cb0692e4fb7c0a1054abf63c040c40e33c3fe40b410b082a1182e47c" diff --git a/pyproject.toml b/pyproject.toml index 6ffca15..686863e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,9 @@ ipympl = "^0.9.4" python-louvain = "^0.16" powerlaw = "^1.5" pyproject-toml = "^0.0.10" +dash = "^2.18.2" +dash-cytoscape = "^1.0.2" +scipy = "^1.14.1" [tool.poetry.group.test.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/ commitizen = ">=3.21.3" diff --git a/src/mastodon_sim/concordia/components/apps.py b/src/mastodon_sim/concordia/components/apps.py index 78e5f56..72ce869 100644 --- a/src/mastodon_sim/concordia/components/apps.py +++ b/src/mastodon_sim/concordia/components/apps.py @@ -639,11 +639,12 @@ def post_toot( ValueError: If the input parameters are invalid. Exception: For any other unexpected errors during posting. """ + return_val = None try: current_user = current_user.split()[0] username = self._get_username(current_user) if self.perform_operations: - self._mastodon_ops.post_status( + return_val = self._mastodon_ops.post_status( login_user=username, status=status, ) @@ -657,6 +658,7 @@ def post_toot( f'Status posted for user: {current_user} ({username}): "{status}"', emoji="📝", ) + # self._print(return_val) except ValueError as e: self._print(f"Invalid input: {e!s}", emoji="❌") @@ -665,10 +667,15 @@ def post_toot( except Exception as e: self._print(f"An unexpected error occurred: {e!s}", emoji="❌") raise - return_msg = f'{current_user} posted a toot!: "{status}"' + if return_val: + return_msg = ( + f"{current_user} posted a toot with Toot ID: {return_val['id']} --- {status}\n" + ) + else: + return_msg = f'{current_user} posted a toot!: "{status}"\n' with file_lock: with open(write_path + "app_logger.txt", "a") as f: - f.write(f"{current_user} posted\n") + f.write(return_msg) return return_msg @app_action @@ -692,12 +699,13 @@ def reply_to_toot( ValueError: If the input parameters are invalid. Exception: For any other unexpected errors during posting. """ + return_val = None try: current_user = current_user.split()[0] target_user = target_user.split()[0] username = self._get_username(current_user) if self.perform_operations: - self._mastodon_ops.post_status( + return_val = self._mastodon_ops.post_status( login_user=username, status=status, in_reply_to_id=in_reply_to_id, @@ -727,9 +735,10 @@ def reply_to_toot( with file_lock: with open(write_path + "app_logger.txt", "a") as f: - f.write( - f"{current_user} replied to a toot by {target_user} with Toot ID:{in_reply_to_id}\n" - ) + if return_val: + f.write( + f"{current_user} replied to a toot by {target_user} with Toot ID: {in_reply_to_id}, new Toot ID: {return_val['id']} --- {status}\n" + ) return return_msg # @app_action