Richard commited on
Commit
a43df2a
·
1 Parent(s): 56c5f7a

Minor improvements

Browse files

- Improve snackbar with pointer_events css and async action
- Use markedjs instead of me.markdown

.gitignore CHANGED
@@ -8,3 +8,6 @@ __pycache__
8
 
9
  # Prompt Tuner App
10
  saved_prompts
 
 
 
 
8
 
9
  # Prompt Tuner App
10
  saved_prompts
11
+
12
+ # System
13
+ .DS_Store
components/snackbar.py CHANGED
@@ -38,6 +38,7 @@ def snackbar(
38
  height="100%",
39
  overflow_x="auto",
40
  overflow_y="auto",
 
41
  position="fixed",
42
  width="100%",
43
  z_index=1000,
@@ -64,6 +65,7 @@ def snackbar(
64
  padding=me.Padding(top=5, bottom=5, right=5, left=15)
65
  if action_label
66
  else me.Padding.all(15),
 
67
  width=300,
68
  )
69
  ):
 
38
  height="100%",
39
  overflow_x="auto",
40
  overflow_y="auto",
41
+ pointer_events="none",
42
  position="fixed",
43
  width="100%",
44
  z_index=1000,
 
65
  padding=me.Padding(top=5, bottom=5, right=5, left=15)
66
  if action_label
67
  else me.Padding.all(15),
68
+ pointer_events="auto",
69
  width=300,
70
  )
71
  ):
eval_table.py CHANGED
@@ -4,6 +4,7 @@ import hashlib
4
  import mesop as me
5
 
6
  from state import Prompt
 
7
 
8
 
9
  @me.component
