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