mesop-app-maker / main.py
Richard
Fix bug with incorrectly place event handler
d45098d
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 (
PROMPT_MODE_REVISE,
PROMPT_MODE_GENERATE,
HELP_TEXT,
TEMPLATES,
EXAMPLE_CHAT_PROMPTS,
)
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_iframe_parents=["https://huggingface.co"],
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,
)
# New file dialog
with mex.dialog(state.show_new_dialog):
me.text("Select a template", type="headline-6")
me.select(
label="Template",
key="template-selector-" + str(state.select_index),
options=[
me.SelectOption(label="Default", value="default.txt"),
me.SelectOption(label="Basic Chat", value="basic_chat.txt"),
me.SelectOption(label="Advanced Chat", value="advanced_chat.txt"),
],
on_selection_change=on_select_template,
)
with mex.dialog_actions():
me.button(
"Close",
key="show_new_dialog",
on_click=handlers.on_hide_component,
)
# Help dialog
with mex.dialog(state.show_help_dialog):
me.text("Usage Instructions", type="headline-6")
me.markdown(HELP_TEXT)
me.link(
text="See Github repository for full instructions.",
url="https://github.com/richard-to/mesop-app-runner",
open_in_new_tab=True,
style=me.Style(color=me.theme_var("primary")),
)
with mex.dialog_actions():
me.button(
"Close",
key="show_help_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.select(
label="App type",
key="prompt_app_type",
value=state.prompt_app_type,
options=[
me.SelectOption(label="General", value="general"),
me.SelectOption(label="Chat", value="chat"),
],
style=me.Style(width="100%", margin=me.Margin(top=30)),
on_selection_change=handlers.on_update_selection,
)
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.tooltip(message="Generate app"):
with me.content_button(on_click=on_run_prompt, type="flat", disabled=state.loading):
me.icon("send")
if state.prompt_mode == "Generate" and state.prompt_app_type == "chat":
me.text("Example prompts", type="headline-6", style=me.Style(margin=me.Margin(top=15)))
for index, chat_prompt in enumerate(EXAMPLE_CHAT_PROMPTS):
with me.box(
key=f"example_prompt-{index}",
on_click=on_click_example_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(_truncate_text(chat_prompt))
# 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(
on_click=on_click_history_prompt,
key=f"prompt-{prompt_history['index']}",
style=me.Style(
background=me.theme_var("surface-container"),
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,
)
mex.toolbar_button(
icon="help",
tooltip="Help",
key="show_help_dialog",
on_click=handlers.on_show_component,
)
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(
type="password",
label="Gemini API Key",
key="api_key",
value=state.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=state.runner_url,
label="Runner URL",
key="runner_url",
on_blur=handlers.on_update_input,
style=me.Style(width="100%"),
disabled=state.loading,
)
with me.box():
me.input(
type="password",
value=state.runner_token,
label="Runner Token",
key="runner_token",
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="add",
tooltip="New file",
key="show_new_dialog",
on_click=handlers.on_show_component,
)
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,
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_click_example_prompt(e: me.ClickEvent):
"""Populates chat box with example prompt."""
state = me.state(State)
_, index = e.key.split("-")
state.prompt = EXAMPLE_CHAT_PROMPTS[int(index)]
state.prompt_placeholder = state.prompt
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.runner_url.removesuffix("/") + state.runner_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.runner_url.removesuffix("/") + "/exec",
data={"token": state.runner_token, "code": base64.b64encode(state.code.encode("utf-8"))},
)
if result.status_code == 200:
state.runner_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)
if not state.prompt:
return
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,
app_type=state.prompt_app_type,
)
else:
state.code = llm.generate_mesop_app(
state.prompt, model_name=state.model, api_key=state.api_key, app_type=state.prompt_app_type
)
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,
app_type=state.prompt_app_type,
)
)
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_select_template(e: me.SelectSelectionChangeEvent):
"""Update editor with selected template"""
state = me.state(State)
state.code_placeholder = TEMPLATES[e.value]
state.code = TEMPLATES[e.value]
state.show_new_dialog = False
state.select_index += 1
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_app_type = prompt_history["app_type"]
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(".,!?;:") + "..."