@@ -59,12 +60,12 @@ def prompt_eval_table(
59
  me.text(str(row_index))
60
  elif row["type"] == "variable":
61
  with me.box(style=_MARKDOWN_BOX_STYLE):
62
- me.markdown(example["variables"][row["variable_name"]])
63
  elif row["type"] == "model_response":
64
  with me.box(style=_MARKDOWN_BOX_STYLE):
65
  prompt_response = response_map[row["prompt"].version].get(response_key)
66
  if prompt_response and prompt_response[0]["output"]:
67
- me.markdown(prompt_response[0]["output"])
68
  else:
69
  with me.box(
70
  style=me.Style(
 
4
  import mesop as me
5
 
6
  from state import Prompt
7
+ from web_components import markedjs_component
8
 
9
 
10
  @me.component
 
60
  me.text(str(row_index))
61
  elif row["type"] == "variable":
62
  with me.box(style=_MARKDOWN_BOX_STYLE):
63
+ markedjs_component(example["variables"][row["variable_name"]])
64
  elif row["type"] == "model_response":
65
  with me.box(style=_MARKDOWN_BOX_STYLE):
66
  prompt_response = response_map[row["prompt"].version].get(response_key)
67
  if prompt_response and prompt_response[0]["output"]:
68
+ markedjs_component(prompt_response[0]["output"])
69
  else:
70
  with me.box(
71
  style=me.Style(
main.py CHANGED
@@ -1,6 +1,7 @@
1
  import copy
2
 
3
  import mesop as me
 
4
 
5
  import components as mex
6
  import dialogs
@@ -10,6 +11,9 @@ from eval_table import prompt_eval_table
10
  from tool_sidebar import tool_sidebar
11
  from helpers import find_prompt, parse_variables
12
  from state import State, Prompt
 
 
 
13
 
14
  _INSTRUCTIONS = """
15
  - Write your prompt.
@@ -23,14 +27,30 @@ _INSTRUCTIONS = """
23
 
24
 
25
  @me.page(
26
- security_policy=me.SecurityPolicy(allowed_iframe_parents=["https://huggingface.co"]),
 
 
 
 
 
 
 
 
 
 
 
27
  )
28
  def app():
29
  state = me.state(State)
30
 
31
- mex.snackbar(
32
- is_visible=state.show_snackbar, label=state.snackbar_message, horizontal_position="start"
 
 
33
  )
 
 
 
34
 
35
  dialogs.update_title()
36
  dialogs.model_settings()
@@ -122,10 +142,10 @@ def app():
122
  with me.box(style=me.Style(padding=me.Padding.all(15), overflow_y="scroll")):
123
  if state.response:
124
  with mex.card(title="Response", style=me.Style(overflow_y="hidden")):
125
- me.markdown(state.response)
126
  else:
127
  with mex.card(title="Prompt Tuner Instructions"):
128
- me.markdown(_INSTRUCTIONS)
129
  else:
130
  # Render eval page
131
  with me.box(style=me.Style(grid_column="1 / -2", overflow_y="scroll")):
@@ -269,6 +289,13 @@ def on_select_rating(e: me.SelectSelectionChangeEvent):
269
  prompt.responses[int(response_index)]["rating"] = e.value
270
 
271
 
 
 
 
 
 
 
 
272
  # Style helpers
273
 
274
  _STYLE_INVISIBLE_TEXTAREA = me.Style(
 
1
  import copy
2
 
3
  import mesop as me
4
+ import mesop.labs as mel
5
 
6
  import components as mex
7
  import dialogs
 
11
  from tool_sidebar import tool_sidebar
12
  from helpers import find_prompt, parse_variables
13
  from state import State, Prompt
14
+ from web_components import AsyncAction
15
+ from web_components import async_action_component
16
+ from web_components import markedjs_component
17
 
18
  _INSTRUCTIONS = """
19
  - Write your prompt.
 
27
 
28
 
29
  @me.page(
30
+ stylesheets=[
31
+ # Other themes here: https://www.jsdelivr.com/package/npm/highlight.js?tab=files&path=styles
32
+ "https://cdn.jsdelivr.net/npm/[email protected]/styles/github-dark.min.css",
33
+ "https://cdn.jsdelivr.net/npm/[email protected]/styles/github.min.css",
34
+ ],
35
+ security_policy=me.SecurityPolicy(
36
+ allowed_script_srcs=[
37
+ "https://cdn.jsdelivr.net",
38
+ ],
39
+ dangerously_disable_trusted_types=True,
40
+ allowed_iframe_parents=["https://huggingface.co"],
41
+ ),
42
  )
43
  def app():
44
  state = me.state(State)
45
 
46
+ action = (
47
+ AsyncAction(value=state.async_action_name, duration_seconds=state.async_action_duration)
48
+ if state.async_action_name
49
+ else None
50
  )
51
+ async_action_component(action=action, on_finished=on_async_action_finished)
52
+
53
+ mex.snackbar(is_visible=state.show_snackbar, label=state.snackbar_message)
54
 
55
  dialogs.update_title()
56
  dialogs.model_settings()
 
142
  with me.box(style=me.Style(padding=me.Padding.all(15), overflow_y="scroll")):
143
  if state.response:
144
  with mex.card(title="Response", style=me.Style(overflow_y="hidden")):
145
+ markedjs_component(state.response)
146
  else:
147
  with mex.card(title="Prompt Tuner Instructions"):
148
+ markedjs_component(_INSTRUCTIONS)
149
  else:
150
  # Render eval page
151
  with me.box(style=me.Style(grid_column="1 / -2", overflow_y="scroll")):
 
289
  prompt.responses[int(response_index)]["rating"] = e.value
290
 
291
 
292
+ def on_async_action_finished(e: mel.WebEvent):
293
+ state = me.state(State)
294
+ state.async_action_name = ""
295
+ state.snackbar_message = ""
296
+ state.show_snackbar = False
297
+
298
+
299
  # Style helpers
300
 
301
  _STYLE_INVISIBLE_TEXTAREA = me.Style(
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
  gunicorn
2
- mesop==0.10.0
3
  google-generativeai
 
1
  gunicorn
2
+ mesop==0.11.1
3
  google-generativeai
state.py CHANGED
@@ -59,6 +59,10 @@ class State:
59
  # Eval comparisons
60
  comparisons: list[int]
61
 
 
62
  show_snackbar: bool = False
63
  snackbar_message: str = ""
64
- snackbar_duration: int = 2
 
 
 
 
59
  # Eval comparisons
60
  comparisons: list[int]
61
 
62
+ # Snackbar
63
  show_snackbar: bool = False
64
  snackbar_message: str = ""
65
+
66
+ # Async action (for snackbar)
67
+ async_action_name: str
68
+ async_action_duration: int = 4
tool_sidebar.py CHANGED
@@ -96,11 +96,7 @@ def on_click_download(e: me.ClickEvent):
96
 
97
  state.snackbar_message = f"Prompt exported as {filename}."
98
  state.show_snackbar = True
99
- yield
100
- time.sleep(state.snackbar_duration)
101
- yield
102
- state.show_snackbar = False
103
- yield
104
 
105
 
106
  def _clean_title(title: str) -> str:
 
96
 
97
  state.snackbar_message = f"Prompt exported as {filename}."
98
  state.show_snackbar = True
99
+ state.async_action_name = "hide_snackbar"
 
 
 
 
100
 
101
 
102
  def _clean_title(title: str) -> str:
web_components/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from web_components.async_action.async_action_component import (
2
+ async_action_component as async_action_component,
3
+ )
4
+
5
+ from web_components.async_action.async_action_component import (
6
+ AsyncAction as AsyncAction,
7
+ )
8
+
9
+ from web_components.copy_to_clipboard.copy_to_clipboard_component import (
10
+ copy_to_clipboard_component as copy_to_clipboard_component,
11
+ )
12
+
13
+ from web_components.markedjs.markedjs_component import (
14
+ markedjs_component as markedjs_component,
15
+ )
web_components/async_action/async_action_component.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ LitElement,
3
+ html,
4
+ } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
5
+
6
+ class AsyncAction extends LitElement {
7
+ static properties = {
8
+ startedEvent: {type: String},
9
+ finishedEvent: {type: String},
10
+ // Storing as string due to https://github.com/google/mesop/issues/730
11
+ // Format: {action: String, duration_seconds: Number}
12
+ action: {type: String},
13
+ isRunning: {type: Boolean},
14
+ };
15
+
16
+ render() {
17
+ return html`<div></div>`;
18
+ }
19
+
20
+ firstUpdated() {
21
+ if (this.action) {
22
+ this.runTimeout(this.action);
23
+ }
24
+ }
25
+
26
+ updated(changedProperties) {
27
+ if (changedProperties.has('action') && this.action) {
28
+ this.runTimeout(this.action);
29
+ }
30
+ }
31
+
32
+ runTimeout(actionJson) {
33
+ const action = JSON.parse(actionJson);
34
+ this.dispatchEvent(
35
+ new MesopEvent(this.startedEvent, {
36
+ action: action,
37
+ }),
38
+ );
39
+ setTimeout(() => {
40
+ this.dispatchEvent(
41
+ new MesopEvent(this.finishedEvent, {
42
+ action: action.value,
43
+ }),
44
+ );
45
+ }, action.duration_seconds * 1000);
46
+ }
47
+ }
48
+
49
+ customElements.define('async-action-component', AsyncAction);
web_components/async_action/async_action_component.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from dataclasses import asdict, dataclass
3
+ from typing import Any, Callable
4
+
5
+ import mesop.labs as mel
6
+
7
+
8
+ @dataclass
9
+ class AsyncAction:
10
+ value: str
11
+ duration_seconds: int
12
+
13
+
14
+ @mel.web_component(path="./async_action_component.js")
15
+ def async_action_component(
16
+ *,
17
+ action: AsyncAction | None = None,
18
+ on_started: Callable[[mel.WebEvent], Any] | None = None,
19
+ on_finished: Callable[[mel.WebEvent], Any] | None = None,
20
+ key: str | None = None,
21
+ ):
22
+ """Creates an invisibe component that will delay state changes asynchronously.
23
+
24
+ Right now this implementation is limited since we basically just pass the key around.
25
+ But ideally we also pass in some kind of value to update when the time out expires.
26
+
27
+ The main benefit of this component is for cases, such as status messages that may
28
+ appear and disappear after some duration. The primary example here is the example
29
+ snackbar widget, which right now blocks the UI when using the sleep yield approach.
30
+
31
+ The other benefit of this component is that it works generically (rather than say
32
+ implementing a custom snackbar widget as a web component).
33
+ """
34
+ events = {
35
+ "startedEvent": on_started,
36
+ "finishedEvent": on_finished,
37
+ }
38
+ return mel.insert_web_component(
39
+ name="async-action-component",
40
+ key=key,
41
+ events={key: value for key, value in events.items() if value is not None},
42
+ properties={"action": json.dumps(asdict(action)) if action else ""},
43
+ )
web_components/copy_to_clipboard/BUILD ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ load("//build_defs:defaults.bzl", "py_library")
2
+
3
+ package(
4
+ default_visibility = ["//build_defs:mesop_examples"],
5
+ )
6
+
7
+ py_library(
8
+ name = "copy_to_clipboard",
9
+ srcs = glob(["*.py"]),
10
+ data = glob(["*.js"]),
11
+ deps = [
12
+ "//mesop",
13
+ ],
14
+ )
web_components/copy_to_clipboard/copy_to_clipboard_component.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ LitElement,
3
+ html,
4
+ } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
5
+
6
+ class CopyToClipboard extends LitElement {
7
+ static properties = {
8
+ text: {type: String},
9
+ };
10
+
11
+ constructor() {
12
+ super();
13
+ this.text = '';
14
+ }
15
+
16
+ render() {
17
+ return html`
18
+ <div @click="${this._onClick}">
19
+ <slot></slot>
20
+ </div>
21
+ `;
22
+ }
23
+
24
+ _onClick() {
25
+ navigator.clipboard
26
+ .writeText(this.text)
27
+ .catch((err) => console.error('Failed to copy text: ', err));
28
+ }
29
+ }
30
+
31
+ customElements.define('copy-to-clipboard-component', CopyToClipboard);
web_components/copy_to_clipboard/copy_to_clipboard_component.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop.labs as mel
2
+
3
+
4
+ @mel.web_component(path="./copy_to_clipboard_component.js")
5
+ def copy_to_clipboard_component(
6
+ *,
7
+ text: str = "",
8
+ key: str | None = None,
9
+ ):
10
+ return mel.insert_web_component(
11
+ name="copy-to-clipboard-component",
12
+ key=key,
13
+ properties={
14
+ "text": text,
15
+ },
16
+ )
web_components/markedjs/BUILD ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ load("//build_defs:defaults.bzl", "py_library")
2
+
3
+ package(
4
+ default_visibility = ["//build_defs:mesop_examples"],
5
+ )
6
+
7
+ py_library(
8
+ name = "markedjs",
9
+ srcs = glob(["*.py"]),
10
+ data = glob(["*.js"]),
11
+ deps = [
12
+ "//mesop",
13
+ ],
14
+ )
web_components/markedjs/markedjs_component.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { html, LitElement } from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
2
+ import { unsafeHTML } from "https://cdn.jsdelivr.net/npm/[email protected]/directives/unsafe-html.js/+esm";
3
+ import dompurify from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
4
+ import { Marked } from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
5
+ import { markedHighlight } from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
6
+ import highlightJs from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
7
+ import mermaid from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
8
+
9
+ /**
10
+ * Markdown component using MarkedJS
11
+ *
12
+ * - HighlightJS used for code highlighting
13
+ * - Mermaid JS integration for diagrams
14
+ */
15
+ class MarkedJsComponent extends LitElement {
16
+ static properties = {
17
+ markdown: { type: String },
18
+ themeBrightness: { type: String },
19
+ darkModeTheme: { type: String },
20
+ lightModeTheme: { type: String },
21
+ };
22
+
23
+ constructor() {
24
+ super();
25
+ mermaid.initialize({ startOnLoad: true });
26
+ this.marked = new Marked(
27
+ markedHighlight({
28
+ langPrefix: "hljs language-",
29
+ highlight(code, lang, info) {
30
+ if (lang === "mermaid") {
31
+ return `<div class="mermaid">${code}</div>`;
32
+ }
33
+ const language = highlightJs.getLanguage(lang) ? lang : "plaintext";
34
+ return highlightJs.highlight(code, { language }).value;
35
+ },
36
+ })
37
+ );
38
+ }
39
+
40
+ createRenderRoot() {
41
+ return this;
42
+ }
43
+
44
+ firstUpdated() {
45
+ this.darkModeUpdated();
46
+ }
47
+
48
+ updated(changedProperties) {
49
+ this.darkModeUpdated();
50
+ }
51
+
52
+ darkModeUpdated() {
53
+ if (this.themeBrightness === "dark") {
54
+ document
55
+ .querySelector(`link[href="${this.darkModeTheme}"]`)
56
+ .removeAttribute("disabled");
57
+ document
58
+ .querySelector(`link[href="${this.lightModeTheme}"]`)
59
+ .setAttribute("disabled", "disabled");
60
+ } else {
61
+ document
62
+ .querySelector(`link[href="${this.darkModeTheme}"]`)
63
+ .setAttribute("disabled", "disabled");
64
+ document
65
+ .querySelector(`link[href="${this.lightModeTheme}"]`)
66
+ .removeAttribute("disabled");
67
+ }
68
+ }
69
+ render() {
70
+ return html`${unsafeHTML(
71
+ dompurify.sanitize(this.marked.parse(this.markdown))
72
+ )}`;
73
+ }
74
+ }
75
+
76
+ customElements.define("markedjs-component", MarkedJsComponent);
web_components/markedjs/markedjs_component.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+ import mesop.labs as mel
3
+
4
+
5
+ @mel.web_component(path="./markedjs_component.js")
6
+ def markedjs_component(
7
+ markdown: str,
8
+ light_mode_theme: str = "https://cdn.jsdelivr.net/npm/[email protected]/styles/github.min.css",
9
+ dark_mode_theme: str = "https://cdn.jsdelivr.net/npm/[email protected]/styles/github-dark.min.css",
10
+ ):
11
+ return mel.insert_web_component(
12
+ name="markedjs-component",
13
+ properties={
14
+ "markdown": markdown,
15
+ "themeBrightness": me.theme_brightness(),
16
+ "lightModeTheme": light_mode_theme,
17
+ "darkModeTheme": dark_mode_theme,
18
+ },
19
+ )