diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..7df4d05c13f2387f87f77103de42959d0a8ca5c3
Binary files /dev/null and b/.DS_Store differ
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000000000000000000000000000000000000..d23d5d7f70b8e86d69dfd44b3502eca4357acdb5
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,21 @@
+load("//build_defs:defaults.bzl", "py_library")
+
+package(
+ default_visibility = ["//build_defs:mesop_examples"],
+)
+
+py_library(
+ name = "demo",
+ srcs = glob(["*.py"]),
+ data = [":screenshots"],
+ deps = [
+ "//mesop",
+ "//mesop/labs",
+ ],
+)
+
+# Make source files available for distribution via pkg_npm
+filegroup(
+ name = "screenshots",
+ srcs = glob(["screenshots/*"]),
+)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..8dc8d02ece8f3e90685e76e6642625fe0b0a72c4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,32 @@
+FROM python:3.10.14-bullseye
+
+RUN apt-get update && \
+ apt-get install -y \
+ # General dependencies
+ locales \
+ locales-all && \
+ # Clean local repository of package files since they won't be needed anymore.
+ # Make sure this line is called after all apt-get update/install commands have
+ # run.
+ apt-get clean && \
+ # Also delete the index files which we also don't need anymore.
+ rm -rf /var/lib/apt/lists/*
+
+ENV LC_ALL en_US.UTF-8
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US.UTF-8
+
+# Install dependencies
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+# Create non-root user
+RUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop
+USER mesop
+
+# Add app code here
+COPY . /srv/mesop-app
+WORKDIR /srv/mesop-app
+
+# Run Mesop through gunicorn. Should be available at localhost:8080
+CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:me"]
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000000000000000000000000000000000000..914cbe68740e24ea9af8fdc5b6988076167886b4
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: gunicorn --bind :8080 main:me
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9497553a39daac51cfbbb000bd3597b9cb2adee0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+---
+title: Mesop Demo Gallery
+emoji: 👓
+colorFrom: blue
+colorTo: purple
+sdk: docker
+pinned: false
+license: apache-2.0
+app_port: 8080
+---
+
+# Mesop demo app
+
+This app demonstrates Mesop's various components and features. Create your own Cloud Run app by following: https://google.github.io/mesop/guides/deployment/
+
+## Development
+
+### Setup
+
+From workspace root:
+
+```sh
+rm -rf demo/venv && \
+virtualenv --python python3 demo/venv && \
+source demo/venv/bin/activate && \
+pip install -r demo/requirements.txt
+```
+
+### Run
+
+1. `cd demo`
+1. `mesop main.py`
+
+## Generate screenshots
+
+If you add more demos and want to re-generate screenshots, do the following steps:
+
+1. in `demo/screenshot.ts` change `test.skip` to `test`
+1. Run: `yarn playwright test demo/screenshot.ts`
+1. Install cwebp using `brew install webp` or download from [here](https://developers.google.com/speed/webp/docs/precompiled).
+1. From the workspace root, run:
+
+```sh
+`for file in demo/screenshots/*; do cwebp -q 50 "$file" -o "${file%.*}.webp"; done`
+```
+
+## Deployment
+
+**Pre-requisites:**
+
+- Make sure you [generate screenshots](#generate-screenshots) before deploying!
+- Ensure a recent version of Mesop has been published to pip, otherwise the demos may not work (because they rely on a new API).
+
+### Deploy to Cloud Run
+
+This app is deployed to Google Cloud Run.
+
+```sh
+gcloud run deploy mesop --source .
+```
+
+See our Mesop deployment [docs](https://google.github.io/mesop/guides/deployment/#deploy-to-google-cloud-run) for more background.
+
+### Deploy to Hugging Face Spaces
+
+> NOTE: You need to update demo/requirements.txt to point to the latest Mesop version because Hugging Face Spaces may use a cached version of Mesop which is too old.
+
+Because Hugging Face Spaces has restrictions on not having binary files (e.g. image files), we cannot push the full Mesop Git repo to Hugging Face Spaces. Instead, we copy just the `demo` directory and turn it into a standalone Git repo which we deploy.
+
+```sh
+./demo/deploy_to_hf.sh ../hf_demo
+```
+
+You can change `../hf_demo` to any dir path outside of your Mesop repo.
+
+> Note: if you get an error in Hugging Face Spaces "No app file", then you can create an "app.py" file in the Spaces UI to manually trigger a build. This seems like a bug with Hugging Face.
diff --git a/__pycache__/audio.cpython-310.pyc b/__pycache__/audio.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c064b77a6bf23d98efb129dd6017102d9130fb49
Binary files /dev/null and b/__pycache__/audio.cpython-310.pyc differ
diff --git a/__pycache__/autocomplete.cpython-310.pyc b/__pycache__/autocomplete.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..33303752d306e05a45bcc5b93e1e72ac62ac94a9
Binary files /dev/null and b/__pycache__/autocomplete.cpython-310.pyc differ
diff --git a/__pycache__/badge.cpython-310.pyc b/__pycache__/badge.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..de3a4f48bdcfa5c901ddfe1e2823ff373b681331
Binary files /dev/null and b/__pycache__/badge.cpython-310.pyc differ
diff --git a/__pycache__/basic_animation.cpython-310.pyc b/__pycache__/basic_animation.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2542f97199ad8e9ed4cb144f0812c2601145b5c6
Binary files /dev/null and b/__pycache__/basic_animation.cpython-310.pyc differ
diff --git a/__pycache__/box.cpython-310.pyc b/__pycache__/box.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..df63321370e9dd7e164ae8c4cc234de6dfbe815e
Binary files /dev/null and b/__pycache__/box.cpython-310.pyc differ
diff --git a/__pycache__/button.cpython-310.pyc b/__pycache__/button.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..26eeca81aed28103161ed094881322228622a7d7
Binary files /dev/null and b/__pycache__/button.cpython-310.pyc differ
diff --git a/__pycache__/chat.cpython-310.pyc b/__pycache__/chat.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b47712fa389324cf4dd5cb7bd7b487850ada04f0
Binary files /dev/null and b/__pycache__/chat.cpython-310.pyc differ
diff --git a/__pycache__/chat_inputs.cpython-310.pyc b/__pycache__/chat_inputs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..77b49970d1b4b9431cae965caae7aba6b334acf6
Binary files /dev/null and b/__pycache__/chat_inputs.cpython-310.pyc differ
diff --git a/__pycache__/checkbox.cpython-310.pyc b/__pycache__/checkbox.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..43af57b19bb36eedf3a5fb12c7bd5eaa58e0780c
Binary files /dev/null and b/__pycache__/checkbox.cpython-310.pyc differ
diff --git a/__pycache__/code_demo.cpython-310.pyc b/__pycache__/code_demo.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ac13f7c66e5abeaff3d018e771e63b937fdc79bd
Binary files /dev/null and b/__pycache__/code_demo.cpython-310.pyc differ
diff --git a/__pycache__/density.cpython-310.pyc b/__pycache__/density.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..234896de6ec025ede5a9b7f7284492396f9a168c
Binary files /dev/null and b/__pycache__/density.cpython-310.pyc differ
diff --git a/__pycache__/dialog.cpython-310.pyc b/__pycache__/dialog.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b91f261a59cff6cb83ce736625aaec33f664153b
Binary files /dev/null and b/__pycache__/dialog.cpython-310.pyc differ
diff --git a/__pycache__/divider.cpython-310.pyc b/__pycache__/divider.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c12befd20778e1b5de79977fbafe239c15efd547
Binary files /dev/null and b/__pycache__/divider.cpython-310.pyc differ
diff --git a/__pycache__/embed.cpython-310.pyc b/__pycache__/embed.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..dffaeb0c08c0f0af79543986ba030650b752f2cc
Binary files /dev/null and b/__pycache__/embed.cpython-310.pyc differ
diff --git a/__pycache__/fancy_chat.cpython-310.pyc b/__pycache__/fancy_chat.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a148b6657db6103bbb7231146cfef71bc31f6c2c
Binary files /dev/null and b/__pycache__/fancy_chat.cpython-310.pyc differ
diff --git a/__pycache__/feedback.cpython-310.pyc b/__pycache__/feedback.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2db907ce9ac5750969273b44e73a71ddd2816e5c
Binary files /dev/null and b/__pycache__/feedback.cpython-310.pyc differ
diff --git a/__pycache__/form_billing.cpython-310.pyc b/__pycache__/form_billing.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..53e8e2186f89aa49710579ece5ecbf3413c740df
Binary files /dev/null and b/__pycache__/form_billing.cpython-310.pyc differ
diff --git a/__pycache__/form_profile.cpython-310.pyc b/__pycache__/form_profile.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..39f8484a81ecb1ed4512067de79d80acd7eb615a
Binary files /dev/null and b/__pycache__/form_profile.cpython-310.pyc differ
diff --git a/__pycache__/github_widget.cpython-310.pyc b/__pycache__/github_widget.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b2395d1ac6dd422e1481ca27981e7fb33f09a6de
Binary files /dev/null and b/__pycache__/github_widget.cpython-310.pyc differ
diff --git a/__pycache__/grid_table.cpython-310.pyc b/__pycache__/grid_table.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5734efb63840e0b5cf26531960363c53d214109b
Binary files /dev/null and b/__pycache__/grid_table.cpython-310.pyc differ
diff --git a/__pycache__/headers.cpython-310.pyc b/__pycache__/headers.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..45da34c455ee2c5fd3c7b69e0dc1d82e93a08892
Binary files /dev/null and b/__pycache__/headers.cpython-310.pyc differ
diff --git a/__pycache__/html_demo.cpython-310.pyc b/__pycache__/html_demo.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4e08b1ea8275e496ce9f3b0ea26d69540cf2ccf0
Binary files /dev/null and b/__pycache__/html_demo.cpython-310.pyc differ
diff --git a/__pycache__/icon.cpython-310.pyc b/__pycache__/icon.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b5a91f975105bbcba83e4c8271e713ef1c202d79
Binary files /dev/null and b/__pycache__/icon.cpython-310.pyc differ
diff --git a/__pycache__/image.cpython-310.pyc b/__pycache__/image.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ba79474ea4f01d4a0c0f1d98c384f98080b09939
Binary files /dev/null and b/__pycache__/image.cpython-310.pyc differ
diff --git a/__pycache__/input.cpython-310.pyc b/__pycache__/input.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..80fd6b6cdaf7e8e673e4eb723fc325762d17e1da
Binary files /dev/null and b/__pycache__/input.cpython-310.pyc differ
diff --git a/__pycache__/link.cpython-310.pyc b/__pycache__/link.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ca191e294f85b515bc14f5d21529f1d7cdba3f99
Binary files /dev/null and b/__pycache__/link.cpython-310.pyc differ
diff --git a/__pycache__/llm_playground.cpython-310.pyc b/__pycache__/llm_playground.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b57ec885e4b1d1979ac5b82b90dfcfd6972848cf
Binary files /dev/null and b/__pycache__/llm_playground.cpython-310.pyc differ
diff --git a/__pycache__/llm_rewriter.cpython-310.pyc b/__pycache__/llm_rewriter.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4b53c362be4bfd3e9a5223e58709f1283ef07346
Binary files /dev/null and b/__pycache__/llm_rewriter.cpython-310.pyc differ
diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4ce10055951209669edbf992d6ac6feb2bfe50aa
Binary files /dev/null and b/__pycache__/main.cpython-310.pyc differ
diff --git a/__pycache__/markdown_demo.cpython-310.pyc b/__pycache__/markdown_demo.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..edbd09658b94c12612c1b8a857d56fbfbf9fe5df
Binary files /dev/null and b/__pycache__/markdown_demo.cpython-310.pyc differ
diff --git a/__pycache__/markdown_editor.cpython-310.pyc b/__pycache__/markdown_editor.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..624762fb2e98d0d29c603d0cfe1af24632ae72cd
Binary files /dev/null and b/__pycache__/markdown_editor.cpython-310.pyc differ
diff --git a/__pycache__/plot.cpython-310.pyc b/__pycache__/plot.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..855b02ec675a30f36c57f98e9e335dde7296fe5f
Binary files /dev/null and b/__pycache__/plot.cpython-310.pyc differ
diff --git a/__pycache__/progress_bar.cpython-310.pyc b/__pycache__/progress_bar.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d2f4c3cc6b392ddcfcbc719e947500f56cdaadb6
Binary files /dev/null and b/__pycache__/progress_bar.cpython-310.pyc differ
diff --git a/__pycache__/progress_spinner.cpython-310.pyc b/__pycache__/progress_spinner.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9b1a3ca55d3f879466767fc99dde8beca95a5fb9
Binary files /dev/null and b/__pycache__/progress_spinner.cpython-310.pyc differ
diff --git a/__pycache__/radio.cpython-310.pyc b/__pycache__/radio.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f9e75896a48c58c0117c185035b116433ff7b07a
Binary files /dev/null and b/__pycache__/radio.cpython-310.pyc differ
diff --git a/__pycache__/select_demo.cpython-310.pyc b/__pycache__/select_demo.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6b9aec8b47211bbea34825a80a0bfb70c2f5863
Binary files /dev/null and b/__pycache__/select_demo.cpython-310.pyc differ
diff --git a/__pycache__/sidenav.cpython-310.pyc b/__pycache__/sidenav.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e8264e285d7c6dae4e959ba2c23fa3af40aabae1
Binary files /dev/null and b/__pycache__/sidenav.cpython-310.pyc differ
diff --git a/__pycache__/slide_toggle.cpython-310.pyc b/__pycache__/slide_toggle.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9f12307a9ce16c142c3fe5f3a456b4b37a4aaa75
Binary files /dev/null and b/__pycache__/slide_toggle.cpython-310.pyc differ
diff --git a/__pycache__/slider.cpython-310.pyc b/__pycache__/slider.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0d76fe62aa210700c133edeeb5e7a08c56754263
Binary files /dev/null and b/__pycache__/slider.cpython-310.pyc differ
diff --git a/__pycache__/snackbar.cpython-310.pyc b/__pycache__/snackbar.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f28afac6663b2e53fea419feb3c3d88a867f1ed5
Binary files /dev/null and b/__pycache__/snackbar.cpython-310.pyc differ
diff --git a/__pycache__/table.cpython-310.pyc b/__pycache__/table.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..957f4b960cc069ed44b64a3e1e1a030e059d016b
Binary files /dev/null and b/__pycache__/table.cpython-310.pyc differ
diff --git a/__pycache__/text.cpython-310.pyc b/__pycache__/text.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d55c2b4b6c6483b58a799d929731bc27b32e7e4c
Binary files /dev/null and b/__pycache__/text.cpython-310.pyc differ
diff --git a/__pycache__/text_to_image.cpython-310.pyc b/__pycache__/text_to_image.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e46dfdc168989c24c1dad99e864b7025a2de63b1
Binary files /dev/null and b/__pycache__/text_to_image.cpython-310.pyc differ
diff --git a/__pycache__/text_to_text.cpython-310.pyc b/__pycache__/text_to_text.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3d54a1db953af274787594cca8904545ef0d9d46
Binary files /dev/null and b/__pycache__/text_to_text.cpython-310.pyc differ
diff --git a/__pycache__/textarea.cpython-310.pyc b/__pycache__/textarea.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..73f7dedd3e326274460e6d3f69c0232b9a4e9540
Binary files /dev/null and b/__pycache__/textarea.cpython-310.pyc differ
diff --git a/__pycache__/tooltip.cpython-310.pyc b/__pycache__/tooltip.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..68d5ff4346c204c57d2e0f0b98199268e129797c
Binary files /dev/null and b/__pycache__/tooltip.cpython-310.pyc differ
diff --git a/__pycache__/uploader.cpython-310.pyc b/__pycache__/uploader.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0b34e35eb73f95fa3ef84c92add4a259789f2165
Binary files /dev/null and b/__pycache__/uploader.cpython-310.pyc differ
diff --git a/__pycache__/video.cpython-310.pyc b/__pycache__/video.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2a41cfe18a4d50ec713723ad434dd445b4bbeada
Binary files /dev/null and b/__pycache__/video.cpython-310.pyc differ
diff --git a/audio.py b/audio.py
new file mode 100644
index 0000000000000000000000000000000000000000..777cbbab84e9ca5b948a0c75e78efdd22d75c615
--- /dev/null
+++ b/audio.py
@@ -0,0 +1,27 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/audio",
+)
+def app():
+ """
+ In order to autoplay audio, set the `autoplay` attribute to `True`,
+ Note that there are autoplay restrictions in modern browsers, including Chrome,
+ are designed to prevent audio or video from playing automatically without user interaction.
+ This is intended to improve user experience and reduce unwanted interruptions.
+ You can check the [autoplay ability of your application](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability)
+ """
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.audio(
+ src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ # autoplay=True
+ )
diff --git a/autocomplete.py b/autocomplete.py
new file mode 100644
index 0000000000000000000000000000000000000000..77b0c83f321d78f1df9e7cf1f4e0bb8f740f6f1b
--- /dev/null
+++ b/autocomplete.py
@@ -0,0 +1,120 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ raw_value: str
+ selected_value: str = "California"
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/autocomplete",
+)
+def app():
+ state = me.state(State)
+
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.autocomplete(
+ label="Select state",
+ value=state.selected_value,
+ options=_make_autocomplete_options(),
+ on_selection_change=on_value_change,
+ on_enter=on_value_change,
+ on_input=on_input,
+ )
+
+ if state.selected_value:
+ me.text("Selected: " + state.selected_value)
+
+
+def on_value_change(
+ e: me.AutocompleteEnterEvent | me.AutocompleteSelectionChangeEvent,
+):
+ state = me.state(State)
+ state.selected_value = e.value
+
+
+def on_input(e: me.InputEvent):
+ state = me.state(State)
+ state.raw_value = e.value
+
+
+def _make_autocomplete_options() -> list[me.AutocompleteOptionGroup]:
+ """Creates and filter autocomplete options.
+
+ The states list assumed to be alphabetized and we group by the first letter of the
+ state's name.
+ """
+ states_options_list = []
+ sub_group = None
+ for state in _STATES:
+ if not sub_group or sub_group.label != state[0]:
+ if sub_group:
+ states_options_list.append(sub_group)
+ sub_group = me.AutocompleteOptionGroup(label=state[0], options=[])
+ sub_group.options.append(me.AutocompleteOption(label=state, value=state))
+ if sub_group:
+ states_options_list.append(sub_group)
+ return states_options_list
+
+
+_STATES = [
+ "Alabama",
+ "Alaska",
+ "Arizona",
+ "Arkansas",
+ "California",
+ "Colorado",
+ "Connecticut",
+ "Delaware",
+ "Florida",
+ "Georgia",
+ "Hawaii",
+ "Idaho",
+ "Illinois",
+ "Indiana",
+ "Iowa",
+ "Kansas",
+ "Kentucky",
+ "Louisiana",
+ "Maine",
+ "Maryland",
+ "Massachusetts",
+ "Michigan",
+ "Minnesota",
+ "Mississippi",
+ "Missouri",
+ "Montana",
+ "Nebraska",
+ "Nevada",
+ "New Hampshire",
+ "New Jersey",
+ "New Mexico",
+ "New York",
+ "North Carolina",
+ "North Dakota",
+ "Ohio",
+ "Oklahoma",
+ "Oregon",
+ "Pennsylvania",
+ "Rhode Island",
+ "South Carolina",
+ "South Dakota",
+ "Tennessee",
+ "Texas",
+ "Utah",
+ "Vermont",
+ "Virginia",
+ "Washington",
+ "West Virginia",
+ "Wisconsin",
+ "Wyoming",
+]
diff --git a/badge.py b/badge.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f43780e48065a166105c44ad06b1620ed5de92b
--- /dev/null
+++ b/badge.py
@@ -0,0 +1,25 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/badge",
+)
+def app():
+ with me.box(
+ style=me.Style(
+ display="block",
+ padding=me.Padding(top=16, right=16, bottom=16, left=16),
+ height=50,
+ width=30,
+ )
+ ):
+ with me.badge(content="1", size="medium"):
+ me.text(text="text with badge")
diff --git a/basic_animation.py b/basic_animation.py
new file mode 100644
index 0000000000000000000000000000000000000000..b6d585dcd4e5392fc8df801c9f8bd57fe48ec0df
--- /dev/null
+++ b/basic_animation.py
@@ -0,0 +1,261 @@
+import time
+from dataclasses import field
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ ex1_rgba: list[int] = field(default_factory=lambda: [255, 0, 0, 1])
+ ex2_opacity: float = 1.0
+ ex3_width: int
+ ex4_left: int
+ ex5_rotate_deg: int
+ ex6_transforms_index: int = 0
+
+
+TRANSFORM_OPERATIONS = [
+ "none",
+ "matrix(1, 2, 3, 4, 5, 6)",
+ "translate(120px, 50%)",
+ "scale(2, 0.5)",
+ "rotate(0.5turn)",
+ "skew(30deg, 20deg)",
+ "scale(0.5) translate(-100%, -100%)",
+]
+
+DEFAULT_MARGIN = me.Style(margin=me.Margin.all(30))
+BUTTON_MARGIN = me.Style(margin=me.Margin.symmetric(vertical=15))
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/basic_animation",
+)
+def app():
+ state = me.state(State)
+
+ with me.box(style=DEFAULT_MARGIN):
+ me.text("Transform color", type="headline-5")
+ me.text(
+ "Changing the color can be used to indicate when a field has been updated."
+ )
+ me.button(
+ "Transform",
+ type="flat",
+ on_click=transform_red_yellow,
+ style=BUTTON_MARGIN,
+ )
+ with me.box(
+ style=me.Style(
+ background=f"rgba({','.join(map(str, state.ex1_rgba))})",
+ width=100,
+ height=100,
+ margin=me.Margin.all(10),
+ )
+ ):
+ me.text("Mesop")
+
+ with me.box(style=DEFAULT_MARGIN):
+ me.text("Fade in / Fade out", type="headline-5")
+ me.text("Fading in/out can be useful for flash/toast components.")
+ me.button(
+ "Transform",
+ type="flat",
+ on_click=transform_fade_in_out,
+ style=BUTTON_MARGIN,
+ )
+ with me.box(
+ style=me.Style(
+ background="red",
+ opacity=state.ex2_opacity,
+ width=100,
+ height=100,
+ margin=me.Margin.all(10),
+ )
+ ):
+ me.text("Mesop")
+
+ with me.box(style=DEFAULT_MARGIN):
+ me.text("Resize", type="headline-5")
+ me.text(
+ "Could be used for things like progress bars or opening closing accordion/tabs."
+ )
+ me.button(
+ "Transform", type="flat", on_click=transform_width, style=BUTTON_MARGIN
+ )
+ with me.box(
+ style=me.Style(
+ background="rgba(0,0,0,1)",
+ width=300,
+ height=20,
+ margin=me.Margin.all(10),
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ background="rgba(255, 0, 0, 1)",
+ width=str(state.ex3_width) + "%",
+ height=20,
+ )
+ ):
+ me.text("")
+
+ with me.box(style=DEFAULT_MARGIN):
+ me.text("Move", type="headline-5")
+ me.text("Could be used for opening and closing sidebars.")
+ me.button(
+ "Transform", type="flat", on_click=transform_margin, style=BUTTON_MARGIN
+ )
+ with me.box():
+ with me.box(
+ style=me.Style(
+ position="relative",
+ background="rgba(255, 0, 0, 1)",
+ left=state.ex4_left,
+ width=30,
+ height=30,
+ )
+ ):
+ me.text("")
+
+ with me.box(style=DEFAULT_MARGIN):
+ me.text("Rotate", type="headline-5")
+ me.text("Uses the rotate CSS property to emulate a rotation animation.")
+ me.button(
+ "Transform", type="flat", on_click=transform_rotate, style=BUTTON_MARGIN
+ )
+ with me.box():
+ with me.box(
+ style=me.Style(
+ background="rgba(255, 0, 0, 1)",
+ rotate=f"{state.ex5_rotate_deg}deg",
+ width=100,
+ height=100,
+ )
+ ):
+ me.text("Mesop")
+
+ with me.box(style=DEFAULT_MARGIN):
+ me.text("Transform", type="headline-5")
+ me.text("Apply a sequence of transformations.")
+ me.button(
+ "Transform",
+ type="flat",
+ on_click=transform_transform,
+ style=BUTTON_MARGIN,
+ )
+ with me.box():
+ with me.box(
+ style=me.Style(
+ background="rgba(255, 0, 0, 1)",
+ transform=TRANSFORM_OPERATIONS[state.ex6_transforms_index],
+ width=100,
+ height=100,
+ )
+ ):
+ me.text("Mesop")
+
+
+def transform_red_yellow(e: me.ClickEvent):
+ """Transform the color from red to yellow or yellow to red."""
+ state = me.state(State)
+
+ if state.ex1_rgba[1] == 0:
+ while state.ex1_rgba[1] < 255:
+ state.ex1_rgba[1] += 10
+ yield
+ time.sleep(0.1)
+ state.ex1_rgba[1] = 255
+ yield
+ else:
+ while state.ex1_rgba[1] > 0:
+ state.ex1_rgba[1] -= 10
+ yield
+ time.sleep(0.1)
+ state.ex1_rgba[1] = 0
+ yield
+
+
+def transform_fade_in_out(e: me.ClickEvent):
+ """Update opacity"""
+ state = me.state(State)
+ if state.ex2_opacity == 0:
+ while state.ex2_opacity < 1:
+ state.ex2_opacity += 0.05
+ yield
+ time.sleep(0.1)
+ state.ex2_opacity = 1.0
+ yield
+ else:
+ while state.ex2_opacity > 0:
+ state.ex2_opacity -= 0.05
+ yield
+ time.sleep(0.1)
+ state.ex2_opacity = 0
+ yield
+
+
+def transform_width(e: me.ClickEvent):
+ """Update the width by percentage."""
+ state = me.state(State)
+ if state.ex3_width == 0:
+ while state.ex3_width < 100:
+ state.ex3_width += 5
+ yield
+ time.sleep(0.1)
+ state.ex3_width = 100
+ yield
+ else:
+ while state.ex3_width > 0:
+ state.ex3_width -= 5
+ yield
+ time.sleep(0.1)
+ state.ex3_width = 0
+ yield
+
+
+def transform_margin(e: me.ClickEvent):
+ """Update the position to create sense of movement."""
+ state = me.state(State)
+ if state.ex4_left == 0:
+ while state.ex4_left < 200:
+ state.ex4_left += 5
+ yield
+ state.ex4_left = 200
+ yield
+ else:
+ while state.ex4_left > 0:
+ state.ex4_left -= 5
+ yield
+ state.ex4_left = 0
+ yield
+
+
+def transform_rotate(e: me.ClickEvent):
+ """Update the degrees to rotate."""
+ state = me.state(State)
+ if state.ex5_rotate_deg == 0:
+ while state.ex5_rotate_deg < 365:
+ state.ex5_rotate_deg += 5
+ yield
+ state.ex5_rotate_deg = 0
+ yield
+
+
+def transform_transform(e: me.ClickEvent):
+ """Update the index to run different transform operations."""
+ state = me.state(State)
+ while state.ex6_transforms_index < len(TRANSFORM_OPERATIONS):
+ yield
+ time.sleep(0.2)
+ state.ex6_transforms_index += 1
+ state.ex6_transforms_index = 0
+ yield
diff --git a/box.py b/box.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a10b28b1227e7ce04b5f8c3624f921fd73611de
--- /dev/null
+++ b/box.py
@@ -0,0 +1,66 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/box",
+)
+def app():
+ with me.box(style=me.Style(background="red", padding=me.Padding.all(16))):
+ with me.box(
+ style=me.Style(
+ background="green",
+ height=50,
+ margin=me.Margin.symmetric(vertical=24, horizontal=12),
+ border=me.Border.symmetric(
+ horizontal=me.BorderSide(width=2, color="pink", style="solid"),
+ vertical=me.BorderSide(width=2, color="orange", style="solid"),
+ ),
+ )
+ ):
+ me.text(text="hi1")
+ me.text(text="hi2")
+
+ with me.box(
+ style=me.Style(
+ background="blue",
+ height=50,
+ margin=me.Margin.all(16),
+ border=me.Border.all(
+ me.BorderSide(width=2, color="yellow", style="dotted")
+ ),
+ border_radius=10,
+ )
+ ):
+ me.text(text="Example with all sides bordered")
+
+ with me.box(
+ style=me.Style(
+ background="purple",
+ height=50,
+ margin=me.Margin.symmetric(vertical=24, horizontal=12),
+ border=me.Border.symmetric(
+ vertical=me.BorderSide(width=4, color="white", style="double")
+ ),
+ )
+ ):
+ me.text(text="Example with top and bottom borders")
+
+ with me.box(
+ style=me.Style(
+ background="cyan",
+ height=50,
+ margin=me.Margin.symmetric(vertical=24, horizontal=12),
+ border=me.Border.symmetric(
+ horizontal=me.BorderSide(width=2, color="black", style="groove")
+ ),
+ )
+ ):
+ me.text(text="Example with left and right borders")
diff --git a/button.py b/button.py
new file mode 100644
index 0000000000000000000000000000000000000000..61552a4c66afeeb037a005de2ec0fb3d49c7eaa0
--- /dev/null
+++ b/button.py
@@ -0,0 +1,31 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/button",
+)
+def main():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text("Button types:", style=me.Style(margin=me.Margin(bottom=12)))
+ with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
+ me.button("default")
+ me.button("raised", type="raised")
+ me.button("flat", type="flat")
+ me.button("stroked", type="stroked")
+
+ me.text(
+ "Button colors:", style=me.Style(margin=me.Margin(top=12, bottom=12))
+ )
+ with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
+ me.button("default", type="flat")
+ me.button("primary", color="primary", type="flat")
+ me.button("secondary", color="accent", type="flat")
+ me.button("warn", color="warn", type="flat")
diff --git a/chat.py b/chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..54828a074be870fe491d2008593c88dba70cb003
--- /dev/null
+++ b/chat.py
@@ -0,0 +1,38 @@
+import random
+import time
+
+import mesop as me
+import mesop.labs as mel
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/chat",
+ title="Mesop Demo Chat",
+ on_load=on_load,
+)
+def page():
+ mel.chat(transform, title="Mesop Demo Chat", bot_user="Mesop Bot")
+
+
+def transform(input: str, history: list[mel.ChatMessage]):
+ for line in random.sample(LINES, random.randint(3, len(LINES) - 1)):
+ time.sleep(0.3)
+ yield line + " "
+
+
+LINES = [
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
+]
diff --git a/chat_inputs.py b/chat_inputs.py
new file mode 100644
index 0000000000000000000000000000000000000000..60da9479d67c6ca4fd5dacc8a9db7f19ebe2c7c9
--- /dev/null
+++ b/chat_inputs.py
@@ -0,0 +1,111 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/chat_inputs",
+)
+def app():
+ with me.box(style=CONTENT_STYLE):
+ subtle_chat_input()
+ elevated_chat_input()
+
+ me.textarea(
+ placeholder="Default chat input",
+ style=me.Style(width="100%"),
+ rows=2,
+ )
+
+
+@me.component
+def subtle_chat_input():
+ with me.box(
+ style=me.Style(
+ border_radius=16,
+ padding=me.Padding.all(8),
+ background=BACKGROUND_COLOR,
+ display="flex",
+ width="100%",
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ flex_grow=1,
+ )
+ ):
+ me.native_textarea(
+ autosize=True,
+ min_rows=4,
+ placeholder="Subtle chat input",
+ style=me.Style(
+ padding=me.Padding(top=16, left=16),
+ background=BACKGROUND_COLOR,
+ outline="none",
+ width="100%",
+ overflow_y="auto",
+ border=me.Border.all(
+ me.BorderSide(style="none"),
+ ),
+ ),
+ )
+ with me.content_button(type="icon"):
+ me.icon("upload")
+ with me.content_button(type="icon"):
+ me.icon("photo")
+ with me.content_button(type="icon"):
+ me.icon("send")
+
+
+@me.component
+def elevated_chat_input():
+ with me.box(
+ style=me.Style(
+ padding=me.Padding.all(8),
+ background="white",
+ display="flex",
+ width="100%",
+ border=me.Border.all(
+ me.BorderSide(width=0, style="solid", color="black")
+ ),
+ box_shadow="0 10px 20px #0000000a, 0 2px 6px #0000000a, 0 0 1px #0000000a",
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ flex_grow=1,
+ )
+ ):
+ me.native_textarea(
+ autosize=True,
+ min_rows=4,
+ placeholder="Elevated chat input",
+ style=me.Style(
+ font_family="monospace",
+ padding=me.Padding(top=16, left=16),
+ background="white",
+ outline="none",
+ width="100%",
+ overflow_y="auto",
+ border=me.Border.all(
+ me.BorderSide(style="none"),
+ ),
+ ),
+ )
+ with me.content_button(type="icon"):
+ me.icon("upload")
+ with me.content_button(type="icon"):
+ me.icon("photo")
+ with me.content_button(type="icon"):
+ me.icon("send")
+
+
+BACKGROUND_COLOR = "#e2e8f0"
+
+CONTENT_STYLE = me.Style(padding=me.Padding.all(16), gap=16, display="grid")
diff --git a/checkbox.py b/checkbox.py
new file mode 100644
index 0000000000000000000000000000000000000000..2593608aa3cff3c5f12034e8d27867c4a28d491b
--- /dev/null
+++ b/checkbox.py
@@ -0,0 +1,36 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ checked: bool
+
+
+def on_update(event: me.CheckboxChangeEvent):
+ state = me.state(State)
+ state.checked = event.checked
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/checkbox",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ state = me.state(State)
+ me.checkbox(
+ "Simple checkbox",
+ on_change=on_update,
+ )
+
+ if state.checked:
+ me.text(text="is checked")
+ else:
+ me.text(text="is not checked")
diff --git a/code_demo.py b/code_demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1c30a469352761f4aca1eea9a9b8a1e66a8d31f
--- /dev/null
+++ b/code_demo.py
@@ -0,0 +1,31 @@
+import inspect
+
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/code_demo",
+)
+def code_demo():
+ with me.box(
+ style=me.Style(
+ padding=me.Padding.all(15),
+ background=me.theme_var("surface-container-lowest"),
+ )
+ ):
+ me.text("Defaults to Python")
+ me.code("a = 123")
+
+ me.text("Can set to other languages")
+ me.code("
foo
", language="html")
+
+ me.text("Bigger code block")
+ me.code(inspect.getsource(me))
diff --git a/density.py b/density.py
new file mode 100644
index 0000000000000000000000000000000000000000..80a4d1fc15bf84450d7e93909be7c55a2dbc840b
--- /dev/null
+++ b/density.py
@@ -0,0 +1,46 @@
+import mesop as me
+
+
+def select_density(e: me.SelectSelectionChangeEvent):
+ me.set_theme_density(int(e.value)) # type: ignore
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/density",
+)
+def main():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.select(
+ label="Density",
+ options=[
+ me.SelectOption(label="0 (least dense)", value="0"),
+ me.SelectOption(label="-1", value="-1"),
+ me.SelectOption(label="-2", value="-2"),
+ me.SelectOption(label="-3", value="-3"),
+ me.SelectOption(label="-4 (most dense)", value="-4"),
+ ],
+ on_selection_change=select_density,
+ )
+ me.text("Button types:", style=me.Style(margin=me.Margin(bottom=12)))
+ with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
+ me.button("default")
+ me.button("raised", type="raised")
+ me.button("flat", type="flat")
+ me.button("stroked", type="stroked")
+
+ me.text(
+ "Button colors:", style=me.Style(margin=me.Margin(top=12, bottom=12))
+ )
+ with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
+ me.button("default", type="flat")
+ me.button("primary", color="primary", type="flat")
+ me.button("secondary", color="accent", type="flat")
+ me.button("warn", color="warn", type="flat")
diff --git a/deploy_to_hf.sh b/deploy_to_hf.sh
new file mode 100755
index 0000000000000000000000000000000000000000..6881c4847d429f9addefd7e60600df29b7894e4e
--- /dev/null
+++ b/deploy_to_hf.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+
+set -e
+
+error_handler() {
+ echo "Error: An error occurred. Exiting script."
+ exit 1
+}
+
+# Set up error handling
+trap error_handler ERR
+
+if [ $# -eq 0 ]; then
+ echo "Error: Please provide a destination path as an argument."
+ exit 1
+fi
+
+DEST_PATH="$1"
+
+if [ ! -d "$DEST_PATH" ]; then
+ echo "Destination path does not exist. Creating it now."
+ mkdir -p "$DEST_PATH"
+fi
+
+# Get the path of this script which is the demo dir.
+DEMO_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+cp -R "$DEMO_DIR/" "$DEST_PATH"
+echo "Demo files have been copied to $DEST_PATH"
+
+cd "$DEST_PATH"
+echo "Changed directory to $DEST_PATH"
+
+echo "Updating allowed iframe parents to include hugging face spaces site..."
+# Find all .py files and update the allowed_iframe_parents list
+find . -name "*.py" -type f | while read -r file; do
+ # Use sed with -i.bak so it woroks on MacOs
+ sed -i.bak 's/allowed_iframe_parents=\["https:\/\/google\.github\.io"\]/allowed_iframe_parents=["https:\/\/google.github.io", "https:\/\/huggingface.co"]/' "$file"
+ # Remove the backup file created by sed
+ rm "${file}.bak"
+done
+echo "Update complete."
+
+git init
+
+git add .
+
+git commit -m "Commit"
+
+# The hf remote may already exist if the script has been run
+# on this dest directory before.
+git remote add hf https://huggingface.co/spaces/wwwillchen/mesop || true
+
+git push hf --force
+
+echo "Pushed to: https://huggingface.co/spaces/wwwillchen/mesop. Check the logs to see that it's deployed correctly."
diff --git a/dialog.py b/dialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..b52fa1928d1fcb16c1d70220152f0c4d2d605dfb
--- /dev/null
+++ b/dialog.py
@@ -0,0 +1,120 @@
+"""Simple dialog that looks similar to Angular Component Dialog."""
+
+from typing import Callable
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ is_open: bool = False
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/dialog",
+)
+def app():
+ state = me.state(State)
+
+ with dialog(
+ is_open=state.is_open,
+ on_click_background=on_click_close_background,
+ ):
+ me.text("Delete File", type="headline-5")
+ with me.box():
+ me.text(text="Would you like to delete cat.jpeg?")
+ with dialog_actions():
+ me.button("No", on_click=on_click_close_dialog)
+ me.button("Yes", on_click=on_click_close_dialog)
+
+ with me.box(style=me.Style(padding=me.Padding.all(30))):
+ me.button(
+ "Open Dialog", type="flat", color="primary", on_click=on_click_dialog_open
+ )
+
+
+def on_click_close_background(e: me.ClickEvent):
+ state = me.state(State)
+ if e.is_target:
+ state.is_open = False
+
+
+def on_click_close_dialog(e: me.ClickEvent):
+ state = me.state(State)
+ state.is_open = False
+
+
+def on_click_dialog_open(e: me.ClickEvent):
+ state = me.state(State)
+ state.is_open = True
+
+
+@me.content_component
+def dialog(*, is_open: bool, on_click_background: Callable | None = None):
+ """Renders a dialog component.
+
+ The design of the dialog borrows from the Angular component dialog. So basically
+ rounded corners and some box shadow.
+
+ Args:
+ is_open: Whether the dialog is visible or not.
+ on_click_background: Event handler for when background is clicked
+ """
+ with me.box(
+ style=me.Style(
+ background="rgba(0, 0, 0, 0.4)"
+ if me.theme_brightness() == "light"
+ else "rgba(255, 255, 255, 0.4)",
+ display="block" if is_open else "none",
+ height="100%",
+ overflow_x="auto",
+ overflow_y="auto",
+ position="fixed",
+ width="100%",
+ z_index=1000,
+ ),
+ ):
+ with me.box(
+ on_click=on_click_background,
+ style=me.Style(
+ place_items="center",
+ display="grid",
+ height="100vh",
+ ),
+ ):
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-lowest"),
+ border_radius=20,
+ box_sizing="content-box",
+ box_shadow=(
+ "0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"
+ ),
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
+ padding=me.Padding.all(20),
+ )
+ ):
+ me.slot()
+
+
+@me.content_component
+def dialog_actions():
+ """Helper component for rendering action buttons so they are right aligned.
+
+ This component is optional. If you want to position action buttons differently,
+ you can just write your own Mesop markup.
+ """
+ with me.box(
+ style=me.Style(
+ display="flex", justify_content="end", gap=5, margin=me.Margin(top=20)
+ )
+ ):
+ me.slot()
diff --git a/divider.py b/divider.py
new file mode 100644
index 0000000000000000000000000000000000000000..33150d132e8a857b2c04fc052dfa9855e70f9adc
--- /dev/null
+++ b/divider.py
@@ -0,0 +1,19 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/divider",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text(text="before")
+ me.divider()
+ me.text(text="after")
diff --git a/embed.py b/embed.py
new file mode 100644
index 0000000000000000000000000000000000000000..593c665547751fd8a73623e4f36bb9e76d3adff0
--- /dev/null
+++ b/embed.py
@@ -0,0 +1,21 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/embed",
+)
+def app():
+ src = "https://google.github.io/mesop/"
+ me.text("Embedding: " + src, style=me.Style(padding=me.Padding.all(15)))
+ me.embed(
+ src=src,
+ style=me.Style(width="100%", height="100%"),
+ )
diff --git a/fancy_chat.py b/fancy_chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac2e3c998ca0f7973ccf64902efde8aec633a367
--- /dev/null
+++ b/fancy_chat.py
@@ -0,0 +1,616 @@
+import random
+import time
+from dataclasses import asdict, dataclass
+from typing import Callable, Literal
+
+import mesop as me
+
+Role = Literal["user", "bot"]
+
+
+_APP_TITLE = "Fancy Mesop Chat"
+_BOT_AVATAR_LETTER = "M"
+_EMPTY_CHAT_MESSAGE = "Get started with an example"
+_EXAMPLE_USER_QUERIES = (
+ "What is Mesop?",
+ "Make me a chat app.",
+ "How do I make a web component?",
+)
+_CHAT_MAX_WIDTH = "800px"
+_MOBILE_BREAKPOINT = 640
+
+
+@dataclass(kw_only=True)
+class ChatMessage:
+ """Chat message metadata."""
+
+ role: Role = "user"
+ content: str = ""
+ edited: bool = False
+ # 1 is positive
+ # -1 is negative
+ # 0 is no rating
+ rating: int = 0
+
+
+@me.stateclass
+class State:
+ input: str
+ output: list[ChatMessage]
+ in_progress: bool
+ sidebar_expanded: bool = False
+ # Need to use dict instead of ChatMessage due to serialization bug.
+ # See: https://github.com/google/mesop/issues/659
+ history: list[list[dict]]
+
+
+def respond_to_chat(input: str, history: list[ChatMessage]):
+ """Displays random canned text.
+
+ Edit this function to process messages with a real chatbot/LLM.
+ """
+ lines = [
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
+ ]
+
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
+ time.sleep(0.3)
+ yield line + " "
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ title="Fancy Mesop Demo Chat",
+ path="/fancy_chat",
+ on_load=on_load,
+)
+def page():
+ state = me.state(State)
+
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-lowest"),
+ display="flex",
+ flex_direction="column",
+ height="100%",
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ display="flex", flex_direction="row", flex_grow=1, overflow="hidden"
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-low"),
+ display="flex",
+ flex_direction="column",
+ flex_shrink=0,
+ position="absolute"
+ if state.sidebar_expanded and _is_mobile()
+ else None,
+ height="100%" if state.sidebar_expanded and _is_mobile() else None,
+ width=300 if state.sidebar_expanded else None,
+ z_index=2000,
+ )
+ ):
+ sidebar()
+
+ with me.box(
+ style=me.Style(
+ display="flex",
+ flex_direction="column",
+ flex_grow=1,
+ padding=me.Padding(left=60)
+ if state.sidebar_expanded and _is_mobile()
+ else None,
+ )
+ ):
+ header()
+ with me.box(style=me.Style(flex_grow=1, overflow_y="scroll")):
+ if state.output:
+ chat_pane()
+ else:
+ examples_pane()
+ chat_input()
+
+
+def sidebar():
+ state = me.state(State)
+ with me.box(
+ style=me.Style(
+ display="flex",
+ flex_direction="column",
+ flex_grow=1,
+ )
+ ):
+ with me.box(style=me.Style(display="flex", gap=20)):
+ menu_icon(icon="menu", tooltip="Menu", on_click=on_click_menu_icon)
+ if state.sidebar_expanded:
+ me.text(
+ _APP_TITLE,
+ style=me.Style(margin=me.Margin(bottom=0, top=14)),
+ type="headline-6",
+ )
+
+ if state.sidebar_expanded:
+ menu_item(icon="add", label="New chat", on_click=on_click_new_chat)
+ else:
+ menu_icon(icon="add", tooltip="New chat", on_click=on_click_new_chat)
+
+ if state.sidebar_expanded:
+ history_pane()
+
+
+def history_pane():
+ state = me.state(State)
+ for index, chat in enumerate(state.history):
+ with me.box(
+ key=f"chat-{index}",
+ on_click=on_click_history,
+ 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(horizontal=10, vertical=10),
+ padding=me.Padding.all(10),
+ text_overflow="ellipsis",
+ ),
+ ):
+ me.text(_truncate_text(chat[0]["content"]))
+
+
+def header():
+ state = me.state(State)
+ with me.box(
+ style=me.Style(
+ align_items="center",
+ background=me.theme_var("surface-container-lowest"),
+ display="flex",
+ gap=5,
+ justify_content="space-between",
+ padding=me.Padding.symmetric(horizontal=20, vertical=10),
+ )
+ ):
+ with me.box(style=me.Style(display="flex", gap=5)):
+ if not state.sidebar_expanded:
+ me.text(
+ _APP_TITLE,
+ style=me.Style(margin=me.Margin(bottom=0)),
+ type="headline-6",
+ )
+
+ with me.box(style=me.Style(display="flex", gap=5)):
+ icon_button(
+ key="",
+ icon="dark_mode" if me.theme_brightness() == "light" else "light_mode",
+ tooltip="Dark mode"
+ if me.theme_brightness() == "light"
+ else "Light mode",
+ on_click=on_click_theme_brightness,
+ )
+
+
+def examples_pane():
+ with me.box(
+ style=me.Style(
+ margin=me.Margin.symmetric(horizontal="auto"),
+ padding=me.Padding.all(15),
+ width=f"min({_CHAT_MAX_WIDTH}, 100%)",
+ )
+ ):
+ with me.box(style=me.Style(margin=me.Margin(top=25), font_size=24)):
+ me.text(_EMPTY_CHAT_MESSAGE)
+
+ with me.box(
+ style=me.Style(
+ display="flex",
+ flex_direction="column" if _is_mobile() else "row",
+ gap=20,
+ margin=me.Margin(top=25),
+ )
+ ):
+ for index, query in enumerate(_EXAMPLE_USER_QUERIES):
+ with me.box(
+ key=f"query-{index}",
+ on_click=on_click_example_user_query,
+ style=me.Style(
+ background=me.theme_var("surface-container-highest"),
+ border_radius=15,
+ padding=me.Padding.all(20),
+ cursor="pointer",
+ ),
+ ):
+ me.text(query)
+
+
+def chat_pane():
+ state = me.state(State)
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-lowest"),
+ color=me.theme_var("on-surface"),
+ display="flex",
+ flex_direction="column",
+ margin=me.Margin.symmetric(horizontal="auto"),
+ padding=me.Padding.all(15),
+ width=f"min({_CHAT_MAX_WIDTH}, 100%)",
+ )
+ ):
+ for index, msg in enumerate(state.output):
+ if msg.role == "user":
+ user_message(message=msg)
+ else:
+ bot_message(message_index=index, message=msg)
+
+ if state.in_progress:
+ with me.box(key="scroll-to", style=me.Style(height=250)):
+ pass
+
+
+def user_message(*, message: ChatMessage):
+ with me.box(
+ style=me.Style(
+ display="flex",
+ gap=15,
+ justify_content="end",
+ margin=me.Margin.all(20),
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-low"),
+ border_radius=10,
+ color=me.theme_var("on-surface-variant"),
+ padding=me.Padding.symmetric(vertical=0, horizontal=10),
+ width="66%",
+ )
+ ):
+ me.markdown(message.content)
+
+
+def bot_message(*, message_index: int, message: ChatMessage):
+ with me.box(style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))):
+ text_avatar(
+ background=me.theme_var("primary"),
+ color=me.theme_var("on-primary"),
+ label=_BOT_AVATAR_LETTER,
+ )
+
+ # Bot message response
+ with me.box(style=me.Style(display="flex", flex_direction="column")):
+ me.markdown(
+ message.content,
+ style=me.Style(color=me.theme_var("on-surface")),
+ )
+
+ # Actions panel
+ with me.box():
+ icon_button(
+ key=f"thumb_up-{message_index}",
+ icon="thumb_up",
+ is_selected=message.rating == 1,
+ tooltip="Good response",
+ on_click=on_click_thumb_up,
+ )
+ icon_button(
+ key=f"thumb_down-{message_index}",
+ icon="thumb_down",
+ is_selected=message.rating == -1,
+ tooltip="Bad response",
+ on_click=on_click_thumb_down,
+ )
+ icon_button(
+ key=f"restart-{message_index}",
+ icon="restart_alt",
+ tooltip="Regenerate answer",
+ on_click=on_click_regenerate,
+ )
+
+
+def chat_input():
+ state = me.state(State)
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container")
+ if _is_mobile()
+ else me.theme_var("surface-container"),
+ border_radius=16,
+ display="flex",
+ margin=me.Margin.symmetric(horizontal="auto", vertical=15),
+ padding=me.Padding.all(8),
+ width=f"min({_CHAT_MAX_WIDTH}, 90%)",
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ flex_grow=1,
+ )
+ ):
+ me.native_textarea(
+ autosize=True,
+ key="chat_input",
+ min_rows=4,
+ on_blur=on_chat_input,
+ shortcuts={
+ me.Shortcut(shift=True, key="Enter"): on_submit_chat_msg,
+ },
+ placeholder="Enter your prompt",
+ style=me.Style(
+ background=me.theme_var("surface-container")
+ if _is_mobile()
+ else me.theme_var("surface-container"),
+ border=me.Border.all(
+ me.BorderSide(style="none"),
+ ),
+ color=me.theme_var("on-surface-variant"),
+ outline="none",
+ overflow_y="auto",
+ padding=me.Padding(top=16, left=16),
+ width="100%",
+ ),
+ value=state.input,
+ )
+ with me.content_button(
+ disabled=state.in_progress,
+ on_click=on_click_submit_chat_msg,
+ type="icon",
+ ):
+ me.icon("send")
+
+
+@me.component
+def text_avatar(*, label: str, background: str, color: str):
+ me.text(
+ label,
+ style=me.Style(
+ background=background,
+ border_radius="50%",
+ color=color,
+ font_size=20,
+ height=40,
+ line_height="1",
+ margin=me.Margin(top=16),
+ padding=me.Padding(top=10),
+ text_align="center",
+ width="40px",
+ ),
+ )
+
+
+@me.component
+def icon_button(
+ *,
+ icon: str,
+ tooltip: str,
+ key: str = "",
+ is_selected: bool = False,
+ on_click: Callable | None = None,
+):
+ selected_style = me.Style(
+ background=me.theme_var("surface-container-low"),
+ color=me.theme_var("on-surface-variant"),
+ )
+ with me.tooltip(message=tooltip):
+ with me.content_button(
+ type="icon",
+ key=key,
+ on_click=on_click,
+ style=selected_style if is_selected else None,
+ ):
+ me.icon(icon)
+
+
+@me.component
+def menu_icon(
+ *, icon: str, tooltip: str, key: str = "", on_click: Callable | None = None
+):
+ with me.tooltip(message=tooltip):
+ with me.content_button(
+ key=key,
+ on_click=on_click,
+ style=me.Style(margin=me.Margin.all(10)),
+ type="icon",
+ ):
+ me.icon(icon)
+
+
+@me.component
+def menu_item(
+ *, icon: str, label: str, key: str = "", on_click: Callable | None = None
+):
+ with me.box(on_click=on_click):
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-high"),
+ border_radius=20,
+ cursor="pointer",
+ display="inline-flex",
+ gap=10,
+ line_height=1,
+ margin=me.Margin.all(10),
+ padding=me.Padding(top=10, left=10, right=20, bottom=10),
+ ),
+ ):
+ me.icon(icon)
+ me.text(label, style=me.Style(height=24, line_height="24px"))
+
+
+# Event Handlers
+
+
+def on_click_example_user_query(e: me.ClickEvent):
+ """Populates the user input with the example query"""
+ state = me.state(State)
+ _, example_index = e.key.split("-")
+ state.input = _EXAMPLE_USER_QUERIES[int(example_index)]
+ me.focus_component(key="chat_input")
+
+
+def on_click_thumb_up(e: me.ClickEvent):
+ """Gives the message a positive rating"""
+ state = me.state(State)
+ _, msg_index = e.key.split("-")
+ msg_index = int(msg_index)
+ state.output[msg_index].rating = 1
+
+
+def on_click_thumb_down(e: me.ClickEvent):
+ """Gives the message a negative rating"""
+ state = me.state(State)
+ _, msg_index = e.key.split("-")
+ msg_index = int(msg_index)
+ state.output[msg_index].rating = -1
+
+
+def on_click_new_chat(e: me.ClickEvent):
+ """Resets messages."""
+ state = me.state(State)
+ if state.output:
+ state.history.insert(0, [asdict(messages) for messages in state.output])
+ state.output = []
+ me.focus_component(key="chat_input")
+
+
+def on_click_history(e: me.ClickEvent):
+ """Loads existing chat from history and saves current chat"""
+ state = me.state(State)
+ _, chat_index = e.key.split("-")
+ chat_messages = [
+ ChatMessage(**chat) for chat in state.history.pop(int(chat_index))
+ ]
+ if state.output:
+ state.history.insert(0, [asdict(messages) for messages in state.output])
+ state.output = chat_messages
+ me.focus_component(key="chat_input")
+
+
+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_click_menu_icon(e: me.ClickEvent):
+ """Expands and collapses sidebar menu."""
+ state = me.state(State)
+ state.sidebar_expanded = not state.sidebar_expanded
+
+
+def on_chat_input(e: me.InputBlurEvent):
+ """Capture chat text input on blur."""
+ state = me.state(State)
+ state.input = e.value
+
+
+def on_click_regenerate(e: me.ClickEvent):
+ """Regenerates response from an existing message"""
+ state = me.state(State)
+ _, msg_index = e.key.split("-")
+ msg_index = int(msg_index)
+
+ # Get the user message which is the previous message
+ user_message = state.output[msg_index - 1]
+ # Get bot message to be regenerated
+ assistant_message = state.output[msg_index]
+ assistant_message.content = ""
+ state.in_progress = True
+ yield
+
+ start_time = time.time()
+ # Send in the old user input and chat history to get the bot response.
+ # We make sure to only pass in the chat history up to this message.
+ output_message = respond_to_chat(
+ user_message.content, state.output[:msg_index]
+ )
+ for content in output_message:
+ assistant_message.content += content
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
+ if (time.time() - start_time) >= 0.25:
+ start_time = time.time()
+ yield
+
+ state.in_progress = False
+ me.focus_component(key="chat_input")
+ yield
+
+
+def on_submit_chat_msg(e: me.TextareaShortcutEvent):
+ state = me.state(State)
+ state.input = e.value
+ yield
+ yield from _submit_chat_msg()
+
+
+def on_click_submit_chat_msg(e: me.ClickEvent):
+ yield from _submit_chat_msg()
+
+
+def _submit_chat_msg():
+ """Handles submitting a chat message."""
+ state = me.state(State)
+ if state.in_progress or not state.input:
+ return
+ input = state.input
+ # Clear the text input.
+ state.input = ""
+ yield
+
+ output = state.output
+ if output is None:
+ output = []
+ output.append(ChatMessage(role="user", content=input))
+ state.in_progress = True
+ me.scroll_into_view(key="scroll-to")
+ yield
+
+ start_time = time.time()
+ # Send user input and chat history to get the bot response.
+ output_message = respond_to_chat(input, state.output)
+ assistant_message = ChatMessage(role="bot")
+ output.append(assistant_message)
+ state.output = output
+ for content in output_message:
+ assistant_message.content += content
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
+ if (time.time() - start_time) >= 0.25:
+ start_time = time.time()
+ yield
+
+ state.in_progress = False
+ me.focus_component(key="chat_input")
+ yield
+
+
+# Helpers
+
+
+def _is_mobile():
+ return me.viewport_size().width < _MOBILE_BREAKPOINT
+
+
+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(".,!?;:") + "..."
diff --git a/feedback.py b/feedback.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff6c82c6bc402929af213e9f44266476f4572287
--- /dev/null
+++ b/feedback.py
@@ -0,0 +1,54 @@
+import mesop as me
+
+
+@me.stateclass
+class FeedbackState:
+ feedback: str = ""
+ reason: str = ""
+ ask_reason: bool = False
+
+
+def on_feedback(isup: bool):
+ state = me.state(FeedbackState)
+ state.feedback = "Thumbs up!" if isup else "Thumbs down!"
+ state.ask_reason = not isup
+
+
+def on_reason_input(e: me.InputEvent):
+ state = me.state(FeedbackState)
+ state.reason = e.value
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/feedback",
+)
+def feedback_page():
+ state = me.state(FeedbackState)
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text("Provide your feedback:", type="headline-5")
+
+ with me.box(style=me.Style(display="flex", flex_direction="row", gap=20)):
+ with me.content_button(type="icon", on_click=lambda _: on_feedback(True)):
+ me.icon("thumb_up")
+ with me.content_button(
+ type="icon", on_click=lambda _: on_feedback(False)
+ ):
+ me.icon("thumb_down")
+
+ if state.ask_reason:
+ with me.box(style=me.Style(margin=me.Margin(top=15))):
+ me.textarea(label="Tell us why", on_input=on_reason_input)
+
+ if state.feedback:
+ with me.box(style=me.Style(margin=me.Margin(top=15))):
+ me.text(f"\n\nFeedback : {state.feedback}")
+ if state.reason:
+ me.text(f"Reason : {state.reason}")
diff --git a/form_billing.py b/form_billing.py
new file mode 100644
index 0000000000000000000000000000000000000000..c837e67bd958d851eee8e66a868dc8c89a19eb54
--- /dev/null
+++ b/form_billing.py
@@ -0,0 +1,225 @@
+from dataclasses import asdict
+from typing import Literal
+
+import mesop as me
+
+ROW_GAP = 15
+BOX_PADDING = 20
+
+
+@me.stateclass
+class State:
+ first_name: str
+ last_name: str
+ username: str
+ email: str
+ address: str
+ address_2: str
+ country: str
+ state: str
+ zip: str
+ payment_type: str
+ name_on_card: str
+ credit_card: str
+ expiration: str
+ cvv: str
+ errors: dict[str, str]
+
+
+def is_mobile():
+ return me.viewport_size().width < 620
+
+
+def calc_input_size(items: int):
+ return int(
+ (me.viewport_size().width - (ROW_GAP * items) - (BOX_PADDING * 2)) / items
+ )
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_density(-3)
+ me.set_theme_mode("system")
+
+
+@me.page(
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/form_billing",
+ on_load=load,
+)
+def page():
+ state = me.state(State)
+
+ with me.box(
+ style=me.Style(
+ padding=me.Padding.all(BOX_PADDING),
+ max_width=800,
+ margin=me.Margin.symmetric(horizontal="auto"),
+ )
+ ):
+ me.text(
+ "Billing form",
+ type="headline-4",
+ style=me.Style(margin=me.Margin(bottom=10)),
+ )
+
+ with form_group():
+ name_width = calc_input_size(2) if is_mobile() else "100%"
+ input_field(label="First name", width=name_width)
+ input_field(label="Last name", width=name_width)
+
+ with form_group():
+ input_field(label="Username")
+
+ with me.box(style=me.Style(display="flex", gap=ROW_GAP)):
+ input_field(label="Email", input_type="email")
+
+ with me.box(style=me.Style(display="flex", gap=ROW_GAP)):
+ input_field(label="Address")
+
+ with form_group():
+ input_field(label="Address 2")
+
+ with form_group():
+ country_state_zip_width = calc_input_size(3) if is_mobile() else "100%"
+ input_field(label="Country", width=country_state_zip_width)
+ input_field(label="State", width=country_state_zip_width)
+ input_field(label="Zip", width=country_state_zip_width)
+
+ divider()
+
+ me.text(
+ "Payment",
+ type="headline-4",
+ style=me.Style(margin=me.Margin(bottom=10)),
+ )
+
+ with form_group(flex_direction="column"):
+ me.radio(
+ key="payment_type",
+ on_change=on_change,
+ options=[
+ me.RadioOption(label="Credit card", value="credit_card"),
+ me.RadioOption(label="Debit card", value="debit_card"),
+ me.RadioOption(label="Paypal", value="paypal"),
+ ],
+ style=me.Style(
+ display="flex", flex_direction="column", margin=me.Margin(bottom=20)
+ ),
+ )
+ if "payment_type" in state.errors:
+ me.text(
+ state.errors["payment_type"],
+ style=me.Style(
+ margin=me.Margin(top=-30, left=5, bottom=15),
+ color=me.theme_var("error"),
+ font_size=13,
+ ),
+ )
+
+ with form_group():
+ payments_width = calc_input_size(2) if is_mobile() else "100%"
+ input_field(label="Name on card", width=payments_width)
+ input_field(label="Credit card", width=payments_width)
+
+ with form_group():
+ input_field(label="Expiration", width=payments_width)
+ input_field(label="CVV", width=payments_width, input_type="number")
+
+ divider()
+
+ me.button(
+ "Continue to checkout",
+ type="flat",
+ style=me.Style(width="100%", padding=me.Padding.all(25), font_size=20),
+ on_click=on_click,
+ )
+
+
+def divider():
+ with me.box(style=me.Style(margin=me.Margin.symmetric(vertical=20))):
+ me.divider()
+
+
+@me.content_component
+def form_group(flex_direction: Literal["row", "column"] = "row"):
+ with me.box(
+ style=me.Style(
+ display="flex", flex_direction=flex_direction, gap=ROW_GAP, width="100%"
+ )
+ ):
+ me.slot()
+
+
+def input_field(
+ *,
+ key: str = "",
+ label: str,
+ value: str = "",
+ width: str | int = "100%",
+ input_type: Literal[
+ "color",
+ "date",
+ "datetime-local",
+ "email",
+ "month",
+ "number",
+ "password",
+ "search",
+ "tel",
+ "text",
+ "time",
+ "url",
+ "week",
+ ] = "text",
+):
+ state = me.state(State)
+ key = key if key else label.lower().replace(" ", "_")
+ with me.box(style=me.Style(flex_grow=1, width=width)):
+ me.input(
+ key=key,
+ label=label,
+ value=value,
+ appearance="outline",
+ color="warn" if key in state.errors else "primary",
+ style=me.Style(width=width),
+ type=input_type,
+ on_blur=on_blur,
+ )
+ if key in state.errors:
+ me.text(
+ state.errors[key],
+ style=me.Style(
+ margin=me.Margin(top=-13, left=15, bottom=15),
+ color=me.theme_var("error"),
+ font_size=13,
+ ),
+ )
+
+
+def on_change(e: me.RadioChangeEvent):
+ state = me.state(State)
+ setattr(state, e.key, e.value)
+
+
+def on_blur(e: me.InputBlurEvent):
+ state = me.state(State)
+ setattr(state, e.key, e.value)
+
+
+def on_click(e: me.ClickEvent):
+ state = me.state(State)
+
+ # Replace with real validation logic.
+ errors = {}
+ for key, value in asdict(state).items(): # type: ignore
+ if key == "error":
+ continue
+ if not value:
+ errors[key] = f"{key.replace('_', ' ').capitalize()} is required"
+ state.errors = errors
+
+ # Replace with form processing logic.
+ if not state.errors:
+ pass
diff --git a/form_profile.py b/form_profile.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7ef667bb0518020bff5cd32b8b89b591bf582c8
--- /dev/null
+++ b/form_profile.py
@@ -0,0 +1,178 @@
+from dataclasses import asdict
+from typing import Literal
+
+import mesop as me
+
+ROW_GAP = 15
+BOX_PADDING = 20
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_density(-3)
+ me.set_theme_mode("system")
+
+
+@me.stateclass
+class State:
+ first_name: str
+ last_name: str
+ username: str
+ email: str
+ facebook: str
+ google: str
+ instagram: str
+ tiktok: str
+ errors: dict[str, str]
+
+
+@me.page(
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/form_profile",
+ on_load=load,
+)
+def page():
+ with me.box(
+ style=me.Style(
+ padding=me.Padding.all(BOX_PADDING),
+ max_width=1000,
+ margin=me.Margin.symmetric(horizontal="auto"),
+ )
+ ):
+ me.text(
+ "Profile",
+ type="headline-4",
+ style=me.Style(margin=me.Margin(bottom=15)),
+ )
+
+ divider()
+
+ with form_group(
+ label="Full name",
+ description="This will be displayed on your profile.",
+ ):
+ input_field(label="First name")
+ input_field(label="Last name")
+
+ divider()
+
+ with form_group(label="Account", description="This info must be unique."):
+ input_field(label="Username")
+ input_field(label="Email", input_type="email")
+
+ divider()
+
+ with form_group(
+ label="Social media", description="Links to your social media profiles"
+ ):
+ input_field(label="Facebook", input_type="url")
+ input_field(key="google", label="Google+", input_type="url")
+ input_field(label="Instagram", input_type="url")
+ input_field(label="TikTok", input_type="url")
+
+ divider()
+
+ me.button(
+ "Save Profile",
+ type="flat",
+ on_click=on_click,
+ style=me.Style(padding=me.Padding.all(25), font_size=20),
+ )
+
+
+def divider():
+ with me.box(style=me.Style(margin=me.Margin.symmetric(vertical=20))):
+ me.divider()
+
+
+@me.content_component
+def form_group(
+ label: str = "",
+ description: str = "",
+):
+ with me.box(
+ style=me.Style(display="flex", gap=25, margin=me.Margin(bottom=20))
+ ):
+ with me.box(style=me.Style(flex_basis="200px")):
+ me.text(label, type="headline-6", style=me.Style(font_weight="bold"))
+ me.text(description, style=me.Style(font_style="italic", font_size=13))
+ with me.box(
+ style=me.Style(
+ display="flex", flex_grow=1, flex_direction="column", width="100%"
+ )
+ ):
+ me.slot()
+
+
+def input_field(
+ *,
+ key: str = "",
+ label: str,
+ value: str = "",
+ width: str | int = "100%",
+ input_type: Literal[
+ "color",
+ "date",
+ "datetime-local",
+ "email",
+ "month",
+ "number",
+ "password",
+ "search",
+ "tel",
+ "text",
+ "time",
+ "url",
+ "week",
+ ] = "text",
+):
+ state = me.state(State)
+ key = key if key else label.lower().replace(" ", "_")
+ with me.box(style=me.Style(flex_grow=1, width=width)):
+ me.input(
+ key=key,
+ label=label,
+ value=value,
+ appearance="outline",
+ color="warn" if key in state.errors else "primary",
+ style=me.Style(width=width),
+ type=input_type,
+ on_blur=on_blur,
+ )
+ if key in state.errors:
+ me.text(
+ state.errors[key],
+ style=me.Style(
+ margin=me.Margin(top=-13, left=15, bottom=15),
+ color=me.theme_var("error"),
+ font_size=13,
+ ),
+ )
+
+
+def on_change(e: me.RadioChangeEvent):
+ state = me.state(State)
+ setattr(state, e.key, e.value)
+
+
+def on_blur(e: me.InputBlurEvent):
+ state = me.state(State)
+ setattr(state, e.key, e.value)
+
+
+def on_click(e: me.ClickEvent):
+ state = me.state(State)
+
+ # Replace with real validation logic.
+ errors = {}
+ for key, value in asdict(state).items(): # type: ignore
+ if key == "error":
+ continue
+ if not value:
+ errors[key] = f"{key.replace('_', ' ').capitalize()} is required"
+ state.errors = errors
+
+ # Replace with form processing logic.
+ if not state.errors:
+ pass
diff --git a/grid_table.py b/grid_table.py
new file mode 100644
index 0000000000000000000000000000000000000000..01e174eb5eb0cdf0d94b7fc98f89b05270c37a3d
--- /dev/null
+++ b/grid_table.py
@@ -0,0 +1,675 @@
+"""Pure Mesop Table built using CSS Grid.
+
+Functionality:
+
+- Column sorting
+- Header styles
+- Row styles
+- Cell styles
+- Cell templating
+- Row click
+- Expandable rows
+- Sticky header
+- Filtering (technically not built-in to the grid table component)
+
+TODOs:
+
+- Pagination
+- Sticky column
+- Control column width
+- Column filtering within grid table
+"""
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Any, Callable, Literal, Protocol
+
+import pandas as pd
+
+import mesop as me
+
+df = pd.DataFrame(
+ data={
+ "NA": [pd.NA, pd.NA, pd.NA],
+ "Index": [3, 2, 1],
+ "Bools": [True, False, True],
+ "Ints": [101, 90, -55],
+ "Floats": [1002.3, 4.5, -1050203.021],
+ "Date Times": [
+ pd.Timestamp("20180310"),
+ pd.Timestamp("20230310"),
+ datetime(2023, 1, 1, 12, 12, 1),
+ ],
+ "Strings": ["Hello", "World", "!"],
+ }
+)
+
+SortDirection = Literal["asc", "desc"]
+
+
+@me.stateclass
+class State:
+ expanded_df_row_index: int | None = None
+ sort_column: str
+ sort_direction: SortDirection = "asc"
+ string_output: str
+ table_filter: str
+ theme: str = "light"
+
+
+@me.page(
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/grid_table",
+)
+def app():
+ state = me.state(State)
+
+ with me.box(style=me.Style(margin=me.Margin.all(30))):
+ me.select(
+ label="Theme",
+ options=[
+ me.SelectOption(label="Light", value="light"),
+ me.SelectOption(label="Dark", value="dark"),
+ ],
+ on_selection_change=on_theme_changed,
+ )
+
+ # Simple example of filtering a data table. This is implemented separately of the
+ # grid table component. For simplicity, we only filter against a single column.
+ me.input(
+ label="Filter by Strings column",
+ style=me.Style(width="100%"),
+ on_blur=on_filter_by_strings,
+ on_enter=on_filter_by_strings,
+ )
+
+ # Grid Table demonstrating all features.
+ grid_table(
+ get_data_frame(),
+ header_config=GridTableHeader(sticky=True),
+ on_click=on_table_cell_click,
+ on_sort=on_table_sort,
+ row_config=GridTableRow(
+ columns={
+ "Bools": GridTableColumn(component=bool_component),
+ "Date Times": GridTableColumn(component=date_component),
+ "Floats": GridTableColumn(component=floats_component),
+ "Ints": GridTableColumn(style=ints_style, sortable=True),
+ "Strings": GridTableColumn(
+ component=strings_component, sortable=True
+ ),
+ },
+ expander=GridTableExpander(
+ component=expander,
+ df_row_index=state.expanded_df_row_index,
+ ),
+ ),
+ sort_column=state.sort_column,
+ sort_direction=state.sort_direction,
+ theme=GridTableThemeLight(striped=True)
+ if state.theme == "light"
+ else GridTableThemeDark(striped=True),
+ )
+
+ # Used for demonstrating "table button" click example.
+ if state.string_output:
+ with me.box(
+ style=me.Style(
+ background="#ececec",
+ color="#333",
+ margin=me.Margin(top=20),
+ padding=me.Padding.all(15),
+ )
+ ):
+ me.text(f"You clicked button: {state.string_output}")
+
+
+@dataclass(kw_only=True)
+class GridTableHeader:
+ """Configuration for the table header
+
+ Attributes:
+
+ sticky: Enables sticky headers
+ style: Overrides default header styles
+ """
+
+ sticky: bool = False
+ style: Callable | None = None
+
+
+@dataclass(kw_only=True)
+class GridTableColumn:
+ """Configuration for a table column
+
+ Attributes:
+
+ component: Custom rendering for the table cell
+ sortable: Whether this column can be sorted or not
+ style: Custom styling for the table cell
+ """
+
+ component: Callable | None = None
+ sortable: bool = False
+ style: Callable | None = None
+
+
+@dataclass(kw_only=True)
+class GridTableExpander:
+ """Configuration for expander table row
+
+ Currently only one row can be expanded at a time.
+
+ Attributes:
+
+ component: Custom rendering for the table row
+ df_row_index: DataFrame row that is expanded.
+ style: Custom styling for the expanded row
+ """
+
+ component: Callable | None = None
+ df_row_index: int | None = None
+ style: Callable | None = None
+
+
+@dataclass(kw_only=True)
+class GridTableRow:
+ """Configuration for the table's rows.
+
+ Attributes:
+
+ columns: A map of column name to column specific configuration
+ expander: Configuration for expanded row
+ style: Custom styles at the row level.
+ """
+
+ columns: dict[str, GridTableColumn] = field(default_factory=lambda: {})
+ expander: GridTableExpander = field(
+ default_factory=lambda: GridTableExpander()
+ )
+ style: Callable | None = None
+
+
+@dataclass(kw_only=True)
+class GridTableCellMeta:
+ """Metadata that is passed into style/component/expander callables.
+
+ This metadata can be used to display things in custom ways based on the data.
+ """
+
+ df_row_index: int
+ df_col_index: int
+ name: str
+ row_index: int
+ value: Any
+
+
+class GridTableTheme(Protocol):
+ """Interface for theming the grid table"""
+
+ def header(self, sortable: bool = False) -> me.Style:
+ return me.Style()
+
+ def sort_icon(self, current_column: str, sort_column: str) -> me.Style:
+ return me.Style()
+
+ def cell(self, cell_meta: GridTableCellMeta) -> me.Style:
+ return me.Style()
+
+ def expander(self, df_row_index: int) -> me.Style:
+ return me.Style()
+
+
+class GridTableThemeDark(GridTableTheme):
+ _HEADER_BG: str = "#28313e"
+ _CELL_BG: str = "#141d2c"
+ _CELL_BG_ALT: str = "#02060c"
+ _COLOR: str = "#fff"
+ _PADDING: me.Padding = me.Padding.all(10)
+ _BORDER: me.Border = me.Border.all(
+ me.BorderSide(width=1, style="solid", color="rgba(255, 255, 255, 0.16)")
+ )
+
+ def __init__(self, striped: bool = False):
+ self.striped = striped
+
+ def header(self, sortable: bool = False) -> me.Style:
+ return me.Style(
+ background=self._HEADER_BG,
+ color=self._COLOR,
+ cursor="pointer" if sortable else "default",
+ padding=self._PADDING,
+ border=self._BORDER,
+ )
+
+ def sort_icon(self, current_column: str, sort_column: str) -> me.Style:
+ return me.Style(
+ color="rgba(255, 255, 255, .8)"
+ if sort_column == current_column
+ else "rgba(255, 255, 255, .4)",
+ # Hack to make the icon align correctly. Will break if user changes the
+ # font size with custom styles.
+ height=16,
+ )
+
+ def cell(self, cell_meta: GridTableCellMeta) -> me.Style:
+ return me.Style(
+ background=self._CELL_BG_ALT
+ if self.striped and cell_meta.row_index % 2
+ else self._CELL_BG,
+ color=self._COLOR,
+ padding=self._PADDING,
+ border=self._BORDER,
+ )
+
+ def expander(self, df_row_index: int) -> me.Style:
+ return me.Style(
+ background=self._CELL_BG,
+ color=self._COLOR,
+ padding=self._PADDING,
+ border=self._BORDER,
+ )
+
+
+class GridTableThemeLight(GridTableTheme):
+ _HEADER_BG: str = "#fff"
+ _CELL_BG: str = "#fff"
+ _CELL_BG_ALT: str = "#f6f6f6"
+ _COLOR: str = "#000"
+ _PADDING: me.Padding = me.Padding.all(10)
+ _HEADER_BORDER: me.Border = me.Border(
+ bottom=me.BorderSide(width=1, style="solid", color="#b2b2b2")
+ )
+ _CELL_BORDER: me.Border = me.Border(
+ bottom=me.BorderSide(width=1, style="solid", color="#d9d9d9")
+ )
+
+ def __init__(self, striped: bool = False):
+ self.striped = striped
+
+ def header(self, sortable: bool = False) -> me.Style:
+ return me.Style(
+ background=self._HEADER_BG,
+ color=self._COLOR,
+ cursor="pointer" if sortable else "default",
+ font_weight="bold",
+ padding=self._PADDING,
+ border=self._HEADER_BORDER,
+ )
+
+ def sort_icon(self, current_column: str, sort_column: str) -> me.Style:
+ return me.Style(
+ color="rgba(0, 0, 0, .8)"
+ if sort_column == current_column
+ else "rgba(0, 0, 0, .4)",
+ # Hack to make the icon align correctly. Will break if user changes the
+ # font size with custom styles.
+ height=18,
+ )
+
+ def cell(self, cell_meta: GridTableCellMeta) -> me.Style:
+ return me.Style(
+ background=self._CELL_BG_ALT
+ if self.striped and cell_meta.row_index % 2
+ else self._CELL_BG,
+ color=self._COLOR,
+ padding=self._PADDING,
+ border=self._CELL_BORDER,
+ )
+
+ def expander(self, df_row_index: int) -> me.Style:
+ return me.Style(
+ background=self._CELL_BG,
+ color=self._COLOR,
+ padding=self._PADDING,
+ border=self._CELL_BORDER,
+ )
+
+
+def get_data_frame():
+ """Helper function to get a sorted/filtered version of the main data frame.
+
+ One drawback of this approach is that we sort/filter the main data frame with every
+ refresh, which may not be efficient for larger data frames.
+ """
+ state = me.state(State)
+
+ # Sort the data frame if sorting is enabled.
+ if state.sort_column:
+ sorted_df = df.sort_values(
+ by=state.sort_column, ascending=state.sort_direction == "asc"
+ )
+ else:
+ sorted_df = df
+
+ # Simple filtering by the Strings column.
+ if state.table_filter:
+ return sorted_df[
+ sorted_df["Strings"].str.lower().str.contains(state.table_filter.lower())
+ ]
+ else:
+ return sorted_df
+
+
+def on_theme_changed(e: me.SelectSelectionChangeEvent):
+ """Changes the theme of the grid table"""
+ state = me.state(State)
+ state.theme = e.value
+
+
+def on_filter_by_strings(e: me.InputBlurEvent | me.InputEnterEvent):
+ """Saves the filtering string to be used in `get_data_frame`"""
+ state = me.state(State)
+ state.table_filter = e.value
+
+
+def on_table_cell_click(e: me.ClickEvent):
+ """If the table cell is clicked, show the expanded content."""
+ state = me.state(State)
+ df_row_index, _ = map(int, e.key.split("-"))
+ if state.expanded_df_row_index == df_row_index:
+ state.expanded_df_row_index = None
+ else:
+ state.expanded_df_row_index = df_row_index
+
+
+def on_table_sort(e: me.ClickEvent):
+ """Handles the table sort event by saving the sort information to be used in `get_data_frame`"""
+ state = me.state(State)
+ column, direction = e.key.split("-")
+ if state.sort_column == column:
+ state.sort_direction = "asc" if direction == "desc" else "desc"
+ else:
+ state.sort_direction = direction # type: ignore
+ state.sort_column = column
+
+
+def expander(df_row_index: int):
+ """Rendering logic for expanded row.
+
+ Here we just display the row data in two columns as text inputs.
+
+ But you can do more advanced things, such as:
+
+ - rendering another table inside the table
+ - fetching data to show drill down data
+ - add a form for data entry
+ """
+ columns = list(df.columns)
+ with me.box(style=me.Style(padding=me.Padding.all(15))):
+ me.text(f"Expanded row: {df_row_index}", type="headline-5")
+ with me.box(
+ style=me.Style(
+ display="grid",
+ grid_template_columns="repeat(2, 1fr)",
+ gap=10,
+ )
+ ):
+ for index, col in enumerate(df.iloc[df_row_index]):
+ me.input(
+ label=columns[index], value=str(col), style=me.Style(width="100%")
+ )
+
+
+def on_click_strings(e: me.ClickEvent):
+ """Click event for the cell button example."""
+ state = me.state(State)
+ state.string_output = e.key
+
+
+def strings_component(meta: GridTableCellMeta):
+ """Example of a cell rendering a button with a click event.
+
+ Note that the behavior is slightly buggy if there is also a cell click event. This
+ event will fire, but so will the cell click event. This is due to
+ https://github.com/google/mesop/issues/268.
+ """
+ me.button(
+ meta.value,
+ key=meta.value,
+ on_click=on_click_strings,
+ style=me.Style(
+ border_radius=3,
+ background="#334053",
+ border=me.Border.all(
+ me.BorderSide(width=1, style="solid", color="rgba(255, 255, 255, 0.16)")
+ ),
+ font_weight="bold",
+ color="#fff",
+ ),
+ )
+
+
+def bool_component(meta: GridTableCellMeta):
+ """Example of a cell rendering icons based on the cell value."""
+ if meta.value:
+ me.icon("check_circle", style=me.Style(color="green"))
+ else:
+ me.icon("cancel", style=me.Style(color="red"))
+
+
+def ints_style(meta: GridTableCellMeta) -> me.Style:
+ """Example of a cell style based on the integer value."""
+ return me.Style(
+ background="#29a529" if meta.value > 0 else "#db4848",
+ color="#fff",
+ padding=me.Padding.all(10),
+ border=me.Border.all(
+ me.BorderSide(width=1, style="solid", color="rgba(255, 255, 255, 0.16)")
+ ),
+ )
+
+
+def floats_component(meta: GridTableCellMeta):
+ """Example of a cell rendering using string formatting."""
+ me.text(f"${meta.value:,.2f}")
+
+
+def date_component(meta: GridTableCellMeta):
+ """Example of a cell rendering using custom date formatting."""
+ me.text(meta.value.strftime("%b %d, %Y at %I:%M %p"))
+
+
+@me.component
+def grid_table(
+ data,
+ *,
+ header_config: GridTableHeader | None = None,
+ on_click: Callable | None = None,
+ on_sort: Callable | None = None,
+ row_config: GridTableRow | None = None,
+ sort_column: str = "",
+ sort_direction: SortDirection = "asc",
+ theme: Any
+ | None = None, # Using Any since Pydantic complains about using a class.
+):
+ """Grid table component.
+
+ Args:
+
+ data: Pandas data frame
+ header_config: Configuration for the table header
+ on_click: Click event that fires when a cell is clicked
+ on_sort: Click event that fires when a sortable header column is clicked
+ row_config: Configuration for the tables's rows
+ sort_column: Current sort column
+ sort_direction: Current sort direction
+ theme: Table theme
+ """
+ with me.box(
+ style=me.Style(
+ display="grid",
+ # This creates a grid with "equal" sized rows based on the columns. We may want to
+ # override this to allow custom widths.
+ grid_template_columns=f"repeat({len(data.columns)}, 1fr)",
+ )
+ ):
+ _theme: GridTableTheme = GridTableThemeLight()
+ if theme:
+ _theme = theme
+
+ if not header_config:
+ header_config = GridTableHeader()
+
+ if not row_config:
+ row_config = GridTableRow()
+
+ col_index_name_map = {}
+
+ # Render the table header
+ for col_index, col in enumerate(data.columns):
+ col_index_name_map[col_index] = col
+ sortable_col = row_config.columns.get(col, GridTableColumn()).sortable
+ with me.box(
+ # Sort key format: ColumName-SortDirection
+ key=_make_sort_key(col, sort_column, sort_direction),
+ style=_make_header_style(
+ theme=_theme, header_config=header_config, sortable=sortable_col
+ ),
+ on_click=on_sort if sortable_col else None,
+ ):
+ with me.box(
+ style=me.Style(
+ display="flex",
+ align_items="center",
+ )
+ ):
+ if sortable_col:
+ # Render sorting icons for sortable columns
+ #
+ # If column is sortable and not selected, always render an up arrow that is de-emphasized
+ # If column is sortable and selected, render the arrow with emphasis
+ # If the column is newly selected, render the up arrow to sort ascending
+ # If the column is selected and reselected, render the opposite arrow
+ me.icon(
+ "arrow_downward"
+ if sort_column == col and sort_direction == "desc"
+ else "arrow_upward",
+ style=_theme.sort_icon(col, sort_column),
+ )
+ me.text(col)
+
+ # Render table rows
+ for row_index, row in enumerate(data.itertuples(name=None)):
+ for col_index, col in enumerate(row[1:]):
+ cell_config = row_config.columns.get(
+ col_index_name_map[col_index], GridTableColumn()
+ )
+ cell_meta = GridTableCellMeta(
+ df_row_index=row[0],
+ df_col_index=col_index,
+ name=col_index_name_map[col_index],
+ row_index=row_index,
+ value=col,
+ )
+ with me.box(
+ # Store the df row index and df col index for the cell click event so we know
+ # which cell is clicked.
+ key=f"{row[0]}-{col_index}",
+ style=_make_cell_style(
+ theme=_theme,
+ cell_meta=cell_meta,
+ column=cell_config,
+ row_style=row_config.style,
+ ),
+ on_click=on_click,
+ ):
+ if cell_config.component:
+ # Render custom cell markup
+ cell_config.component(cell_meta)
+ else:
+ me.text(str(col))
+
+ # Render the expander if it's enabled and a row has been selected.
+ if (
+ row_config.expander.component
+ and row_config.expander.df_row_index == row[0]
+ ):
+ with me.box(
+ style=_make_expander_style(
+ df_row_index=row[0],
+ col_span=len(data.columns),
+ expander_style=row_config.expander.style,
+ theme=_theme,
+ )
+ ):
+ row_config.expander.component(row[0])
+
+
+def _make_header_style(
+ *, theme: GridTableTheme, header_config: GridTableHeader, sortable: bool
+) -> me.Style:
+ """Renders the header style
+
+ Precendence of styles:
+
+ - Header style override
+ - Theme default
+ """
+
+ # Default styles
+ style = theme.header(sortable)
+ if header_config.style:
+ style = header_config.style(sortable)
+
+ if header_config.sticky:
+ style.position = "sticky"
+ style.top = 0
+
+ return style
+
+
+def _make_sort_key(col: str, sort_column: str, sort_direction: SortDirection):
+ if col == sort_column:
+ return f"{sort_column}-{sort_direction}"
+ return f"{col}-asc"
+
+
+def _make_cell_style(
+ *,
+ theme: GridTableTheme,
+ cell_meta: GridTableCellMeta,
+ column: GridTableColumn,
+ row_style: Callable | None = None,
+) -> me.Style:
+ """Renders the cell style
+
+ Precendence of styles:
+
+ - Cell style override
+ - Row style override
+ - Theme Default
+ """
+ style = theme.cell(cell_meta)
+
+ if column.style:
+ style = column.style(cell_meta)
+ elif row_style:
+ style = row_style(cell_meta)
+
+ return style
+
+
+def _make_expander_style(
+ *,
+ theme: GridTableTheme,
+ df_row_index: int,
+ col_span: int,
+ expander_style: Callable | None = None,
+) -> me.Style:
+ """Renders the expander style
+
+ Precendence of styles:
+
+ - Cell style override
+ - Theme default
+ """
+ style = theme.expander(df_row_index)
+ if expander_style:
+ style = expander_style(df_row_index)
+
+ style.grid_column = f"span {col_span}"
+
+ return style
diff --git a/headers.py b/headers.py
new file mode 100644
index 0000000000000000000000000000000000000000..018e928486cb15542790aab7eab70fe8ba634689
--- /dev/null
+++ b/headers.py
@@ -0,0 +1,201 @@
+from dataclasses import fields
+
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/headers",
+)
+def app():
+ is_mobile = me.viewport_size().width < 640
+
+ with me.box(style=me.Style(margin=me.Margin(bottom=15))):
+ # Two section basic header with fluid width.
+ # As an example, we don't use mobile view here since the header is short enough.
+ with header(max_width=None):
+ with header_section():
+ me.text(
+ "Mesop", type="headline-6", style=me.Style(margin=me.Margin(bottom=0))
+ )
+
+ with header_section():
+ me.button("Home")
+ me.button("About")
+ me.button("FAQ")
+
+ with me.box(style=me.Style(margin=me.Margin(bottom=15))):
+ # Two section basic header.
+ with header(is_mobile=is_mobile):
+ with header_section():
+ me.text(
+ "Mesop", type="headline-6", style=me.Style(margin=me.Margin(bottom=0))
+ )
+
+ with header_section():
+ me.button("Home")
+ me.button("About")
+ me.button("FAQ")
+
+ with me.box(style=me.Style(margin=me.Margin(bottom=15))):
+ # Three section basic header.
+ with header(is_mobile=is_mobile):
+ with header_section():
+ me.text(
+ "Mesop", type="headline-6", style=me.Style(margin=me.Margin(bottom=0))
+ )
+
+ with header_section():
+ me.button("Home")
+ me.button("About")
+ me.button("FAQ")
+
+ with header_section():
+ me.button("Login", type="flat")
+
+ with me.box(style=me.Style(margin=me.Margin(bottom=15))):
+ # Centered header with overrides and icons
+ with header(is_mobile=is_mobile, style=me.Style(justify_content="center")):
+ with header_section():
+ with me.content_button(
+ style=me.Style(
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
+ )
+ ):
+ me.icon("home")
+ me.text("Home")
+ with me.content_button(
+ style=me.Style(
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
+ )
+ ):
+ me.icon("info")
+ me.text("About")
+ with me.content_button(
+ style=me.Style(
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
+ )
+ ):
+ me.icon("contact_support")
+ me.text("FAQ")
+ with me.content_button(
+ style=me.Style(
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
+ )
+ ):
+ me.icon("login")
+ me.text("Login")
+
+ with me.box(style=me.Style(margin=me.Margin(bottom=15))):
+ # Header with overridden background
+ with header(
+ is_mobile=is_mobile, style=me.Style(background="#0F0F11", color="#E3E3E3")
+ ):
+ with header_section():
+ me.text(
+ "Mesop", type="headline-6", style=me.Style(margin=me.Margin(bottom=0))
+ )
+
+ with header_section():
+ me.button("Home", type="stroked", style=me.Style(color="#E3E3E3"))
+ me.button("About", type="stroked", style=me.Style(color="#E3E3E3"))
+ me.button("FAQ", type="stroked", style=me.Style(color="#E3E3E3"))
+
+ with header_section():
+ me.button("Login", type="flat")
+
+
+@me.content_component
+def header(
+ *,
+ style: me.Style | None = None,
+ is_mobile: bool = False,
+ max_width: int | None = 1000,
+):
+ """Creates a simple header component.
+
+ Args:
+ style: Override the default styles, such as background color, etc.
+ is_mobile: Use mobile layout. Arranges each section vertically.
+ max_width: Sets the maximum width of the header. Use None for fluid header.
+ """
+ default_flex_style = (
+ _DEFAULT_MOBILE_FLEX_STYLE if is_mobile else _DEFAULT_FLEX_STYLE
+ )
+ if max_width and me.viewport_size().width >= max_width:
+ default_flex_style = merge_styles(
+ default_flex_style,
+ me.Style(width=max_width, margin=me.Margin.symmetric(horizontal="auto")),
+ )
+
+ # The style override is a bit hacky here since we apply the override styles to both
+ # boxes here which could cause problems depending on what styles are added.
+ with me.box(style=merge_styles(_DEFAULT_STYLE, style)):
+ with me.box(style=merge_styles(default_flex_style, style)):
+ me.slot()
+
+
+@me.content_component
+def header_section():
+ """Adds a section to the header."""
+ with me.box(style=me.Style(display="flex", gap=5)):
+ me.slot()
+
+
+def merge_styles(
+ default: me.Style, overrides: me.Style | None = None
+) -> me.Style:
+ """Merges two styles together.
+
+ Args:
+ default: The starting style
+ overrides: Any set styles will override styles in default
+ """
+ if not overrides:
+ overrides = me.Style()
+
+ default_fields = {
+ field.name: getattr(default, field.name) for field in fields(me.Style)
+ }
+ override_fields = {
+ field.name: getattr(overrides, field.name)
+ for field in fields(me.Style)
+ if getattr(overrides, field.name) is not None
+ }
+
+ return me.Style(**default_fields | override_fields)
+
+
+_DEFAULT_STYLE = me.Style(
+ background=me.theme_var("surface-container"),
+ border=me.Border.symmetric(
+ vertical=me.BorderSide(
+ width=1,
+ style="solid",
+ color=me.theme_var("outline-variant"),
+ )
+ ),
+ padding=me.Padding.all(10),
+)
+
+_DEFAULT_FLEX_STYLE = me.Style(
+ align_items="center",
+ display="flex",
+ gap=5,
+ justify_content="space-between",
+)
+
+_DEFAULT_MOBILE_FLEX_STYLE = me.Style(
+ align_items="center",
+ display="flex",
+ flex_direction="column",
+ gap=12,
+ justify_content="center",
+)
diff --git a/html_demo.py b/html_demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..375d693c70b9abb3fc9ab2aa488abfaee6589755
--- /dev/null
+++ b/html_demo.py
@@ -0,0 +1,33 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/html_demo",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text("Sanitized HTML", type="headline-5")
+ me.html(
+ """
+ Custom HTML
+ mesop
+ """,
+ mode="sanitized",
+ )
+
+ with me.box(style=me.Style(margin=me.Margin.symmetric(vertical=24))):
+ me.divider()
+
+ me.text("Sandboxed HTML", type="headline-5")
+ me.html(
+ "hi",
+ mode="sandboxed",
+ )
diff --git a/icon.py b/icon.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b73f976dd968b2ad1b230af1c011944f454317a
--- /dev/null
+++ b/icon.py
@@ -0,0 +1,18 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/icon",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text("home icon")
+ me.icon(icon="home")
diff --git a/image.py b/image.py
new file mode 100644
index 0000000000000000000000000000000000000000..42486947f592fcba58b74a03adb60f4224091837
--- /dev/null
+++ b/image.py
@@ -0,0 +1,21 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/image",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.image(
+ src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ alt="Grapefruit",
+ style=me.Style(width="100%"),
+ )
diff --git a/input.py b/input.py
new file mode 100644
index 0000000000000000000000000000000000000000..60da20dca6dee74141fdbe7bc5f0f557d16ce1a2
--- /dev/null
+++ b/input.py
@@ -0,0 +1,28 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ input: str = ""
+
+
+def on_blur(e: me.InputBlurEvent):
+ state = me.state(State)
+ state.input = e.value
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/input",
+)
+def app():
+ s = me.state(State)
+ me.input(label="Basic input", on_blur=on_blur)
+ me.text(text=s.input)
diff --git a/link.py b/link.py
new file mode 100644
index 0000000000000000000000000000000000000000..9174a13fc352e42f63a10add0393f56fdc4ef5f3
--- /dev/null
+++ b/link.py
@@ -0,0 +1,36 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/link",
+)
+def link():
+ with me.box(
+ style=me.Style(
+ margin=me.Margin.all(15), display="flex", flex_direction="column", gap=10
+ )
+ ):
+ me.link(
+ text="Open in same tab",
+ url="https://google.github.io/mesop/",
+ style=me.Style(color=me.theme_var("primary")),
+ )
+ me.link(
+ text="Open in new tab",
+ open_in_new_tab=True,
+ url="https://google.github.io/mesop/",
+ style=me.Style(color=me.theme_var("primary")),
+ )
+ me.link(
+ text="Styled link",
+ url="https://google.github.io/mesop/",
+ style=me.Style(color=me.theme_var("tertiary"), text_decoration="none"),
+ )
diff --git a/llm_playground.py b/llm_playground.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdd51d61f560bf0ffdae4cb5ee6598bfde4d61e2
--- /dev/null
+++ b/llm_playground.py
@@ -0,0 +1,558 @@
+import random
+import time
+from typing import Callable
+
+import mesop as me
+
+_TEMPERATURE_MIN = 0.0
+_TEMPERATURE_MAX = 2.0
+_TOKEN_LIMIT_MIN = 1
+_TOKEN_LIMIT_MAX = 8192
+
+
+@me.stateclass
+class State:
+ title: str = "LLM Playground"
+ # Prompt / Response
+ input: str
+ response: str
+ # Tab open/close
+ prompt_tab: bool = True
+ response_tab: bool = True
+ # Model configs
+ selected_model: str = "gemini-1.5"
+ selected_region: str = "us-east4"
+ temperature: float = 1.0
+ temperature_for_input: float = 1.0
+ token_limit: int = _TOKEN_LIMIT_MAX
+ token_limit_for_input: int = _TOKEN_LIMIT_MAX
+ stop_sequence: str = ""
+ stop_sequences: list[str]
+ # Modal
+ modal_open: bool = False
+ # Workaround for clearing inputs
+ clear_prompt_count: int = 0
+ clear_sequence_count: int = 0
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/llm_playground",
+ title="LLM Playground",
+)
+def page():
+ state = me.state(State)
+
+ # Modal
+ with modal(modal_open=state.modal_open):
+ me.text("Get code", type="headline-5")
+ if "gemini" in state.selected_model:
+ me.text(
+ "Use the following code in your application to request a model response."
+ )
+ with me.box(style=_STYLE_CODE_BOX):
+ me.markdown(
+ _GEMINI_CODE_TEXT.format(
+ content=state.input.replace('"', '\\"'),
+ model=state.selected_model,
+ region=state.selected_region,
+ stop_sequences=make_stop_sequence_str(state.stop_sequences),
+ token_limit=state.token_limit,
+ temperature=state.temperature,
+ )
+ )
+ else:
+ me.text(
+ "You can use the following code to start integrating your current prompt and settings into your application."
+ )
+ with me.box(style=_STYLE_CODE_BOX):
+ me.markdown(
+ _GPT_CODE_TEXT.format(
+ content=state.input.replace('"', '\\"').replace("\n", "\\n"),
+ model=state.selected_model,
+ stop_sequences=make_stop_sequence_str(state.stop_sequences),
+ token_limit=state.token_limit,
+ temperature=state.temperature,
+ )
+ )
+ me.button(label="Close", type="raised", on_click=on_click_modal)
+
+ # Main content
+ with me.box(style=_STYLE_CONTAINER):
+ # Main Header
+ with me.box(style=_STYLE_MAIN_HEADER):
+ with me.box(style=_STYLE_TITLE_BOX):
+ me.text(
+ state.title,
+ type="headline-6",
+ style=me.Style(line_height="24px", margin=me.Margin(bottom=0)),
+ )
+
+ # Toolbar Header
+ with me.box(style=_STYLE_CONFIG_HEADER):
+ icon_button(
+ icon="code", tooltip="Code", label="CODE", on_click=on_click_show_code
+ )
+
+ # Main Content
+ with me.box(style=_STYLE_MAIN_COLUMN):
+ # Prompt Tab
+ with tab_box(header="Prompt", key="prompt_tab"):
+ me.textarea(
+ label="Write your prompt here, insert media and then click Submit",
+ # Workaround: update key to clear input.
+ key=f"prompt-{state.clear_prompt_count}",
+ on_input=on_prompt_input,
+ style=_STYLE_INPUT_WIDTH,
+ )
+ me.button(label="Submit", type="flat", on_click=on_click_submit)
+ me.button(label="Clear", on_click=on_click_clear)
+
+ # Response Tab
+ with tab_box(header="Response", key="response_tab"):
+ if state.response:
+ me.markdown(state.response)
+ else:
+ me.markdown(
+ "The model will generate a response after you click Submit."
+ )
+
+ # LLM Config
+ with me.box(style=_STYLE_CONFIG_COLUMN):
+ me.select(
+ options=[
+ me.SelectOption(label="Gemini 1.5", value="gemini-1.5"),
+ me.SelectOption(label="Chat-GPT Turbo", value="gpt-3.5-turbo"),
+ ],
+ label="Model",
+ style=_STYLE_INPUT_WIDTH,
+ on_selection_change=on_model_select,
+ value=state.selected_model,
+ )
+
+ if "gemini" in state.selected_model:
+ me.select(
+ options=[
+ me.SelectOption(label="us-central1 (Iowa)", value="us-central1"),
+ me.SelectOption(
+ label="us-east4 (North Virginia)", value="us-east4"
+ ),
+ ],
+ label="Region",
+ style=_STYLE_INPUT_WIDTH,
+ on_selection_change=on_region_select,
+ value=state.selected_region,
+ )
+
+ me.text("Temperature", style=_STYLE_SLIDER_LABEL)
+ with me.box(style=_STYLE_SLIDER_INPUT_BOX):
+ with me.box(style=_STYLE_SLIDER_WRAP):
+ me.slider(
+ min=_TEMPERATURE_MIN,
+ max=_TEMPERATURE_MAX,
+ step=0.1,
+ style=_STYLE_SLIDER,
+ on_value_change=on_slider_temperature,
+ value=state.temperature,
+ )
+ me.input(
+ style=_STYLE_SLIDER_INPUT,
+ value=str(state.temperature_for_input),
+ on_input=on_input_temperature,
+ )
+
+ me.text("Output Token Limit", style=_STYLE_SLIDER_LABEL)
+ with me.box(style=_STYLE_SLIDER_INPUT_BOX):
+ with me.box(style=_STYLE_SLIDER_WRAP):
+ me.slider(
+ min=_TOKEN_LIMIT_MIN,
+ max=_TOKEN_LIMIT_MAX,
+ style=_STYLE_SLIDER,
+ on_value_change=on_slider_token_limit,
+ value=state.token_limit,
+ )
+ me.input(
+ style=_STYLE_SLIDER_INPUT,
+ value=str(state.token_limit_for_input),
+ on_input=on_input_token_limit,
+ )
+
+ with me.box(style=_STYLE_STOP_SEQUENCE_BOX):
+ with me.box(style=_STYLE_STOP_SEQUENCE_WRAP):
+ me.input(
+ label="Add stop sequence",
+ style=_STYLE_INPUT_WIDTH,
+ on_input=on_stop_sequence_input,
+ # Workaround: update key to clear input.
+ key=f"input-sequence-{state.clear_sequence_count}",
+ )
+ with me.content_button(
+ style=me.Style(margin=me.Margin(left=10)),
+ on_click=on_click_add_stop_sequence,
+ ):
+ with me.tooltip(message="Add stop Sequence"):
+ me.icon(icon="add_circle")
+
+ # Stop sequence "chips"
+ for index, sequence in enumerate(state.stop_sequences):
+ me.button(
+ key=f"sequence-{index}",
+ label=sequence,
+ on_click=on_click_remove_stop_sequence,
+ type="raised",
+ style=_STYLE_STOP_SEQUENCE_CHIP,
+ )
+
+
+# HELPER COMPONENTS
+
+
+@me.component
+def icon_button(*, icon: str, label: str, tooltip: str, on_click: Callable):
+ """Icon button with text and tooltip."""
+ with me.content_button(on_click=on_click):
+ with me.tooltip(message=tooltip):
+ with me.box(style=me.Style(display="flex")):
+ me.icon(icon=icon)
+ me.text(
+ label, style=me.Style(line_height="24px", margin=me.Margin(left=5))
+ )
+
+
+@me.content_component
+def tab_box(*, header: str, key: str):
+ """Collapsible tab box"""
+ state = me.state(State)
+ tab_open = getattr(state, key)
+ with me.box(style=me.Style(width="100%", margin=me.Margin(bottom=20))):
+ # Tab Header
+ with me.box(
+ key=key,
+ on_click=on_click_tab_header,
+ style=me.Style(padding=_DEFAULT_PADDING, border=_DEFAULT_BORDER),
+ ):
+ with me.box(style=me.Style(display="flex")):
+ me.icon(
+ icon="keyboard_arrow_down" if tab_open else "keyboard_arrow_right"
+ )
+ me.text(
+ header,
+ style=me.Style(
+ line_height="24px", margin=me.Margin(left=5), font_weight="bold"
+ ),
+ )
+ # Tab Content
+ with me.box(
+ style=me.Style(
+ padding=_DEFAULT_PADDING,
+ border=_DEFAULT_BORDER,
+ display="block" if tab_open else "none",
+ )
+ ):
+ me.slot()
+
+
+@me.content_component
+def modal(modal_open: bool):
+ """Basic modal box."""
+ with me.box(style=_make_modal_background_style(modal_open)):
+ with me.box(style=_STYLE_MODAL_CONTAINER):
+ with me.box(style=_STYLE_MODAL_CONTENT):
+ me.slot()
+
+
+# EVENT HANDLERS
+
+
+def on_click_clear(e: me.ClickEvent):
+ """Click event for clearing prompt text."""
+ state = me.state(State)
+ state.clear_prompt_count += 1
+ state.input = ""
+ state.response = ""
+
+
+def on_prompt_input(e: me.InputEvent):
+ """Capture prompt input."""
+ state = me.state(State)
+ state.input = e.value
+
+
+def on_model_select(e: me.SelectSelectionChangeEvent):
+ """Event to select model."""
+ state = me.state(State)
+ state.selected_model = e.value
+
+
+def on_region_select(e: me.SelectSelectionChangeEvent):
+ """Event to select GCP region (Gemini models only)."""
+ state = me.state(State)
+ state.selected_region = e.value
+
+
+def on_slider_temperature(e: me.SliderValueChangeEvent):
+ """Event to adjust temperature slider value."""
+ state = me.state(State)
+ state.temperature = float(e.value)
+ state.temperature_for_input = state.temperature
+
+
+def on_input_temperature(e: me.InputEvent):
+ """Event to adjust temperature slider value by input."""
+ state = me.state(State)
+ try:
+ temperature = float(e.value)
+ if _TEMPERATURE_MIN <= temperature <= _TEMPERATURE_MAX:
+ state.temperature = temperature
+ except ValueError:
+ pass
+
+
+def on_slider_token_limit(e: me.SliderValueChangeEvent):
+ """Event to adjust token limit slider value."""
+ state = me.state(State)
+ state.token_limit = int(e.value)
+ state.token_limit_for_input = state.token_limit
+
+
+def on_input_token_limit(e: me.InputEvent):
+ """Event to adjust token limit slider value by input."""
+ state = me.state(State)
+ try:
+ token_limit = int(e.value)
+ if _TOKEN_LIMIT_MIN <= token_limit <= _TOKEN_LIMIT_MAX:
+ state.token_limit = token_limit
+ except ValueError:
+ pass
+
+
+def on_stop_sequence_input(e: me.InputEvent):
+ """Capture stop sequence input."""
+ state = me.state(State)
+ state.stop_sequence = e.value
+
+
+def on_click_add_stop_sequence(e: me.ClickEvent):
+ """Save stop sequence. Will create "chip" for the sequence in the input."""
+ state = me.state(State)
+ if state.stop_sequence:
+ state.stop_sequences.append(state.stop_sequence)
+ state.clear_sequence_count += 1
+
+
+def on_click_remove_stop_sequence(e: me.ClickEvent):
+ """Click event that removes the stop sequence that was clicked."""
+ state = me.state(State)
+ index = int(e.key.replace("sequence-", ""))
+ del state.stop_sequences[index]
+
+
+def on_click_tab_header(e: me.ClickEvent):
+ """Open and closes tab content."""
+ state = me.state(State)
+ setattr(state, e.key, not getattr(state, e.key))
+
+
+def on_click_show_code(e: me.ClickEvent):
+ """Opens modal to show generated code for the given model configuration."""
+ state = me.state(State)
+ state.modal_open = True
+
+
+def on_click_modal(e: me.ClickEvent):
+ """Allows modal to be closed."""
+ state = me.state(State)
+ if state.modal_open:
+ state.modal_open = False
+
+
+def on_click_submit(e: me.ClickEvent):
+ """Submits prompt to test model configuration.
+
+ This example returns canned text. A real implementation
+ would call APIs against the given configuration.
+ """
+ state = me.state(State)
+ for line in transform(state.input):
+ state.response += line
+ yield
+
+
+def transform(input: str):
+ """Transform function that returns canned responses."""
+ for line in random.sample(LINES, random.randint(3, len(LINES) - 1)):
+ time.sleep(0.3)
+ yield line + " "
+
+
+LINES = [
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
+]
+
+
+# HELPERS
+
+_GEMINI_CODE_TEXT = """
+```python
+import base64
+import vertexai
+from vertexai.generative_models import GenerativeModel, Part, FinishReason
+import vertexai.preview.generative_models as generative_models
+
+def generate():
+ vertexai.init(project="", location="{region}")
+ model = GenerativeModel("{model}")
+ responses = model.generate_content(
+ [\"\"\"{content}\"\"\"],
+ generation_config=generation_config,
+ safety_settings=safety_settings,
+ stream=True,
+ )
+
+ for response in responses:
+ print(response.text, end="")
+
+
+generation_config = {{
+ "max_output_tokens": {token_limit},
+ "stop_sequences": [{stop_sequences}],
+ "temperature": {temperature},
+ "top_p": 0.95,
+}}
+
+safety_settings = {{
+ generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
+ generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
+ generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
+ generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
+}}
+
+generate()
+```
+""".strip()
+
+_GPT_CODE_TEXT = """
+```python
+from openai import OpenAI
+client = OpenAI()
+
+response = client.chat.completions.create(
+ model="{model}",
+ messages=[
+ {{
+ "role": "user",
+ "content": "{content}"
+ }}
+ ],
+ temperature={temperature},
+ max_tokens={token_limit},
+ top_p=1,
+ frequency_penalty=0,
+ presence_penalty=0,
+ stop=[{stop_sequences}]
+)
+```
+""".strip()
+
+
+def make_stop_sequence_str(stop_sequences: list[str]) -> str:
+ """Formats stop sequences for code output (list of strings)."""
+ return ",".join(map(lambda s: f'"{s}"', stop_sequences))
+
+
+# STYLES
+
+
+def _make_modal_background_style(modal_open: bool) -> me.Style:
+ """Makes style for modal background.
+
+ Args:
+ modal_open: Whether the modal is open.
+ """
+ return me.Style(
+ display="block" if modal_open else "none",
+ position="fixed",
+ z_index=1000,
+ width="100%",
+ height="100%",
+ overflow_x="auto",
+ overflow_y="auto",
+ background="rgba(0,0,0,0.4)",
+ )
+
+
+_DEFAULT_PADDING = me.Padding.all(15)
+_DEFAULT_BORDER = me.Border.all(
+ me.BorderSide(color=me.theme_var("outline-variant"), width=1, style="solid")
+)
+
+_STYLE_INPUT_WIDTH = me.Style(width="100%")
+_STYLE_SLIDER_INPUT_BOX = me.Style(display="flex", flex_wrap="wrap")
+_STYLE_SLIDER_WRAP = me.Style(flex_grow=1)
+_STYLE_SLIDER_LABEL = me.Style(padding=me.Padding(bottom=10))
+_STYLE_SLIDER = me.Style(width="90%")
+_STYLE_SLIDER_INPUT = me.Style(width=75)
+
+_STYLE_STOP_SEQUENCE_BOX = me.Style(display="flex")
+_STYLE_STOP_SEQUENCE_WRAP = me.Style(flex_grow=1)
+
+_STYLE_CONTAINER = me.Style(
+ display="grid",
+ grid_template_columns="5fr 2fr",
+ grid_template_rows="auto 5fr",
+ height="100vh",
+)
+
+_STYLE_MAIN_HEADER = me.Style(
+ border=_DEFAULT_BORDER, padding=me.Padding.all(15)
+)
+
+_STYLE_MAIN_COLUMN = me.Style(
+ border=_DEFAULT_BORDER,
+ padding=me.Padding.all(15),
+ overflow_y="scroll",
+)
+
+_STYLE_CONFIG_COLUMN = me.Style(
+ border=_DEFAULT_BORDER,
+ padding=me.Padding.all(15),
+ overflow_y="scroll",
+)
+
+_STYLE_TITLE_BOX = me.Style(display="inline-block")
+
+_STYLE_CONFIG_HEADER = me.Style(
+ border=_DEFAULT_BORDER, padding=me.Padding.all(10)
+)
+
+_STYLE_STOP_SEQUENCE_CHIP = me.Style(margin=me.Margin.all(3))
+
+_STYLE_MODAL_CONTAINER = me.Style(
+ background=me.theme_var("surface-container-high"),
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
+ width="min(1024px, 100%)",
+ box_sizing="content-box",
+ height="100vh",
+ overflow_y="scroll",
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
+)
+
+_STYLE_MODAL_CONTENT = me.Style(margin=me.Margin.all(30))
+
+_STYLE_CODE_BOX = me.Style(
+ font_size=13,
+ margin=me.Margin.symmetric(vertical=10, horizontal=0),
+)
diff --git a/llm_rewriter.py b/llm_rewriter.py
new file mode 100644
index 0000000000000000000000000000000000000000..32280693576c9eebd078ce0106438bf94bdd0c23
--- /dev/null
+++ b/llm_rewriter.py
@@ -0,0 +1,408 @@
+import random
+import time
+from dataclasses import dataclass
+from typing import Literal
+
+import mesop as me
+
+Role = Literal["user", "assistant"]
+
+
+@dataclass(kw_only=True)
+class ChatMessage:
+ """Chat message metadata."""
+
+ role: Role = "user"
+ content: str = ""
+ edited: bool = False
+
+
+@me.stateclass
+class State:
+ input: str
+ output: list[ChatMessage]
+ in_progress: bool
+ rewrite: str
+ rewrite_message_index: int
+ preview_rewrite: str
+ preview_original: str
+ modal_open: bool
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("light")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/llm_rewriter",
+ title="LLM Rewriter",
+)
+def page():
+ state = me.state(State)
+
+ # Modal
+ with me.box(style=_make_modal_background_style(state.modal_open)):
+ with me.box(style=_STYLE_MODAL_CONTAINER):
+ with me.box(style=_STYLE_MODAL_CONTENT):
+ me.textarea(
+ label="Rewrite",
+ style=_STYLE_INPUT_WIDTH,
+ value=state.rewrite,
+ on_input=on_rewrite_input,
+ )
+ with me.box():
+ me.button(
+ "Submit Rewrite",
+ color="primary",
+ type="flat",
+ on_click=on_click_submit_rewrite,
+ )
+ me.button(
+ "Cancel",
+ on_click=on_click_cancel_rewrite,
+ )
+ with me.box(style=_STYLE_PREVIEW_CONTAINER):
+ with me.box(style=_STYLE_PREVIEW_ORIGINAL):
+ me.text("Original Message", type="headline-6")
+ me.markdown(state.preview_original)
+
+ with me.box(style=_STYLE_PREVIEW_REWRITE):
+ me.text("Preview Rewrite", type="headline-6")
+ me.markdown(state.preview_rewrite)
+
+ # Chat UI
+ with me.box(style=_STYLE_APP_CONTAINER):
+ me.text(_TITLE, type="headline-5", style=_STYLE_TITLE)
+ with me.box(style=_STYLE_CHAT_BOX):
+ for index, msg in enumerate(state.output):
+ with me.box(
+ style=_make_style_chat_bubble_wrapper(msg.role),
+ key=f"msg-{index}",
+ on_click=on_click_rewrite_msg,
+ ):
+ if msg.role == _ROLE_ASSISTANT:
+ me.text(
+ _display_username(_BOT_USER_DEFAULT, msg.edited),
+ style=_STYLE_CHAT_BUBBLE_NAME,
+ )
+ with me.box(style=_make_chat_bubble_style(msg.role, msg.edited)):
+ if msg.role == _ROLE_USER:
+ me.text(msg.content, style=_STYLE_CHAT_BUBBLE_PLAINTEXT)
+ else:
+ me.markdown(msg.content)
+ with me.tooltip(message="Rewrite response"):
+ me.icon(icon="edit_note")
+
+ if state.in_progress:
+ with me.box(key="scroll-to", style=me.Style(height=250)):
+ pass
+ with me.box(style=_STYLE_CHAT_INPUT_BOX):
+ with me.box(style=me.Style(flex_grow=1)):
+ me.input(
+ label=_LABEL_INPUT,
+ # Workaround: update key to clear input.
+ key=f"input-{len(state.output)}",
+ on_input=on_chat_input,
+ on_enter=on_click_submit_chat_msg,
+ style=_STYLE_CHAT_INPUT,
+ )
+ with me.content_button(
+ color="primary",
+ type="flat",
+ disabled=state.in_progress,
+ on_click=on_click_submit_chat_msg,
+ style=_STYLE_CHAT_BUTTON,
+ ):
+ me.icon(
+ _LABEL_BUTTON_IN_PROGRESS if state.in_progress else _LABEL_BUTTON
+ )
+
+
+# Event Handlers
+
+
+def on_chat_input(e: me.InputEvent):
+ """Capture chat text input."""
+ state = me.state(State)
+ state.input = e.value
+
+
+def on_rewrite_input(e: me.InputEvent):
+ """Capture rewrite text input."""
+ state = me.state(State)
+ state.preview_rewrite = e.value
+
+
+def on_click_rewrite_msg(e: me.ClickEvent):
+ """Shows rewrite modal when a message is clicked.
+
+ Edit this function to persist rewritten messages.
+ """
+ state = me.state(State)
+ index = int(e.key.replace("msg-", ""))
+ message = state.output[index]
+ if message.role == _ROLE_USER or state.in_progress:
+ return
+ state.modal_open = True
+ state.rewrite = message.content
+ state.rewrite_message_index = index
+ state.preview_original = message.content
+ state.preview_rewrite = message.content
+
+
+def on_click_submit_rewrite(e: me.ClickEvent):
+ """Submits rewrite message."""
+ state = me.state(State)
+ state.modal_open = False
+ message = state.output[state.rewrite_message_index]
+ if message.content != state.preview_rewrite:
+ message.content = state.preview_rewrite
+ message.edited = True
+ state.rewrite_message_index = 0
+ state.rewrite = ""
+ state.preview_original = ""
+ state.preview_rewrite = ""
+
+
+def on_click_cancel_rewrite(e: me.ClickEvent):
+ """Hides rewrite modal."""
+ state = me.state(State)
+ state.modal_open = False
+ state.rewrite_message_index = 0
+ state.rewrite = ""
+ state.preview_original = ""
+ state.preview_rewrite = ""
+
+
+def on_click_submit_chat_msg(e: me.ClickEvent | me.InputEnterEvent):
+ """Handles submitting a chat message."""
+ state = me.state(State)
+ if state.in_progress or not state.input:
+ return
+ input = state.input
+ state.input = ""
+ yield
+
+ output = state.output
+ if output is None:
+ output = []
+ output.append(ChatMessage(role=_ROLE_USER, content=input))
+ state.in_progress = True
+ me.scroll_into_view(key="scroll-to")
+ yield
+
+ start_time = time.time()
+ output_message = respond_to_chat(input, state.output)
+ assistant_message = ChatMessage(role=_ROLE_ASSISTANT)
+ output.append(assistant_message)
+ state.output = output
+ for content in output_message:
+ assistant_message.content += content
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
+ if (time.time() - start_time) >= 0.25:
+ start_time = time.time()
+ yield
+
+ state.in_progress = False
+ yield
+
+
+# Transform function for processing chat messages.
+
+
+def respond_to_chat(input: str, history: list[ChatMessage]):
+ """Displays random canned text.
+
+ Edit this function to process messages with a real chatbot/LLM.
+ """
+ lines = [
+ (
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, "
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+ ),
+ "Laoreet sit amet cursus sit amet dictum sit amet.",
+ "At lectus urna duis convallis.",
+ "A pellentesque sit amet porttitor eget.",
+ "Mauris nunc congue nisi vitae suscipit tellus mauris a diam.",
+ "Aliquet lectus proin nibh nisl condimentum id.",
+ "Integer malesuada nunc vel risus commodo viverra maecenas accumsan.",
+ "Tempor id eu nisl nunc mi.",
+ "Id consectetur purus ut faucibus pulvinar.",
+ "Mauris pharetra et ultrices neque ornare.",
+ "Facilisis magna etiam tempor orci.",
+ "Mauris pharetra et ultrices neque.",
+ "Sit amet facilisis magna etiam tempor orci.",
+ "Amet consectetur adipiscing elit pellentesque habitant morbi tristique.",
+ "Egestas erat imperdiet sed euismod.",
+ "Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida.",
+ "Habitant morbi tristique senectus et netus et malesuada.",
+ ]
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
+ time.sleep(0.25)
+ yield line + " "
+
+
+# Constants
+
+_TITLE = "LLM Rewriter"
+
+_ROLE_USER = "user"
+_ROLE_ASSISTANT = "assistant"
+
+_BOT_USER_DEFAULT = "mesop-bot"
+
+
+# Styles
+
+_COLOR_BACKGROUND = me.theme_var("background")
+_COLOR_CHAT_BUBBLE_YOU = me.theme_var("surface-container-low")
+_COLOR_CHAT_BUBBLE_BOT = me.theme_var("secondary-container")
+_COLOR_CHAT_BUUBBLE_EDITED = me.theme_var("tertiary-container")
+
+_DEFAULT_PADDING = me.Padding.all(20)
+_DEFAULT_BORDER_SIDE = me.BorderSide(
+ width="1px", style="solid", color=me.theme_var("secondary-fixed")
+)
+
+_LABEL_BUTTON = "send"
+_LABEL_BUTTON_IN_PROGRESS = "pending"
+_LABEL_INPUT = "Enter your prompt"
+
+_STYLE_INPUT_WIDTH = me.Style(width="100%")
+
+_STYLE_APP_CONTAINER = me.Style(
+ background=_COLOR_BACKGROUND,
+ display="flex",
+ flex_direction="column",
+ height="100%",
+ margin=me.Margin.symmetric(vertical=0, horizontal="auto"),
+ width="min(1024px, 100%)",
+ box_shadow="0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f",
+ padding=me.Padding(top=20, left=20, right=20),
+)
+_STYLE_TITLE = me.Style(padding=me.Padding(left=10))
+_STYLE_CHAT_BOX = me.Style(
+ flex_grow=1,
+ overflow_y="scroll",
+ padding=_DEFAULT_PADDING,
+ margin=me.Margin(bottom=20),
+ border_radius="10px",
+ border=me.Border(
+ left=_DEFAULT_BORDER_SIDE,
+ right=_DEFAULT_BORDER_SIDE,
+ top=_DEFAULT_BORDER_SIDE,
+ bottom=_DEFAULT_BORDER_SIDE,
+ ),
+)
+_STYLE_CHAT_INPUT = me.Style(width="100%")
+_STYLE_CHAT_INPUT_BOX = me.Style(
+ padding=me.Padding(top=30), display="flex", flex_direction="row"
+)
+_STYLE_CHAT_BUTTON = me.Style(margin=me.Margin(top=8, left=8))
+_STYLE_CHAT_BUBBLE_NAME = me.Style(
+ font_weight="bold",
+ font_size="12px",
+ padding=me.Padding(left=15, right=15, bottom=5),
+)
+_STYLE_CHAT_BUBBLE_PLAINTEXT = me.Style(margin=me.Margin.symmetric(vertical=15))
+
+_STYLE_MODAL_CONTAINER = me.Style(
+ background=me.theme_var("surface-container"),
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
+ width="min(1024px, 100%)",
+ box_sizing="content-box",
+ height="100%",
+ overflow_y="scroll",
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
+)
+
+_STYLE_MODAL_CONTENT = me.Style(margin=me.Margin.all(20))
+
+_STYLE_PREVIEW_CONTAINER = me.Style(
+ display="grid",
+ grid_template_columns="repeat(2, 1fr)",
+)
+
+_STYLE_PREVIEW_ORIGINAL = me.Style(
+ color=me.theme_var("on-surface"), padding=_DEFAULT_PADDING
+)
+
+_STYLE_PREVIEW_REWRITE = me.Style(
+ background=_COLOR_CHAT_BUUBBLE_EDITED, padding=_DEFAULT_PADDING
+)
+
+
+def _make_style_chat_bubble_wrapper(role: Role) -> me.Style:
+ """Generates styles for chat bubble position.
+
+ Args:
+ role: Chat bubble alignment depends on the role
+ """
+ align_items = "end" if role == _ROLE_USER else "start"
+ return me.Style(
+ display="flex",
+ flex_direction="column",
+ align_items=align_items,
+ )
+
+
+def _make_chat_bubble_style(role: Role, edited: bool) -> me.Style:
+ """Generates styles for chat bubble.
+
+ Args:
+ role: Chat bubble background color depends on the role
+ edited: Whether chat message was edited or not.
+ """
+ background = _COLOR_CHAT_BUBBLE_YOU
+ if role == _ROLE_ASSISTANT:
+ background = _COLOR_CHAT_BUBBLE_BOT
+ if edited:
+ background = _COLOR_CHAT_BUUBBLE_EDITED
+
+ return me.Style(
+ width="80%",
+ font_size="13px",
+ background=background,
+ border_radius="15px",
+ padding=me.Padding(right=15, left=15, bottom=3),
+ margin=me.Margin(bottom=10),
+ border=me.Border(
+ left=_DEFAULT_BORDER_SIDE,
+ right=_DEFAULT_BORDER_SIDE,
+ top=_DEFAULT_BORDER_SIDE,
+ bottom=_DEFAULT_BORDER_SIDE,
+ ),
+ )
+
+
+def _make_modal_background_style(modal_open: bool) -> me.Style:
+ """Makes style for modal background.
+
+ Args:
+ modal_open: Whether the modal is open.
+ """
+ return me.Style(
+ display="block" if modal_open else "none",
+ position="fixed",
+ z_index=1000,
+ width="100%",
+ height="100%",
+ overflow_x="auto",
+ overflow_y="auto",
+ background="rgba(0,0,0,0.4)",
+ )
+
+
+def _display_username(username: str, edited: bool = False) -> str:
+ """Displays the username
+
+ Args:
+ username: Name of the user
+ edited: Whether the message has been edited.
+ """
+ edited_text = " (edited)" if edited else ""
+ return username + edited_text
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..99e7d016f0a875fba6b45da71daf3e5de8e3f841
--- /dev/null
+++ b/main.py
@@ -0,0 +1,678 @@
+# Disable import sort ordering due to the hack needed
+# to ensure local imports.
+# ruff: noqa: E402
+
+import base64
+import inspect
+import os
+import sys
+from dataclasses import dataclass
+from typing import Literal
+
+import mesop as me
+
+# Append the current directory to sys.path to ensure local imports work
+# This is required so mesop/examples/__init__.py can import the modules
+# imported below.
+current_dir = os.path.dirname(os.path.abspath(__file__))
+if current_dir not in sys.path:
+ sys.path.append(current_dir)
+
+import glob
+
+import audio as audio
+import autocomplete as autocomplete
+import badge as badge
+import basic_animation as basic_animation
+import box as box
+import button as button
+import chat as chat
+import chat_inputs as chat_inputs
+import checkbox as checkbox
+import code_demo as code_demo # cannot call it code due to python library naming conflict
+import density as density
+import dialog as dialog
+import divider as divider
+import embed as embed
+import fancy_chat as fancy_chat
+import feedback as feedback
+import form_billing as form_billing
+import form_profile as form_profile
+import grid_table as grid_table
+import headers as headers
+import html_demo as html_demo
+import icon as icon
+import image as image
+import input as input
+import link as link
+import llm_playground as llm_playground
+import llm_rewriter as llm_rewriter
+import markdown_demo as markdown_demo # cannot call it markdown due to python library naming conflict
+import markdown_editor as markdown_editor
+import plot as plot
+import progress_bar as progress_bar
+import progress_spinner as progress_spinner
+import radio as radio
+import select_demo as select_demo # cannot call it select due to python library naming conflict
+import sidenav as sidenav
+import slide_toggle as slide_toggle
+import slider as slider
+import snackbar as snackbar
+import table as table
+import text as text
+import text_to_image as text_to_image
+import text_to_text as text_to_text
+import textarea as textarea
+import tooltip as tooltip
+import uploader as uploader
+import video as video
+
+
+@dataclass
+class Example:
+ # module_name (should also be the path name)
+ name: str
+
+
+@dataclass
+class Section:
+ name: str
+ examples: list[Example]
+
+
+FIRST_SECTIONS = [
+ Section(
+ name="Quick start",
+ examples=[
+ Example(name="chat"),
+ Example(name="text_to_image"),
+ Example(name="text_to_text"),
+ ],
+ ),
+ Section(
+ name="Use cases",
+ examples=[
+ Example(name="fancy_chat"),
+ Example(name="llm_rewriter"),
+ Example(name="llm_playground"),
+ Example(name="markdown_editor"),
+ ],
+ ),
+ Section(
+ name="Patterns",
+ examples=[
+ Example(name="dialog"),
+ Example(name="grid_table"),
+ Example(name="headers"),
+ Example(name="snackbar"),
+ Example(name="chat_inputs"),
+ Example(name="form_billing"),
+ Example(name="form_profile"),
+ ],
+ ),
+ Section(
+ name="Features",
+ examples=[
+ Example(name="density"),
+ ],
+ ),
+ Section(
+ name="Misc",
+ examples=[
+ Example(name="basic_animation"),
+ Example(name="feedback"),
+ ],
+ ),
+]
+
+COMPONENTS_SECTIONS = [
+ Section(
+ name="Layout",
+ examples=[
+ Example(name="box"),
+ Example(name="sidenav"),
+ ],
+ ),
+ Section(
+ name="Text",
+ examples=[
+ Example(name="text"),
+ Example(name="markdown_demo"),
+ Example(name="code_demo"),
+ ],
+ ),
+ Section(
+ name="Media",
+ examples=[
+ Example(name="image"),
+ Example(name="audio"),
+ Example(name="video"),
+ ],
+ ),
+ Section(
+ name="Form",
+ examples=[
+ Example(name="autocomplete"),
+ Example(name="button"),
+ Example(name="checkbox"),
+ Example(name="input"),
+ Example(name="textarea"),
+ Example(name="radio"),
+ Example(name="select_demo"),
+ Example(name="slide_toggle"),
+ Example(name="slider"),
+ Example(name="uploader"),
+ ],
+ ),
+ Section(
+ name="Visual",
+ examples=[
+ Example(name="badge"),
+ Example(name="divider"),
+ Example(name="icon"),
+ Example(name="progress_bar"),
+ Example(name="progress_spinner"),
+ Example(name="table"),
+ Example(name="tooltip"),
+ ],
+ ),
+ Section(
+ name="Web",
+ examples=[
+ Example(name="embed"),
+ Example(name="html_demo"),
+ Example(name="link"),
+ ],
+ ),
+ Section(
+ name="Others",
+ examples=[
+ Example(name="plot"),
+ ],
+ ),
+]
+
+ALL_SECTIONS = FIRST_SECTIONS + COMPONENTS_SECTIONS
+
+BORDER_SIDE = me.BorderSide(
+ style="solid",
+ width=1,
+ color="#dcdcdc",
+)
+
+
+@me.stateclass
+class State:
+ current_demo: str
+ panel_fullscreen: Literal["preview", "editor", None] = None
+
+
+screenshots: dict[str, str] = {}
+
+
+def load_home_page(e: me.LoadEvent):
+ if me.state(ThemeState).dark_mode:
+ me.set_theme_mode("dark")
+ else:
+ me.set_theme_mode("system")
+ yield
+ screenshot_dir = os.path.join(current_dir, "screenshots")
+ screenshot_files = glob.glob(os.path.join(screenshot_dir, "*.webp"))
+
+ for screenshot_file in screenshot_files:
+ image_name = os.path.basename(screenshot_file).split(".")[0]
+ with open(screenshot_file, "rb") as image_file:
+ encoded_string = base64.b64encode(image_file.read()).decode()
+ screenshots[image_name] = "data:image/webp;base64," + encoded_string
+
+ yield
+
+
+@me.page(
+ title="Mesop Demos",
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ on_load=load_home_page,
+)
+def main_page():
+ header()
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("background"),
+ flex_grow=1,
+ display="flex",
+ )
+ ):
+ if is_desktop():
+ side_menu()
+ with me.box(
+ style=me.Style(
+ width="calc(100% - 150px)" if is_desktop() else "100%",
+ display="flex",
+ gap=24,
+ flex_direction="column",
+ padding=me.Padding.all(24),
+ overflow_y="auto",
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ height="calc(100vh - 120px)",
+ )
+ ):
+ for section in ALL_SECTIONS:
+ with me.box(style=me.Style(margin=me.Margin(bottom=28))):
+ me.text(
+ section.name,
+ style=me.Style(
+ font_weight=500,
+ font_size=20,
+ margin=me.Margin(
+ bottom=16,
+ ),
+ ),
+ )
+ with me.box(
+ style=me.Style(
+ display="flex",
+ flex_direction="row",
+ flex_wrap="wrap",
+ gap=28,
+ )
+ ):
+ for example in section.examples:
+ example_card(example.name)
+
+
+def navigate_example_card(e: me.ClickEvent):
+ me.navigate("/embed/" + e.key)
+
+
+def example_card(name: str):
+ with me.box(
+ key=name,
+ on_click=navigate_example_card,
+ style=me.Style(
+ border=me.Border.all(
+ me.BorderSide(
+ width=1,
+ color="rgb(220, 220, 220)",
+ style="solid",
+ )
+ ),
+ box_shadow="rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, rgba(0, 0, 0, 0.14) 0px 2px 2px, rgba(0, 0, 0, 0.12) 0px 1px 5px",
+ cursor="pointer",
+ width="min(100%, 150px)",
+ border_radius=12,
+ background=me.theme_var("background"),
+ ),
+ ):
+ image_url = screenshots.get(name, "")
+ me.box(
+ style=me.Style(
+ background=f'url("{image_url}") center / cover',
+ height=112,
+ width=150,
+ )
+ )
+ me.text(
+ format_example_name(name),
+ style=me.Style(
+ font_weight=500,
+ font_size=18,
+ padding=me.Padding.all(12),
+ border=me.Border(
+ top=me.BorderSide(
+ width=1,
+ style="solid",
+ color="rgb(220, 220, 220)",
+ )
+ ),
+ ),
+ )
+
+
+def on_load_embed(e: me.LoadEvent):
+ if me.state(ThemeState).dark_mode:
+ me.set_theme_mode("dark")
+ else:
+ me.set_theme_mode("system")
+ if not is_desktop():
+ me.state(State).panel_fullscreen = "preview"
+
+
+def create_main_fn(example: Example):
+ @me.page(
+ on_load=on_load_embed,
+ title="Mesop Demos",
+ path="/embed/" + example.name,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ )
+ def main():
+ with me.box(
+ style=me.Style(
+ height="100%",
+ display="flex",
+ flex_direction="column",
+ background=me.theme_var("background"),
+ )
+ ):
+ header(demo_name=example.name)
+ body(example.name)
+
+ return main
+
+
+for section in FIRST_SECTIONS + COMPONENTS_SECTIONS:
+ for example in section.examples:
+ create_main_fn(example)
+
+
+def body(current_demo: str):
+ state = me.state(State)
+ with me.box(
+ style=me.Style(
+ flex_grow=1,
+ display="flex",
+ )
+ ):
+ if is_desktop():
+ side_menu()
+ src = "/" + current_demo
+ with me.box(
+ style=me.Style(
+ width="calc(100% - 150px)" if is_desktop() else "100%",
+ display="grid",
+ grid_template_columns="1fr 1fr"
+ if state.panel_fullscreen is None
+ else "1fr",
+ )
+ ):
+ if state.panel_fullscreen != "editor":
+ demo_ui(src)
+ if state.panel_fullscreen != "preview":
+ demo_code(inspect.getsource(get_module(current_demo)))
+
+
+def demo_ui(src: str):
+ state = me.state(State)
+ with me.box(
+ style=me.Style(flex_grow=1),
+ ):
+ with me.box(
+ style=me.Style(
+ display="flex",
+ justify_content="space-between",
+ align_items="center",
+ border=me.Border(bottom=BORDER_SIDE),
+ )
+ ):
+ me.text(
+ "Preview",
+ style=me.Style(
+ font_weight=500,
+ padding=me.Padding.all(14),
+ ),
+ )
+ if is_desktop():
+ with me.tooltip(
+ position="above",
+ message="Minimize"
+ if state.panel_fullscreen == "preview"
+ else "Maximize",
+ ):
+ with me.content_button(type="icon", on_click=toggle_fullscreen):
+ me.icon(
+ "close_fullscreen"
+ if state.panel_fullscreen == "preview"
+ else "fullscreen"
+ )
+ else:
+ swap_button()
+ me.embed(
+ src=src,
+ style=me.Style(
+ border=me.Border.all(me.BorderSide(width=0)),
+ border_radius=2,
+ height="calc(100vh - 106px)",
+ width="100%",
+ ),
+ )
+
+
+def swap_button():
+ state = me.state(State)
+ with me.tooltip(
+ position="above",
+ message="Swap for code"
+ if state.panel_fullscreen == "preview"
+ else "Swap for preview",
+ ):
+ with me.content_button(type="icon", on_click=swap_fullscreen):
+ me.icon("swap_horiz")
+
+
+def swap_fullscreen(e: me.ClickEvent):
+ state = me.state(State)
+ if state.panel_fullscreen == "preview":
+ state.panel_fullscreen = "editor"
+ else:
+ state.panel_fullscreen = "preview"
+
+
+def toggle_fullscreen(e: me.ClickEvent):
+ state = me.state(State)
+ if state.panel_fullscreen == "preview":
+ state.panel_fullscreen = None
+ else:
+ state.panel_fullscreen = "preview"
+
+
+def demo_code(code_arg: str):
+ with me.box(
+ style=me.Style(
+ flex_grow=1,
+ overflow_x="hidden",
+ overflow_y="hidden",
+ border=me.Border(
+ left=BORDER_SIDE,
+ ),
+ background=me.theme_var("surface-container-low"),
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ display="flex",
+ justify_content="space-between",
+ align_items="center",
+ border=me.Border(bottom=BORDER_SIDE),
+ background=me.theme_var("background"),
+ )
+ ):
+ me.text(
+ "Code",
+ style=me.Style(
+ font_weight=500,
+ padding=me.Padding.all(14),
+ ),
+ )
+ if not is_desktop():
+ swap_button()
+ # Use four backticks for code fence to avoid conflicts with backticks being used
+ # within the displayed code.
+ me.markdown(
+ f"""````python
+{code_arg}
+````
+ """,
+ style=me.Style(
+ border=me.Border(
+ right=BORDER_SIDE,
+ ),
+ font_size=13,
+ height="calc(100vh - 106px)",
+ overflow_y="auto",
+ width="100%",
+ ),
+ )
+
+
+def header(demo_name: str | None = None):
+ with me.box(
+ style=me.Style(
+ border=me.Border(
+ bottom=me.BorderSide(
+ style="solid",
+ width=1,
+ color="#dcdcdc",
+ )
+ ),
+ overflow_x="clip",
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ display="flex",
+ align_items="end",
+ justify_content="space-between",
+ margin=me.Margin(left=12, right=12, bottom=12),
+ font_size=24,
+ )
+ ):
+ with me.box(style=me.Style(display="flex")):
+ with me.box(
+ style=me.Style(display="flex", cursor="pointer"),
+ on_click=navigate_home,
+ ):
+ me.text(
+ "Mesop", style=me.Style(font_weight=700, margin=me.Margin(right=8))
+ )
+ me.text("Demos ")
+ if demo_name:
+ me.text(
+ "— " + format_example_name(demo_name),
+ style=me.Style(white_space="nowrap", text_overflow="ellipsis"),
+ )
+ with me.box(style=me.Style(display="flex", align_items="baseline")):
+ with me.box(
+ style=me.Style(
+ display="flex",
+ align_items="baseline",
+ ),
+ ):
+ me.link(
+ text="google/mesop",
+ url="https://github.com/google/mesop/",
+ open_in_new_tab=True,
+ style=me.Style(
+ font_size=18,
+ color=me.theme_var("primary"),
+ text_decoration="none",
+ margin=me.Margin(left=8, right=4, bottom=-16, top=-16),
+ ),
+ )
+ me.text(
+ "v" + me.__version__,
+ style=me.Style(font_size=18, margin=me.Margin(left=16)),
+ )
+ with me.content_button(
+ type="icon",
+ style=me.Style(left=8, right=4, top=4),
+ on_click=toggle_theme,
+ ):
+ me.icon(
+ "light_mode" if me.theme_brightness() == "dark" else "dark_mode"
+ )
+
+
+@me.stateclass
+class ThemeState:
+ dark_mode: bool
+
+
+def toggle_theme(e: me.ClickEvent):
+ if me.theme_brightness() == "light":
+ me.set_theme_mode("dark")
+ me.state(ThemeState).dark_mode = True
+ else:
+ me.set_theme_mode("light")
+ me.state(ThemeState).dark_mode = False
+
+
+def navigate_home(e: me.ClickEvent):
+ me.navigate("/")
+
+
+def side_menu():
+ with me.box(
+ style=me.Style(
+ padding=me.Padding.all(12),
+ width=150,
+ flex_grow=0,
+ line_height="1.5",
+ border=me.Border(right=BORDER_SIDE),
+ overflow_x="hidden",
+ height="calc(100vh - 60px)",
+ overflow_y="auto",
+ )
+ ):
+ for section in FIRST_SECTIONS:
+ nav_section(section)
+ with me.box(
+ style=me.Style(
+ margin=me.Margin.symmetric(
+ horizontal=-16,
+ vertical=16,
+ ),
+ )
+ ):
+ me.divider()
+ me.text(
+ "Components",
+ style=me.Style(
+ letter_spacing="0.5px",
+ margin=me.Margin(bottom=6),
+ ),
+ )
+ for section in COMPONENTS_SECTIONS:
+ nav_section(section)
+
+
+def nav_section(section: Section):
+ with me.box(style=me.Style(margin=me.Margin(bottom=12))):
+ me.text(section.name, style=me.Style(font_weight=700))
+ for example in section.examples:
+ example_name = format_example_name(example.name)
+ path = f"/embed/{example.name}"
+ with me.box(
+ style=me.Style(color=me.theme_var("primary"), cursor="pointer"),
+ on_click=set_demo,
+ key=path,
+ ):
+ me.text(example_name)
+
+
+def set_demo(e: me.ClickEvent):
+ me.navigate(e.key)
+
+
+def format_example_name(name: str):
+ return (
+ (" ".join(name.split("_")))
+ .capitalize()
+ .replace("Llm", "LLM")
+ .replace(" demo", "")
+ )
+
+
+def get_module(module_name: str):
+ if module_name in globals():
+ return globals()[module_name]
+ raise me.MesopDeveloperException(f"Module {module_name} not supported")
+
+
+def is_desktop():
+ return me.viewport_size().width > 760
diff --git a/markdown_demo.py b/markdown_demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c38886f6e01d9af36251ce8a05a34dab634b038
--- /dev/null
+++ b/markdown_demo.py
@@ -0,0 +1,86 @@
+import mesop as me
+
+SAMPLE_MARKDOWN = """
+# Sample Markdown Document
+
+## Table of Contents
+1. [Headers](#headers)
+2. [Emphasis](#emphasis)
+3. [Lists](#lists)
+4. [Links](#links)
+5. [Code](#code)
+6. [Blockquotes](#blockquotes)
+7. [Tables](#tables)
+8. [Horizontal Rules](#horizontal-rules)
+
+## Headers
+# Header 1
+## Header 2
+### Header 3
+#### Header 4
+##### Header 5
+###### Header 6
+
+## Emphasis
+*Italic text* or _Italic text_
+**Bold text** or __Bold text__
+***Bold and Italic*** or ___Bold and Italic___
+
+## Lists
+
+### Unordered List
+- Item 1
+- Item 2
+ - Subitem 2.1
+ - Subitem 2.2
+
+### Ordered List
+1. First item
+2. Second item
+ 1. Subitem 2.1
+ 2. Subitem 2.2
+
+## Links
+[Google](https://www.google.com/)
+
+## Inline Code
+
+Inline `code`
+
+## Code
+
+```python
+import mesop as me
+
+
+@me.page(path="/hello_world")
+def app():
+ me.text("Hello World")
+```
+
+
+## Table
+
+First Header | Second Header
+------------- | -------------
+Content Cell | Content Cell
+Content Cell | Content Cell
+"""
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/markdown_demo",
+ on_load=on_load,
+)
+def app():
+ with me.box(
+ style=me.Style(background=me.theme_var("surface-container-lowest"))
+ ):
+ me.markdown(SAMPLE_MARKDOWN, style=me.Style(margin=me.Margin.all(15)))
diff --git a/markdown_editor.py b/markdown_editor.py
new file mode 100644
index 0000000000000000000000000000000000000000..71727fadbc2f29c49dc420c9b73ec8c92aeffeb0
--- /dev/null
+++ b/markdown_editor.py
@@ -0,0 +1,196 @@
+from dataclasses import dataclass, field
+
+import mesop as me
+
+_INTRO_TEXT = """
+# Mesop Markdown Editor Example
+
+This example shows how to make a simple markdown editor.
+""".strip()
+
+
+@dataclass(kw_only=True)
+class Note:
+ """Content of note."""
+
+ content: str = ""
+
+
+@me.stateclass
+class State:
+ notes: list[Note] = field(default_factory=lambda: [Note(content=_INTRO_TEXT)])
+ selected_note_index: int = 0
+ selected_note_content: str = _INTRO_TEXT
+ show_preview: bool = True
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/markdown_editor",
+ title="Markdown Editor",
+)
+def page():
+ state = me.state(State)
+
+ with me.box(style=_style_container(state.show_preview)):
+ # Note list column
+ with me.box(style=_STYLE_NOTES_NAV):
+ # Toolbar
+ with me.box(style=_STYLE_TOOLBAR):
+ with me.content_button(on_click=on_click_new):
+ with me.tooltip(message="New note"):
+ me.icon(icon="add_notes")
+ with me.content_button(on_click=on_click_hide):
+ with me.tooltip(
+ message="Hide preview" if state.show_preview else "Show preview"
+ ):
+ me.icon(icon="hide_image")
+
+ # Note list
+ for index, note in enumerate(state.notes):
+ with me.box(
+ key=f"note-{index}",
+ on_click=on_click_note,
+ style=_style_note_row(index == state.selected_note_index),
+ ):
+ me.text(_render_note_excerpt(note.content))
+
+ # Markdown Editor Column
+ with me.box(style=_STYLE_EDITOR):
+ me.native_textarea(
+ value=state.selected_note_content,
+ style=_STYLE_TEXTAREA,
+ on_input=on_text_input,
+ )
+
+ # Markdown Preview Column
+ if state.show_preview:
+ with me.box(style=_STYLE_PREVIEW):
+ if state.selected_note_index < len(state.notes):
+ me.markdown(state.notes[state.selected_note_index].content)
+
+
+# HELPERS
+
+_EXCERPT_CHAR_LIMIT = 90
+
+
+def _render_note_excerpt(content: str) -> str:
+ if len(content) <= _EXCERPT_CHAR_LIMIT:
+ return content
+ return content[:_EXCERPT_CHAR_LIMIT] + "..."
+
+
+# EVENT HANDLERS
+
+
+def on_click_new(e: me.ClickEvent):
+ state = me.state(State)
+ # Need to update the initial value of the editor text area so we can
+ # trigger a diff to reset the editor to empty. Need to yield this change.
+ # for this to work.
+ state.selected_note_content = state.notes[state.selected_note_index].content
+ yield
+ # Reset the initial value of the editor text area to empty since the new note
+ # has no content.
+ state.selected_note_content = ""
+ state.notes.append(Note())
+ state.selected_note_index = len(state.notes) - 1
+ yield
+
+
+def on_click_hide(e: me.ClickEvent):
+ """Hides/Shows preview Markdown pane."""
+ state = me.state(State)
+ state.show_preview = bool(not state.show_preview)
+
+
+def on_click_note(e: me.ClickEvent):
+ """Selects a note from the note list."""
+ state = me.state(State)
+ note_id = int(e.key.replace("note-", ""))
+ note = state.notes[note_id]
+ state.selected_note_index = note_id
+ state.selected_note_content = note.content
+
+
+def on_text_input(e: me.InputEvent):
+ """Captures text in editor."""
+ state = me.state(State)
+ state.notes[state.selected_note_index].content = e.value
+
+
+# STYLES
+
+_BACKGROUND_COLOR = me.theme_var("surface-container-lowest")
+_FONT_COLOR = me.theme_var("on-surface-variant")
+_NOTE_ROW_FONT_COLOR = me.theme_var("on-surface")
+_NOTE_ROW_FONT_SIZE = "14px"
+_SELECTED_ROW_BACKGROUND_COLOR = me.theme_var("surface-variant")
+_DEFAULT_BORDER_STYLE = me.BorderSide(
+ width=1, style="solid", color=me.theme_var("outline-variant")
+)
+
+
+def _style_container(show_preview: bool = True) -> me.Style:
+ return me.Style(
+ background=_BACKGROUND_COLOR,
+ color=_FONT_COLOR,
+ display="grid",
+ grid_template_columns="2fr 4fr 4fr" if show_preview else "2fr 8fr",
+ height="100vh",
+ )
+
+
+def _style_note_row(selected: bool = False) -> me.Style:
+ return me.Style(
+ color=_NOTE_ROW_FONT_COLOR,
+ font_size=_NOTE_ROW_FONT_SIZE,
+ background=_SELECTED_ROW_BACKGROUND_COLOR if selected else "none",
+ padding=me.Padding.all(10),
+ border=me.Border(bottom=_DEFAULT_BORDER_STYLE),
+ height="100px",
+ overflow_x="hidden",
+ overflow_y="hidden",
+ )
+
+
+_STYLE_NOTES_NAV = me.Style(overflow_y="scroll", padding=me.Padding.all(15))
+
+
+_STYLE_TOOLBAR = me.Style(
+ padding=me.Padding.all(5),
+ border=me.Border(bottom=_DEFAULT_BORDER_STYLE),
+)
+
+
+_STYLE_EDITOR = me.Style(
+ overflow_y="hidden",
+ padding=me.Padding(left=20, right=15, top=20, bottom=0),
+ border=me.Border(
+ left=_DEFAULT_BORDER_STYLE,
+ right=_DEFAULT_BORDER_STYLE,
+ ),
+)
+
+
+_STYLE_PREVIEW = me.Style(
+ overflow_y="scroll", padding=me.Padding.symmetric(vertical=0, horizontal=20)
+)
+
+
+_STYLE_TEXTAREA = me.Style(
+ color=_FONT_COLOR,
+ background=_BACKGROUND_COLOR,
+ outline="none", # Hides focus border
+ border=me.Border.all(me.BorderSide(style="none")),
+ width="100%",
+ height="100%",
+)
diff --git a/plot.py b/plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e4a515d1dbe073fd9b2d70ef62f530f0de3a038
--- /dev/null
+++ b/plot.py
@@ -0,0 +1,25 @@
+from matplotlib.figure import Figure
+
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/plot",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ # Create matplotlib figure without using pyplot:
+ fig = Figure()
+ ax = fig.subplots() # type: ignore
+ ax.plot([1, 2]) # type: ignore
+
+ me.text("Example using matplotlib:", type="headline-5")
+ me.plot(fig, style=me.Style(width="100%"))
diff --git a/progress_bar.py b/progress_bar.py
new file mode 100644
index 0000000000000000000000000000000000000000..4505cc01260d72ed35ededbb285e6d3275f3e922
--- /dev/null
+++ b/progress_bar.py
@@ -0,0 +1,18 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/progress_bar",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text("Default progress bar", type="headline-5")
+ me.progress_bar()
diff --git a/progress_spinner.py b/progress_spinner.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d3fa73d541897c23318a65616e74b332201701a
--- /dev/null
+++ b/progress_spinner.py
@@ -0,0 +1,17 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/progress_spinner",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.progress_spinner()
diff --git a/radio.py b/radio.py
new file mode 100644
index 0000000000000000000000000000000000000000..de3d6a7a58ad9b13344d9cdf5c4f2e9addafa914
--- /dev/null
+++ b/radio.py
@@ -0,0 +1,37 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ radio_value: str = "2"
+
+
+def on_change(event: me.RadioChangeEvent):
+ s = me.state(State)
+ s.radio_value = event.value
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/radio",
+)
+def app():
+ s = me.state(State)
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text("Horizontal radio options")
+ me.radio(
+ on_change=on_change,
+ options=[
+ me.RadioOption(label="Option 1", value="1"),
+ me.RadioOption(label="Option 2", value="2"),
+ ],
+ value=s.radio_value,
+ )
+ me.text(text="Selected radio value: " + s.radio_value)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..29bbe8f85db142602bf08ef6f8ccc8fc62bb534a
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+mesop>=0.10.0
+Flask==3.0.0
+gunicorn==22.0.0
+Werkzeug==3.0.1
+# For examples:
+matplotlib
+numpy
+pandas
diff --git a/screenshot.ts b/screenshot.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c88da74bca79e9a08a59907ba9a4082b4c60fadb
--- /dev/null
+++ b/screenshot.ts
@@ -0,0 +1,30 @@
+import {test, expect} from '@playwright/test';
+
+import * as fs from 'fs';
+import * as path from 'path';
+
+// Filter for Python files (.py extension)
+const pythonDemoFiles = fs
+ .readdirSync(__dirname)
+ .filter((file) => path.extname(file) === '.py');
+
+console.log(pythonDemoFiles);
+
+// Remove the skip if you want to re-generate the screenshots.
+test('screenshot each demo', async ({page}) => {
+ // This will take a while.
+ test.setTimeout(0);
+
+ await page.setViewportSize({width: 400, height: 300});
+
+ for (const demoFile of pythonDemoFiles) {
+ const demo = demoFile.slice(0, -3);
+ await page.goto('/' + demo);
+ await new Promise((resolve) => setTimeout(resolve, 3000));
+ // Take a full-page screenshot
+ await page.screenshot({
+ path: `demo/screenshots/${demo}.png`,
+ fullPage: true,
+ });
+ }
+});
diff --git a/screenshots/audio.png b/screenshots/audio.png
new file mode 100644
index 0000000000000000000000000000000000000000..04d1a0dd722f2992cd140706e7a5e820369d1cbf
Binary files /dev/null and b/screenshots/audio.png differ
diff --git a/screenshots/audio.webp b/screenshots/audio.webp
new file mode 100644
index 0000000000000000000000000000000000000000..1b92e6f4642e0a81a19551d21a3e7364b22ec17b
Binary files /dev/null and b/screenshots/audio.webp differ
diff --git a/screenshots/autocomplete.png b/screenshots/autocomplete.png
new file mode 100644
index 0000000000000000000000000000000000000000..01ccb80181755aa36f3dd63ac13013459ef06b48
Binary files /dev/null and b/screenshots/autocomplete.png differ
diff --git a/screenshots/autocomplete.webp b/screenshots/autocomplete.webp
new file mode 100644
index 0000000000000000000000000000000000000000..8f7196bece25986d98a57eb638a7af3a0ae36c82
Binary files /dev/null and b/screenshots/autocomplete.webp differ
diff --git a/screenshots/badge.png b/screenshots/badge.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e1b644f28d2cec368691709708dd3c7d85fc1cf
Binary files /dev/null and b/screenshots/badge.png differ
diff --git a/screenshots/badge.webp b/screenshots/badge.webp
new file mode 100644
index 0000000000000000000000000000000000000000..7998e83357d4f7df0ac79345f02c528f773828d4
Binary files /dev/null and b/screenshots/badge.webp differ
diff --git a/screenshots/basic_animation.png b/screenshots/basic_animation.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e577fd78345188164968a614dea876c8eca8428
Binary files /dev/null and b/screenshots/basic_animation.png differ
diff --git a/screenshots/basic_animation.webp b/screenshots/basic_animation.webp
new file mode 100644
index 0000000000000000000000000000000000000000..3923c89e77f7fda648d8e077eddafaf4ac6dcd7a
Binary files /dev/null and b/screenshots/basic_animation.webp differ
diff --git a/screenshots/box.png b/screenshots/box.png
new file mode 100644
index 0000000000000000000000000000000000000000..dda0c74f67673956860cb790729dc0d308c0acc2
Binary files /dev/null and b/screenshots/box.png differ
diff --git a/screenshots/box.webp b/screenshots/box.webp
new file mode 100644
index 0000000000000000000000000000000000000000..82d0c6411ec26758aa075d905c358f9b42b1f31b
Binary files /dev/null and b/screenshots/box.webp differ
diff --git a/screenshots/button.png b/screenshots/button.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fe389ba2f8bb64b8e8afe073d98a1527bd60107
Binary files /dev/null and b/screenshots/button.png differ
diff --git a/screenshots/button.webp b/screenshots/button.webp
new file mode 100644
index 0000000000000000000000000000000000000000..7bc5a8116ca352be2f42a9aeb3cd23767ee89a71
Binary files /dev/null and b/screenshots/button.webp differ
diff --git a/screenshots/chat.png b/screenshots/chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f1a49fb0a45f4cadf931a82683925cf7a923403
Binary files /dev/null and b/screenshots/chat.png differ
diff --git a/screenshots/chat.webp b/screenshots/chat.webp
new file mode 100644
index 0000000000000000000000000000000000000000..867a5a1799fe6690afefde122e4afca791334506
Binary files /dev/null and b/screenshots/chat.webp differ
diff --git a/screenshots/chat_inputs.png b/screenshots/chat_inputs.png
new file mode 100644
index 0000000000000000000000000000000000000000..474bcd9dd9be2d1f507633b7bda865af5286ee91
Binary files /dev/null and b/screenshots/chat_inputs.png differ
diff --git a/screenshots/chat_inputs.webp b/screenshots/chat_inputs.webp
new file mode 100644
index 0000000000000000000000000000000000000000..e926b794208f1d324e2c5184379f928377d1a04a
Binary files /dev/null and b/screenshots/chat_inputs.webp differ
diff --git a/screenshots/checkbox.png b/screenshots/checkbox.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc2b703e13511305b1d05f36e28da790778a0245
Binary files /dev/null and b/screenshots/checkbox.png differ
diff --git a/screenshots/checkbox.webp b/screenshots/checkbox.webp
new file mode 100644
index 0000000000000000000000000000000000000000..8986af1283b5f210f796fe8a15c7a489d15e88f9
Binary files /dev/null and b/screenshots/checkbox.webp differ
diff --git a/screenshots/code_demo.png b/screenshots/code_demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..57a406bf03822806e203f1c3fe816851d2e32d2f
Binary files /dev/null and b/screenshots/code_demo.png differ
diff --git a/screenshots/code_demo.webp b/screenshots/code_demo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..6a7fb66646c53e3e784fe39f80031118c6872384
Binary files /dev/null and b/screenshots/code_demo.webp differ
diff --git a/screenshots/density.png b/screenshots/density.png
new file mode 100644
index 0000000000000000000000000000000000000000..34e8a97c795974f75887a45151bc85aefc404606
Binary files /dev/null and b/screenshots/density.png differ
diff --git a/screenshots/density.webp b/screenshots/density.webp
new file mode 100644
index 0000000000000000000000000000000000000000..1f8f3541bfada4c68cab0f092502644e9e4dd25c
Binary files /dev/null and b/screenshots/density.webp differ
diff --git a/screenshots/dialog.png b/screenshots/dialog.png
new file mode 100644
index 0000000000000000000000000000000000000000..d34aaa9813c8b74f5287d9eac444310d518ee9fb
Binary files /dev/null and b/screenshots/dialog.png differ
diff --git a/screenshots/dialog.webp b/screenshots/dialog.webp
new file mode 100644
index 0000000000000000000000000000000000000000..26e63ee6af0650aea934495caddef005bd086805
Binary files /dev/null and b/screenshots/dialog.webp differ
diff --git a/screenshots/divider.png b/screenshots/divider.png
new file mode 100644
index 0000000000000000000000000000000000000000..cf572e7f0afa28ad480dd83bfe2804e2d7e3927c
Binary files /dev/null and b/screenshots/divider.png differ
diff --git a/screenshots/divider.webp b/screenshots/divider.webp
new file mode 100644
index 0000000000000000000000000000000000000000..f0fbf396ca835a50659693b63513af4546cbc298
Binary files /dev/null and b/screenshots/divider.webp differ
diff --git a/screenshots/embed.png b/screenshots/embed.png
new file mode 100644
index 0000000000000000000000000000000000000000..21e43bdecca8b9b2ff49b424eb6f6a40bd4c57de
Binary files /dev/null and b/screenshots/embed.png differ
diff --git a/screenshots/embed.webp b/screenshots/embed.webp
new file mode 100644
index 0000000000000000000000000000000000000000..e4ce9011181432708b6675079fce0ad00d4d1134
Binary files /dev/null and b/screenshots/embed.webp differ
diff --git a/screenshots/fancy_chat.png b/screenshots/fancy_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..7832e74b3322a8ba934508b8e50c5cc43ee5acae
Binary files /dev/null and b/screenshots/fancy_chat.png differ
diff --git a/screenshots/fancy_chat.webp b/screenshots/fancy_chat.webp
new file mode 100644
index 0000000000000000000000000000000000000000..6620d769bcb0724a3dcb37788d10cbbd1b936cc1
Binary files /dev/null and b/screenshots/fancy_chat.webp differ
diff --git a/screenshots/feedback.png b/screenshots/feedback.png
new file mode 100644
index 0000000000000000000000000000000000000000..306960b3b31ff1662ae25037f4647ddaff10f913
Binary files /dev/null and b/screenshots/feedback.png differ
diff --git a/screenshots/feedback.webp b/screenshots/feedback.webp
new file mode 100644
index 0000000000000000000000000000000000000000..6a3918e7608b629d5fa5d57dc8ecfed01b148217
Binary files /dev/null and b/screenshots/feedback.webp differ
diff --git a/screenshots/form_billing.png b/screenshots/form_billing.png
new file mode 100644
index 0000000000000000000000000000000000000000..668dfc3199c27f018b2f0c9569ecc127b3ac7f43
Binary files /dev/null and b/screenshots/form_billing.png differ
diff --git a/screenshots/form_billing.webp b/screenshots/form_billing.webp
new file mode 100644
index 0000000000000000000000000000000000000000..e6010739e685b226d3643f8b787cd344f5175c5b
Binary files /dev/null and b/screenshots/form_billing.webp differ
diff --git a/screenshots/form_profile.png b/screenshots/form_profile.png
new file mode 100644
index 0000000000000000000000000000000000000000..d34b3970f5c3ee4dd26c6f5eea6cd16cbf7edc39
Binary files /dev/null and b/screenshots/form_profile.png differ
diff --git a/screenshots/form_profile.webp b/screenshots/form_profile.webp
new file mode 100644
index 0000000000000000000000000000000000000000..2a5508db32231702bbfc79ae35f867158f19ce73
Binary files /dev/null and b/screenshots/form_profile.webp differ
diff --git a/screenshots/grid_table.png b/screenshots/grid_table.png
new file mode 100644
index 0000000000000000000000000000000000000000..aaccb0b39d6ae0c17699aae29cc003891c0b31aa
Binary files /dev/null and b/screenshots/grid_table.png differ
diff --git a/screenshots/grid_table.webp b/screenshots/grid_table.webp
new file mode 100644
index 0000000000000000000000000000000000000000..834c3e968d6e351021d17ec8bee669bd3482501f
Binary files /dev/null and b/screenshots/grid_table.webp differ
diff --git a/screenshots/headers.png b/screenshots/headers.png
new file mode 100644
index 0000000000000000000000000000000000000000..5805e42038ff8e28452f1dcae1b05c2219f7f791
Binary files /dev/null and b/screenshots/headers.png differ
diff --git a/screenshots/headers.webp b/screenshots/headers.webp
new file mode 100644
index 0000000000000000000000000000000000000000..76eb819c4cb5444f80454d328b0bf2e1d655072b
Binary files /dev/null and b/screenshots/headers.webp differ
diff --git a/screenshots/html_demo.png b/screenshots/html_demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..b05d74b1899d0824f63656178958e29666b3b90f
Binary files /dev/null and b/screenshots/html_demo.png differ
diff --git a/screenshots/html_demo.webp b/screenshots/html_demo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a6f1fc8a7e5d7355f655f04ef9c263788b412c4d
Binary files /dev/null and b/screenshots/html_demo.webp differ
diff --git a/screenshots/icon.png b/screenshots/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..d502ef899cb61ca1e2e5bc0cd73671761b1a060a
Binary files /dev/null and b/screenshots/icon.png differ
diff --git a/screenshots/icon.webp b/screenshots/icon.webp
new file mode 100644
index 0000000000000000000000000000000000000000..30659c1f34f7952c3715acde7ad498bdf1c958eb
Binary files /dev/null and b/screenshots/icon.webp differ
diff --git a/screenshots/image.png b/screenshots/image.png
new file mode 100644
index 0000000000000000000000000000000000000000..43e3a1d5ebff8290538091f32ef2e5119dd43208
Binary files /dev/null and b/screenshots/image.png differ
diff --git a/screenshots/image.webp b/screenshots/image.webp
new file mode 100644
index 0000000000000000000000000000000000000000..dcbbd6a36573224810ab4dfebcfedfa17fed1e90
Binary files /dev/null and b/screenshots/image.webp differ
diff --git a/screenshots/input.png b/screenshots/input.png
new file mode 100644
index 0000000000000000000000000000000000000000..43cef51e404698381085a85b33580fbdb7d88ac5
Binary files /dev/null and b/screenshots/input.png differ
diff --git a/screenshots/input.webp b/screenshots/input.webp
new file mode 100644
index 0000000000000000000000000000000000000000..52e66db7a9f4ab23e4976838802d022db9655eed
Binary files /dev/null and b/screenshots/input.webp differ
diff --git a/screenshots/link.png b/screenshots/link.png
new file mode 100644
index 0000000000000000000000000000000000000000..57ba404d0ff2f3d2a5a7deaba1139ce3d0992780
Binary files /dev/null and b/screenshots/link.png differ
diff --git a/screenshots/link.webp b/screenshots/link.webp
new file mode 100644
index 0000000000000000000000000000000000000000..3c03184590bd5c82f7ebf7fb5676b956552f958d
Binary files /dev/null and b/screenshots/link.webp differ
diff --git a/screenshots/llm_playground.png b/screenshots/llm_playground.png
new file mode 100644
index 0000000000000000000000000000000000000000..c75c686f1185bc077c3c8488c111a24f41d5272c
Binary files /dev/null and b/screenshots/llm_playground.png differ
diff --git a/screenshots/llm_playground.webp b/screenshots/llm_playground.webp
new file mode 100644
index 0000000000000000000000000000000000000000..9b7693a9d4b87a840a09cb93ef6b1ddfa44c40e9
Binary files /dev/null and b/screenshots/llm_playground.webp differ
diff --git a/screenshots/llm_rewriter.png b/screenshots/llm_rewriter.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3ace4361046450738cc60e13a70a6659d5962bd
Binary files /dev/null and b/screenshots/llm_rewriter.png differ
diff --git a/screenshots/llm_rewriter.webp b/screenshots/llm_rewriter.webp
new file mode 100644
index 0000000000000000000000000000000000000000..9829b635c069ef09155cfc02aa7dcf6b15861afc
Binary files /dev/null and b/screenshots/llm_rewriter.webp differ
diff --git a/screenshots/main.png b/screenshots/main.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e849b23a1567aa6a8f68759a59d900df0c4476c
Binary files /dev/null and b/screenshots/main.png differ
diff --git a/screenshots/main.webp b/screenshots/main.webp
new file mode 100644
index 0000000000000000000000000000000000000000..3243831584237884dab92b945cde29611b4994d8
Binary files /dev/null and b/screenshots/main.webp differ
diff --git a/screenshots/markdown_demo.png b/screenshots/markdown_demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..ceb661e3319dc932a4cc9b525be725eb43dc23b7
Binary files /dev/null and b/screenshots/markdown_demo.png differ
diff --git a/screenshots/markdown_demo.webp b/screenshots/markdown_demo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..0577335518689d2c91ef87a1c414a11fc35243e6
Binary files /dev/null and b/screenshots/markdown_demo.webp differ
diff --git a/screenshots/markdown_editor.png b/screenshots/markdown_editor.png
new file mode 100644
index 0000000000000000000000000000000000000000..c2c828272109a1c1f30ba327ce4560788271196c
Binary files /dev/null and b/screenshots/markdown_editor.png differ
diff --git a/screenshots/markdown_editor.webp b/screenshots/markdown_editor.webp
new file mode 100644
index 0000000000000000000000000000000000000000..3a48c0e8c3a597b3a77d0366b763da8608841628
Binary files /dev/null and b/screenshots/markdown_editor.webp differ
diff --git a/screenshots/plot.png b/screenshots/plot.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a3ff5fe02e0c67a28206816870e7bc74bb68d59
Binary files /dev/null and b/screenshots/plot.png differ
diff --git a/screenshots/plot.webp b/screenshots/plot.webp
new file mode 100644
index 0000000000000000000000000000000000000000..0238bd995cf530762c6ffb571e13360354876bcb
Binary files /dev/null and b/screenshots/plot.webp differ
diff --git a/screenshots/progress_bar.png b/screenshots/progress_bar.png
new file mode 100644
index 0000000000000000000000000000000000000000..9139972894e14b434100c3965863de65337ce67e
Binary files /dev/null and b/screenshots/progress_bar.png differ
diff --git a/screenshots/progress_bar.webp b/screenshots/progress_bar.webp
new file mode 100644
index 0000000000000000000000000000000000000000..18ef8e586f79ec517bed53a7ed5c462e1fcda763
Binary files /dev/null and b/screenshots/progress_bar.webp differ
diff --git a/screenshots/progress_spinner.png b/screenshots/progress_spinner.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b7a461b5bdab61379d6b87751289404d482faa8
Binary files /dev/null and b/screenshots/progress_spinner.png differ
diff --git a/screenshots/progress_spinner.webp b/screenshots/progress_spinner.webp
new file mode 100644
index 0000000000000000000000000000000000000000..aeb7ec55ecc4eddc4af1a19954bc9f51c1beabd8
Binary files /dev/null and b/screenshots/progress_spinner.webp differ
diff --git a/screenshots/radio.png b/screenshots/radio.png
new file mode 100644
index 0000000000000000000000000000000000000000..39f58daa04b7476af7b906a764e0af505b34ec20
Binary files /dev/null and b/screenshots/radio.png differ
diff --git a/screenshots/radio.webp b/screenshots/radio.webp
new file mode 100644
index 0000000000000000000000000000000000000000..e1c4bd06bdacfdadec92f5042f7c85e468027d4d
Binary files /dev/null and b/screenshots/radio.webp differ
diff --git a/screenshots/select_demo.png b/screenshots/select_demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d6610337adaab693b041ff41159db39c1d21f1c
Binary files /dev/null and b/screenshots/select_demo.png differ
diff --git a/screenshots/select_demo.webp b/screenshots/select_demo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..264009ba97b147e56966239f3741c3fc20af296e
Binary files /dev/null and b/screenshots/select_demo.webp differ
diff --git a/screenshots/sidenav.png b/screenshots/sidenav.png
new file mode 100644
index 0000000000000000000000000000000000000000..41a723fc5404730b320684c258a38003f151bc26
Binary files /dev/null and b/screenshots/sidenav.png differ
diff --git a/screenshots/sidenav.webp b/screenshots/sidenav.webp
new file mode 100644
index 0000000000000000000000000000000000000000..010df721545497df517f7a9b6fbc5dd37983cb2c
Binary files /dev/null and b/screenshots/sidenav.webp differ
diff --git a/screenshots/slide_toggle.png b/screenshots/slide_toggle.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d1dd60359ebb3fd1108e76cc8ff2c71ed1e33bf
Binary files /dev/null and b/screenshots/slide_toggle.png differ
diff --git a/screenshots/slide_toggle.webp b/screenshots/slide_toggle.webp
new file mode 100644
index 0000000000000000000000000000000000000000..988ddaad2954436251da873fbfacfeea178a63cf
Binary files /dev/null and b/screenshots/slide_toggle.webp differ
diff --git a/screenshots/slider.png b/screenshots/slider.png
new file mode 100644
index 0000000000000000000000000000000000000000..284a6d47807c9bc64dd6b4ea08f82aae5eca52e7
Binary files /dev/null and b/screenshots/slider.png differ
diff --git a/screenshots/slider.webp b/screenshots/slider.webp
new file mode 100644
index 0000000000000000000000000000000000000000..6c1c32ec6739edb58bc055ba80af9eebde5a6989
Binary files /dev/null and b/screenshots/slider.webp differ
diff --git a/screenshots/snackbar.png b/screenshots/snackbar.png
new file mode 100644
index 0000000000000000000000000000000000000000..f59a26d55db949bb9b4666541a170e8ff27ad1d5
Binary files /dev/null and b/screenshots/snackbar.png differ
diff --git a/screenshots/snackbar.webp b/screenshots/snackbar.webp
new file mode 100644
index 0000000000000000000000000000000000000000..61b616e0fa5111f671328193d48cbefc886aac77
Binary files /dev/null and b/screenshots/snackbar.webp differ
diff --git a/screenshots/table.png b/screenshots/table.png
new file mode 100644
index 0000000000000000000000000000000000000000..e975895fd6818da0b8addd287bc0a7dfe498a313
Binary files /dev/null and b/screenshots/table.png differ
diff --git a/screenshots/table.webp b/screenshots/table.webp
new file mode 100644
index 0000000000000000000000000000000000000000..89c12f8ccc66a2138174041dfca0af658fe77a1f
Binary files /dev/null and b/screenshots/table.webp differ
diff --git a/screenshots/text.png b/screenshots/text.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bf92aa7ab96422c4deed7bb7a9911b28f969d1e
Binary files /dev/null and b/screenshots/text.png differ
diff --git a/screenshots/text.webp b/screenshots/text.webp
new file mode 100644
index 0000000000000000000000000000000000000000..7b6a967a531608730886b6d1c8376da0cff6cef5
Binary files /dev/null and b/screenshots/text.webp differ
diff --git a/screenshots/text_to_image.png b/screenshots/text_to_image.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3f5d105c4ab216f6d2ed5b1d785da61777635db
Binary files /dev/null and b/screenshots/text_to_image.png differ
diff --git a/screenshots/text_to_image.webp b/screenshots/text_to_image.webp
new file mode 100644
index 0000000000000000000000000000000000000000..198429cb5cbe593dedb591f6a9bfd5768c588207
Binary files /dev/null and b/screenshots/text_to_image.webp differ
diff --git a/screenshots/text_to_text.png b/screenshots/text_to_text.png
new file mode 100644
index 0000000000000000000000000000000000000000..c47d2f45bf2f0b3a6de671fc6e019dc61c73ade0
Binary files /dev/null and b/screenshots/text_to_text.png differ
diff --git a/screenshots/text_to_text.webp b/screenshots/text_to_text.webp
new file mode 100644
index 0000000000000000000000000000000000000000..5efd65625b736896f70fcc07a10c8a3a577a4fb1
Binary files /dev/null and b/screenshots/text_to_text.webp differ
diff --git a/screenshots/textarea.png b/screenshots/textarea.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e15844672590f5e301a6096d109631e5fc09a76
Binary files /dev/null and b/screenshots/textarea.png differ
diff --git a/screenshots/textarea.webp b/screenshots/textarea.webp
new file mode 100644
index 0000000000000000000000000000000000000000..cbb229bf1ffee30982bd16a9334efca8fa0be4ac
Binary files /dev/null and b/screenshots/textarea.webp differ
diff --git a/screenshots/tooltip.png b/screenshots/tooltip.png
new file mode 100644
index 0000000000000000000000000000000000000000..2732adff0f92dffafdd007f9ba4f9000468b4abf
Binary files /dev/null and b/screenshots/tooltip.png differ
diff --git a/screenshots/tooltip.webp b/screenshots/tooltip.webp
new file mode 100644
index 0000000000000000000000000000000000000000..22fd30a2a67109dcf3fbf02c6064fd55c5d098b3
Binary files /dev/null and b/screenshots/tooltip.webp differ
diff --git a/screenshots/uploader.png b/screenshots/uploader.png
new file mode 100644
index 0000000000000000000000000000000000000000..fbbc0fec6954fd8bf11e554f68bc461e1f1d84df
Binary files /dev/null and b/screenshots/uploader.png differ
diff --git a/screenshots/uploader.webp b/screenshots/uploader.webp
new file mode 100644
index 0000000000000000000000000000000000000000..e75945100e2e2a85b649b430bfd83da987cde497
Binary files /dev/null and b/screenshots/uploader.webp differ
diff --git a/screenshots/video.png b/screenshots/video.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e41fa7f12f667bdd54bfc92d0d8b989a39b4844
Binary files /dev/null and b/screenshots/video.png differ
diff --git a/screenshots/video.webp b/screenshots/video.webp
new file mode 100644
index 0000000000000000000000000000000000000000..05ed719614cdbdc912bbd4e917189acaa5dd442c
Binary files /dev/null and b/screenshots/video.webp differ
diff --git a/select_demo.py b/select_demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..859e75c208459cf579d018214cc2b2a0bd1195c5
--- /dev/null
+++ b/select_demo.py
@@ -0,0 +1,40 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ selected_values: list[str]
+
+
+def on_selection_change(e: me.SelectSelectionChangeEvent):
+ s = me.state(State)
+ s.selected_values = e.values
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/select_demo",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text(text="Select")
+ me.select(
+ label="Select",
+ options=[
+ me.SelectOption(label="label 1", value="value1"),
+ me.SelectOption(label="label 2", value="value2"),
+ me.SelectOption(label="label 3", value="value3"),
+ ],
+ on_selection_change=on_selection_change,
+ style=me.Style(width=500),
+ multiple=True,
+ )
+ s = me.state(State)
+ me.text(text="Selected values: " + ", ".join(s.selected_values))
diff --git a/sidenav.py b/sidenav.py
new file mode 100644
index 0000000000000000000000000000000000000000..eeb6048c7963d529ced8446ed5f9ed425294e5f2
--- /dev/null
+++ b/sidenav.py
@@ -0,0 +1,42 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ sidenav_open: bool
+
+
+def on_click(e: me.ClickEvent):
+ s = me.state(State)
+ s.sidenav_open = not s.sidenav_open
+
+
+SIDENAV_WIDTH = 200
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/sidenav",
+)
+def app():
+ state = me.state(State)
+ with me.sidenav(
+ opened=state.sidenav_open, style=me.Style(width=SIDENAV_WIDTH)
+ ):
+ me.text("Inside sidenav")
+
+ with me.box(
+ style=me.Style(
+ margin=me.Margin(left=SIDENAV_WIDTH if state.sidenav_open else 0),
+ ),
+ ):
+ with me.content_button(on_click=on_click):
+ me.icon("menu")
+ me.markdown("Main content")
diff --git a/slide_toggle.py b/slide_toggle.py
new file mode 100644
index 0000000000000000000000000000000000000000..80855a996c1b73ac8f4bf77ec85458ce91c2a6a5
--- /dev/null
+++ b/slide_toggle.py
@@ -0,0 +1,29 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ toggled: bool = False
+
+
+def on_change(event: me.SlideToggleChangeEvent):
+ s = me.state(State)
+ s.toggled = not s.toggled
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/slide_toggle",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.slide_toggle(label="Slide toggle", on_change=on_change)
+ s = me.state(State)
+ me.text(text=f"Toggled: {s.toggled}")
diff --git a/slider.py b/slider.py
new file mode 100644
index 0000000000000000000000000000000000000000..684d60c0c73f950b6049c4e7a90d395a50980725
--- /dev/null
+++ b/slider.py
@@ -0,0 +1,48 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ initial_input_value: str = "50.0"
+ initial_slider_value: float = 50.0
+ slider_value: float = 50.0
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/slider",
+)
+def app():
+ state = me.state(State)
+ with me.box(
+ style=me.Style(
+ display="flex", flex_direction="column", margin=me.Margin.all(15)
+ )
+ ):
+ me.input(
+ label="Slider value",
+ appearance="outline",
+ value=state.initial_input_value,
+ on_input=on_input,
+ )
+ me.slider(on_value_change=on_value_change, value=state.initial_slider_value)
+ me.text(text=f"Value: {me.state(State).slider_value}")
+
+
+def on_value_change(event: me.SliderValueChangeEvent):
+ state = me.state(State)
+ state.slider_value = event.value
+ state.initial_input_value = str(state.slider_value)
+
+
+def on_input(event: me.InputEvent):
+ state = me.state(State)
+ state.initial_slider_value = float(event.value)
+ state.slider_value = state.initial_slider_value
diff --git a/snackbar.py b/snackbar.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2f1beabc5c4de6cc30bb176111b6d180b03f80f
--- /dev/null
+++ b/snackbar.py
@@ -0,0 +1,192 @@
+"""Simple snackbar component that is similar to Angular Component Snackbar."""
+
+import time
+from typing import Callable, Literal
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ is_visible: bool = False
+ duration: int = 0
+ horizontal_position: str = "center"
+ vertical_position: str = "end"
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/snackbar",
+)
+def app():
+ me.set_theme_mode("dark")
+ state = me.state(State)
+
+ snackbar(
+ label="Cannonball!!!",
+ action_label="Splash",
+ on_click_action=on_click_snackbar_close,
+ is_visible=state.is_visible,
+ horizontal_position=state.horizontal_position, # type: ignore
+ vertical_position=state.vertical_position, # type: ignore
+ )
+
+ with me.box(style=me.Style(padding=me.Padding.all(30))):
+ with me.box():
+ me.select(
+ label="Horizontal Position",
+ on_selection_change=on_horizontal_position_change,
+ options=[
+ me.SelectOption(label="start", value="start"),
+ me.SelectOption(label="center", value="center"),
+ me.SelectOption(label="end", value="end"),
+ ],
+ )
+
+ with me.box():
+ me.select(
+ label="Vertical Position",
+ on_selection_change=on_vertical_position_change,
+ options=[
+ me.SelectOption(label="start", value="start"),
+ me.SelectOption(label="center", value="center"),
+ me.SelectOption(label="end", value="end"),
+ ],
+ )
+
+ with me.box():
+ me.select(
+ label="Duration",
+ on_selection_change=on_duration_change,
+ options=[
+ me.SelectOption(label="None", value="0"),
+ me.SelectOption(label="3 seconds", value="3"),
+ ],
+ )
+
+ me.button(
+ "Trigger snackbar",
+ type="flat",
+ color="primary",
+ on_click=on_click_snackbar_open,
+ )
+
+
+def on_horizontal_position_change(e: me.SelectSelectionChangeEvent):
+ state = me.state(State)
+ state.horizontal_position = e.value
+
+
+def on_vertical_position_change(e: me.SelectSelectionChangeEvent):
+ state = me.state(State)
+ state.vertical_position = e.value
+
+
+def on_duration_change(e: me.SelectSelectionChangeEvent):
+ state = me.state(State)
+ state.duration = int(e.value)
+
+
+def on_click_snackbar_close(e: me.ClickEvent):
+ state = me.state(State)
+ state.is_visible = False
+
+
+def on_click_snackbar_open(e: me.ClickEvent):
+ state = me.state(State)
+ state.is_visible = True
+
+ # Use yield to create a timed snackbar message.
+ if state.duration:
+ yield
+ time.sleep(state.duration)
+ state.is_visible = False
+ yield
+ else:
+ yield
+
+
+@me.component
+def snackbar(
+ *,
+ is_visible: bool,
+ label: str,
+ action_label: str | None = None,
+ on_click_action: Callable | None = None,
+ horizontal_position: Literal["start", "center", "end"] = "center",
+ vertical_position: Literal["start", "center", "end"] = "end",
+):
+ """Creates a snackbar.
+
+ By default the snackbar is rendered at bottom center.
+
+ The on_click_action should typically close the snackbar as part of its actions. If no
+ click event is included, you'll need to manually hide the snackbar.
+
+ Note that there is one issue with this snackbar example. No actions are possible when
+ using "time.sleep and yield" to imitate a status message that fades away after a
+ period of time.
+
+ Args:
+ is_visible: Whether the snackbar is currently visible or not.
+ label: Message for the snackbar
+ action_label: Optional message for the action of the snackbar
+ on_click_action: Optional click event when action is triggered.
+ horizontal_position: Horizontal position of the snackbar
+ vertical_position: Vertical position of the snackbar
+ """
+ with me.box(
+ style=me.Style(
+ display="block" if is_visible else "none",
+ height="100%",
+ overflow_x="auto",
+ overflow_y="auto",
+ position="fixed",
+ pointer_events="none",
+ width="100%",
+ z_index=1000,
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ align_items=vertical_position,
+ height="100%",
+ display="flex",
+ justify_content=horizontal_position,
+ )
+ ):
+ with me.box(
+ style=me.Style(
+ align_items="center",
+ background=me.theme_var("on-surface-variant"),
+ border_radius=5,
+ box_shadow=(
+ "0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"
+ ),
+ display="flex",
+ font_size=14,
+ justify_content="space-between",
+ margin=me.Margin.all(10),
+ padding=me.Padding(top=5, bottom=5, right=5, left=15)
+ if action_label
+ else me.Padding.all(15),
+ pointer_events="auto",
+ width=300,
+ )
+ ):
+ me.text(
+ label, style=me.Style(color=me.theme_var("surface-container-lowest"))
+ )
+ if action_label:
+ me.button(
+ action_label,
+ on_click=on_click_action,
+ style=me.Style(color=me.theme_var("primary-container")),
+ )
diff --git a/table.py b/table.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ce66e2287107cb4aa95238d3e6d4f39a8bdd8d5
--- /dev/null
+++ b/table.py
@@ -0,0 +1,71 @@
+from datetime import datetime
+
+import numpy as np
+import pandas as pd
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ selected_cell: str = "No cell selected."
+
+
+df = pd.DataFrame(
+ data={
+ "NA": [pd.NA, pd.NA, pd.NA],
+ "Index": [3, 2, 1],
+ "Bools": [True, False, np.bool_(True)],
+ "Ints": [101, 90, np.int64(-55)],
+ "Floats": [2.3, 4.5, np.float64(-3.000000003)],
+ "Strings": ["Hello", "World", "!"],
+ "Date Times": [
+ pd.Timestamp("20180310"),
+ pd.Timestamp("20230310"),
+ datetime(2023, 1, 1, 12, 12, 1),
+ ],
+ }
+)
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/table",
+)
+def app():
+ state = me.state(State)
+
+ with me.box(style=me.Style(padding=me.Padding.all(10), width=500)):
+ me.table(
+ df,
+ on_click=on_click,
+ header=me.TableHeader(sticky=True),
+ columns={
+ "NA": me.TableColumn(sticky=True),
+ "Index": me.TableColumn(sticky=True),
+ },
+ )
+
+ with me.box(
+ style=me.Style(
+ background=me.theme_var("surface-container-high"),
+ margin=me.Margin.all(10),
+ padding=me.Padding.all(10),
+ )
+ ):
+ me.text(state.selected_cell)
+
+
+def on_click(e: me.TableClickEvent):
+ state = me.state(State)
+ state.selected_cell = (
+ f"Selected cell at col {e.col_index} and row {e.row_index} "
+ f"with value {df.iat[e.row_index, e.col_index]!s}"
+ )
diff --git a/tests/__pycache__/demo_test.cpython-310-pytest-7.4.3.pyc b/tests/__pycache__/demo_test.cpython-310-pytest-7.4.3.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..522927210f3306beca5e81c4932ee671e31bbe64
Binary files /dev/null and b/tests/__pycache__/demo_test.cpython-310-pytest-7.4.3.pyc differ
diff --git a/text.py b/text.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f13c7117c7d445651d92f9171f70a5c61a844a3
--- /dev/null
+++ b/text.py
@@ -0,0 +1,28 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/text",
+)
+def text():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text(text="headline-1: Hello, world!", type="headline-1")
+ me.text(text="headline-2: Hello, world!", type="headline-2")
+ me.text(text="headline-3: Hello, world!", type="headline-3")
+ me.text(text="headline-4: Hello, world!", type="headline-4")
+ me.text(text="headline-5: Hello, world!", type="headline-5")
+ me.text(text="headline-6: Hello, world!", type="headline-6")
+ me.text(text="subtitle-1: Hello, world!", type="subtitle-1")
+ me.text(text="subtitle-2: Hello, world!", type="subtitle-2")
+ me.text(text="body-1: Hello, world!", type="body-1")
+ me.text(text="body-2: Hello, world!", type="body-2")
+ me.text(text="caption: Hello, world!", type="caption")
+ me.text(text="button: Hello, world!", type="button")
diff --git a/text_to_image.py b/text_to_image.py
new file mode 100644
index 0000000000000000000000000000000000000000..b2891702c2d532085ea771dd81d1708538752773
--- /dev/null
+++ b/text_to_image.py
@@ -0,0 +1,25 @@
+import mesop as me
+import mesop.labs as mel
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/text_to_image",
+ title="Text to Image Example",
+)
+def app():
+ mel.text_to_image(
+ generate_image,
+ title="Text to Image Example",
+ )
+
+
+def generate_image(prompt: str):
+ return "https://www.google.com/logos/doodles/2024/earth-day-2024-6753651837110453-2xa.gif"
diff --git a/text_to_text.py b/text_to_text.py
new file mode 100644
index 0000000000000000000000000000000000000000..2bd5b599ebdca00d1ef75abd0a1da1a548988e39
--- /dev/null
+++ b/text_to_text.py
@@ -0,0 +1,25 @@
+import mesop as me
+import mesop.labs as mel
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/text_to_text",
+ title="Text to Text Example",
+)
+def app():
+ mel.text_to_text(
+ upper_case_stream,
+ title="Text to Text Example",
+ )
+
+
+def upper_case_stream(s: str):
+ return "Echo: " + s
diff --git a/textarea.py b/textarea.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4be1d0f0c6db035be5b6217f26759b76be83f48
--- /dev/null
+++ b/textarea.py
@@ -0,0 +1,69 @@
+import mesop as me
+
+
+@me.stateclass
+class State:
+ input: str = ""
+ output: str = ""
+
+
+def on_blur(e: me.InputBlurEvent):
+ state = me.state(State)
+ state.input = e.value
+ state.output = e.value
+
+
+def on_newline(e: me.TextareaShortcutEvent):
+ state = me.state(State)
+ state.input = e.value + "\n"
+
+
+def on_submit(e: me.TextareaShortcutEvent):
+ state = me.state(State)
+ state.input = e.value
+ state.output = e.value
+
+
+def on_clear(e: me.TextareaShortcutEvent):
+ state = me.state(State)
+ state.input = ""
+ state.output = ""
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/textarea",
+)
+def app():
+ s = me.state(State)
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text(
+ "Press enter to submit.",
+ style=me.Style(margin=me.Margin(bottom=15)),
+ )
+ me.text(
+ "Press shift+enter to create new line.",
+ style=me.Style(margin=me.Margin(bottom=15)),
+ )
+ me.text(
+ "Press shift+meta+enter to clear text.",
+ style=me.Style(margin=me.Margin(bottom=15)),
+ )
+ me.textarea(
+ label="Basic input",
+ value=s.input,
+ on_blur=on_blur,
+ shortcuts={
+ me.Shortcut(key="enter"): on_submit,
+ me.Shortcut(shift=True, key="ENTER"): on_newline,
+ me.Shortcut(shift=True, meta=True, key="Enter"): on_clear,
+ },
+ )
+ me.text(text=s.output)
diff --git a/tooltip.py b/tooltip.py
new file mode 100644
index 0000000000000000000000000000000000000000..e763393d9a453f4d8e8c61bb834b0f91cc30cc44
--- /dev/null
+++ b/tooltip.py
@@ -0,0 +1,18 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/tooltip",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ with me.tooltip(message="Tooltip message"):
+ me.text(text="Hello, World")
diff --git a/uploader.py b/uploader.py
new file mode 100644
index 0000000000000000000000000000000000000000..730c58efbc500edbf9d135c6744a87aa92c38ea7
--- /dev/null
+++ b/uploader.py
@@ -0,0 +1,52 @@
+import base64
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ file: me.UploadedFile
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/uploader",
+)
+def app():
+ state = me.state(State)
+ with me.box(style=me.Style(padding=me.Padding.all(15))):
+ me.uploader(
+ label="Upload Image",
+ accepted_file_types=["image/jpeg", "image/png"],
+ on_upload=handle_upload,
+ type="flat",
+ color="primary",
+ style=me.Style(font_weight="bold"),
+ )
+
+ if state.file.size:
+ with me.box(style=me.Style(margin=me.Margin.all(10))):
+ me.text(f"File name: {state.file.name}")
+ me.text(f"File size: {state.file.size}")
+ me.text(f"File type: {state.file.mime_type}")
+
+ with me.box(style=me.Style(margin=me.Margin.all(10))):
+ me.image(src=_convert_contents_data_url(state.file))
+
+
+def handle_upload(event: me.UploadEvent):
+ state = me.state(State)
+ state.file = event.file
+
+
+def _convert_contents_data_url(file: me.UploadedFile) -> str:
+ return (
+ f"data:{file.mime_type};base64,{base64.b64encode(file.getvalue()).decode()}"
+ )
diff --git a/video.py b/video.py
new file mode 100644
index 0000000000000000000000000000000000000000..88c86041d40a5098b8af9e014ff4fe09b883f1b6
--- /dev/null
+++ b/video.py
@@ -0,0 +1,20 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io", "https://huggingface.co"]
+ ),
+ path="/video",
+)
+def app():
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.video(
+ src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ style=me.Style(height=300, width=300),
+ )