mesop-app-maker / main.py
Richard
Update old Gemini versions + remove mesop version pin
b46ca23
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-flash-latest",
value="gemini-flash-latest",
),
me.SelectOption(
label="gemini-flash-lite-latest",
value="gemini-flash-lite-latest",
),
],
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(".,!?;:") + "..."