Richard commited on
Commit
f3d45a9
·
0 Parent(s):

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Mesop
2
+ .env
3
+
4
+ # Python
5
+ __pycache__
6
+ .pytest_cache
7
+
8
+ # System
9
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10.14-bullseye
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y \
5
+ # General dependencies
6
+ locales \
7
+ locales-all && \
8
+ # Clean local repository of package files since they won't be needed anymore.
9
+ # Make sure this line is called after all apt-get update/install commands have
10
+ # run.
11
+ apt-get clean && \
12
+ # Also delete the index files which we also don't need anymore.
13
+ rm -rf /var/lib/apt/lists/*
14
+
15
+ ENV LC_ALL en_US.UTF-8
16
+ ENV LANG en_US.UTF-8
17
+ ENV LANGUAGE en_US.UTF-8
18
+
19
+ # Install dependencies
20
+ COPY requirements.txt .
21
+ RUN pip install -r requirements.txt
22
+
23
+ # Create non-root user
24
+ RUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop
25
+ USER mesop
26
+
27
+ # Add app code here
28
+ COPY --chown=mesop:mesop . /srv/mesop-app
29
+ WORKDIR /srv/mesop-app
30
+
31
+ # Run Mesop through gunicorn. Should be available at localhost:8080
32
+ CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:me", "--timeout", "300"]
README.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Mesop App Maker
3
+ emoji: 🏭
4
+ colorFrom: yellow
5
+ colorTo: pink
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ app_port: 8080
10
+ ---
11
+
12
+ # Mesop App Maker
13
+
14
+ Editor to generate, edit, and view Mesop apps using LLMs.
15
+
16
+ ## Usage
17
+
18
+ The Mesop App Maker consists of two Mesop apps, the editor and the app runner.
19
+
20
+ ### The editor
21
+
22
+ The editor is the Mesop app that allows you to generate, edit, and view Mesop apps.
23
+
24
+ ```shell
25
+ cd editor
26
+ pip install -r requirements.txt # First time only
27
+ mesop main.py
28
+ ```
29
+
30
+ You will need a Gemini API key to use the Mesop app generate functionality.
31
+
32
+ ### The runner
33
+
34
+ > The runner has been moved to https://github.com/richard-to/mesop-app-runner.
35
+
36
+ The [Mesop App Runner](https://github.com/richard-to/mesop-app-runner) uses Docker to avoid potentially destructive code changes.
37
+
38
+ It can be started using these commands:
39
+
40
+ ```shell
41
+ docker stop mesop-app-runner;
42
+ docker rm mesop-app;
43
+ docker build -t mesop-app-runner . && docker run --name mesop-app-runner -d -p 8080:8080 mesop-app-runner;
44
+ ```
45
+
46
+ ## Screenshots
47
+
48
+ ### Generate app
49
+
50
+ <img width="1312" alt="Screenshot 2024-08-05 at 5 29 44 PM" src="https://github.com/user-attachments/assets/d96afd8a-3c09-4d12-8749-00deddc7f8f5">
51
+
52
+ ### Preview app
53
+
54
+ <img width="1312" alt="Screenshot 2024-08-05 at 5 31 35 PM" src="https://github.com/user-attachments/assets/1a826d44-c87b-4c79-aeaf-29bc8da3b1c0">
components/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from components.button import toolbar_button as toolbar_button
2
+ from components.button import button_toggle as button_toggle
3
+ from components.card import card as card
4
+ from components.card import expandable_card as expandable_card
5
+ from components.dialog import dialog as dialog
6
+ from components.dialog import dialog_actions as dialog_actions
7
+ from components.panel import panel as panel
8
+ from components.snackbar import snackbar as snackbar
components/button.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Literal, Any
2
+
3
+ import mesop as me
4
+
5
+ from components import helpers
6
+
7
+
8
+ @me.component()
9
+ def toolbar_button(
10
+ *,
11
+ icon: str,
12
+ tooltip: str,
13
+ on_click: Callable[[me.ClickEvent], Any] | None = None,
14
+ key: str = "",
15
+ ):
16
+ with me.content_button(
17
+ on_click=on_click,
18
+ key=key,
19
+ type="icon",
20
+ ):
21
+ with me.tooltip(message=tooltip):
22
+ me.icon(icon)
23
+
24
+
25
+ @me.component()
26
+ def button(
27
+ label: str | None = None,
28
+ *,
29
+ on_click: Callable[[me.ClickEvent], Any] | None = None,
30
+ type: Literal["raised", "flat", "stroked"] | None = None,
31
+ color: Literal["primary", "accent", "warn"] | None = None,
32
+ disable_ripple: bool = False,
33
+ disabled: bool = False,
34
+ style: me.Style | None = None,
35
+ key: str | None = None,
36
+ ) -> None:
37
+ me.button(
38
+ label=label,
39
+ on_click=on_click,
40
+ type=type,
41
+ color=color,
42
+ disable_ripple=disable_ripple,
43
+ disabled=disabled,
44
+ key=key,
45
+ style=helpers.merge_styles(me.Style(border_radius=10), style),
46
+ )
47
+
48
+
49
+ @me.component()
50
+ def button_toggle(
51
+ labels: list[str],
52
+ selected: str = "",
53
+ on_click: Callable | None = None,
54
+ key: str = "",
55
+ ):
56
+ """Simple version of Angular Component Button toggle.
57
+
58
+ Only supports single selection for now.
59
+
60
+ Args:
61
+ labels: Text labels for buttons
62
+ selected: Selected label
63
+ on_click: Event to handle button clicks on the button toggle
64
+ key: The key will be used as as prefix along with the selected label
65
+ """
66
+ with me.box(style=me.Style(display="flex", font_weight="bold", font_size=14)):
67
+ last_index = len(labels) - 1
68
+
69
+ for index, label in enumerate(labels):
70
+ if index == 0:
71
+ element = "first"
72
+ elif index == last_index:
73
+ element = "last"
74
+ else:
75
+ element = "default"
76
+
77
+ with me.box(
78
+ key=key + "_" + label,
79
+ on_click=on_click,
80
+ style=me.Style(
81
+ align_items="center",
82
+ display="flex",
83
+ # Handle selected case
84
+ background=_SELECTED_BG
85
+ if label == selected
86
+ else me.theme_var("surface-container-lowest"),
87
+ padding=_SELECTED_PADDING if label == selected else _PADDING,
88
+ cursor="default" if label == selected else "pointer",
89
+ # Handle single button case (should just use a button in this case)
90
+ border=_LAST_BORDER if last_index == 0 else _BORDER_MAP[element],
91
+ border_radius=_BORDER_RADIUS if last_index == 0 else _BORDER_RADIUS_MAP[element],
92
+ ),
93
+ ):
94
+ if label in selected:
95
+ me.icon("check")
96
+ me.text(label)
97
+
98
+
99
+ _SELECTED_BG = me.theme_var("primary-container")
100
+
101
+ _PADDING = me.Padding(left=15, right=15, top=10, bottom=10)
102
+ _SELECTED_PADDING = me.Padding(left=15, right=15, top=5, bottom=5)
103
+
104
+ _BORDER_RADIUS = "20px"
105
+
106
+ _DEFAULT_BORDER_STYLE = me.BorderSide(width=1, color=me.theme_var("outline"), style="solid")
107
+ _BORDER = me.Border(
108
+ left=_DEFAULT_BORDER_STYLE, top=_DEFAULT_BORDER_STYLE, bottom=_DEFAULT_BORDER_STYLE
109
+ )
110
+ _LAST_BORDER = me.Border.all(_DEFAULT_BORDER_STYLE)
111
+
112
+ _BORDER_MAP = {
113
+ "first": _BORDER,
114
+ "last": _LAST_BORDER,
115
+ "default": _BORDER,
116
+ }
117
+
118
+ _BORDER_RADIUS_MAP = {
119
+ "first": "20px 0 0 20px",
120
+ "last": "0px 20px 20px 0",
121
+ "default": "0",
122
+ }
components/card.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from components.helpers import merge_styles
2
+
3
+ from typing import Callable
4
+
5
+ import mesop as me
6
+
7
+
8
+ @me.content_component
9
+ def card(*, title: str = "", style: me.Style | None = None, key: str = ""):
10
+ """Creates a simple card component similar to Angular Component.
11
+
12
+ Args:
13
+ title: If empty, not title will be shown
14
+ style: Override the default styles of the card box
15
+ key: Not really useful here
16
+ """
17
+ with me.box(key=key, style=merge_styles(_DEFAULT_CARD_STYLE, style)):
18
+ if title:
19
+ me.text(
20
+ title,
21
+ style=me.Style(font_size=16, font_weight="bold", margin=me.Margin(bottom=15)),
22
+ )
23
+
24
+ me.slot()
25
+
26
+
27
+ @me.content_component
28
+ def expandable_card(
29
+ *,
30
+ title: str = "",
31
+ expanded: bool = False,
32
+ on_click_header: Callable | None = None,
33
+ style: me.Style | None = None,
34
+ key: str = "",
35
+ ):
36
+ """Creates a simple card component that is expandable.
37
+
38
+ Args:
39
+ title: If empty, no title will be shown but the expander will still be shown
40
+ expanded: Whether the card is expanded or not
41
+ on_click_header: Click handler for expanding card
42
+ style: Override the default styles of the card box
43
+ key: Key for the component
44
+ """
45
+ with me.box(key=key, style=merge_styles(_DEFAULT_CARD_STYLE, style)):
46
+ with me.box(
47
+ on_click=on_click_header,
48
+ style=me.Style(
49
+ align_items="center",
50
+ display="flex",
51
+ justify_content="space-between",
52
+ ),
53
+ ):
54
+ me.text(
55
+ title,
56
+ style=me.Style(font_size=16, font_weight="bold"),
57
+ )
58
+ me.icon("keyboard_arrow_up" if expanded else "keyboard_arrow_down")
59
+
60
+ with me.box(style=me.Style(margin=me.Margin(top=15), display="block" if expanded else "none")):
61
+ me.slot()
62
+
63
+
64
+ _DEFAULT_CARD_STYLE = me.Style(
65
+ background=me.theme_var("surface-container-lowest"),
66
+ border_radius=10,
67
+ border=me.Border.all(
68
+ me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid")
69
+ ),
70
+ padding=me.Padding.all(15),
71
+ margin=me.Margin(bottom=15),
72
+ )
components/dialog.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+
3
+
4
+ @me.content_component
5
+ def dialog(is_open: bool):
6
+ """Renders a dialog component.
7
+
8
+ The design of the dialog borrows from the Angular component dialog. So basically
9
+ rounded corners and some box shadow.
10
+
11
+ One current drawback is that it's not possible to close the dialog
12
+ by clicking on the overlay background. This is due to
13
+ https://github.com/google/mesop/issues/268.
14
+
15
+ Args:
16
+ is_open: Whether the dialog is visible or not.
17
+ """
18
+ with me.box(
19
+ style=me.Style(
20
+ background="rgba(0, 0, 0, 0.4)"
21
+ if me.theme_brightness() == "light"
22
+ else "rgba(255, 255, 255, 0.4)",
23
+ display="block" if is_open else "none",
24
+ height="100%",
25
+ overflow_x="auto",
26
+ overflow_y="auto",
27
+ position="fixed",
28
+ width="100%",
29
+ z_index=1000,
30
+ )
31
+ ):
32
+ with me.box(
33
+ style=me.Style(
34
+ align_items="center",
35
+ display="grid",
36
+ height="100vh",
37
+ justify_items="center",
38
+ )
39
+ ):
40
+ with me.box(
41
+ style=me.Style(
42
+ background=me.theme_var("surface-container-lowest"),
43
+ border_radius=20,
44
+ box_sizing="content-box",
45
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
46
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
47
+ padding=me.Padding.all(20),
48
+ )
49
+ ):
50
+ me.slot()
51
+
52
+
53
+ @me.content_component
54
+ def dialog_actions():
55
+ """Helper component for rendering action buttons so they are right aligned.
56
+
57
+ This component is optional. If you want to position action buttons differently,
58
+ you can just write your own Mesop markup.
59
+ """
60
+ with me.box(
61
+ style=me.Style(display="flex", gap=5, justify_content="end", margin=me.Margin(top=20))
62
+ ):
63
+ me.slot()
components/helpers.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import fields
2
+
3
+ import mesop as me
4
+
5
+
6
+ def merge_styles(default: me.Style, overrides: me.Style | None = None) -> me.Style:
7
+ """Merges two styles together.
8
+
9
+ Args:
10
+ default: The starting style
11
+ overrides: Any set styles will override styles in default
12
+ """
13
+ if not overrides:
14
+ overrides = me.Style()
15
+
16
+ default_fields = {field.name: getattr(default, field.name) for field in fields(me.Style)}
17
+ override_fields = {
18
+ field.name: getattr(overrides, field.name)
19
+ for field in fields(me.Style)
20
+ if getattr(overrides, field.name) is not None
21
+ }
22
+
23
+ return me.Style(**default_fields | override_fields)
components/panel.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+
3
+ import mesop as me
4
+
5
+
6
+ @me.content_component
7
+ def panel(is_open: bool, title: str, on_click_close: Callable | None = None, key: str = ""):
8
+ """Slide-in panel from right side."""
9
+ with me.box(
10
+ style=me.Style(
11
+ display="block" if is_open else "none",
12
+ height="100%",
13
+ overflow_x="auto",
14
+ overflow_y="auto",
15
+ pointer_events="none",
16
+ position="fixed",
17
+ width="100%",
18
+ z_index=1000,
19
+ )
20
+ ):
21
+ with me.box(
22
+ style=me.Style(
23
+ align_items="center",
24
+ display="grid",
25
+ height="calc(100vh - 10px)",
26
+ justify_items="end",
27
+ )
28
+ ):
29
+ with me.box(
30
+ style=me.Style(
31
+ background=me.theme_var("surface-container-low"),
32
+ border_radius=5,
33
+ box_sizing="border-box",
34
+ border=me.Border.all(
35
+ me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid"),
36
+ ),
37
+ margin=me.Margin(top=10, right=5),
38
+ padding=me.Padding.all(20),
39
+ pointer_events="auto",
40
+ height="100%",
41
+ width="30%",
42
+ )
43
+ ):
44
+ with me.box(
45
+ style=me.Style(
46
+ align_items="center",
47
+ display="flex",
48
+ justify_content="space-between",
49
+ margin=me.Margin(bottom=15),
50
+ ),
51
+ ):
52
+ me.text(title, style=me.Style(font_size=16, font_weight="bold"))
53
+ with me.box(key=f"show_{key}", on_click=on_click_close, style=me.Style(cursor="pointer")):
54
+ me.icon("close")
55
+
56
+ me.slot()
components/snackbar.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Literal
2
+
3
+ import mesop as me
4
+
5
+
6
+ @me.component
7
+ def snackbar(
8
+ *,
9
+ is_visible: bool,
10
+ label: str,
11
+ action_label: str | None = None,
12
+ on_click_action: Callable | None = None,
13
+ horizontal_position: Literal["start", "center", "end"] = "center",
14
+ vertical_position: Literal["start", "center", "end"] = "end",
15
+ ):
16
+ """Creates a snackbar.
17
+
18
+ By default the snackbar is rendered at bottom center.
19
+
20
+ The on_click_action should typically close the snackbar as part of its actions. If no
21
+ click event is included, you'll need to manually hide the snackbar.
22
+
23
+ Note that there is one issue with this snackbar example. No actions are possible until
24
+ the snackbar is dismissed or closed. This is due to the fixed box that gets created when
25
+ the snackbar is visible.
26
+
27
+ Args:
28
+ is_visible: Whether the snackbar is currently visible or not.
29
+ label: Message for the snackbar
30
+ action_label: Optional message for the action of the snackbar
31
+ on_click_action: Optional click event when action is triggered.
32
+ horizontal_position: Horizontal position of the snackbar
33
+ vertical_position: Vertical position of the snackbar
34
+ """
35
+ with me.box(
36
+ style=me.Style(
37
+ display="block" if is_visible else "none",
38
+ height="100%",
39
+ overflow_x="auto",
40
+ overflow_y="auto",
41
+ pointer_events="none",
42
+ position="fixed",
43
+ width="100%",
44
+ z_index=1000,
45
+ )
46
+ ):
47
+ with me.box(
48
+ style=me.Style(
49
+ align_items=vertical_position,
50
+ display="flex",
51
+ height="100%",
52
+ justify_content=horizontal_position,
53
+ )
54
+ ):
55
+ with me.box(
56
+ style=me.Style(
57
+ align_items="center",
58
+ background=me.theme_var("on-surface-variant"),
59
+ border_radius=5,
60
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
61
+ display="flex",
62
+ font_size=14,
63
+ justify_content="space-between",
64
+ margin=me.Margin.all(10),
65
+ padding=me.Padding(top=5, bottom=5, right=5, left=15)
66
+ if action_label
67
+ else me.Padding.all(15),
68
+ pointer_events="auto",
69
+ min_width=300,
70
+ max_width=500,
71
+ )
72
+ ):
73
+ me.text(label, style=me.Style(color=me.theme_var("surface-container-lowest")))
74
+ if action_label:
75
+ me.button(
76
+ action_label,
77
+ on_click=on_click_action,
78
+ style=me.Style(color=me.theme_var("primary-container")),
79
+ )
constants.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ DEFAULT_URL = "http://localhost:8080"
2
+ EXAMPLE_PROGRAM = """
3
+ import mesop as me
4
+
5
+ @me.page()
6
+ def app():
7
+ me.text("Hello World")
8
+ """.strip()
9
+ PROMPT_MODE_GENERATE = "Generate"
10
+ PROMPT_MODE_REVISE = "Revise"
handlers.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+
3
+ from state import State
4
+
5
+
6
+ def on_show_component(e: me.ClickEvent):
7
+ """Generic event to show a component."""
8
+ state = me.state(State)
9
+ setattr(state, e.key, True)
10
+
11
+
12
+ def on_hide_component(e: me.ClickEvent):
13
+ """Generic event to hide a component."""
14
+ state = me.state(State)
15
+ setattr(state, e.key, False)
16
+
17
+
18
+ def on_update_input(e: me.InputBlurEvent | me.InputEvent | me.InputEnterEvent):
19
+ """Generic event to update input values."""
20
+ state = me.state(State)
21
+ setattr(state, e.key, e.value)
22
+
23
+
24
+ def on_update_selection(e: me.SelectSelectionChangeEvent):
25
+ """Generic event to update input values."""
26
+ state = me.state(State)
27
+ setattr(state, e.key, e.value)
llm.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+
3
+
4
+ with open("prompt.txt") as f:
5
+ SYSTEM_INSTRUCTION = f.read()
6
+
7
+
8
+ GENERATE_APP_BASE_PROMPT = """
9
+ Your task is to write a Mesop app.
10
+
11
+ Instructions:
12
+ 1. For the @me.page decorator, leave it empty like this `@me.page()`
13
+ 2. Event handler functions cannot use lambdas. You must use functions.
14
+ 3. Event handle functions only pass in the event type. They do not accept extra parameters.
15
+ 4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
16
+ 5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
17
+ 6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
18
+ 7. For buttons, prefer using type="flat", especially if it is the primary button.
19
+ 8. Only output the python code.
20
+
21
+ Here is a description of the app I want you to write:
22
+
23
+ <APP_DESCRIPTION>
24
+
25
+ """.strip()
26
+
27
+ REVISE_APP_BASE_PROMPT = """
28
+ Your task is to modify a Mesop app given the code and a description.
29
+
30
+ Make sure to remember these rules when making modifications:
31
+ 1. For the @me.page decorator, leave it empty like this `@me.page()`
32
+ 2. Event handler functions cannot use lambdas. You must use functions.
33
+ 3. Event handle functions only pass in the event type. They do not accept extra parameters.
34
+ 4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
35
+ 5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
36
+ 6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
37
+ 7. For buttons, prefer using type="flat", especially if it is the primary button.
38
+ 8. Only output the python code.
39
+
40
+ Here is is the code for the app:
41
+
42
+ ```
43
+ <APP_CODE>
44
+ ```
45
+
46
+ Here is a description of the changes I want:
47
+
48
+ <APP_CHANGES>
49
+
50
+ """.strip()
51
+
52
+
53
+ def make_model(api_key: str, model_name: str) -> genai.GenerativeModel:
54
+ genai.configure(api_key=api_key)
55
+
56
+ generation_config = {
57
+ "temperature": 1,
58
+ "top_p": 0.95,
59
+ "top_k": 64,
60
+ "max_output_tokens": 32768,
61
+ }
62
+
63
+ safety_settings = [
64
+ {
65
+ "category": "HARM_CATEGORY_HARASSMENT",
66
+ "threshold": "BLOCK_NONE",
67
+ },
68
+ {
69
+ "category": "HARM_CATEGORY_HATE_SPEECH",
70
+ "threshold": "BLOCK_NONE",
71
+ },
72
+ {
73
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
74
+ "threshold": "BLOCK_NONE",
75
+ },
76
+ {
77
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
78
+ "threshold": "BLOCK_NONE",
79
+ },
80
+ ]
81
+
82
+ return genai.GenerativeModel(
83
+ model_name=model_name,
84
+ system_instruction=SYSTEM_INSTRUCTION,
85
+ safety_settings=safety_settings,
86
+ generation_config=generation_config,
87
+ )
88
+
89
+
90
+ def generate_mesop_app(msg: str, model_name: str, api_key: str) -> str:
91
+ model = make_model(api_key, model_name)
92
+ response = model.generate_content(
93
+ GENERATE_APP_BASE_PROMPT.replace("<APP_DESCRIPTION>", msg), request_options={"timeout": 120}
94
+ )
95
+ return response.text
96
+
97
+
98
+ def adjust_mesop_app(code: str, msg: str, model_name: str, api_key: str) -> str:
99
+ model = make_model(api_key, model_name)
100
+ response = model.generate_content(
101
+ REVISE_APP_BASE_PROMPT.replace("<APP_CODE>", code).replace("<APP_CHANGES>", msg),
102
+ request_options={"timeout": 120},
103
+ )
104
+ return response.text
main.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import time
3
+
4
+ import requests
5
+ import mesop as me
6
+ import mesop.labs as mel
7
+
8
+ import components as mex
9
+ import handlers
10
+ import llm
11
+ from constants import DEFAULT_URL, PROMPT_MODE_REVISE, PROMPT_MODE_GENERATE
12
+ from state import State
13
+ from web_components import code_mirror_editor_component
14
+ from web_components import AsyncAction
15
+ from web_components import async_action_component
16
+
17
+
18
+ @me.page(
19
+ title="Mesop App Maker",
20
+ stylesheets=[
21
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css",
22
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/tomorrow-night-eighties.min.css",
23
+ ],
24
+ security_policy=me.SecurityPolicy(
25
+ allowed_connect_srcs=[
26
+ "https://cdnjs.cloudflare.com",
27
+ "*.fonts.gstatic.com",
28
+ ],
29
+ allowed_script_srcs=[
30
+ "https://cdn.jsdelivr.net",
31
+ "https://cdnjs.cloudflare.com",
32
+ "*.fonts.gstatic.com",
33
+ ],
34
+ ),
35
+ )
36
+ def main():
37
+ state = me.state(State)
38
+
39
+ action = (
40
+ AsyncAction(value=state.async_action_name, duration_seconds=state.async_action_duration)
41
+ if state.async_action_name
42
+ else None
43
+ )
44
+ async_action_component(action=action, on_finished=on_async_action_finished)
45
+
46
+ # Status snackbar
47
+ mex.snackbar(
48
+ label=state.info,
49
+ is_visible=state.show_status_snackbar,
50
+ )
51
+
52
+ # Error dialog
53
+ with mex.dialog(state.show_error_dialog):
54
+ me.text("Failed to upload code", type="headline-6")
55
+ with me.box(
56
+ style=me.Style(max_width=500, max_height=300, overflow_x="scroll", overflow_y="scroll")
57
+ ):
58
+ me.code(
59
+ state.error.replace("\n", " \n"),
60
+ )
61
+ with mex.dialog_actions():
62
+ me.button(
63
+ "Close",
64
+ key="show_error_dialog",
65
+ on_click=handlers.on_hide_component,
66
+ )
67
+
68
+ # Generate code panel
69
+ with mex.panel(
70
+ is_open=state.show_generate_panel,
71
+ title="Generate Code",
72
+ on_click_close=handlers.on_hide_component,
73
+ key="generate_panel",
74
+ ):
75
+ mex.button_toggle(
76
+ [PROMPT_MODE_GENERATE, PROMPT_MODE_REVISE],
77
+ selected=state.prompt_mode,
78
+ on_click=on_click_prompt_mode,
79
+ )
80
+ me.textarea(
81
+ value=state.prompt_placeholder,
82
+ rows=10,
83
+ label="What changes do you want to make?"
84
+ if state.prompt_mode == PROMPT_MODE_REVISE
85
+ else "What do you want to make?",
86
+ key="prompt",
87
+ on_blur=handlers.on_update_input,
88
+ disabled=state.loading,
89
+ style=me.Style(width="100%", margin=me.Margin(top=15)),
90
+ )
91
+ with me.content_button(on_click=on_run_prompt, type="flat", disabled=state.loading):
92
+ me.icon("send")
93
+
94
+ # Prompt history panel
95
+ with mex.panel(
96
+ is_open=state.show_prompt_history_panel,
97
+ title="Prompt History",
98
+ on_click_close=handlers.on_hide_component,
99
+ key="prompt_history_panel",
100
+ ):
101
+ for prompt_history in reversed(state.prompt_history):
102
+ with me.box(
103
+ key=f"prompt-{prompt_history['index']}",
104
+ on_click=on_click_history_prompt,
105
+ style=me.Style(
106
+ background=me.theme_var("surface-container"),
107
+ border=me.Border.all(
108
+ me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid")
109
+ ),
110
+ border_radius=5,
111
+ cursor="pointer",
112
+ margin=me.Margin.symmetric(vertical=10),
113
+ padding=me.Padding.all(10),
114
+ text_overflow="ellipsis",
115
+ ),
116
+ ):
117
+ me.text(prompt_history["mode"], style=me.Style(font_weight="bold", font_size=13))
118
+ me.text(_truncate_text(prompt_history["prompt"]))
119
+
120
+ with me.box(
121
+ style=me.Style(
122
+ display="grid",
123
+ grid_template_columns="1fr 2fr 35fr" if state.menu_open else "1fr 40fr",
124
+ height="100vh",
125
+ )
126
+ ):
127
+ with me.box(
128
+ style=me.Style(
129
+ background=me.theme_var("surface-container"),
130
+ padding=me.Padding.all(10),
131
+ border=me.Border(
132
+ right=me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid"),
133
+ ),
134
+ )
135
+ ):
136
+ mex.toolbar_button(
137
+ icon="menu",
138
+ tooltip="Close menu" if state.menu_open else "Open menu",
139
+ on_click=on_toggle_sidebar_menu,
140
+ )
141
+
142
+ mex.toolbar_button(
143
+ icon="settings",
144
+ tooltip="Settings",
145
+ on_click=on_open_settings,
146
+ )
147
+
148
+ mex.toolbar_button(
149
+ icon="light_mode" if me.theme_brightness() == "dark" else "dark_mode",
150
+ tooltip="Switch to " + ("light mode" if me.theme_brightness() == "dark" else "dark mode"),
151
+ on_click=on_click_theme_brightness,
152
+ )
153
+
154
+ if state.menu_open and state.menu_open_type == "settings":
155
+ with me.box(
156
+ style=me.Style(
157
+ background=me.theme_var("surface-container-low"),
158
+ padding=me.Padding.all(15),
159
+ border=me.Border(
160
+ right=me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid")
161
+ ),
162
+ display="flex",
163
+ flex_direction="column",
164
+ height="100vh",
165
+ )
166
+ ):
167
+ me.text(
168
+ "Settings",
169
+ style=me.Style(font_weight="bold", margin=me.Margin(bottom=10)),
170
+ )
171
+ me.input(
172
+ label="API Key", key="api_key", on_blur=handlers.on_update_input, disabled=state.loading
173
+ )
174
+ me.select(
175
+ label="Model",
176
+ options=[
177
+ me.SelectOption(
178
+ label="gemini-1.5-flash",
179
+ value="gemini-1.5-flash",
180
+ ),
181
+ me.SelectOption(
182
+ label="gemini-1.5-pro",
183
+ value="gemini-1.5-pro",
184
+ ),
185
+ ],
186
+ key="model",
187
+ value=state.model,
188
+ on_selection_change=handlers.on_update_selection,
189
+ disabled=state.loading,
190
+ )
191
+ with me.box():
192
+ me.input(
193
+ value=DEFAULT_URL,
194
+ label="URL",
195
+ key="url",
196
+ on_blur=handlers.on_update_input,
197
+ style=me.Style(width="100%"),
198
+ disabled=state.loading,
199
+ )
200
+
201
+ # Main content
202
+ with me.box(
203
+ style=me.Style(
204
+ background=me.theme_var("surface-container-lowest"),
205
+ display="flex",
206
+ flex_direction="column",
207
+ flex_grow=1,
208
+ height="100%",
209
+ )
210
+ ):
211
+ # Toolbar
212
+ with me.box(
213
+ style=me.Style(
214
+ display="grid",
215
+ grid_template_columns="1fr 1fr",
216
+ grid_template_rows="1fr 20fr",
217
+ height="calc(100vh - 5px)",
218
+ )
219
+ ):
220
+ with me.box(
221
+ style=me.Style(
222
+ grid_column_start=1,
223
+ grid_column_end=3,
224
+ background=me.theme_var("surface-container"),
225
+ padding=me.Padding.all(5),
226
+ border=me.Border(
227
+ bottom=me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid"),
228
+ ),
229
+ )
230
+ ):
231
+ with me.box(style=me.Style(display="flex", flex_direction="row")):
232
+ with me.box(
233
+ style=me.Style(
234
+ flex_grow=1,
235
+ display="flex",
236
+ flex_direction="row",
237
+ )
238
+ ):
239
+ mex.toolbar_button(
240
+ icon="bolt",
241
+ tooltip="Generate code",
242
+ key="show_generate_panel",
243
+ on_click=on_show_generate_panel,
244
+ )
245
+
246
+ if state.prompt_history:
247
+ mex.toolbar_button(
248
+ icon="history",
249
+ tooltip="Prompt history",
250
+ key="show_prompt_history_panel",
251
+ on_click=on_show_prompt_history_panel,
252
+ )
253
+
254
+ with me.box(
255
+ style=me.Style(
256
+ flex_grow=1, display="flex", flex_direction="row", justify_content="end"
257
+ )
258
+ ):
259
+ mex.toolbar_button(
260
+ icon="refresh",
261
+ tooltip="Load URL",
262
+ on_click=on_load_url,
263
+ )
264
+ mex.toolbar_button(
265
+ icon="play_arrow",
266
+ tooltip="Run code",
267
+ on_click=on_run_code,
268
+ )
269
+
270
+ # Code editor pane
271
+ with me.box(
272
+ style=me.Style(
273
+ background=me.theme_var("surface-container-lowest"),
274
+ overflow_x="scroll",
275
+ overflow_y="scroll",
276
+ )
277
+ ):
278
+ code_mirror_editor_component(
279
+ code=state.code_placeholder,
280
+ theme="default" if me.theme_brightness() == "light" else "tomorrow-night-eighties",
281
+ on_editor_blur=on_code_input,
282
+ )
283
+
284
+ # App preview pane
285
+ with me.box():
286
+ me.embed(
287
+ key=str(state.iframe_index),
288
+ src=state.loaded_url or DEFAULT_URL,
289
+ style=me.Style(
290
+ background=me.theme_var("surface-container-lowest"),
291
+ width="100%",
292
+ height="100%",
293
+ border=me.Border.all(me.BorderSide(width=0)),
294
+ ),
295
+ )
296
+
297
+
298
+ def on_toggle_sidebar_menu(e: me.ClickEvent):
299
+ """Toggles sidebar menu expansion."""
300
+ state = me.state(State)
301
+ state.menu_open = not state.menu_open
302
+
303
+
304
+ def on_click_theme_brightness(e: me.ClickEvent):
305
+ """Toggles dark mode."""
306
+ if me.theme_brightness() == "light":
307
+ me.set_theme_mode("dark")
308
+ else:
309
+ me.set_theme_mode("light")
310
+
311
+
312
+ def on_open_settings(e: me.ClickEvent):
313
+ """Shows settings menu."""
314
+ state = me.state(State)
315
+ state.menu_open = True
316
+ state.menu_open_type = "settings"
317
+
318
+
319
+ def on_click_prompt_mode(e: me.ClickEvent):
320
+ """Toggles prompt modes - generate / revision."""
321
+ state = me.state(State)
322
+ state.prompt_mode = (
323
+ PROMPT_MODE_REVISE if state.prompt_mode == PROMPT_MODE_GENERATE else PROMPT_MODE_GENERATE
324
+ )
325
+
326
+
327
+ def on_code_input(e: mel.WebEvent):
328
+ """Captures code input into state on blur."""
329
+ state = me.state(State)
330
+ state.code = e.value["code"]
331
+ state.code_placeholder = e.value["code"]
332
+
333
+
334
+ def on_load_url(e: me.ClickEvent):
335
+ """Loads the Mesop app page into the iframe."""
336
+ state = me.state(State)
337
+ state.code_placeholder = state.code
338
+ yield
339
+ state.loaded_url = state.url + state.url_path
340
+ state.iframe_index += 1
341
+ yield
342
+
343
+
344
+ def on_run_code(e: me.ClickEvent):
345
+ """Tries to upload code to the Mesop app runner."""
346
+ state = me.state(State)
347
+ state.code_placeholder = state.code
348
+ yield
349
+ result = requests.post(
350
+ state.url + "/exec", data={"code": base64.b64encode(state.code.encode("utf-8"))}
351
+ )
352
+ if result.status_code == 200:
353
+ state.url_path = result.content.decode("utf-8")
354
+ yield from on_load_url(e)
355
+ else:
356
+ state.show_error_dialog = True
357
+ state.error = result.content.decode("utf-8")
358
+ yield
359
+
360
+
361
+ def on_run_prompt(e: me.ClickEvent):
362
+ """Generate code from prompt."""
363
+ state = me.state(State)
364
+
365
+ state.prompt_placeholder = state.prompt
366
+ yield
367
+ time.sleep(0.4)
368
+ state.prompt_placeholder = ""
369
+ yield
370
+
371
+ state.loading = True
372
+ yield
373
+
374
+ if state.prompt_mode == PROMPT_MODE_REVISE:
375
+ state.code = llm.adjust_mesop_app(
376
+ state.code, state.prompt, model_name=state.model, api_key=state.api_key
377
+ )
378
+ else:
379
+ state.code = llm.generate_mesop_app(state.prompt, model_name=state.model, api_key=state.api_key)
380
+
381
+ state.code = state.code.strip().removeprefix("```python").removesuffix("```")
382
+ state.code_placeholder = state.code
383
+ state.info = (
384
+ "Your code adjustment has been applied!"
385
+ if state.prompt_mode == PROMPT_MODE_REVISE
386
+ else "Your Mesop app has been generated!"
387
+ )
388
+ state.prompt_history.append(
389
+ dict(
390
+ prompt=state.prompt, code=state.code, index=len(state.prompt_history), mode=state.prompt_mode
391
+ )
392
+ )
393
+
394
+ state.prompt_mode = PROMPT_MODE_REVISE
395
+ state.loading = False
396
+ yield
397
+
398
+ state.show_status_snackbar = True
399
+ state.async_action_name = "hide_status_snackbar"
400
+ yield
401
+
402
+
403
+ def on_show_prompt_history_panel(e: me.ClickEvent):
404
+ """Show prompt history panel"""
405
+ state = me.state(State)
406
+ state.show_prompt_history_panel = True
407
+ state.show_generate_panel = False
408
+
409
+
410
+ def on_show_generate_panel(e: me.ClickEvent):
411
+ """Show generate panel and focus on prompt text area"""
412
+ state = me.state(State)
413
+ state.show_generate_panel = True
414
+ state.show_prompt_history_panel = False
415
+ yield
416
+ me.focus_component(key="prompt")
417
+ yield
418
+
419
+
420
+ def on_click_history_prompt(e: me.ClickEvent):
421
+ """Set previous prompt/code"""
422
+ state = me.state(State)
423
+ index = int(e.key.replace("prompt-", ""))
424
+ prompt_history = state.prompt_history[index]
425
+ state.prompt_placeholder = prompt_history["prompt"]
426
+ state.prompt = state.prompt_placeholder
427
+ state.code_placeholder = prompt_history["code"]
428
+ state.code = state.code_placeholder
429
+ state.prompt_mode = prompt_history["mode"]
430
+ state.show_prompt_history_panel = False
431
+ state.show_generate_panel = True
432
+ yield
433
+ me.focus_component(key="prompt")
434
+ yield
435
+
436
+
437
+ def on_async_action_finished(e: mel.WebEvent):
438
+ state = me.state(State)
439
+ state.async_action_name = ""
440
+ state.info = ""
441
+ state.show_status_snackbar = False
442
+
443
+
444
+ def _truncate_text(text, char_limit=100):
445
+ """Truncates text that is too long."""
446
+ if len(text) <= char_limit:
447
+ return text
448
+ truncated_text = text[:char_limit].rsplit(" ", 1)[0]
449
+ return truncated_text.rstrip(".,!?;:") + "..."
prompt.txt ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ mesop==0.12.2
2
+ requests
3
+ google_generativeai
4
+ gunicorn
ruff.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ line-length = 100
2
+ indent-width = 2
state.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+
3
+ import constants as c
4
+
5
+
6
+ @me.stateclass
7
+ class State:
8
+ # App level
9
+ loading: bool = False
10
+ error: str
11
+ info: str
12
+
13
+ # Settings
14
+ api_key: str
15
+ model: str = "gemini-1.5-flash"
16
+ url: str = c.DEFAULT_URL
17
+
18
+ # Generate prompt panel
19
+ prompt_mode: str = "Generate"
20
+ prompt_placeholder: str
21
+ prompt: str
22
+
23
+ # Prompt history panel
24
+ prompt_history: list[dict] # Format: {"prompt", "code", "index", "mode"}
25
+
26
+ # Code editor
27
+ code_placeholder: str = c.EXAMPLE_PROGRAM
28
+ code: str = c.EXAMPLE_PROGRAM
29
+
30
+ # App preview
31
+ run_result: str
32
+ url_path: str = "/"
33
+ loaded_url: str
34
+ iframe_index: int
35
+
36
+ # Sidebar
37
+ menu_open: bool = True
38
+ menu_open_type: str = "settings"
39
+
40
+ # Sub-screens
41
+ show_error_dialog: bool = False
42
+ show_generate_panel: bool = False
43
+ show_prompt_history_panel: bool = False
44
+ show_status_snackbar: bool = False
45
+
46
+ # Async action
47
+ async_action_name: str
48
+ async_action_duration: int = 3
web_components/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from web_components.code_mirror_editor.code_mirror_editor_component import (
2
+ code_mirror_editor_component as code_mirror_editor_component,
3
+ )
4
+
5
+ from web_components.async_action.async_action_component import (
6
+ async_action_component as async_action_component,
7
+ )
8
+
9
+ from web_components.async_action.async_action_component import (
10
+ AsyncAction as AsyncAction,
11
+ )
web_components/async_action/async_action_component.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ LitElement,
3
+ html,
4
+ } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
5
+
6
+ class AsyncAction extends LitElement {
7
+ static properties = {
8
+ startedEvent: {type: String},
9
+ finishedEvent: {type: String},
10
+ // Storing as string due to https://github.com/google/mesop/issues/730
11
+ // Format: {action: String, duration_seconds: Number}
12
+ action: {type: String},
13
+ isRunning: {type: Boolean},
14
+ };
15
+
16
+ render() {
17
+ return html`<div></div>`;
18
+ }
19
+
20
+ firstUpdated() {
21
+ if (this.action) {
22
+ this.runTimeout(this.action);
23
+ }
24
+ }
25
+
26
+ updated(changedProperties) {
27
+ if (changedProperties.has('action') && this.action) {
28
+ this.runTimeout(this.action);
29
+ }
30
+ }
31
+
32
+ runTimeout(actionJson) {
33
+ const action = JSON.parse(actionJson);
34
+ this.dispatchEvent(
35
+ new MesopEvent(this.startedEvent, {
36
+ action: action,
37
+ }),
38
+ );
39
+ setTimeout(() => {
40
+ this.dispatchEvent(
41
+ new MesopEvent(this.finishedEvent, {
42
+ action: action.value,
43
+ }),
44
+ );
45
+ }, action.duration_seconds * 1000);
46
+ }
47
+ }
48
+
49
+ customElements.define('async-action-component', AsyncAction);
web_components/async_action/async_action_component.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from dataclasses import asdict, dataclass
3
+ from typing import Any, Callable
4
+
5
+ import mesop.labs as mel
6
+
7
+
8
+ @dataclass
9
+ class AsyncAction:
10
+ value: str
11
+ duration_seconds: int
12
+
13
+
14
+ @mel.web_component(path="./async_action_component.js")
15
+ def async_action_component(
16
+ *,
17
+ action: AsyncAction | None = None,
18
+ on_started: Callable[[mel.WebEvent], Any] | None = None,
19
+ on_finished: Callable[[mel.WebEvent], Any] | None = None,
20
+ key: str | None = None,
21
+ ):
22
+ """Creates an invisibe component that will delay state changes asynchronously.
23
+
24
+ Right now this implementation is limited since we basically just pass the key around.
25
+ But ideally we also pass in some kind of value to update when the time out expires.
26
+
27
+ The main benefit of this component is for cases, such as status messages that may
28
+ appear and disappear after some duration. The primary example here is the example
29
+ snackbar widget, which right now blocks the UI when using the sleep yield approach.
30
+
31
+ The other benefit of this component is that it works generically (rather than say
32
+ implementing a custom snackbar widget as a web component).
33
+ """
34
+ events = {
35
+ "startedEvent": on_started,
36
+ "finishedEvent": on_finished,
37
+ }
38
+ return mel.insert_web_component(
39
+ name="async-action-component",
40
+ key=key,
41
+ events={key: value for key, value in events.items() if value is not None},
42
+ properties={"action": json.dumps(asdict(action)) if action else ""},
43
+ )
web_components/code_mirror_editor/code_mirror_editor_component.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ LitElement,
3
+ html,
4
+ } from "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js";
5
+
6
+ import "https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.js";
7
+ import "https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/python/python.js";
8
+
9
+ class CodeMirrorEditorComponent extends LitElement {
10
+ static properties = {
11
+ code: { type: String },
12
+ theme: { type: String },
13
+ editorBlurEvent: { type: String },
14
+ height: { type: String },
15
+ width: { type: String },
16
+ };
17
+
18
+ constructor() {
19
+ super();
20
+ this.width = "100%";
21
+ this.height = "100%";
22
+ this.code = "";
23
+ (this.theme = "default"), (this.editor = null);
24
+ this.editorState = null;
25
+ }
26
+
27
+ createRenderRoot() {
28
+ return this;
29
+ }
30
+
31
+ firstUpdated() {
32
+ this.renderEditor();
33
+ }
34
+
35
+ renderEditor() {
36
+ this.editor = CodeMirror.fromTextArea(this.querySelector("#editor"), {
37
+ mode: "python",
38
+ lineNumbers: true,
39
+ theme: this.theme,
40
+ readOnly: false,
41
+ });
42
+ this.editor.setValue(this.code);
43
+ this.editor.setSize(this.width, this.height);
44
+ this.editor.on("blur", (cm) => {
45
+ if (this.editorBlurEvent) {
46
+ this.code = cm.getValue();
47
+ this.dispatchEvent(
48
+ new MesopEvent(this.editorBlurEvent, {
49
+ code: this.code,
50
+ })
51
+ );
52
+ }
53
+ });
54
+ }
55
+
56
+ updated(changedProperties) {
57
+ if (changedProperties.has("code")) {
58
+ if (this.code !== this.editor.getValue()) {
59
+ this.editor.setValue(this.code);
60
+ }
61
+ }
62
+ if (changedProperties.has("theme")) {
63
+ this.editor.setOption("theme", this.theme);
64
+ }
65
+ }
66
+
67
+ render() {
68
+ return html` <textarea id="editor"></textarea>`;
69
+ }
70
+ }
71
+
72
+ customElements.define(
73
+ "code-mirror-editor-component",
74
+ CodeMirrorEditorComponent
75
+ );
web_components/code_mirror_editor/code_mirror_editor_component.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Callable
2
+
3
+ import mesop.labs as mel
4
+
5
+
6
+ @mel.web_component(path="./code_mirror_editor_component.js")
7
+ def code_mirror_editor_component(
8
+ *,
9
+ code: str = "",
10
+ theme: str = "default",
11
+ on_editor_blur: Callable[[mel.WebEvent], Any] | None = None,
12
+ height: str = "100%",
13
+ width: str = "100%",
14
+ key: str | None = None,
15
+ ):
16
+ events = {}
17
+ if on_editor_blur:
18
+ events["editorBlurEvent"] = on_editor_blur
19
+
20
+ return mel.insert_web_component(
21
+ name="code-mirror-editor-component",
22
+ key=key,
23
+ events=events,
24
+ properties={
25
+ "code": code,
26
+ "theme": theme,
27
+ "height": height,
28
+ "width": width,
29
+ },
30
+ )