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