mesop-app-maker / main.py
Richard
Initial commit
f3d45a9
raw
history blame
13 kB
import base64
import time
import requests
import mesop as me
import mesop.labs as mel
import components as mex
import handlers
import llm
from constants import DEFAULT_URL, PROMPT_MODE_REVISE, PROMPT_MODE_GENERATE
from state import State
from web_components import code_mirror_editor_component
from web_components import AsyncAction
from web_components import async_action_component
@me.page(
title="Mesop App Maker",
stylesheets=[
"https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css",
"https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/tomorrow-night-eighties.min.css",
],
security_policy=me.SecurityPolicy(
allowed_connect_srcs=[
"https://cdnjs.cloudflare.com",
"*.fonts.gstatic.com",
],
allowed_script_srcs=[
"https://cdn.jsdelivr.net",
"https://cdnjs.cloudflare.com",
"*.fonts.gstatic.com",
],
),
)
def main():
state = me.state(State)
action = (
AsyncAction(value=state.async_action_name, duration_seconds=state.async_action_duration)
if state.async_action_name
else None
)
async_action_component(action=action, on_finished=on_async_action_finished)
# Status snackbar
mex.snackbar(
label=state.info,
is_visible=state.show_status_snackbar,
)
# Error dialog
with mex.dialog(state.show_error_dialog):
me.text("Failed to upload code", type="headline-6")
with me.box(
style=me.Style(max_width=500, max_height=300, overflow_x="scroll", overflow_y="scroll")
):
me.code(
state.error.replace("\n", " \n"),
)
with mex.dialog_actions():
me.button(
"Close",
key="show_error_dialog",
on_click=handlers.on_hide_component,
)
# Generate code panel
with mex.panel(
is_open=state.show_generate_panel,
title="Generate Code",
on_click_close=handlers.on_hide_component,
key="generate_panel",
):
mex.button_toggle(
[PROMPT_MODE_GENERATE, PROMPT_MODE_REVISE],
selected=state.prompt_mode,
on_click=on_click_prompt_mode,
)
me.textarea(
value=state.prompt_placeholder,
rows=10,
label="What changes do you want to make?"
if state.prompt_mode == PROMPT_MODE_REVISE
else "What do you want to make?",
key="prompt",
on_blur=handlers.on_update_input,
disabled=state.loading,
style=me.Style(width="100%", margin=me.Margin(top=15)),
)
with me.content_button(on_click=on_run_prompt, type="flat", disabled=state.loading):
me.icon("send")
# Prompt history panel
with mex.panel(
is_open=state.show_prompt_history_panel,
title="Prompt History",
on_click_close=handlers.on_hide_component,
key="prompt_history_panel",
):
for prompt_history in reversed(state.prompt_history):
with me.box(
key=f"prompt-{prompt_history['index']}",
on_click=on_click_history_prompt,
style=me.Style(
background=me.theme_var("surface-container"),
border=me.Border.all(
me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid")
),
border_radius=5,
cursor="pointer",
margin=me.Margin.symmetric(vertical=10),
padding=me.Padding.all(10),
text_overflow="ellipsis",
),
):
me.text(prompt_history["mode"], style=me.Style(font_weight="bold", font_size=13))
me.text(_truncate_text(prompt_history["prompt"]))
with me.box(
style=me.Style(
display="grid",
grid_template_columns="1fr 2fr 35fr" if state.menu_open else "1fr 40fr",
height="100vh",
)
):
with me.box(
style=me.Style(
background=me.theme_var("surface-container"),
padding=me.Padding.all(10),
border=me.Border(
right=me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid"),
),
)
):
mex.toolbar_button(
icon="menu",
tooltip="Close menu" if state.menu_open else "Open menu",
on_click=on_toggle_sidebar_menu,
)
mex.toolbar_button(
icon="settings",
tooltip="Settings",
on_click=on_open_settings,
)
mex.toolbar_button(
icon="light_mode" if me.theme_brightness() == "dark" else "dark_mode",
tooltip="Switch to " + ("light mode" if me.theme_brightness() == "dark" else "dark mode"),
on_click=on_click_theme_brightness,
)
if state.menu_open and state.menu_open_type == "settings":
with me.box(
style=me.Style(
background=me.theme_var("surface-container-low"),
padding=me.Padding.all(15),
border=me.Border(
right=me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid")
),
display="flex",
flex_direction="column",
height="100vh",
)
):
me.text(
"Settings",
style=me.Style(font_weight="bold", margin=me.Margin(bottom=10)),
)
me.input(
label="API Key", key="api_key", on_blur=handlers.on_update_input, disabled=state.loading
)
me.select(
label="Model",
options=[
me.SelectOption(
label="gemini-1.5-flash",
value="gemini-1.5-flash",
),
me.SelectOption(
label="gemini-1.5-pro",
value="gemini-1.5-pro",
),
],
key="model",
value=state.model,
on_selection_change=handlers.on_update_selection,
disabled=state.loading,
)
with me.box():
me.input(
value=DEFAULT_URL,
label="URL",
key="url",
on_blur=handlers.on_update_input,
style=me.Style(width="100%"),
disabled=state.loading,
)
# Main content
with me.box(
style=me.Style(
background=me.theme_var("surface-container-lowest"),
display="flex",
flex_direction="column",
flex_grow=1,
height="100%",
)
):
# Toolbar
with me.box(
style=me.Style(
display="grid",
grid_template_columns="1fr 1fr",
grid_template_rows="1fr 20fr",
height="calc(100vh - 5px)",
)
):
with me.box(
style=me.Style(
grid_column_start=1,
grid_column_end=3,
background=me.theme_var("surface-container"),
padding=me.Padding.all(5),
border=me.Border(
bottom=me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid"),
),
)
):
with me.box(style=me.Style(display="flex", flex_direction="row")):
with me.box(
style=me.Style(
flex_grow=1,
display="flex",
flex_direction="row",
)
):
mex.toolbar_button(
icon="bolt",
tooltip="Generate code",
key="show_generate_panel",
on_click=on_show_generate_panel,
)
if state.prompt_history:
mex.toolbar_button(
icon="history",
tooltip="Prompt history",
key="show_prompt_history_panel",
on_click=on_show_prompt_history_panel,
)
with me.box(
style=me.Style(
flex_grow=1, display="flex", flex_direction="row", justify_content="end"
)
):
mex.toolbar_button(
icon="refresh",
tooltip="Load URL",
on_click=on_load_url,
)
mex.toolbar_button(
icon="play_arrow",
tooltip="Run code",
on_click=on_run_code,
)
# Code editor pane
with me.box(
style=me.Style(
background=me.theme_var("surface-container-lowest"),
overflow_x="scroll",
overflow_y="scroll",
)
):
code_mirror_editor_component(
code=state.code_placeholder,
theme="default" if me.theme_brightness() == "light" else "tomorrow-night-eighties",
on_editor_blur=on_code_input,
)
# App preview pane
with me.box():
me.embed(
key=str(state.iframe_index),
src=state.loaded_url or DEFAULT_URL,
style=me.Style(
background=me.theme_var("surface-container-lowest"),
width="100%",
height="100%",
border=me.Border.all(me.BorderSide(width=0)),
),
)
def on_toggle_sidebar_menu(e: me.ClickEvent):
"""Toggles sidebar menu expansion."""
state = me.state(State)
state.menu_open = not state.menu_open
def on_click_theme_brightness(e: me.ClickEvent):
"""Toggles dark mode."""
if me.theme_brightness() == "light":
me.set_theme_mode("dark")
else:
me.set_theme_mode("light")
def on_open_settings(e: me.ClickEvent):
"""Shows settings menu."""
state = me.state(State)
state.menu_open = True
state.menu_open_type = "settings"
def on_click_prompt_mode(e: me.ClickEvent):
"""Toggles prompt modes - generate / revision."""
state = me.state(State)
state.prompt_mode = (
PROMPT_MODE_REVISE if state.prompt_mode == PROMPT_MODE_GENERATE else PROMPT_MODE_GENERATE
)
def on_code_input(e: mel.WebEvent):
"""Captures code input into state on blur."""
state = me.state(State)
state.code = e.value["code"]
state.code_placeholder = e.value["code"]
def on_load_url(e: me.ClickEvent):
"""Loads the Mesop app page into the iframe."""
state = me.state(State)
state.code_placeholder = state.code
yield
state.loaded_url = state.url + state.url_path
state.iframe_index += 1
yield
def on_run_code(e: me.ClickEvent):
"""Tries to upload code to the Mesop app runner."""
state = me.state(State)
state.code_placeholder = state.code
yield
result = requests.post(
state.url + "/exec", data={"code": base64.b64encode(state.code.encode("utf-8"))}
)
if result.status_code == 200:
state.url_path = result.content.decode("utf-8")
yield from on_load_url(e)
else:
state.show_error_dialog = True
state.error = result.content.decode("utf-8")
yield
def on_run_prompt(e: me.ClickEvent):
"""Generate code from prompt."""
state = me.state(State)
state.prompt_placeholder = state.prompt
yield
time.sleep(0.4)
state.prompt_placeholder = ""
yield
state.loading = True
yield
if state.prompt_mode == PROMPT_MODE_REVISE:
state.code = llm.adjust_mesop_app(
state.code, state.prompt, model_name=state.model, api_key=state.api_key
)
else:
state.code = llm.generate_mesop_app(state.prompt, model_name=state.model, api_key=state.api_key)
state.code = state.code.strip().removeprefix("```python").removesuffix("```")
state.code_placeholder = state.code
state.info = (
"Your code adjustment has been applied!"
if state.prompt_mode == PROMPT_MODE_REVISE
else "Your Mesop app has been generated!"
)
state.prompt_history.append(
dict(
prompt=state.prompt, code=state.code, index=len(state.prompt_history), mode=state.prompt_mode
)
)
state.prompt_mode = PROMPT_MODE_REVISE
state.loading = False
yield
state.show_status_snackbar = True
state.async_action_name = "hide_status_snackbar"
yield
def on_show_prompt_history_panel(e: me.ClickEvent):
"""Show prompt history panel"""
state = me.state(State)
state.show_prompt_history_panel = True
state.show_generate_panel = False
def on_show_generate_panel(e: me.ClickEvent):
"""Show generate panel and focus on prompt text area"""
state = me.state(State)
state.show_generate_panel = True
state.show_prompt_history_panel = False
yield
me.focus_component(key="prompt")
yield
def on_click_history_prompt(e: me.ClickEvent):
"""Set previous prompt/code"""
state = me.state(State)
index = int(e.key.replace("prompt-", ""))
prompt_history = state.prompt_history[index]
state.prompt_placeholder = prompt_history["prompt"]
state.prompt = state.prompt_placeholder
state.code_placeholder = prompt_history["code"]
state.code = state.code_placeholder
state.prompt_mode = prompt_history["mode"]
state.show_prompt_history_panel = False
state.show_generate_panel = True
yield
me.focus_component(key="prompt")
yield
def on_async_action_finished(e: mel.WebEvent):
state = me.state(State)
state.async_action_name = ""
state.info = ""
state.show_status_snackbar = False
def _truncate_text(text, char_limit=100):
"""Truncates text that is too long."""
if len(text) <= char_limit:
return text
truncated_text = text[:char_limit].rsplit(" ", 1)[0]
return truncated_text.rstrip(".,!?;:") + "..."