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(".,!?;:") + "..."