Skip to content

Feature/reclickable button #672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/addnewcomponent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@ def helloworldcomponent(
"id": component_id,
"label": label,
"size": size,
"state_key": "label"
}

service.append_component(component)
return "hello world"
```

NOTE: Be sure to include either "value" or "state_key" in your component. The value
corresponding to the "state_key" (or "value", if "state_key" is not found) defines
the state of the frontend component.

---

## 2) Modify `preswald/preswald/interfaces/__init__.py`
Expand Down
10,001 changes: 10,001 additions & 0 deletions examples/lego-challenge/data/lego_pile_reduced.csv

Large diffs are not rendered by default.

250 changes: 250 additions & 0 deletions examples/lego-challenge/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import pandas as pd
import plotly.express as px
from numpy.random import randint
from plotly.graph_objects import Figure

from preswald import (
Workflow,
alert,
button,
connect,
get_df,
image,
plotly,
selectbox,
separator,
table,
text,
topbar,
)


# ------ Define helper functions ------


def get_area_of_foot(shoe_size: int) -> float:
"""Uses EU shoe size to estimate the area of the foot in cm^2
Args:
shoe_size: EU shoe size

Returns:
area_of_foot: area of the foot in cm^2

References:
[foot length](https://en.wikipedia.org/wiki/Shoe_size)
[foot width](https://oaji.net/articles/2015/1264-1431011777.pdf)
"""

foot_length = (2 / 3) * (shoe_size - 2)
foot_width = 0.35 * foot_length

# Assuming the foot is a rectangle
return foot_length * foot_width


def get_num_legos_stepped_on(shoe_size: int) -> int:
"""Uses the area of the foot to estimate the number of legos stepped on
Args:
shoe_size: EU shoe size

Returns:
num_legos_stepped_on: number of legos stepped on
"""
area_of_foot = get_area_of_foot(shoe_size)
# Assuming each lego has an area of 2.34 x 2.34 cm (3x3 studs)
area_of_lego_block = 2.34 * 2.34

num_legos_per_foot = area_of_foot / area_of_lego_block
num_legos_stepped_on = num_legos_per_foot * 2

# Round to an integer and add some randomness (maybe you'll get lucky.... or not!)
return int(num_legos_stepped_on) + randint(-10, 10)


def get_lego_color_map(legos_stepped_on: pd.DataFrame) -> dict:
"""Get the color mappings for the legos stepped on
Args:
legos_stepped_on: subset of the lego pile

Returns:
color_mappings: dictionary of color mappings
"""
colors = legos_stepped_on[["color_name", "rgb"]].drop_duplicates()
color_mappings = {}
for __, row in colors.iterrows():
color_mappings[row["color_name"]] = "#" + row["rgb"]
return color_mappings


def visualize_lego_colors(legos_stepped_on: pd.DataFrame) -> Figure:
"""Visualizes the colors of the legos stepped on using a treemap

Args:
legos_stepped_on: subset of the lego pile

Returns:
fig: plotly figure of the treemap
"""
color_map = get_lego_color_map(legos_stepped_on)
fig = px.treemap(
legos_stepped_on.color_name.value_counts().reset_index(drop=False),
path=["color_name"],
values="count",
color="color_name",
color_discrete_map=color_map,
title="Legos Stepped on by Color",
)
fig.update_traces(
textposition="top center",
hovertemplate="<b>Color</b>: %{label} \n" + "<b>Count</b>: %{value}",
)
fig.update_layout(template="plotly_white")
return fig


def get_death_roll(legos_stepped_on: pd.DataFrame) -> pd.DataFrame:
"""Get a table of the minifigs/minidolls stepped on
Args:
legos_stepped_on: subset of the lego pile
Returns:
death_roll: Minifigs/Minidolls (and heads) stepped on
"""

death_roll = legos_stepped_on[
[
name in ("Minifigs", "Minifig Heads", "Minidoll Heads")
for name in legos_stepped_on.part_cat_name
]
][["part_name", "img_url"]]
return death_roll


def calculate_damage(legos_stepped_on: pd.DataFrame) -> int:
"""Calculate the damage taken from the legos stepped on

Each lego piece is worth 1 point of damage, except Duplo which is 2 points.

Args:
legos_stepped_on: subset of the lego pile

Returns:
damage: damage taken
"""
base_damage = len(legos_stepped_on)
duplo_damage = legos_stepped_on.part_cat_name.str.contains("Duplo").sum()
return base_damage + duplo_damage


# ------ Define app workflow ------

# Create a workflow instance
workflow = Workflow()


@workflow.atom()
def render_topbar():
topbar()


@workflow.atom()
def render_header():
text("# Welcome to the Lego Challenge! 🧱")


@workflow.atom()
def dump_out_legos():
connect()
lego_pile = get_df("lego_pile")
text(
"You walk into the challenge room and every inch of the floor is **covered** in Legos."
)
text(f"## There are {len(lego_pile)} Legos! 😱😨")
table(lego_pile.head(100))
separator()
return lego_pile


@workflow.atom()
def challenge_user():
text("# Are you brave enough to take a step?")


@workflow.atom()
def choose_shoe_size() -> int:
text("Please select your shoe size (EU)")
shoe_size = selectbox(
"shoe_size",
options=list(range(30, 50)),
default=None,
)
text(f"You have selected size {shoe_size}.")
return shoe_size


@workflow.atom(dependencies=["choose_shoe_size"])
def take_a_step() -> bool:
step = button("Take a step", can_be_reclicked=True)
separator()
return step


@workflow.atom(dependencies=["take_a_step", "choose_shoe_size", "dump_out_legos"])
def get_legos_stepped_on(take_a_step, choose_shoe_size, dump_out_legos):
if take_a_step:
# Sample the legos stepped on
num_legos_stepped_on = get_num_legos_stepped_on(choose_shoe_size)
legos_stepped_on = dump_out_legos.sample(num_legos_stepped_on)

# Get one random minifig head
token_minifig = dump_out_legos[
(dump_out_legos.part_cat_name == "Minifig Heads")
& (dump_out_legos.img_url.notna())
].sample(1)

# Add a random minifig to legos_stepped_on (in case no minifigs are stepped on)
legos_stepped_on = pd.concat([legos_stepped_on, token_minifig])
num_legos_stepped_on += 1

text(f"# 💥💥You stepped on {num_legos_stepped_on} legos!💥💥", size=50)
return legos_stepped_on


@workflow.atom(dependencies=["take_a_step", "get_legos_stepped_on"])
def render_legos_stepped_on(take_a_step, get_legos_stepped_on):
if take_a_step:
colors_fig = visualize_lego_colors(get_legos_stepped_on)
plotly(colors_fig)


@workflow.atom(dependencies=["take_a_step", "get_legos_stepped_on"])
def render_death_roll(take_a_step, get_legos_stepped_on):
if take_a_step:
# Show Minifigs vanquished with step
minifigs_vanquished = get_death_roll(get_legos_stepped_on)
text(
f"## **You vanquished {len(minifigs_vanquished)} Minifig{'s' if len(minifigs_vanquished) > 1 else ''} 💀⚔️**"
)
text("_(victorious trumpet sounds)_")

if len(minifigs_vanquished.dropna()) > 0:
# Get the first minifig in the death roll that has an image
featured_enemy = minifigs_vanquished.dropna().iloc[0]
text("### 🏆 Featured Enemy:")
text(featured_enemy["part_name"])
image(
featured_enemy["img_url"],
alt=featured_enemy["part_name"],
size=0.7,
)


@workflow.atom(dependencies=["take_a_step", "get_legos_stepped_on"])
def render_damage(take_a_step, get_legos_stepped_on):
if take_a_step:
damage = calculate_damage(get_legos_stepped_on)
text(f"## Total Damage Taken: {damage}")
alert("Damage calculation is 1 point per Lego, 2 points per Duplo.")


# Execute the workflow
results = workflow.execute()
20 changes: 20 additions & 0 deletions examples/lego-challenge/preswald.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[project]
title = "Step on a Lego"
version = "0.1.0"
port = 8501
entrypoint = "hello.py"
slug = "chemery-online-assessment"

[branding]
name = "The Lego Challenge"
logo = "images/logo.png"
favicon = "images/favicon.ico"
primaryColor = "#000000"

[data.lego_pile]
type = "csv"
path = "data/lego_pile_reduced.csv"

[logging]
level = "INFO" # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
3 changes: 2 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ const App = () => {
if (!component || !component.id) return component;

const currentState = comm.getComponentState(component.id);
const stateKey = component.state_key || 'value';
return {
...component,
value: currentState !== undefined ? currentState : component.value,
value: currentState !== undefined ? currentState : component[stateKey],
error: null,
};
})
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/DynamicComponents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ const MemoizedComponent = memo(
key={componentKey}
onClick={() => {
if (component.onClick) {
handleUpdate(componentId, true);
const newStatefulValue = {
...component.stateful_value,
value: true,
};
handleUpdate(componentId, newStatefulValue);
}
}}
disabled={component.disabled}
Expand Down
24 changes: 14 additions & 10 deletions frontend/src/utils/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,25 @@ class WebSocketClient {

case 'state_update':
if (data.component_id) {
this.componentStates[data.component_id] = data.value;
const stateKey = component.state_key || 'value';
this.componentStates[data.component_id] = data[stateKey];
}
console.log('[WebSocket] Component state updated:', {
componentId: data.component_id,
value: data.value,
value: data[stateKey],
});
break;

case 'components':
if (data.components?.rows) {
data.components.rows.forEach((row) => {
row.forEach((component) => {
if (component.id && 'value' in component) {
this.componentStates[component.id] = component.value;
const stateKey = component.state_key || 'value';
if (component.id) {
this.componentStates[component.id] = component[stateKey];
console.log('[WebSocket] Component state updated:', {
componentId: component.id,
value: component.value,
value: component[stateKey],
});
}
});
Expand Down Expand Up @@ -292,11 +294,12 @@ class PostMessageClient {

case 'state_update':
if (data.component_id) {
this.componentStates[data.component_id] = data.value;
const stateKey = data.state_key || 'value';
this.componentStates[data.component_id] = data[stateKey];
}
console.log('[PostMessage] Component state updated:', {
componentId: data.component_id,
value: data.value,
value: data[stateKey],
});
this._notifySubscribers(data);
break;
Expand All @@ -305,11 +308,12 @@ class PostMessageClient {
if (data.components && data.components.rows) {
data.components.rows.forEach((row) => {
row.forEach((component) => {
if (component.id && 'value' in component) {
this.componentStates[component.id] = component.value;
const stateKey = component.state_key || 'value';
if (component.id) {
this.componentStates[component.id] = component[stateKey];
console.log('[PostMessage] Component state updated:', {
componentId: component.id,
value: component.value,
value: component[stateKey],
});
}
});
Expand Down
Loading