Spaces:
Running
Running
Richard
commited on
Commit
·
f3d45a9
0
Parent(s):
Initial commit
Browse files- .gitignore +9 -0
- Dockerfile +32 -0
- README.md +54 -0
- components/__init__.py +8 -0
- components/button.py +122 -0
- components/card.py +72 -0
- components/dialog.py +63 -0
- components/helpers.py +23 -0
- components/panel.py +56 -0
- components/snackbar.py +79 -0
- constants.py +10 -0
- handlers.py +27 -0
- llm.py +104 -0
- main.py +449 -0
- prompt.txt +0 -0
- requirements.txt +4 -0
- ruff.toml +2 -0
- state.py +48 -0
- web_components/__init__.py +11 -0
- web_components/async_action/async_action_component.js +49 -0
- web_components/async_action/async_action_component.py +43 -0
- web_components/code_mirror_editor/code_mirror_editor_component.js +75 -0
- web_components/code_mirror_editor/code_mirror_editor_component.py +30 -0
.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 |
+
)
|