Richard commited on
Commit
18f7b1e
·
0 Parent(s):

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Python
2
+ __pycache__
3
+ .pytest_cache
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Mesop Prompt Tuner
2
+
3
+ Prompt tuner UI built using [Mesop](https://google.github.io/mesop/). This is a
4
+ work in progress.
components/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from components.button import button_toggle as button_toggle
2
+ from components.card import card as card
3
+ from components.card import expanable_card as expanable_card
4
+ from components.dialog import dialog as dialog
5
+ from components.dialog import dialog_actions as dialog_actions
6
+ from components.header import header as header
7
+ from components.header import header_section as header_section
8
+ from components.sidebar import icon_sidebar as icon_sidebar
9
+ from components.sidebar import icon_menu_item as icon_menu_item
10
+ from components.table import prompt_eval_table as prompt_eval_table
components/button.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+
3
+ import mesop as me
4
+
5
+
6
+ @me.component()
7
+ def button_toggle(
8
+ labels: list[str],
9
+ selected: str = "",
10
+ on_click: Callable | None = None,
11
+ key: str = "",
12
+ ):
13
+ """Simple version of Angular Component Button toggle.
14
+
15
+ Only supports single selection for now.
16
+
17
+ Args:
18
+ labels: Text labels for buttons
19
+ selected: Selected label
20
+ on_click: Event to handle button clicks on the button toggle
21
+ key: The key will be used as as prefix along with the selected label
22
+ """
23
+ with me.box(style=me.Style(display="flex", font_weight="bold", font_size=14)):
24
+ last_index = len(labels) - 1
25
+
26
+ for index, label in enumerate(labels):
27
+ if index == 0:
28
+ element = "first"
29
+ elif index == last_index:
30
+ element = "last"
31
+ else:
32
+ element = "default"
33
+
34
+ with me.box(
35
+ key=key + "_" + label,
36
+ on_click=on_click,
37
+ style=me.Style(
38
+ align_items="center",
39
+ display="flex",
40
+ # Handle selected case
41
+ background=_SELECTED_BG if label == selected else "#FFF",
42
+ padding=_SELECTED_PADDING if label == selected else _PADDING,
43
+ cursor="default" if label == selected else "pointer",
44
+ # Handle single button case (should just use a button in this case)
45
+ border=_LAST_BORDER if last_index == 0 else _BORDER_MAP[element],
46
+ border_radius=_BORDER_RADIUS if last_index == 0 else _BORDER_RADIUS_MAP[element],
47
+ ),
48
+ ):
49
+ if label in selected:
50
+ me.icon("check")
51
+ me.text(label)
52
+
53
+
54
+ _SELECTED_BG = "#DEE2F9"
55
+
56
+ _PADDING = me.Padding(left=15, right=15, top=10, bottom=10)
57
+ _SELECTED_PADDING = me.Padding(left=15, right=15, top=5, bottom=5)
58
+
59
+ _BORDER_RADIUS = "20px"
60
+
61
+ _DEFAULT_BORDER_STYLE = me.BorderSide(width=1, color="#74777E", style="solid")
62
+ _BORDER = me.Border(
63
+ left=_DEFAULT_BORDER_STYLE, top=_DEFAULT_BORDER_STYLE, bottom=_DEFAULT_BORDER_STYLE
64
+ )
65
+ _LAST_BORDER = me.Border.all(_DEFAULT_BORDER_STYLE)
66
+
67
+ _BORDER_MAP = {
68
+ "first": _BORDER,
69
+ "last": _LAST_BORDER,
70
+ "default": _BORDER,
71
+ }
72
+
73
+ _BORDER_RADIUS_MAP = {
74
+ "first": "20px 0 0 20px",
75
+ "last": "0px 20px 20px 0",
76
+ "default": "0",
77
+ }
components/card.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from components.helpers import merge_styles
2
+
3
+ from typing import Callable
4
+
5
+ import mesop as me
6
+
7
+
8
+ @me.content_component
9
+ def card(*, title: str = "", style: me.Style | None = None, key: str = ""):
10
+ """Creates a simple card component similar to Angular Component.
11
+
12
+ Args:
13
+ title: If empty, not title will be shown
14
+ style: Override the default styles of the card box
15
+ key: Not really useful here
16
+ """
17
+ with me.box(key=key, style=merge_styles(_DEFAULT_CARD_STYLE, style)):
18
+ if title:
19
+ me.text(
20
+ title,
21
+ style=me.Style(font_size=16, font_weight="bold", margin=me.Margin(bottom=15)),
22
+ )
23
+
24
+ me.slot()
25
+
26
+
27
+ @me.content_component
28
+ def expanable_card(
29
+ *,
30
+ title: str = "",
31
+ expanded: bool = False,
32
+ on_click_header: Callable | None = None,
33
+ style: me.Style | None = None,
34
+ key: str = "",
35
+ ):
36
+ """Creates a simple card component that is expandable.
37
+
38
+ Args:
39
+ title: If empty, no title will be shown but the expander will still be shown
40
+ expanded: Whether the card is expanded or not
41
+ on_click_header: Click handler for expanding card
42
+ style: Override the default styles of the card box
43
+ key: Key for the component
44
+ """
45
+ with me.box(key=key, style=merge_styles(_DEFAULT_CARD_STYLE, style)):
46
+ with me.box(
47
+ on_click=on_click_header,
48
+ style=me.Style(
49
+ align_items="center",
50
+ display="flex",
51
+ justify_content="space-between",
52
+ ),
53
+ ):
54
+ me.text(
55
+ title,
56
+ style=me.Style(font_size=16, font_weight="bold"),
57
+ )
58
+ me.icon("keyboard_arrow_up" if expanded else "keyboard_arrow_down")
59
+
60
+ with me.box(style=me.Style(margin=me.Margin(top=15), display="block" if expanded else "none")):
61
+ me.slot()
62
+
63
+
64
+ _DEFAULT_CARD_STYLE = me.Style(
65
+ background="#FFF",
66
+ border_radius=10,
67
+ border=me.Border.all(me.BorderSide(width=1, color="#DEE2E6", style="solid")),
68
+ padding=me.Padding.all(15),
69
+ margin=me.Margin(bottom=15),
70
+ )
components/dialog.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+
3
+
4
+ @me.content_component
5
+ def dialog(is_open: bool):
6
+ """Renders a dialog component.
7
+
8
+ The design of the dialog borrows from the Angular component dialog. So basically
9
+ rounded corners and some box shadow.
10
+
11
+ One current drawback is that it's not possible to close the dialog
12
+ by clicking on the overlay background. This is due to
13
+ https://github.com/google/mesop/issues/268.
14
+
15
+ Args:
16
+ is_open: Whether the dialog is visible or not.
17
+ """
18
+ with me.box(
19
+ style=me.Style(
20
+ background="rgba(0,0,0,0.4)",
21
+ display="block" if is_open else "none",
22
+ height="100%",
23
+ overflow_x="auto",
24
+ overflow_y="auto",
25
+ position="fixed",
26
+ width="100%",
27
+ z_index=1000,
28
+ )
29
+ ):
30
+ with me.box(
31
+ style=me.Style(
32
+ align_items="center",
33
+ display="grid",
34
+ height="100vh",
35
+ justify_items="center",
36
+ )
37
+ ):
38
+ with me.box(
39
+ style=me.Style(
40
+ background="#fff",
41
+ border_radius=20,
42
+ box_sizing="content-box",
43
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
44
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
45
+ padding=me.Padding.all(20),
46
+ )
47
+ ):
48
+ me.slot()
49
+
50
+
51
+ @me.content_component
52
+ def dialog_actions():
53
+ """Helper component for rendering action buttons so they are right aligned.
54
+
55
+ This component is optional. If you want to position action buttons differently,
56
+ you can just write your own Mesop markup.
57
+ """
58
+ with me.box(
59
+ style=me.Style(display="flex", gap=5, justify_content="end", margin=me.Margin(top=20))
60
+ ):
61
+ me.slot()
components/header.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from components.helpers import merge_styles
2
+
3
+ import mesop as me
4
+
5
+
6
+ @me.content_component
7
+ def header(
8
+ *,
9
+ style: me.Style | None = None,
10
+ is_mobile: bool = False,
11
+ max_width: int | None = 1000,
12
+ ):
13
+ """Creates a simple header component.
14
+
15
+ Args:
16
+ style: Override the default styles, such as background color, etc.
17
+ is_mobile: Use mobile layout. Arranges each section vertically.
18
+ max_width: Sets the maximum width of the header. Use None for fluid header.
19
+ """
20
+ default_flex_style = _DEFAULT_MOBILE_FLEX_STYLE if is_mobile else _DEFAULT_FLEX_STYLE
21
+ if max_width and me.viewport_size().width >= max_width:
22
+ default_flex_style = merge_styles(
23
+ default_flex_style,
24
+ me.Style(width=max_width, margin=me.Margin.symmetric(horizontal="auto")),
25
+ )
26
+
27
+ # The style override is a bit hacky here since we apply the override styles to both
28
+ # boxes here which could cause problems depending on what styles are added.
29
+ with me.box(style=merge_styles(_DEFAULT_STYLE, style)):
30
+ with me.box(style=merge_styles(default_flex_style, style)):
31
+ me.slot()
32
+
33
+
34
+ @me.content_component
35
+ def header_section():
36
+ """Adds a section to the header."""
37
+ with me.box(style=me.Style(display="flex", gap=5)):
38
+ me.slot()
39
+
40
+
41
+ _DEFAULT_STYLE = me.Style(
42
+ background="#F5F8FC",
43
+ border=me.Border.symmetric(vertical=me.BorderSide(width=1, style="solid", color="#DEE2E6")),
44
+ padding=me.Padding.all(10),
45
+ )
46
+
47
+ _DEFAULT_FLEX_STYLE = me.Style(
48
+ align_items="center",
49
+ display="flex",
50
+ gap=5,
51
+ justify_content="space-between",
52
+ )
53
+
54
+ _DEFAULT_MOBILE_FLEX_STYLE = me.Style(
55
+ align_items="center",
56
+ display="flex",
57
+ flex_direction="column",
58
+ gap=12,
59
+ justify_content="center",
60
+ )
components/helpers.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import fields
2
+
3
+ import mesop as me
4
+
5
+
6
+ def merge_styles(default: me.Style, overrides: me.Style | None = None) -> me.Style:
7
+ """Merges two styles together.
8
+
9
+ Args:
10
+ default: The starting style
11
+ overrides: Any set styles will override styles in default
12
+ """
13
+ if not overrides:
14
+ overrides = me.Style()
15
+
16
+ default_fields = {field.name: getattr(default, field.name) for field in fields(me.Style)}
17
+ override_fields = {
18
+ field.name: getattr(overrides, field.name)
19
+ for field in fields(me.Style)
20
+ if getattr(overrides, field.name) is not None
21
+ }
22
+
23
+ return me.Style(**default_fields | override_fields)
components/sidebar.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+
3
+ import mesop as me
4
+
5
+
6
+ @me.content_component
7
+ def icon_sidebar():
8
+ """Creates a sidebar that contains icon menu items.
9
+
10
+ Technically, does not have to be relegated to just icons menu items, but leaving it
11
+ more specific for now.
12
+ """
13
+ with me.box(
14
+ style=me.Style(
15
+ background="#F5F8FC",
16
+ border=me.Border.symmetric(horizontal=me.BorderSide(width=1, style="solid", color="#DEE2E6")),
17
+ )
18
+ ):
19
+ me.slot()
20
+
21
+
22
+ @me.component
23
+ def icon_menu_item(*, icon: str, tooltip: str, on_click: Callable | None = None, key: str = ""):
24
+ """Creates a menu item that displays as an icon.
25
+
26
+ - Unfortunately, we can't add a hover style
27
+ - TODO: Add a way to determine the active menu item selected
28
+ """
29
+ with me.box(
30
+ key=key,
31
+ on_click=on_click,
32
+ style=me.Style(
33
+ border=me.Border(bottom=me.BorderSide(width=1, color="#DEE2E6", style="solid")),
34
+ cursor="pointer",
35
+ padding=me.Padding.all(15),
36
+ ),
37
+ ):
38
+ with me.tooltip(message=tooltip):
39
+ me.icon(icon)
components/table.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+
3
+ _NUM_REQUIRED_ROWS = 3
4
+
5
+
6
+ @me.component
7
+ def prompt_eval_table(prompt):
8
+ """Creates a grid table for displaying and comparing different prompt version runs."""
9
+ # Add a row for each variable
10
+ num_vars = len(prompt.variables)
11
+ table_size = num_vars + _NUM_REQUIRED_ROWS
12
+ with me.box(
13
+ style=me.Style(
14
+ border=me.Border.all(me.BorderSide(width=1, style="solid", color="#DEE2E6")),
15
+ display="grid",
16
+ grid_template_columns=f"1fr repeat({num_vars}, 20fr) 20fr 1fr"
17
+ if num_vars
18
+ else "1fr 20fr 1fr",
19
+ margin=me.Margin.all(15),
20
+ )
21
+ ):
22
+ # Render first row. This row only displays the Prompt version.
23
+ for i in range(table_size):
24
+ with me.box(
25
+ style=me.Style(
26
+ background="#fff",
27
+ border=me.Border.all(me.BorderSide(width=1, style="solid", color="#DEE2E6")),
28
+ color="#000",
29
+ font_weight="bold",
30
+ padding=me.Padding.all(10),
31
+ )
32
+ ):
33
+ if i == num_vars + 1:
34
+ me.text(f"Version {prompt.version}")
35
+ else:
36
+ me.text("")
37
+
38
+ # Render second row. This row only displays the headers of the table:
39
+ # variable names, model response, avg rating.
40
+ header_row = [""] + prompt.variables + ["Model response"] + [""]
41
+ for header_text in header_row:
42
+ with me.box(
43
+ style=me.Style(
44
+ background="#FFF",
45
+ border=me.Border.all(me.BorderSide(width=1, style="solid", color="#DEE2E6")),
46
+ color="#0063FF" if header_text and header_text != "Model response" else "#333",
47
+ padding=me.Padding.all(10),
48
+ )
49
+ ):
50
+ # Handle the variable header case.
51
+ if header_text and header_text != "Model response":
52
+ me.text("{{" + header_text + "}}")
53
+ else:
54
+ me.text(header_text)
55
+
56
+ # Render the data rows by going through the prompt responses.
57
+ for index, example in enumerate(prompt.responses):
58
+ content_row = (
59
+ [index]
60
+ + [example["variables"][v] for v in prompt.variables]
61
+ + [example["output"], example.get("rating", "")]
62
+ )
63
+ for row in content_row:
64
+ with me.box(
65
+ style=me.Style(
66
+ background="#fff",
67
+ border=me.Border.all(me.BorderSide(width=1, style="solid", color="#DEE2E6")),
68
+ color="#000",
69
+ padding=me.Padding.all(10),
70
+ )
71
+ ):
72
+ me.text(row)
main.py ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ import re
3
+
4
+ import mesop as me
5
+
6
+ import components as mex
7
+
8
+ _DIALOG_INPUT_WIDTH = 350
9
+
10
+ _MODEL_TEMPERATURE_MAX = 2
11
+ _MODEL_TEMPERATURE_MIN = 0
12
+
13
+ _INSTRUCTIONS = """
14
+ - Write your prompt.
15
+ - You can use variables using this syntax `{{VARIABLE_NAME}}`.
16
+ - If you used variables, populate them from the `Set variables` dialog.
17
+ - Adjust model settings if necessary from the `Model settings` dialog.
18
+ - When you're ready, press the run button.
19
+ - If you make adjustments to your prompt or model settings, pressing run will create a
20
+ new version of your prompt.
21
+ """.strip()
22
+
23
+ _RE_VARIABLES = re.compile(r"\{\{(\w+)\}\}")
24
+
25
+
26
+ @dataclass
27
+ class Prompt:
28
+ prompt: str = ""
29
+ model: str = ""
30
+ model_temperature: float = 0.0
31
+ system_instructions: str = ""
32
+ version: int = 0
33
+ variables: list[str] = field(default_factory=lambda: [])
34
+ # Storing the responses as a dict to workaround bug with lists
35
+ # of nested dataclass.
36
+ responses: list[dict] = field(default_factory=lambda: [])
37
+
38
+
39
+ @me.stateclass
40
+ class State:
41
+ # Main UI variables
42
+ system_prompt_card_expanded: bool = False
43
+ title: str = "Untitled Prompt"
44
+ temp_title: str
45
+ system_instructions: str
46
+ prompt: str
47
+ response: str
48
+ version: int = 0
49
+
50
+ # Prompt variables
51
+ prompt_variables: dict[str, str]
52
+
53
+ # Model info
54
+ model: str = "gemini-1.5-flash"
55
+ model_temperature: float = 1.0
56
+ model_temperature_input: str = "1.0"
57
+
58
+ # Dialogs
59
+ dialog_show_title: bool = False
60
+ dialog_show_model_settings: bool = False
61
+ dialog_show_prompt_variables: bool = False
62
+ dialog_show_generate_prompt: bool = False
63
+ dialog_show_version_history: bool = False
64
+ prompts: list[Prompt]
65
+
66
+ # LLM Generate functionality
67
+ prompt_gen_task_description: str
68
+
69
+ # Valid modes: Prompt or Eval
70
+ mode: str = "Prompt"
71
+
72
+
73
+ @me.page()
74
+ def app():
75
+ state = me.state(State)
76
+
77
+ # Update prompt title dialog
78
+ with mex.dialog(state.dialog_show_title):
79
+ me.text("Update Prompt Title", type="headline-6")
80
+ me.input(
81
+ label="Title",
82
+ value=state.temp_title,
83
+ on_blur=on_update_input,
84
+ key="temp_title",
85
+ style=me.Style(width=_DIALOG_INPUT_WIDTH),
86
+ )
87
+ with mex.dialog_actions():
88
+ me.button("Cancel", on_click=on_close_dialog, key="dialog_show_title")
89
+ me.button("Save", type="flat", disabled=not state.temp_title.strip(), on_click=on_save_title)
90
+
91
+ # Dialog for controlling Model settings
92
+ with mex.dialog(state.dialog_show_model_settings):
93
+ me.text("Model Settings", type="headline-6")
94
+ with me.box():
95
+ me.select(
96
+ label="Model",
97
+ key="model",
98
+ options=[
99
+ me.SelectOption(label="Gemini 1.5 Flash", value="gemini-1.5-flash"),
100
+ me.SelectOption(label="Gemini 1.5 Pro", value="gemini-1.5-pro"),
101
+ ],
102
+ value=state.model,
103
+ style=me.Style(width=_DIALOG_INPUT_WIDTH),
104
+ on_selection_change=on_update_input,
105
+ )
106
+ with me.box():
107
+ me.text("Temperature", style=me.Style(font_weight="bold"))
108
+ with me.box(style=me.Style(display="flex", gap=10, width=_DIALOG_INPUT_WIDTH)):
109
+ me.slider(
110
+ min=_MODEL_TEMPERATURE_MIN,
111
+ max=_MODEL_TEMPERATURE_MAX,
112
+ step=0.1,
113
+ style=me.Style(width=260),
114
+ on_value_change=on_slider_temperature,
115
+ value=state.model_temperature,
116
+ )
117
+ me.input(
118
+ value=state.model_temperature_input,
119
+ on_input=on_input_temperature,
120
+ style=me.Style(width=60),
121
+ )
122
+
123
+ with mex.dialog_actions():
124
+ me.button(
125
+ "Close",
126
+ key="dialog_show_model_settings",
127
+ on_click=on_close_dialog,
128
+ )
129
+
130
+ # Dialog for setting variables
131
+ with mex.dialog(state.dialog_show_prompt_variables):
132
+ me.text("Prompt Variables", type="headline-6")
133
+ if not state.prompt_variables:
134
+ me.text("No variables defined in prompt.", style=me.Style(width=_DIALOG_INPUT_WIDTH))
135
+ else:
136
+ with me.box(
137
+ style=me.Style(display="flex", justify_content="end", margin=me.Margin(bottom=15))
138
+ ):
139
+ me.button("Generate", type="flat", on_click=on_click_generate_variables)
140
+ variable_names = set(_parse_variables(state.prompt))
141
+ with me.box(style=me.Style(display="flex", flex_direction="column")):
142
+ for name, value in state.prompt_variables.items():
143
+ if name not in variable_names:
144
+ continue
145
+ me.textarea(
146
+ label=name,
147
+ value=value,
148
+ on_blur=on_input_variable,
149
+ style=me.Style(width=_DIALOG_INPUT_WIDTH),
150
+ key=name,
151
+ )
152
+
153
+ with mex.dialog_actions():
154
+ me.button("Close", on_click=on_close_dialog, key="dialog_show_prompt_variables")
155
+
156
+ # Dialog for showing prompt version history
157
+ with mex.dialog(state.dialog_show_version_history):
158
+ me.text("Version history", type="headline-6")
159
+ me.select(
160
+ label="Select Version",
161
+ options=[
162
+ me.SelectOption(label=f"v{prompt.version}", value=str(prompt.version))
163
+ for prompt in state.prompts
164
+ ],
165
+ style=me.Style(width=_DIALOG_INPUT_WIDTH),
166
+ on_selection_change=on_select_version,
167
+ )
168
+ with mex.dialog_actions():
169
+ me.button("Close", key="dialog_show_version_history", on_click=on_close_dialog)
170
+
171
+ # Dialog for generating a prompt with LLM assistance
172
+ # TODO: Integrate with LLM
173
+ with mex.dialog(state.dialog_show_generate_prompt):
174
+ me.text("Generate Prompt", type="headline-6")
175
+ me.textarea(
176
+ label="Describe your task",
177
+ value=state.prompt_gen_task_description,
178
+ on_blur=on_update_input,
179
+ key="prompt_gen_task_description",
180
+ style=me.Style(width=_DIALOG_INPUT_WIDTH),
181
+ )
182
+ with mex.dialog_actions():
183
+ me.button("Close", key="dialog_show_generate_prompt", on_click=on_close_dialog)
184
+ me.button("Generate", type="flat", on_click=on_click_generate_prompt)
185
+
186
+ with me.box(
187
+ style=me.Style(
188
+ background="#FDFDFD",
189
+ display="grid",
190
+ grid_template_columns="50fr 50fr 1fr",
191
+ grid_template_rows="1fr 50fr",
192
+ height="100vh",
193
+ )
194
+ ):
195
+ with me.box(style=me.Style(grid_column="1 / -1")):
196
+ with mex.header(max_width=None):
197
+ with mex.header_section():
198
+ with me.box(on_click=on_click_title, style=me.Style(cursor="pointer")):
199
+ me.text(
200
+ state.title,
201
+ style=me.Style(font_size=16, font_weight="bold"),
202
+ )
203
+ if state.version:
204
+ me.text(f"v{state.version}")
205
+
206
+ with mex.header_section():
207
+ mex.button_toggle(
208
+ labels=["Prompt", "Eval"], selected=state.mode, on_click=on_click_mode_toggle
209
+ )
210
+
211
+ if state.mode == "Prompt":
212
+ # Render prompt creation page
213
+ with me.box(
214
+ style=me.Style(padding=me.Padding(left=15, top=15, bottom=15), overflow_y="scroll")
215
+ ):
216
+ with mex.expanable_card(
217
+ title="System Instructions",
218
+ expanded=state.system_prompt_card_expanded,
219
+ on_click_header=on_click_system_instructions_header,
220
+ ):
221
+ me.native_textarea(
222
+ autosize=True,
223
+ min_rows=2,
224
+ placeholder="Optional tone and style instructions for the model",
225
+ value=state.system_instructions,
226
+ on_blur=on_update_input,
227
+ style=_STYLE_INVISIBLE_TEXTAREA,
228
+ key="system_instructions",
229
+ )
230
+
231
+ with mex.card(title="Prompt"):
232
+ me.native_textarea(
233
+ autosize=True,
234
+ min_rows=2,
235
+ placeholder="Enter your prompt",
236
+ value=state.prompt,
237
+ on_blur=on_update_prompt,
238
+ style=_STYLE_INVISIBLE_TEXTAREA,
239
+ key="prompt",
240
+ )
241
+
242
+ with me.box(
243
+ style=me.Style(align_items="center", display="flex", justify_content="space-between")
244
+ ):
245
+ with me.content_button(
246
+ type="flat",
247
+ disabled=not state.prompt,
248
+ on_click=on_click_run,
249
+ style=me.Style(border_radius="10"),
250
+ ):
251
+ with me.tooltip(message="Run prompt"):
252
+ me.icon("play_arrow")
253
+ me.button(
254
+ "Generate prompt",
255
+ disabled=bool(state.prompt),
256
+ style=me.Style(background="#EBF1FD", border_radius="10"),
257
+ on_click=on_open_dialog,
258
+ key="dialog_show_generate_prompt",
259
+ )
260
+
261
+ with me.box(style=me.Style(padding=me.Padding.all(15))):
262
+ if state.response:
263
+ with mex.card(title="Response", style=me.Style(height="100%")):
264
+ me.markdown(state.response)
265
+ else:
266
+ with mex.card(title="Prompt Tuner Instructions", style=me.Style(height="100%")):
267
+ me.markdown(_INSTRUCTIONS)
268
+ else:
269
+ # Render eval page
270
+ with me.box(style=me.Style(grid_column="1 / -2")):
271
+ prompt = _find_prompt(state.prompts, state.version)
272
+ if prompt:
273
+ mex.prompt_eval_table(prompt)
274
+
275
+ with mex.icon_sidebar():
276
+ if state.mode == "Prompt":
277
+ mex.icon_menu_item(
278
+ icon="tune",
279
+ tooltip="Model settings",
280
+ key="dialog_show_model_settings",
281
+ on_click=on_open_dialog,
282
+ )
283
+ mex.icon_menu_item(
284
+ icon="data_object",
285
+ tooltip="Set variables",
286
+ key="dialog_show_prompt_variables",
287
+ on_click=on_open_dialog,
288
+ )
289
+ mex.icon_menu_item(
290
+ icon="history",
291
+ tooltip="Version history",
292
+ key="dialog_show_version_history",
293
+ on_click=on_open_dialog,
294
+ )
295
+ if state.mode == "Prompt":
296
+ mex.icon_menu_item(icon="code", tooltip="Get code")
297
+
298
+
299
+ # Event handlers
300
+
301
+
302
+ def on_click_system_instructions_header(e: me.ClickEvent):
303
+ """Open/close system instructions card."""
304
+ state = me.state(State)
305
+ state.system_prompt_card_expanded = not state.system_prompt_card_expanded
306
+
307
+
308
+ def on_click_run(e: me.ClickEvent):
309
+ state = me.state(State)
310
+ num_versions = len(state.prompts)
311
+ if state.version:
312
+ current_prompt_meta = state.prompts[state.version - 1]
313
+ else:
314
+ current_prompt_meta = Prompt()
315
+
316
+ variable_names = set(_parse_variables(state.prompt))
317
+ prompt_variables = {k: v for k, v in state.prompt_variables.items() if k in variable_names}
318
+
319
+ if (
320
+ current_prompt_meta.prompt != state.prompt
321
+ or current_prompt_meta.system_instructions != state.system_instructions
322
+ or current_prompt_meta.model != state.model
323
+ or current_prompt_meta.model_temperature != state.model_temperature
324
+ ):
325
+ new_version = num_versions + 1
326
+ state.prompts.append(
327
+ Prompt(
328
+ version=new_version,
329
+ prompt=state.prompt,
330
+ system_instructions=state.system_instructions,
331
+ model=state.model,
332
+ model_temperature=state.model_temperature,
333
+ variables=list(variable_names),
334
+ )
335
+ )
336
+ state.version = new_version
337
+
338
+ prompt = state.prompt
339
+ for k, v in prompt_variables.items():
340
+ prompt = prompt.replace("{{" + k + "}}", v)
341
+ state.response = "Version v" + str(state.version) + "\n\n" + prompt
342
+ state.prompts[-1].responses.append(dict(output=state.response, variables=prompt_variables))
343
+
344
+
345
+ def on_click_title(e: me.ClickEvent):
346
+ """Show dialog for editing the title of the prompt."""
347
+ state = me.state(State)
348
+ state.temp_title = state.title
349
+ state.dialog_show_title = True
350
+
351
+
352
+ def on_update_prompt(e: me.InputBlurEvent):
353
+ """Saves the prompt.
354
+
355
+ Any new variables will be extracted from the prompt and added to prompt variables in
356
+ the variables dialog.
357
+ """
358
+ state = me.state(State)
359
+ state.prompt = e.value.strip()
360
+ variable_names = _parse_variables(state.prompt)
361
+ for variable_name in variable_names:
362
+ if variable_name not in state.prompt_variables:
363
+ state.prompt_variables[variable_name] = ""
364
+
365
+
366
+ def on_save_title(e: me.InputBlurEvent):
367
+ """Saves the title and closes the dialog."""
368
+ state = me.state(State)
369
+ if state.temp_title:
370
+ state.title = state.temp_title
371
+ state.dialog_show_title = False
372
+
373
+
374
+ def on_slider_temperature(e: me.SliderValueChangeEvent):
375
+ """Adjust temperature slider value."""
376
+ state = me.state(State)
377
+ state.model_temperature = float(e.value)
378
+ state.model_temperature_input = str(state.model_temperature)
379
+
380
+
381
+ def on_input_temperature(e: me.InputEvent):
382
+ """Adjust temperature slider value by input."""
383
+ state = me.state(State)
384
+ try:
385
+ model_temperature = float(e.value)
386
+ if _MODEL_TEMPERATURE_MIN <= model_temperature <= _MODEL_TEMPERATURE_MAX:
387
+ state.model_temperature = model_temperature
388
+ except ValueError:
389
+ pass
390
+
391
+
392
+ def on_input_variable(e: me.InputBlurEvent):
393
+ """Generic event to save input variables.
394
+
395
+ TODO: Probably should prefix the key to avoid key collisions.
396
+ """
397
+ state = me.state(State)
398
+ state.prompt_variables[e.key] = e.value
399
+
400
+
401
+ def on_select_version(e: me.SelectSelectionChangeEvent):
402
+ """Update UI to show the selected prompt version and close the dialog."""
403
+ state = me.state(State)
404
+ selected_version = int(e.value)
405
+ prompt = _find_prompt(state.prompts, selected_version)
406
+ if prompt != Prompt():
407
+ state.prompt = prompt.prompt
408
+ state.version = prompt.version
409
+ state.system_instructions = prompt.system_instructions
410
+ state.model = prompt.model
411
+ state.model_temperature = prompt.model_temperature
412
+ state.model_temperature_input = str(prompt.model_temperature)
413
+ # If there is an existing response, select the most recent one.
414
+ if prompt.responses:
415
+ state.prompt_variables = prompt.responses[-1]["variables"]
416
+ state.response = prompt.responses[-1]["output"]
417
+ else:
418
+ state.response = ""
419
+ state.dialog_show_version_history = False
420
+
421
+
422
+ def on_click_generate_prompt(e: me.ClickEvent):
423
+ """Generates an improved prompt based on the given task description and closes dialog.
424
+
425
+ TODO: Implement this logic.
426
+ """
427
+ state = me.state(State)
428
+ state.prompt = state.prompt_gen_task_description + " Improve prompt stuff here"
429
+ state.dialog_show_generate_prompt = False
430
+
431
+
432
+ def on_click_generate_variables(e: me.ClickEvent):
433
+ """Generates values for the given empty variables.
434
+
435
+ TODO: Implement this logic.
436
+ """
437
+ state = me.state(State)
438
+ variable_names = set(_parse_variables(state.prompt))
439
+ for name, value in state.prompt_variables.items():
440
+ if name in variable_names and not value:
441
+ state.prompt_variables[name] = "Generate variable " + name
442
+
443
+
444
+ def on_click_mode_toggle(e: me.ClickEvent):
445
+ """Toggle between Prompt and Eval modes."""
446
+ state = me.state(State)
447
+ state.mode = "Eval" if state.mode == "Prompt" else "Prompt"
448
+
449
+
450
+ # Generic event handlers
451
+
452
+
453
+ def on_open_dialog(e: me.ClickEvent):
454
+ """Generic event to open a dialog."""
455
+ state = me.state(State)
456
+ setattr(state, e.key, True)
457
+
458
+
459
+ def on_close_dialog(e: me.ClickEvent):
460
+ """Generic event to close a dialog."""
461
+ state = me.state(State)
462
+ setattr(state, e.key, False)
463
+
464
+
465
+ def on_update_input(e: me.InputBlurEvent | me.SelectSelectionChangeEvent):
466
+ """Generic event to update input/select values."""
467
+ state = me.state(State)
468
+ setattr(state, e.key, e.value)
469
+
470
+
471
+ # Helper functions
472
+
473
+
474
+ def _parse_variables(prompt: str) -> list[str]:
475
+ return _RE_VARIABLES.findall(prompt)
476
+
477
+
478
+ def _find_prompt(prompts: list[Prompt], version: int) -> Prompt:
479
+ # We don't expected too many versions, so we'll just loop through the list to find the
480
+ # right version.
481
+ for prompt in prompts:
482
+ if prompt.version == version:
483
+ return prompt
484
+ return Prompt()
485
+
486
+
487
+ # Style helpers
488
+
489
+ _STYLE_INVISIBLE_TEXTAREA = me.Style(
490
+ overflow_y="hidden",
491
+ width="100%",
492
+ outline="none",
493
+ border=me.Border.all(me.BorderSide(style="none")),
494
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gunicorn
2
+ mesop
ruff.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ line-length = 100
2
+ indent-width = 2