Richard commited on
Commit
3d1636d
·
1 Parent(s): ff599c7

Update prompts + add chat optimized prompt

Browse files
components/dialog.py CHANGED
@@ -26,7 +26,7 @@ def dialog(is_open: bool):
26
  overflow_y="auto",
27
  position="fixed",
28
  width="100%",
29
- z_index=1000,
30
  )
31
  ):
32
  with me.box(
 
26
  overflow_y="auto",
27
  position="fixed",
28
  width="100%",
29
+ z_index=2000,
30
  )
31
  ):
32
  with me.box(
constants.py CHANGED
@@ -21,10 +21,22 @@ HELP_TEXT = """
21
 
22
  **Setting up a Mesop Runner instance**
23
 
24
- *If running on Hugging Face, you will need to fork the Mesop App Runner space.*
25
 
26
  - Start up an instance of the [Mesop App Runner](https://github.com/richard-to/mesop-app-runner).
27
  - See [Github repository](https://github.com/richard-to/mesop-app-runner) for more details.
28
  - Provide the Runner URL to your instance.
29
  - Provide the Runner Token to your runner instance.
30
  """.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  **Setting up a Mesop Runner instance**
23
 
24
+ *If running on Hugging Face, you will need to duplicate the [Mesop App Runner space](https://huggingface.co/spaces/richard-to/mesop-app-runner).*
25
 
26
  - Start up an instance of the [Mesop App Runner](https://github.com/richard-to/mesop-app-runner).
27
  - See [Github repository](https://github.com/richard-to/mesop-app-runner) for more details.
28
  - Provide the Runner URL to your instance.
29
  - Provide the Runner Token to your runner instance.
30
  """.strip()
31
+
32
+
33
+ TEMPLATES = {}
34
+ for template in ["default.txt", "basic_chat.txt", "advanced_chat.txt"]:
35
+ with open(f"templates/{template}") as f:
36
+ TEMPLATES[template] = f.read()
37
+
38
+
39
+ EXAMPLE_CHAT_PROMPTS = []
40
+ for example in ["basic_prompt.txt", "detailed_prompt.txt"]:
41
+ with open(f"example_prompts/{example}") as f:
42
+ EXAMPLE_CHAT_PROMPTS.append(f.read())
example_prompts/basic_prompt.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Build a basic chat app.
2
+
3
+ Here are the requirements, which you should follow carefully.
4
+
5
+ 1. The chat messages should use bubble chat styled messages
6
+ 2. Add thumbs up and thumbs down buttons for the bot messages. These buttons should be
7
+ aligned BELOW the bot message.
example_prompts/detailed_prompt.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Build a chat app that contains the following a features.
2
+
3
+ 1. There should be a header that says "Mesop chat".
4
+ 2. This header should be at the very TOP.
5
+ 2. There should a chat input should at the TOP, just BELOW the header
6
+ 4. The chat messages will go BELOW the chat input.
7
+ 5. The chat message styling should use rounded corners and colored background.
8
+ 6. There should also be button for dark mode.
llm.py CHANGED
@@ -1,17 +1,31 @@
 
 
1
  import google.generativeai as genai
2
 
3
 
4
- with open("prompt.txt") as f:
5
  SYSTEM_INSTRUCTION = f.read()
6
 
7
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  GENERATE_APP_BASE_PROMPT = """
9
  Your task is to write a Mesop app.
10
 
11
  Instructions:
12
  1. For the @me.page decorator, leave it empty like this `@me.page()`
13
  2. Event handler functions cannot use lambdas. You must use functions.
14
- 3. Event handle functions only pass in the event type. They do not accept extra parameters.
15
  4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
16
  5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
17
  6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
@@ -30,7 +44,7 @@ Your task is to modify a Mesop app given the code and a description.
30
  Make sure to remember these rules when making modifications:
31
  1. For the @me.page decorator, leave it empty like this `@me.page()`
32
  2. Event handler functions cannot use lambdas. You must use functions.
33
- 3. Event handle functions only pass in the event type. They do not accept extra parameters.
34
  4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
35
  5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
36
  6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
@@ -50,7 +64,56 @@ Here is a description of the changes I want:
50
  """.strip()
51
 
52
 
53
- def make_model(api_key: str, model_name: str) -> genai.GenerativeModel:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  genai.configure(api_key=api_key)
55
 
56
  generation_config = {
@@ -81,24 +144,43 @@ def make_model(api_key: str, model_name: str) -> genai.GenerativeModel:
81
 
82
  return genai.GenerativeModel(
83
  model_name=model_name,
84
- system_instruction=SYSTEM_INSTRUCTION,
85
  safety_settings=safety_settings,
86
  generation_config=generation_config,
87
  )
88
 
89
 
90
- def generate_mesop_app(msg: str, model_name: str, api_key: str) -> str:
91
- model = make_model(api_key, model_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  response = model.generate_content(
93
- GENERATE_APP_BASE_PROMPT.replace("<APP_DESCRIPTION>", msg), request_options={"timeout": 120}
 
94
  )
95
  return response.text
96
 
97
 
98
- def adjust_mesop_app(code: str, msg: str, model_name: str, api_key: str) -> str:
99
- model = make_model(api_key, model_name)
100
  response = model.generate_content(
101
- REVISE_APP_BASE_PROMPT.replace("<APP_CODE>", code).replace("<APP_CHANGES>", msg),
102
  request_options={"timeout": 120},
103
  )
104
  return response.text
 
1
+ from typing import Tuple
2
+
3
  import google.generativeai as genai
4
 
5
 
6
+ with open("prompt/base_system_instructions.txt") as f:
7
  SYSTEM_INSTRUCTION = f.read()
8
 
9
 
10
+ with open("prompt/base_examples.txt") as f:
11
+ DEFAULT_EXAMPLES = f.read()
12
+
13
+
14
+ with open("prompt/chat_elements_examples.txt") as f:
15
+ CHAT_ELEMENTS_EXAMPLES = f.read()
16
+
17
+
18
+ with open("prompt/chat_examples.txt") as f:
19
+ CHAT_EXAMPLES = f.read()
20
+
21
+
22
  GENERATE_APP_BASE_PROMPT = """
23
  Your task is to write a Mesop app.
24
 
25
  Instructions:
26
  1. For the @me.page decorator, leave it empty like this `@me.page()`
27
  2. Event handler functions cannot use lambdas. You must use functions.
28
+ 3. Event handler functions only pass in the event type. They do not accept extra parameters.
29
  4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
30
  5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
31
  6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
 
44
  Make sure to remember these rules when making modifications:
45
  1. For the @me.page decorator, leave it empty like this `@me.page()`
46
  2. Event handler functions cannot use lambdas. You must use functions.
47
+ 3. Event handler functions only pass in the event type. They do not accept extra parameters.
48
  4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
49
  5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
50
  6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
 
64
  """.strip()
65
 
66
 
67
+ GENERATE_CHAT_APP_BASE_PROMPT = """
68
+ Your task is to write a Mesop chat app.
69
+
70
+ Instructions:
71
+ 1. For the @me.page decorator, leave it empty like this `@me.page()`
72
+ 2. Event handler functions cannot use lambdas. You must use functions.
73
+ 3. Event handler functions only pass in the event type. They do not accept extra parameters.
74
+ 4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
75
+ 5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
76
+ 6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
77
+ 7. For buttons, prefer using type="flat", especially if it is the primary button.
78
+ 8. Remember that me.box is a like a div. So you can use similar CSS styles for layout.
79
+ 9. Only output the python code.
80
+
81
+ Here is a description of the chat app I want you to write:
82
+
83
+ <APP_DESCRIPTION>
84
+
85
+ """.strip()
86
+
87
+ REVISE_CHAT_APP_BASE_PROMPT = """
88
+ Your task is to modify a Mesop chat app given the code and a description.
89
+
90
+ Make sure to remember these rules when making modifications:
91
+ 1. For the @me.page decorator, leave it empty like this `@me.page()`
92
+ 2. Event handler functions cannot use lambdas. You must use functions.
93
+ 3. Event handler functions only pass in the event type. They do not accept extra parameters.
94
+ 4. For padding, make sure to use the the `me.Padding` object rather than a string or int.
95
+ 5. For margin, make sure to use the the `me.Margin` object rather than a string or int.
96
+ 6. For border, make sure to use the the `me.Border` and `me.BorderSide` objects rather than a string.
97
+ 7. For buttons, prefer using type="flat", especially if it is the primary button.
98
+ 8. Remember that me.box is a like a div. So you can use similar CSS styles for layout.
99
+ 9. Only output the python code.
100
+
101
+ Here is is the code for the chat app:
102
+
103
+ ```
104
+ <APP_CODE>
105
+ ```
106
+
107
+ Here is a description of the changes I want:
108
+
109
+ <APP_CHANGES>
110
+
111
+ """.strip()
112
+
113
+
114
+ def make_model(
115
+ api_key: str, model_name: str, additional_instructions: str
116
+ ) -> genai.GenerativeModel:
117
  genai.configure(api_key=api_key)
118
 
119
  generation_config = {
 
144
 
145
  return genai.GenerativeModel(
146
  model_name=model_name,
147
+ system_instruction=SYSTEM_INSTRUCTION + additional_instructions,
148
  safety_settings=safety_settings,
149
  generation_config=generation_config,
150
  )
151
 
152
 
153
+ def get_prompt_examples(app_type: str) -> str:
154
+ if app_type == "chat":
155
+ return CHAT_ELEMENTS_EXAMPLES + CHAT_EXAMPLES
156
+ return DEFAULT_EXAMPLES
157
+
158
+
159
+ def get_generate_prompt_base(app_type: str) -> str:
160
+ if app_type == "chat":
161
+ return GENERATE_CHAT_APP_BASE_PROMPT
162
+ return GENERATE_APP_BASE_PROMPT
163
+
164
+
165
+ def get_revise_prompt_base(app_type: str) -> str:
166
+ if app_type == "chat":
167
+ return REVISE_CHAT_APP_BASE_PROMPT
168
+ return REVISE_APP_BASE_PROMPT
169
+
170
+
171
+ def generate_mesop_app(msg: str, model_name: str, api_key: str, app_type: str) -> str:
172
+ model = make_model(api_key, model_name, get_prompt_examples(app_type))
173
  response = model.generate_content(
174
+ get_generate_prompt_base(app_type).replace("<APP_DESCRIPTION>", msg),
175
+ request_options={"timeout": 120},
176
  )
177
  return response.text
178
 
179
 
180
+ def adjust_mesop_app(code: str, msg: str, model_name: str, api_key: str, app_type: str) -> str:
181
+ model = make_model(api_key, model_name, get_prompt_examples(app_type))
182
  response = model.generate_content(
183
+ get_revise_prompt_base(app_type).replace("<APP_CODE>", code).replace("<APP_CHANGES>", msg),
184
  request_options={"timeout": 120},
185
  )
186
  return response.text
main.py CHANGED
@@ -13,6 +13,8 @@ from constants import (
13
  PROMPT_MODE_REVISE,
14
  PROMPT_MODE_GENERATE,
15
  HELP_TEXT,
 
 
16
  )
17
  from state import State
18
  from web_components import code_mirror_editor_component
@@ -71,6 +73,26 @@ def main():
71
  on_click=handlers.on_hide_component,
72
  )
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # Help dialog
75
  with mex.dialog(state.show_help_dialog):
76
  me.text("Usage Instructions", type="headline-6")
@@ -94,6 +116,19 @@ def main():
94
  selected=state.prompt_mode,
95
  on_click=on_click_prompt_mode,
96
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  me.textarea(
98
  value=state.prompt_placeholder,
99
  rows=10,
@@ -105,8 +140,31 @@ def main():
105
  disabled=state.loading,
106
  style=me.Style(width="100%", margin=me.Margin(top=15)),
107
  )
108
- with me.content_button(on_click=on_run_prompt, type="flat", disabled=state.loading):
109
- me.icon("send")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  # Prompt history panel
112
  with mex.panel(
@@ -275,6 +333,13 @@ def main():
275
  flex_direction="row",
276
  )
277
  ):
 
 
 
 
 
 
 
278
  mex.toolbar_button(
279
  icon="bolt",
280
  tooltip="Generate code",
@@ -363,6 +428,14 @@ def on_click_prompt_mode(e: me.ClickEvent):
363
  )
364
 
365
 
 
 
 
 
 
 
 
 
366
  def on_code_input(e: mel.WebEvent):
367
  """Captures code input into state on blur."""
368
  state = me.state(State)
@@ -401,6 +474,8 @@ def on_run_code(e: me.ClickEvent):
401
  def on_run_prompt(e: me.ClickEvent):
402
  """Generate code from prompt."""
403
  state = me.state(State)
 
 
404
 
405
  state.prompt_placeholder = state.prompt
406
  yield
@@ -413,10 +488,16 @@ def on_run_prompt(e: me.ClickEvent):
413
 
414
  if state.prompt_mode == PROMPT_MODE_REVISE:
415
  state.code = llm.adjust_mesop_app(
416
- state.code, state.prompt, model_name=state.model, api_key=state.api_key
 
 
 
 
417
  )
418
  else:
419
- state.code = llm.generate_mesop_app(state.prompt, model_name=state.model, api_key=state.api_key)
 
 
420
 
421
  state.code = state.code.strip().removeprefix("```python").removesuffix("```")
422
  state.code_placeholder = state.code
@@ -427,7 +508,11 @@ def on_run_prompt(e: me.ClickEvent):
427
  )
428
  state.prompt_history.append(
429
  dict(
430
- prompt=state.prompt, code=state.code, index=len(state.prompt_history), mode=state.prompt_mode
 
 
 
 
431
  )
432
  )
433
 
@@ -440,6 +525,15 @@ def on_run_prompt(e: me.ClickEvent):
440
  yield
441
 
442
 
 
 
 
 
 
 
 
 
 
443
  def on_show_prompt_history_panel(e: me.ClickEvent):
444
  """Show prompt history panel"""
445
  state = me.state(State)
@@ -466,6 +560,7 @@ def on_click_history_prompt(e: me.ClickEvent):
466
  state.prompt = state.prompt_placeholder
467
  state.code_placeholder = prompt_history["code"]
468
  state.code = state.code_placeholder
 
469
  state.prompt_mode = prompt_history["mode"]
470
  state.show_prompt_history_panel = False
471
  state.show_generate_panel = True
 
13
  PROMPT_MODE_REVISE,
14
  PROMPT_MODE_GENERATE,
15
  HELP_TEXT,
16
+ TEMPLATES,
17
+ EXAMPLE_CHAT_PROMPTS,
18
  )
19
  from state import State
20
  from web_components import code_mirror_editor_component
 
73
  on_click=handlers.on_hide_component,
74
  )
75
 
76
+ # New file dialog
77
+ with mex.dialog(state.show_new_dialog):
78
+ me.text("Select a template", type="headline-6")
79
+ me.select(
80
+ label="Template",
81
+ key="template-selector-" + str(state.select_index),
82
+ options=[
83
+ me.SelectOption(label="Default", value="default.txt"),
84
+ me.SelectOption(label="Basic Chat", value="basic_chat.txt"),
85
+ me.SelectOption(label="Advanced Chat", value="advanced_chat.txt"),
86
+ ],
87
+ on_selection_change=on_select_template,
88
+ )
89
+ with mex.dialog_actions():
90
+ me.button(
91
+ "Close",
92
+ key="show_new_dialog",
93
+ on_click=handlers.on_hide_component,
94
+ )
95
+
96
  # Help dialog
97
  with mex.dialog(state.show_help_dialog):
98
  me.text("Usage Instructions", type="headline-6")
 
116
  selected=state.prompt_mode,
117
  on_click=on_click_prompt_mode,
118
  )
119
+
120
+ me.select(
121
+ label="App type",
122
+ key="prompt_app_type",
123
+ value=state.prompt_app_type,
124
+ options=[
125
+ me.SelectOption(label="General", value="general"),
126
+ me.SelectOption(label="Chat", value="chat"),
127
+ ],
128
+ style=me.Style(width="100%", margin=me.Margin(top=30)),
129
+ on_selection_change=handlers.on_update_selection,
130
+ )
131
+
132
  me.textarea(
133
  value=state.prompt_placeholder,
134
  rows=10,
 
140
  disabled=state.loading,
141
  style=me.Style(width="100%", margin=me.Margin(top=15)),
142
  )
143
+
144
+ with me.tooltip(message="Generate app"):
145
+ with me.content_button(on_click=on_run_prompt, type="flat", disabled=state.loading):
146
+ me.icon("send")
147
+
148
+ if state.prompt_mode == "Generate" and state.prompt_app_type == "chat":
149
+ me.text("Example prompts", type="headline-6", style=me.Style(margin=me.Margin(top=15)))
150
+
151
+ for index, chat_prompt in enumerate(EXAMPLE_CHAT_PROMPTS):
152
+ with me.box(
153
+ key=f"example_prompt-{index}",
154
+ on_click=on_click_example_prompt,
155
+ style=me.Style(
156
+ background=me.theme_var("surface-container"),
157
+ border=me.Border.all(
158
+ me.BorderSide(width=1, color=me.theme_var("outline-variant"), style="solid")
159
+ ),
160
+ border_radius=5,
161
+ cursor="pointer",
162
+ margin=me.Margin.symmetric(vertical=10),
163
+ padding=me.Padding.all(10),
164
+ text_overflow="ellipsis",
165
+ ),
166
+ ):
167
+ me.text(_truncate_text(chat_prompt))
168
 
169
  # Prompt history panel
170
  with mex.panel(
 
333
  flex_direction="row",
334
  )
335
  ):
336
+ mex.toolbar_button(
337
+ icon="add",
338
+ tooltip="New file",
339
+ key="show_new_dialog",
340
+ on_click=handlers.on_show_component,
341
+ )
342
+
343
  mex.toolbar_button(
344
  icon="bolt",
345
  tooltip="Generate code",
 
428
  )
429
 
430
 
431
+ def on_click_example_prompt(e: me.ClickEvent):
432
+ """Populates chat box with example prompt."""
433
+ state = me.state(State)
434
+ _, index = e.key.split("-")
435
+ state.prompt = EXAMPLE_CHAT_PROMPTS[int(index)]
436
+ state.prompt_placeholder = state.prompt
437
+
438
+
439
  def on_code_input(e: mel.WebEvent):
440
  """Captures code input into state on blur."""
441
  state = me.state(State)
 
474
  def on_run_prompt(e: me.ClickEvent):
475
  """Generate code from prompt."""
476
  state = me.state(State)
477
+ if not state.prompt:
478
+ return
479
 
480
  state.prompt_placeholder = state.prompt
481
  yield
 
488
 
489
  if state.prompt_mode == PROMPT_MODE_REVISE:
490
  state.code = llm.adjust_mesop_app(
491
+ state.code,
492
+ state.prompt,
493
+ model_name=state.model,
494
+ api_key=state.api_key,
495
+ app_type=state.prompt_app_type,
496
  )
497
  else:
498
+ state.code = llm.generate_mesop_app(
499
+ state.prompt, model_name=state.model, api_key=state.api_key, app_type=state.prompt_app_type
500
+ )
501
 
502
  state.code = state.code.strip().removeprefix("```python").removesuffix("```")
503
  state.code_placeholder = state.code
 
508
  )
509
  state.prompt_history.append(
510
  dict(
511
+ prompt=state.prompt,
512
+ code=state.code,
513
+ index=len(state.prompt_history),
514
+ mode=state.prompt_mode,
515
+ app_type=state.prompt_app_type,
516
  )
517
  )
518
 
 
525
  yield
526
 
527
 
528
+ def on_select_template(e: me.SelectSelectionChangeEvent):
529
+ """Update editor with selected template"""
530
+ state = me.state(State)
531
+ state.code_placeholder = TEMPLATES[e.value]
532
+ state.code = TEMPLATES[e.value]
533
+ state.show_new_dialog = False
534
+ state.select_index += 1
535
+
536
+
537
  def on_show_prompt_history_panel(e: me.ClickEvent):
538
  """Show prompt history panel"""
539
  state = me.state(State)
 
560
  state.prompt = state.prompt_placeholder
561
  state.code_placeholder = prompt_history["code"]
562
  state.code = state.code_placeholder
563
+ state.prompt_app_type = prompt_history["app_type"]
564
  state.prompt_mode = prompt_history["mode"]
565
  state.show_prompt_history_panel = False
566
  state.show_generate_panel = True
prompt.txt DELETED
The diff for this file is too large to render. See raw diff
 
prompt/base_examples.txt ADDED
@@ -0,0 +1,1853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This section provides examples of Mesop code usage. Each example is wrapped in <example> tags.
2
+
3
+ <example>
4
+ import random
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ import mesop as me
10
+
11
+ Role = Literal["user", "assistant"]
12
+
13
+
14
+ @dataclass(kw_only=True)
15
+ class ChatMessage:
16
+ """Chat message metadata."""
17
+
18
+ role: Role = "user"
19
+ content: str = ""
20
+ edited: bool = False
21
+
22
+
23
+ @me.stateclass
24
+ class State:
25
+ input: str
26
+ output: list[ChatMessage]
27
+ in_progress: bool
28
+ rewrite: str
29
+ rewrite_message_index: int
30
+ preview_rewrite: str
31
+ preview_original: str
32
+ modal_open: bool
33
+
34
+
35
+ @me.page(
36
+ security_policy=me.SecurityPolicy(
37
+ allowed_iframe_parents=["https://google.github.io"]
38
+ ),
39
+ path="/llm_rewriter",
40
+ title="LLM Rewriter",
41
+ )
42
+ def page():
43
+ state = me.state(State)
44
+
45
+ # Modal
46
+ with me.box(style=_make_modal_background_style(state.modal_open)):
47
+ with me.box(style=_STYLE_MODAL_CONTAINER):
48
+ with me.box(style=_STYLE_MODAL_CONTENT):
49
+ me.textarea(
50
+ label="Rewrite",
51
+ style=_STYLE_INPUT_WIDTH,
52
+ value=state.rewrite,
53
+ on_input=on_rewrite_input,
54
+ )
55
+ with me.box():
56
+ me.button(
57
+ "Submit Rewrite",
58
+ color="primary",
59
+ type="flat",
60
+ on_click=on_click_submit_rewrite,
61
+ )
62
+ me.button(
63
+ "Cancel",
64
+ on_click=on_click_cancel_rewrite,
65
+ )
66
+ with me.box(style=_STYLE_PREVIEW_CONTAINER):
67
+ with me.box(style=_STYLE_PREVIEW_ORIGINAL):
68
+ me.text("Original Message", type="headline-6")
69
+ me.markdown(state.preview_original)
70
+
71
+ with me.box(style=_STYLE_PREVIEW_REWRITE):
72
+ me.text("Preview Rewrite", type="headline-6")
73
+ me.markdown(state.preview_rewrite)
74
+
75
+ # Chat UI
76
+ with me.box(style=_STYLE_APP_CONTAINER):
77
+ with me.box(style=_make_style_chat_ui_container(bool(_TITLE))):
78
+ me.text(_TITLE, type="headline-5", style=_STYLE_TITLE)
79
+ with me.box(style=_STYLE_CHAT_BOX):
80
+ for index, msg in enumerate(state.output):
81
+ with me.box(
82
+ style=_make_style_chat_bubble_wrapper(msg.role),
83
+ key=f"msg-{index}",
84
+ on_click=on_click_rewrite_msg,
85
+ ):
86
+ if msg.role == _ROLE_ASSISTANT:
87
+ me.text(
88
+ _display_username(_BOT_USER_DEFAULT, msg.edited),
89
+ style=_STYLE_CHAT_BUBBLE_NAME,
90
+ )
91
+ with me.box(style=_make_chat_bubble_style(msg.role, msg.edited)):
92
+ if msg.role == _ROLE_USER:
93
+ me.text(msg.content, style=_STYLE_CHAT_BUBBLE_PLAINTEXT)
94
+ else:
95
+ me.markdown(msg.content)
96
+ with me.tooltip(message="Rewrite response"):
97
+ me.icon(icon="edit_note")
98
+
99
+ if state.in_progress:
100
+ with me.box(key="scroll-to", style=me.Style(height=300)):
101
+ pass
102
+ with me.box(style=_STYLE_CHAT_INPUT_BOX):
103
+ with me.box(style=me.Style(flex_grow=1)):
104
+ me.input(
105
+ label=_LABEL_INPUT,
106
+ # Workaround: update key to clear input.
107
+ key=f"input-{len(state.output)}",
108
+ on_input=on_chat_input,
109
+ on_enter=on_click_submit_chat_msg,
110
+ style=_STYLE_CHAT_INPUT,
111
+ )
112
+ with me.content_button(
113
+ color="primary",
114
+ type="flat",
115
+ disabled=state.in_progress,
116
+ on_click=on_click_submit_chat_msg,
117
+ style=_STYLE_CHAT_BUTTON,
118
+ ):
119
+ me.icon(
120
+ _LABEL_BUTTON_IN_PROGRESS if state.in_progress else _LABEL_BUTTON
121
+ )
122
+
123
+
124
+ # Event Handlers
125
+
126
+
127
+ def on_chat_input(e: me.InputEvent):
128
+ """Capture chat text input."""
129
+ state = me.state(State)
130
+ state.input = e.value
131
+
132
+
133
+ def on_rewrite_input(e: me.InputEvent):
134
+ """Capture rewrite text input."""
135
+ state = me.state(State)
136
+ state.preview_rewrite = e.value
137
+
138
+
139
+ def on_click_rewrite_msg(e: me.ClickEvent):
140
+ """Shows rewrite modal when a message is clicked.
141
+
142
+ Edit this function to persist rewritten messages.
143
+ """
144
+ state = me.state(State)
145
+ index = int(e.key.replace("msg-", ""))
146
+ message = state.output[index]
147
+ if message.role == _ROLE_USER or state.in_progress:
148
+ return
149
+ state.modal_open = True
150
+ state.rewrite = message.content
151
+ state.rewrite_message_index = index
152
+ state.preview_original = message.content
153
+ state.preview_rewrite = message.content
154
+
155
+
156
+ def on_click_submit_rewrite(e: me.ClickEvent):
157
+ """Submits rewrite message."""
158
+ state = me.state(State)
159
+ state.modal_open = False
160
+ message = state.output[state.rewrite_message_index]
161
+ if message.content != state.preview_rewrite:
162
+ message.content = state.preview_rewrite
163
+ message.edited = True
164
+ state.rewrite_message_index = 0
165
+ state.rewrite = ""
166
+ state.preview_original = ""
167
+ state.preview_rewrite = ""
168
+
169
+
170
+ def on_click_cancel_rewrite(e: me.ClickEvent):
171
+ """Hides rewrite modal."""
172
+ state = me.state(State)
173
+ state.modal_open = False
174
+ state.rewrite_message_index = 0
175
+ state.rewrite = ""
176
+ state.preview_original = ""
177
+ state.preview_rewrite = ""
178
+
179
+
180
+ def on_click_submit_chat_msg(e: me.ClickEvent | me.InputEnterEvent):
181
+ """Handles submitting a chat message."""
182
+ state = me.state(State)
183
+ if state.in_progress or not state.input:
184
+ return
185
+ input = state.input
186
+ state.input = ""
187
+ yield
188
+
189
+ output = state.output
190
+ if output is None:
191
+ output = []
192
+ output.append(ChatMessage(role=_ROLE_USER, content=input))
193
+ state.in_progress = True
194
+ yield
195
+
196
+ me.scroll_into_view(key="scroll-to")
197
+ time.sleep(0.15)
198
+ yield
199
+
200
+ start_time = time.time()
201
+ output_message = respond_to_chat(input, state.output)
202
+ assistant_message = ChatMessage(role=_ROLE_ASSISTANT)
203
+ output.append(assistant_message)
204
+ state.output = output
205
+ for content in output_message:
206
+ assistant_message.content += content
207
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
208
+ if (time.time() - start_time) >= 0.25:
209
+ start_time = time.time()
210
+ yield
211
+
212
+ state.in_progress = False
213
+ yield
214
+
215
+
216
+ # Transform function for processing chat messages.
217
+
218
+
219
+ def respond_to_chat(input: str, history: list[ChatMessage]):
220
+ """Displays random canned text.
221
+
222
+ Edit this function to process messages with a real chatbot/LLM.
223
+ """
224
+ lines = [
225
+ (
226
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, "
227
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
228
+ ),
229
+ "Laoreet sit amet cursus sit amet dictum sit amet.",
230
+ "At lectus urna duis convallis.",
231
+ "A pellentesque sit amet porttitor eget.",
232
+ "Mauris nunc congue nisi vitae suscipit tellus mauris a diam.",
233
+ "Aliquet lectus proin nibh nisl condimentum id.",
234
+ "Integer malesuada nunc vel risus commodo viverra maecenas accumsan.",
235
+ "Tempor id eu nisl nunc mi.",
236
+ "Id consectetur purus ut faucibus pulvinar.",
237
+ "Mauris pharetra et ultrices neque ornare.",
238
+ "Facilisis magna etiam tempor orci.",
239
+ "Mauris pharetra et ultrices neque.",
240
+ "Sit amet facilisis magna etiam tempor orci.",
241
+ "Amet consectetur adipiscing elit pellentesque habitant morbi tristique.",
242
+ "Egestas erat imperdiet sed euismod.",
243
+ "Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida.",
244
+ "Habitant morbi tristique senectus et netus et malesuada.",
245
+ ]
246
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
247
+ yield line + " "
248
+
249
+
250
+ # Constants
251
+
252
+ _TITLE = "LLM Rewriter"
253
+
254
+ _ROLE_USER = "user"
255
+ _ROLE_ASSISTANT = "assistant"
256
+
257
+ _BOT_USER_DEFAULT = "mesop-bot"
258
+
259
+
260
+ # Styles
261
+
262
+ _COLOR_BACKGROUND = "#f0f4f8"
263
+ _COLOR_CHAT_BUBBLE_YOU = "#f2f2f2"
264
+ _COLOR_CHAT_BUBBLE_BOT = "#ebf3ff"
265
+ _COLOR_CHAT_BUUBBLE_EDITED = "#f2ebff"
266
+
267
+ _DEFAULT_PADDING = me.Padding.all(20)
268
+ _DEFAULT_BORDER_SIDE = me.BorderSide(
269
+ width="1px", style="solid", color="#ececec"
270
+ )
271
+
272
+ _LABEL_BUTTON = "send"
273
+ _LABEL_BUTTON_IN_PROGRESS = "pending"
274
+ _LABEL_INPUT = "Enter your prompt"
275
+
276
+ _STYLE_INPUT_WIDTH = me.Style(width="100%")
277
+
278
+ _STYLE_APP_CONTAINER = me.Style(
279
+ background=_COLOR_BACKGROUND,
280
+ display="grid",
281
+ height="100vh",
282
+ grid_template_columns="repeat(1, 1fr)",
283
+ )
284
+ _STYLE_TITLE = me.Style(padding=me.Padding(left=10))
285
+ _STYLE_CHAT_BOX = me.Style(
286
+ height="100%",
287
+ overflow_y="scroll",
288
+ padding=_DEFAULT_PADDING,
289
+ margin=me.Margin(bottom=20),
290
+ border_radius="10px",
291
+ border=me.Border(
292
+ left=_DEFAULT_BORDER_SIDE,
293
+ right=_DEFAULT_BORDER_SIDE,
294
+ top=_DEFAULT_BORDER_SIDE,
295
+ bottom=_DEFAULT_BORDER_SIDE,
296
+ ),
297
+ )
298
+ _STYLE_CHAT_INPUT = me.Style(width="100%")
299
+ _STYLE_CHAT_INPUT_BOX = me.Style(
300
+ padding=me.Padding(top=30), display="flex", flex_direction="row"
301
+ )
302
+ _STYLE_CHAT_BUTTON = me.Style(margin=me.Margin(top=8, left=8))
303
+ _STYLE_CHAT_BUBBLE_NAME = me.Style(
304
+ font_weight="bold",
305
+ font_size="12px",
306
+ padding=me.Padding(left=15, right=15, bottom=5),
307
+ )
308
+ _STYLE_CHAT_BUBBLE_PLAINTEXT = me.Style(margin=me.Margin.symmetric(vertical=15))
309
+
310
+ _STYLE_MODAL_CONTAINER = me.Style(
311
+ background="#fff",
312
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
313
+ width="min(1024px, 100%)",
314
+ box_sizing="content-box",
315
+ height="100vh",
316
+ overflow_y="scroll",
317
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
318
+ )
319
+
320
+ _STYLE_MODAL_CONTENT = me.Style(margin=me.Margin.all(20))
321
+
322
+ _STYLE_PREVIEW_CONTAINER = me.Style(
323
+ display="grid",
324
+ grid_template_columns="repeat(2, 1fr)",
325
+ )
326
+
327
+ _STYLE_PREVIEW_ORIGINAL = me.Style(color="#777", padding=_DEFAULT_PADDING)
328
+
329
+ _STYLE_PREVIEW_REWRITE = me.Style(
330
+ background=_COLOR_CHAT_BUUBBLE_EDITED, padding=_DEFAULT_PADDING
331
+ )
332
+
333
+
334
+ def _make_style_chat_ui_container(has_title: bool) -> me.Style:
335
+ """Generates styles for chat UI container depending on if there is a title or not.
336
+
337
+ Args:
338
+ has_title: Whether the Chat UI is display a title or not.
339
+ """
340
+ return me.Style(
341
+ display="grid",
342
+ grid_template_columns="repeat(1, 1fr)",
343
+ grid_template_rows="1fr 14fr 1fr" if has_title else "5fr 1fr",
344
+ margin=me.Margin.symmetric(vertical=0, horizontal="auto"),
345
+ width="min(1024px, 100%)",
346
+ height="100vh",
347
+ background="#fff",
348
+ box_shadow=(
349
+ "0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"
350
+ ),
351
+ padding=me.Padding(top=20, left=20, right=20),
352
+ )
353
+
354
+
355
+ def _make_style_chat_bubble_wrapper(role: Role) -> me.Style:
356
+ """Generates styles for chat bubble position.
357
+
358
+ Args:
359
+ role: Chat bubble alignment depends on the role
360
+ """
361
+ align_items = "end" if role == _ROLE_USER else "start"
362
+ return me.Style(
363
+ display="flex",
364
+ flex_direction="column",
365
+ align_items=align_items,
366
+ )
367
+
368
+
369
+ def _make_chat_bubble_style(role: Role, edited: bool) -> me.Style:
370
+ """Generates styles for chat bubble.
371
+
372
+ Args:
373
+ role: Chat bubble background color depends on the role
374
+ edited: Whether chat message was edited or not.
375
+ """
376
+ background = _COLOR_CHAT_BUBBLE_YOU
377
+ if role == _ROLE_ASSISTANT:
378
+ background = _COLOR_CHAT_BUBBLE_BOT
379
+ if edited:
380
+ background = _COLOR_CHAT_BUUBBLE_EDITED
381
+
382
+ return me.Style(
383
+ width="80%",
384
+ font_size="13px",
385
+ background=background,
386
+ border_radius="15px",
387
+ padding=me.Padding(right=15, left=15, bottom=3),
388
+ margin=me.Margin(bottom=10),
389
+ border=me.Border(
390
+ left=_DEFAULT_BORDER_SIDE,
391
+ right=_DEFAULT_BORDER_SIDE,
392
+ top=_DEFAULT_BORDER_SIDE,
393
+ bottom=_DEFAULT_BORDER_SIDE,
394
+ ),
395
+ )
396
+
397
+
398
+ def _make_modal_background_style(modal_open: bool) -> me.Style:
399
+ """Makes style for modal background.
400
+
401
+ Args:
402
+ modal_open: Whether the modal is open.
403
+ """
404
+ return me.Style(
405
+ display="block" if modal_open else "none",
406
+ position="fixed",
407
+ z_index=1000,
408
+ width="100%",
409
+ height="100%",
410
+ overflow_x="auto",
411
+ overflow_y="auto",
412
+ background="rgba(0,0,0,0.4)",
413
+ )
414
+
415
+
416
+ def _display_username(username: str, edited: bool = False) -> str:
417
+ """Displays the username
418
+
419
+ Args:
420
+ username: Name of the user
421
+ edited: Whether the message has been edited.
422
+ """
423
+ edited_text = " (edited)" if edited else ""
424
+ return username + edited_text
425
+
426
+ <example>
427
+
428
+ <example>
429
+ import random
430
+ import time
431
+ from typing import Callable
432
+
433
+ import mesop as me
434
+
435
+ _TEMPERATURE_MIN = 0.0
436
+ _TEMPERATURE_MAX = 2.0
437
+ _TOKEN_LIMIT_MIN = 1
438
+ _TOKEN_LIMIT_MAX = 8192
439
+
440
+
441
+ @me.stateclass
442
+ class State:
443
+ title: str = "LLM Playground"
444
+ # Prompt / Response
445
+ input: str
446
+ response: str
447
+ # Tab open/close
448
+ prompt_tab: bool = True
449
+ response_tab: bool = True
450
+ # Model configs
451
+ selected_model: str = "gemini-1.5"
452
+ selected_region: str = "us-east4"
453
+ temperature: float = 1.0
454
+ temperature_for_input: float = 1.0
455
+ token_limit: int = _TOKEN_LIMIT_MAX
456
+ token_limit_for_input: int = _TOKEN_LIMIT_MAX
457
+ stop_sequence: str = ""
458
+ stop_sequences: list[str]
459
+ # Modal
460
+ modal_open: bool = False
461
+ # Workaround for clearing inputs
462
+ clear_prompt_count: int = 0
463
+ clear_sequence_count: int = 0
464
+
465
+
466
+ @me.page(
467
+ security_policy=me.SecurityPolicy(
468
+ allowed_iframe_parents=["https://google.github.io"]
469
+ ),
470
+ path="/llm_playground",
471
+ title="LLM Playground",
472
+ )
473
+ def page():
474
+ state = me.state(State)
475
+
476
+ # Modal
477
+ with modal(modal_open=state.modal_open):
478
+ me.text("Get code", type="headline-5")
479
+ if "gemini" in state.selected_model:
480
+ me.text(
481
+ "Use the following code in your application to request a model response."
482
+ )
483
+ with me.box(style=_STYLE_CODE_BOX):
484
+ me.markdown(
485
+ _GEMINI_CODE_TEXT.format(
486
+ content=state.input.replace('"', '\\"'),
487
+ model=state.selected_model,
488
+ region=state.selected_region,
489
+ stop_sequences=make_stop_sequence_str(state.stop_sequences),
490
+ token_limit=state.token_limit,
491
+ temperature=state.temperature,
492
+ )
493
+ )
494
+ else:
495
+ me.text(
496
+ "You can use the following code to start integrating your current prompt and settings into your application."
497
+ )
498
+ with me.box(style=_STYLE_CODE_BOX):
499
+ me.markdown(
500
+ _GPT_CODE_TEXT.format(
501
+ content=state.input.replace('"', '\\"').replace("\n", "\\n"),
502
+ model=state.selected_model,
503
+ stop_sequences=make_stop_sequence_str(state.stop_sequences),
504
+ token_limit=state.token_limit,
505
+ temperature=state.temperature,
506
+ )
507
+ )
508
+ me.button(label="Close", type="raised", on_click=on_click_modal)
509
+
510
+ # Main content
511
+ with me.box(style=_STYLE_CONTAINER):
512
+ # Main Header
513
+ with me.box(style=_STYLE_MAIN_HEADER):
514
+ with me.box(style=_STYLE_TITLE_BOX):
515
+ me.text(
516
+ state.title,
517
+ type="headline-6",
518
+ style=me.Style(line_height="24px", margin=me.Margin(bottom=0)),
519
+ )
520
+
521
+ # Toolbar Header
522
+ with me.box(style=_STYLE_CONFIG_HEADER):
523
+ icon_button(
524
+ icon="code", tooltip="Code", label="CODE", on_click=on_click_show_code
525
+ )
526
+
527
+ # Main Content
528
+ with me.box(style=_STYLE_MAIN_COLUMN):
529
+ # Prompt Tab
530
+ with tab_box(header="Prompt", key="prompt_tab"):
531
+ me.textarea(
532
+ label="Write your prompt here, insert media and then click Submit",
533
+ # Workaround: update key to clear input.
534
+ key=f"prompt-{state.clear_prompt_count}",
535
+ on_input=on_prompt_input,
536
+ style=_STYLE_INPUT_WIDTH,
537
+ )
538
+ me.button(label="Submit", type="flat", on_click=on_click_submit)
539
+ me.button(label="Clear", on_click=on_click_clear)
540
+
541
+ # Response Tab
542
+ with tab_box(header="Response", key="response_tab"):
543
+ if state.response:
544
+ me.markdown(state.response)
545
+ else:
546
+ me.markdown(
547
+ "The model will generate a response after you click Submit."
548
+ )
549
+
550
+ # LLM Config
551
+ with me.box(style=_STYLE_CONFIG_COLUMN):
552
+ me.select(
553
+ options=[
554
+ me.SelectOption(label="Gemini 1.5", value="gemini-1.5"),
555
+ me.SelectOption(label="Chat-GPT Turbo", value="gpt-3.5-turbo"),
556
+ ],
557
+ label="Model",
558
+ style=_STYLE_INPUT_WIDTH,
559
+ on_selection_change=on_model_select,
560
+ value=state.selected_model,
561
+ )
562
+
563
+ if "gemini" in state.selected_model:
564
+ me.select(
565
+ options=[
566
+ me.SelectOption(label="us-central1 (Iowa)", value="us-central1"),
567
+ me.SelectOption(
568
+ label="us-east4 (North Virginia)", value="us-east4"
569
+ ),
570
+ ],
571
+ label="Region",
572
+ style=_STYLE_INPUT_WIDTH,
573
+ on_selection_change=on_region_select,
574
+ value=state.selected_region,
575
+ )
576
+
577
+ me.text("Temperature", style=_STYLE_SLIDER_LABEL)
578
+ with me.box(style=_STYLE_SLIDER_INPUT_BOX):
579
+ with me.box(style=_STYLE_SLIDER_WRAP):
580
+ me.slider(
581
+ min=_TEMPERATURE_MIN,
582
+ max=_TEMPERATURE_MAX,
583
+ step=0.1,
584
+ style=_STYLE_SLIDER,
585
+ on_value_change=on_slider_temperature,
586
+ value=state.temperature,
587
+ )
588
+ me.input(
589
+ style=_STYLE_SLIDER_INPUT,
590
+ value=str(state.temperature_for_input),
591
+ on_input=on_input_temperature,
592
+ )
593
+
594
+ me.text("Output Token Limit", style=_STYLE_SLIDER_LABEL)
595
+ with me.box(style=_STYLE_SLIDER_INPUT_BOX):
596
+ with me.box(style=_STYLE_SLIDER_WRAP):
597
+ me.slider(
598
+ min=_TOKEN_LIMIT_MIN,
599
+ max=_TOKEN_LIMIT_MAX,
600
+ style=_STYLE_SLIDER,
601
+ on_value_change=on_slider_token_limit,
602
+ value=state.token_limit,
603
+ )
604
+ me.input(
605
+ style=_STYLE_SLIDER_INPUT,
606
+ value=str(state.token_limit_for_input),
607
+ on_input=on_input_token_limit,
608
+ )
609
+
610
+ with me.box(style=_STYLE_STOP_SEQUENCE_BOX):
611
+ with me.box(style=_STYLE_STOP_SEQUENCE_WRAP):
612
+ me.input(
613
+ label="Add stop sequence",
614
+ style=_STYLE_INPUT_WIDTH,
615
+ on_input=on_stop_sequence_input,
616
+ # Workaround: update key to clear input.
617
+ key=f"input-sequence-{state.clear_sequence_count}",
618
+ )
619
+ with me.content_button(
620
+ style=me.Style(margin=me.Margin(left=10)),
621
+ on_click=on_click_add_stop_sequence,
622
+ ):
623
+ with me.tooltip(message="Add stop Sequence"):
624
+ me.icon(icon="add_circle")
625
+
626
+ # Stop sequence "chips"
627
+ for index, sequence in enumerate(state.stop_sequences):
628
+ me.button(
629
+ key=f"sequence-{index}",
630
+ label=sequence,
631
+ on_click=on_click_remove_stop_sequence,
632
+ type="raised",
633
+ style=_STYLE_STOP_SEQUENCE_CHIP,
634
+ )
635
+
636
+
637
+ # HELPER COMPONENTS
638
+
639
+
640
+ @me.component
641
+ def icon_button(*, icon: str, label: str, tooltip: str, on_click: Callable):
642
+ """Icon button with text and tooltip."""
643
+ with me.content_button(on_click=on_click):
644
+ with me.tooltip(message=tooltip):
645
+ with me.box(style=me.Style(display="flex")):
646
+ me.icon(icon=icon)
647
+ me.text(
648
+ label, style=me.Style(line_height="24px", margin=me.Margin(left=5))
649
+ )
650
+
651
+
652
+ @me.content_component
653
+ def tab_box(*, header: str, key: str):
654
+ """Collapsible tab box"""
655
+ state = me.state(State)
656
+ tab_open = getattr(state, key)
657
+ with me.box(style=me.Style(width="100%", margin=me.Margin(bottom=20))):
658
+ # Tab Header
659
+ with me.box(
660
+ key=key,
661
+ on_click=on_click_tab_header,
662
+ style=me.Style(padding=_DEFAULT_PADDING, border=_DEFAULT_BORDER),
663
+ ):
664
+ with me.box(style=me.Style(display="flex")):
665
+ me.icon(
666
+ icon="keyboard_arrow_down" if tab_open else "keyboard_arrow_right"
667
+ )
668
+ me.text(
669
+ header,
670
+ style=me.Style(
671
+ line_height="24px", margin=me.Margin(left=5), font_weight="bold"
672
+ ),
673
+ )
674
+ # Tab Content
675
+ with me.box(
676
+ style=me.Style(
677
+ padding=_DEFAULT_PADDING,
678
+ border=_DEFAULT_BORDER,
679
+ display="block" if tab_open else "none",
680
+ )
681
+ ):
682
+ me.slot()
683
+
684
+
685
+ @me.content_component
686
+ def modal(modal_open: bool):
687
+ """Basic modal box."""
688
+ with me.box(style=_make_modal_background_style(modal_open)):
689
+ with me.box(style=_STYLE_MODAL_CONTAINER):
690
+ with me.box(style=_STYLE_MODAL_CONTENT):
691
+ me.slot()
692
+
693
+
694
+ # EVENT HANDLERS
695
+
696
+
697
+ def on_click_clear(e: me.ClickEvent):
698
+ """Click event for clearing prompt text."""
699
+ state = me.state(State)
700
+ state.clear_prompt_count += 1
701
+ state.input = ""
702
+ state.response = ""
703
+
704
+
705
+ def on_prompt_input(e: me.InputEvent):
706
+ """Capture prompt input."""
707
+ state = me.state(State)
708
+ state.input = e.value
709
+
710
+
711
+ def on_model_select(e: me.SelectSelectionChangeEvent):
712
+ """Event to select model."""
713
+ state = me.state(State)
714
+ state.selected_model = e.value
715
+
716
+
717
+ def on_region_select(e: me.SelectSelectionChangeEvent):
718
+ """Event to select GCP region (Gemini models only)."""
719
+ state = me.state(State)
720
+ state.selected_region = e.value
721
+
722
+
723
+ def on_slider_temperature(e: me.SliderValueChangeEvent):
724
+ """Event to adjust temperature slider value."""
725
+ state = me.state(State)
726
+ state.temperature = float(e.value)
727
+ state.temperature_for_input = state.temperature
728
+
729
+
730
+ def on_input_temperature(e: me.InputEvent):
731
+ """Event to adjust temperature slider value by input."""
732
+ state = me.state(State)
733
+ try:
734
+ temperature = float(e.value)
735
+ if _TEMPERATURE_MIN <= temperature <= _TEMPERATURE_MAX:
736
+ state.temperature = temperature
737
+ except ValueError:
738
+ pass
739
+
740
+
741
+ def on_slider_token_limit(e: me.SliderValueChangeEvent):
742
+ """Event to adjust token limit slider value."""
743
+ state = me.state(State)
744
+ state.token_limit = int(e.value)
745
+ state.token_limit_for_input = state.token_limit
746
+
747
+
748
+ def on_input_token_limit(e: me.InputEvent):
749
+ """Event to adjust token limit slider value by input."""
750
+ state = me.state(State)
751
+ try:
752
+ token_limit = int(e.value)
753
+ if _TOKEN_LIMIT_MIN <= token_limit <= _TOKEN_LIMIT_MAX:
754
+ state.token_limit = token_limit
755
+ except ValueError:
756
+ pass
757
+
758
+
759
+ def on_stop_sequence_input(e: me.InputEvent):
760
+ """Capture stop sequence input."""
761
+ state = me.state(State)
762
+ state.stop_sequence = e.value
763
+
764
+
765
+ def on_click_add_stop_sequence(e: me.ClickEvent):
766
+ """Save stop sequence. Will create "chip" for the sequence in the input."""
767
+ state = me.state(State)
768
+ if state.stop_sequence:
769
+ state.stop_sequences.append(state.stop_sequence)
770
+ state.clear_sequence_count += 1
771
+
772
+
773
+ def on_click_remove_stop_sequence(e: me.ClickEvent):
774
+ """Click event that removes the stop sequence that was clicked."""
775
+ state = me.state(State)
776
+ index = int(e.key.replace("sequence-", ""))
777
+ del state.stop_sequences[index]
778
+
779
+
780
+ def on_click_tab_header(e: me.ClickEvent):
781
+ """Open and closes tab content."""
782
+ state = me.state(State)
783
+ setattr(state, e.key, not getattr(state, e.key))
784
+
785
+
786
+ def on_click_show_code(e: me.ClickEvent):
787
+ """Opens modal to show generated code for the given model configuration."""
788
+ state = me.state(State)
789
+ state.modal_open = True
790
+
791
+
792
+ def on_click_modal(e: me.ClickEvent):
793
+ """Allows modal to be closed."""
794
+ state = me.state(State)
795
+ if state.modal_open:
796
+ state.modal_open = False
797
+
798
+
799
+ def on_click_submit(e: me.ClickEvent):
800
+ """Submits prompt to test model configuration.
801
+
802
+ This example returns canned text. A real implementation
803
+ would call APIs against the given configuration.
804
+ """
805
+ state = me.state(State)
806
+ for line in transform(state.input):
807
+ state.response += line
808
+ yield
809
+
810
+
811
+ def transform(input: str):
812
+ """Transform function that returns canned responses."""
813
+ for line in random.sample(LINES, random.randint(3, len(LINES) - 1)):
814
+ time.sleep(0.3)
815
+ yield line + " "
816
+
817
+
818
+ LINES = [
819
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
820
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
821
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
822
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
823
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
824
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
825
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
826
+ ]
827
+
828
+
829
+ # HELPERS
830
+
831
+ _GEMINI_CODE_TEXT = """
832
+ ```python
833
+ import base64
834
+ import vertexai
835
+ from vertexai.generative_models import GenerativeModel, Part, FinishReason
836
+ import vertexai.preview.generative_models as generative_models
837
+
838
+ def generate():
839
+ vertexai.init(project="<YOUR-PROJECT-ID>", location="{region}")
840
+ model = GenerativeModel("{model}")
841
+ responses = model.generate_content(
842
+ [\"\"\"{content}\"\"\"],
843
+ generation_config=generation_config,
844
+ safety_settings=safety_settings,
845
+ stream=True,
846
+ )
847
+
848
+ for response in responses:
849
+ print(response.text, end="")
850
+
851
+
852
+ generation_config = {{
853
+ "max_output_tokens": {token_limit},
854
+ "stop_sequences": [{stop_sequences}],
855
+ "temperature": {temperature},
856
+ "top_p": 0.95,
857
+ }}
858
+
859
+ safety_settings = {{
860
+ generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
861
+ generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
862
+ generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
863
+ generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
864
+ }}
865
+
866
+ generate()
867
+ ```
868
+ """.strip()
869
+
870
+ _GPT_CODE_TEXT = """
871
+ ```python
872
+ from openai import OpenAI
873
+ client = OpenAI()
874
+
875
+ response = client.chat.completions.create(
876
+ model="{model}",
877
+ messages=[
878
+ {{
879
+ "role": "user",
880
+ "content": "{content}"
881
+ }}
882
+ ],
883
+ temperature={temperature},
884
+ max_tokens={token_limit},
885
+ top_p=1,
886
+ frequency_penalty=0,
887
+ presence_penalty=0,
888
+ stop=[{stop_sequences}]
889
+ )
890
+ ```
891
+ """.strip()
892
+
893
+
894
+ def make_stop_sequence_str(stop_sequences: list[str]) -> str:
895
+ """Formats stop sequences for code output (list of strings)."""
896
+ return ",".join(map(lambda s: f'"{s}"', stop_sequences))
897
+
898
+
899
+ # STYLES
900
+
901
+
902
+ def _make_modal_background_style(modal_open: bool) -> me.Style:
903
+ """Makes style for modal background.
904
+
905
+ Args:
906
+ modal_open: Whether the modal is open.
907
+ """
908
+ return me.Style(
909
+ display="block" if modal_open else "none",
910
+ position="fixed",
911
+ z_index=1000,
912
+ width="100%",
913
+ height="100%",
914
+ overflow_x="auto",
915
+ overflow_y="auto",
916
+ background="rgba(0,0,0,0.4)",
917
+ )
918
+
919
+
920
+ _DEFAULT_PADDING = me.Padding.all(15)
921
+ _DEFAULT_BORDER = me.Border.all(
922
+ me.BorderSide(color="#e0e0e0", width=1, style="solid")
923
+ )
924
+
925
+ _STYLE_INPUT_WIDTH = me.Style(width="100%")
926
+ _STYLE_SLIDER_INPUT_BOX = me.Style(display="flex", flex_wrap="wrap")
927
+ _STYLE_SLIDER_WRAP = me.Style(flex_grow=1)
928
+ _STYLE_SLIDER_LABEL = me.Style(padding=me.Padding(bottom=10))
929
+ _STYLE_SLIDER = me.Style(width="90%")
930
+ _STYLE_SLIDER_INPUT = me.Style(width=75)
931
+
932
+ _STYLE_STOP_SEQUENCE_BOX = me.Style(display="flex")
933
+ _STYLE_STOP_SEQUENCE_WRAP = me.Style(flex_grow=1)
934
+
935
+ _STYLE_CONTAINER = me.Style(
936
+ display="grid",
937
+ grid_template_columns="5fr 2fr",
938
+ grid_template_rows="auto 5fr",
939
+ height="100vh",
940
+ )
941
+
942
+ _STYLE_MAIN_HEADER = me.Style(
943
+ border=_DEFAULT_BORDER, padding=me.Padding.all(15)
944
+ )
945
+
946
+ _STYLE_MAIN_COLUMN = me.Style(
947
+ border=_DEFAULT_BORDER,
948
+ padding=me.Padding.all(15),
949
+ overflow_y="scroll",
950
+ )
951
+
952
+ _STYLE_CONFIG_COLUMN = me.Style(
953
+ border=_DEFAULT_BORDER,
954
+ padding=me.Padding.all(15),
955
+ overflow_y="scroll",
956
+ )
957
+
958
+ _STYLE_TITLE_BOX = me.Style(display="inline-block")
959
+
960
+ _STYLE_CONFIG_HEADER = me.Style(
961
+ border=_DEFAULT_BORDER, padding=me.Padding.all(10)
962
+ )
963
+
964
+ _STYLE_STOP_SEQUENCE_CHIP = me.Style(margin=me.Margin.all(3))
965
+
966
+ _STYLE_MODAL_CONTAINER = me.Style(
967
+ background="#fff",
968
+ margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
969
+ width="min(1024px, 100%)",
970
+ box_sizing="content-box",
971
+ height="100vh",
972
+ overflow_y="scroll",
973
+ box_shadow=("0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"),
974
+ )
975
+
976
+ _STYLE_MODAL_CONTENT = me.Style(margin=me.Margin.all(30))
977
+
978
+ _STYLE_CODE_BOX = me.Style(
979
+ font_size=13,
980
+ margin=me.Margin.symmetric(vertical=10, horizontal=0),
981
+ padding=me.Padding.all(10),
982
+ border=me.Border.all(me.BorderSide(color="#e0e0e0", width=1, style="solid")),
983
+ )
984
+
985
+ <example>
986
+
987
+ <example>
988
+ import time
989
+ from dataclasses import dataclass
990
+ from typing import Callable, Generator, Literal
991
+
992
+ import mesop as me
993
+
994
+ Role = Literal["user", "assistant"]
995
+
996
+ _ROLE_USER = "user"
997
+ _ROLE_ASSISTANT = "assistant"
998
+
999
+ _BOT_USER_DEFAULT = "mesop-bot"
1000
+
1001
+ _COLOR_BACKGROUND = me.theme_var("background")
1002
+ _COLOR_CHAT_BUBBLE_YOU = me.theme_var("surface-container-low")
1003
+ _COLOR_CHAT_BUBBLE_BOT = me.theme_var("secondary-container")
1004
+
1005
+ _DEFAULT_PADDING = me.Padding.all(20)
1006
+ _DEFAULT_BORDER_SIDE = me.BorderSide(
1007
+ width="1px", style="solid", color=me.theme_var("secondary-fixed")
1008
+ )
1009
+
1010
+ _LABEL_BUTTON = "send"
1011
+ _LABEL_BUTTON_IN_PROGRESS = "pending"
1012
+ _LABEL_INPUT = "Enter your prompt"
1013
+
1014
+ _STYLE_APP_CONTAINER = me.Style(
1015
+ background=_COLOR_BACKGROUND,
1016
+ display="grid",
1017
+ height="100vh",
1018
+ grid_template_columns="repeat(1, 1fr)",
1019
+ )
1020
+ _STYLE_TITLE = me.Style(padding=me.Padding(left=10))
1021
+ _STYLE_CHAT_BOX = me.Style(
1022
+ height="100%",
1023
+ overflow_y="scroll",
1024
+ padding=_DEFAULT_PADDING,
1025
+ margin=me.Margin(bottom=20),
1026
+ border_radius="10px",
1027
+ border=me.Border(
1028
+ left=_DEFAULT_BORDER_SIDE,
1029
+ right=_DEFAULT_BORDER_SIDE,
1030
+ top=_DEFAULT_BORDER_SIDE,
1031
+ bottom=_DEFAULT_BORDER_SIDE,
1032
+ ),
1033
+ )
1034
+ _STYLE_CHAT_INPUT = me.Style(width="100%")
1035
+ _STYLE_CHAT_INPUT_BOX = me.Style(
1036
+ padding=me.Padding(top=30), display="flex", flex_direction="row"
1037
+ )
1038
+ _STYLE_CHAT_BUTTON = me.Style(margin=me.Margin(top=8, left=8))
1039
+ _STYLE_CHAT_BUBBLE_NAME = me.Style(
1040
+ font_weight="bold",
1041
+ font_size="13px",
1042
+ padding=me.Padding(left=15, right=15, bottom=5),
1043
+ )
1044
+ _STYLE_CHAT_BUBBLE_PLAINTEXT = me.Style(margin=me.Margin.symmetric(vertical=15))
1045
+
1046
+
1047
+ def _make_style_chat_ui_container(has_title: bool) -> me.Style:
1048
+ """Generates styles for chat UI container depending on if there is a title or not.
1049
+
1050
+ Args:
1051
+ has_title: Whether the Chat UI is display a title or not.
1052
+ """
1053
+ return me.Style(
1054
+ display="grid",
1055
+ grid_template_columns="repeat(1, 1fr)",
1056
+ grid_template_rows="1fr 14fr 1fr" if has_title else "5fr 1fr",
1057
+ margin=me.Margin.symmetric(vertical=0, horizontal="auto"),
1058
+ width="min(1024px, 100%)",
1059
+ height="100vh",
1060
+ background=_COLOR_BACKGROUND,
1061
+ box_shadow=(
1062
+ "0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"
1063
+ ),
1064
+ padding=me.Padding(top=20, left=20, right=20),
1065
+ )
1066
+
1067
+
1068
+ def _make_style_chat_bubble_wrapper(role: Role) -> me.Style:
1069
+ """Generates styles for chat bubble position.
1070
+
1071
+ Args:
1072
+ role: Chat bubble alignment depends on the role
1073
+ """
1074
+ align_items = "end" if role == _ROLE_USER else "start"
1075
+ return me.Style(
1076
+ display="flex",
1077
+ flex_direction="column",
1078
+ align_items=align_items,
1079
+ )
1080
+
1081
+
1082
+ def _make_chat_bubble_style(role: Role) -> me.Style:
1083
+ """Generates styles for chat bubble.
1084
+
1085
+ Args:
1086
+ role: Chat bubble background color depends on the role
1087
+ """
1088
+ background = (
1089
+ _COLOR_CHAT_BUBBLE_YOU if role == _ROLE_USER else _COLOR_CHAT_BUBBLE_BOT
1090
+ )
1091
+ return me.Style(
1092
+ width="80%",
1093
+ font_size="16px",
1094
+ line_height="1.5",
1095
+ background=background,
1096
+ border_radius="15px",
1097
+ padding=me.Padding(right=15, left=15, bottom=3),
1098
+ margin=me.Margin(bottom=10),
1099
+ border=me.Border(
1100
+ left=_DEFAULT_BORDER_SIDE,
1101
+ right=_DEFAULT_BORDER_SIDE,
1102
+ top=_DEFAULT_BORDER_SIDE,
1103
+ bottom=_DEFAULT_BORDER_SIDE,
1104
+ ),
1105
+ )
1106
+
1107
+
1108
+ @dataclass(kw_only=True)
1109
+ class ChatMessage:
1110
+ """Chat message metadata."""
1111
+
1112
+ role: Role = "user"
1113
+ content: str = ""
1114
+
1115
+
1116
+ @me.stateclass
1117
+ class State:
1118
+ input: str
1119
+ output: list[ChatMessage]
1120
+ in_progress: bool = False
1121
+
1122
+
1123
+ def on_blur(e: me.InputBlurEvent):
1124
+ state = me.state(State)
1125
+ state.input = e.value
1126
+
1127
+
1128
+ def chat(
1129
+ transform: Callable[
1130
+ [str, list[ChatMessage]], Generator[str, None, None] | str
1131
+ ],
1132
+ *,
1133
+ title: str | None = None,
1134
+ bot_user: str = _BOT_USER_DEFAULT,
1135
+ ):
1136
+ """Creates a simple chat UI which takes in a prompt and chat history and returns a
1137
+ response to the prompt.
1138
+
1139
+ This function creates event handlers for text input and output operations
1140
+ using the provided function `transform` to process the input and generate the output.
1141
+
1142
+ Args:
1143
+ transform: Function that takes in a prompt and chat history and returns a response to the prompt.
1144
+ title: Headline text to display at the top of the UI.
1145
+ bot_user: Name of your bot / assistant.
1146
+ """
1147
+ state = me.state(State)
1148
+
1149
+ def on_click_submit(e: me.ClickEvent):
1150
+ yield from submit()
1151
+
1152
+ def on_input_enter(e: me.InputEnterEvent):
1153
+ state = me.state(State)
1154
+ state.input = e.value
1155
+ yield from submit()
1156
+ me.focus_component(key=f"input-{len(state.output)}")
1157
+ yield
1158
+
1159
+ def submit():
1160
+ state = me.state(State)
1161
+ if state.in_progress or not state.input:
1162
+ return
1163
+ input = state.input
1164
+ state.input = ""
1165
+ yield
1166
+
1167
+ output = state.output
1168
+ if output is None:
1169
+ output = []
1170
+ output.append(ChatMessage(role=_ROLE_USER, content=input))
1171
+ state.in_progress = True
1172
+ yield
1173
+
1174
+ me.scroll_into_view(key="scroll-to")
1175
+ time.sleep(0.15)
1176
+ yield
1177
+
1178
+ start_time = time.time()
1179
+ output_message = transform(input, state.output)
1180
+ assistant_message = ChatMessage(role=_ROLE_ASSISTANT)
1181
+ output.append(assistant_message)
1182
+ state.output = output
1183
+
1184
+ for content in output_message:
1185
+ assistant_message.content += content
1186
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
1187
+ if (time.time() - start_time) >= 0.25:
1188
+ start_time = time.time()
1189
+ yield
1190
+ state.in_progress = False
1191
+ me.focus_component(key=f"input-{len(state.output)}")
1192
+ yield
1193
+
1194
+ def toggle_theme(e: me.ClickEvent):
1195
+ if me.theme_brightness() == "light":
1196
+ me.set_theme_mode("dark")
1197
+ else:
1198
+ me.set_theme_mode("light")
1199
+
1200
+ with me.box(style=_STYLE_APP_CONTAINER):
1201
+ with me.content_button(
1202
+ type="icon",
1203
+ style=me.Style(position="absolute", right=4, top=8),
1204
+ on_click=toggle_theme,
1205
+ ):
1206
+ me.icon("light_mode" if me.theme_brightness() == "dark" else "dark_mode")
1207
+ with me.box(style=_make_style_chat_ui_container(bool(title))):
1208
+ if title:
1209
+ me.text(title, type="headline-5", style=_STYLE_TITLE)
1210
+ with me.box(style=_STYLE_CHAT_BOX):
1211
+ for msg in state.output:
1212
+ with me.box(style=_make_style_chat_bubble_wrapper(msg.role)):
1213
+ if msg.role == _ROLE_ASSISTANT:
1214
+ me.text(bot_user, style=_STYLE_CHAT_BUBBLE_NAME)
1215
+ with me.box(style=_make_chat_bubble_style(msg.role)):
1216
+ if msg.role == _ROLE_USER:
1217
+ me.text(msg.content, style=_STYLE_CHAT_BUBBLE_PLAINTEXT)
1218
+ else:
1219
+ me.markdown(msg.content)
1220
+
1221
+ if state.in_progress:
1222
+ with me.box(key="scroll-to", style=me.Style(height=300)):
1223
+ pass
1224
+
1225
+ with me.box(style=_STYLE_CHAT_INPUT_BOX):
1226
+ with me.box(style=me.Style(flex_grow=1)):
1227
+ me.input(
1228
+ label=_LABEL_INPUT,
1229
+ # Workaround: update key to clear input.
1230
+ key=f"input-{len(state.output)}",
1231
+ on_blur=on_blur,
1232
+ on_enter=on_input_enter,
1233
+ style=_STYLE_CHAT_INPUT,
1234
+ )
1235
+ with me.content_button(
1236
+ color="primary",
1237
+ type="flat",
1238
+ disabled=state.in_progress,
1239
+ on_click=on_click_submit,
1240
+ style=_STYLE_CHAT_BUTTON,
1241
+ ):
1242
+ me.icon(
1243
+ _LABEL_BUTTON_IN_PROGRESS if state.in_progress else _LABEL_BUTTON
1244
+ )
1245
+
1246
+ <example>
1247
+
1248
+ <example>
1249
+ import types
1250
+ from typing import Callable, Generator, Literal, cast
1251
+
1252
+ import mesop as me
1253
+
1254
+
1255
+ @me.stateclass
1256
+ class State:
1257
+ input: str
1258
+ output: str
1259
+ textarea_key: int
1260
+
1261
+
1262
+ def text_io(
1263
+ transform: Callable[[str], Generator[str, None, None] | str],
1264
+ *,
1265
+ title: str | None = None,
1266
+ transform_mode: Literal["append", "replace"] = "replace",
1267
+ ):
1268
+ """Deprecated: Use `text_to_text` instead which provides the same functionality
1269
+ with better default settings.
1270
+
1271
+ This function creates event handlers for text input and output operations
1272
+ using the provided transform function to process the input and generate the output.
1273
+
1274
+ Args:
1275
+ transform: Function that takes in a string input and either returns or yields a string output.
1276
+ title: Headline text to display at the top of the UI
1277
+ transform_mode: Specifies how the output should be updated when yielding an output using a generator.
1278
+ - "append": Concatenates each new piece of text to the existing output.
1279
+ - "replace": Replaces the existing output with each new piece of text.
1280
+ """
1281
+ print(
1282
+ "\033[93m[warning]\033[0m text_io is deprecated, use text_to_text instead"
1283
+ )
1284
+ text_to_text(transform=transform, title=title, transform_mode=transform_mode)
1285
+
1286
+
1287
+ def text_to_text(
1288
+ transform: Callable[[str], Generator[str, None, None] | str],
1289
+ *,
1290
+ title: str | None = None,
1291
+ transform_mode: Literal["append", "replace"] = "append",
1292
+ ):
1293
+ """Creates a simple UI which takes in a text input and returns a text output.
1294
+
1295
+ This function creates event handlers for text input and output operations
1296
+ using the provided transform function to process the input and generate the output.
1297
+
1298
+ Args:
1299
+ transform: Function that takes in a string input and either returns or yields a string output.
1300
+ title: Headline text to display at the top of the UI
1301
+ transform_mode: Specifies how the output should be updated when yielding an output using a generator.
1302
+ - "append": Concatenates each new piece of text to the existing output.
1303
+ - "replace": Replaces the existing output with each new piece of text.
1304
+ """
1305
+
1306
+ def on_input(e: me.InputEvent):
1307
+ state = me.state(State)
1308
+ state.input = e.value
1309
+
1310
+ def on_click_generate(e: me.ClickEvent):
1311
+ state = me.state(State)
1312
+ output = transform(state.input)
1313
+ if isinstance(output, types.GeneratorType):
1314
+ for val in output:
1315
+ if transform_mode == "append":
1316
+ state.output += val
1317
+ elif transform_mode == "replace":
1318
+ state.output = val
1319
+ else:
1320
+ raise ValueError(f"Unsupported transform_mode: {transform_mode}")
1321
+ yield
1322
+ else:
1323
+ # `output` is a str, however type inference doesn't
1324
+ # work w/ generator's unusual ininstance check.
1325
+ state.output = cast(str, output)
1326
+ yield
1327
+
1328
+ def on_click_clear(e: me.ClickEvent):
1329
+ state = me.state(State)
1330
+ state.input = ""
1331
+ state.textarea_key += 1
1332
+
1333
+ with me.box(
1334
+ style=me.Style(
1335
+ background="#f0f4f8",
1336
+ height="100%",
1337
+ )
1338
+ ):
1339
+ with me.box(
1340
+ style=me.Style(
1341
+ background="#f0f4f8",
1342
+ padding=me.Padding(top=24, left=24, right=24, bottom=24),
1343
+ display="flex",
1344
+ flex_direction="column",
1345
+ )
1346
+ ):
1347
+ if title:
1348
+ me.text(title, type="headline-5")
1349
+ with me.box(
1350
+ style=me.Style(
1351
+ margin=me.Margin(left="auto", right="auto"),
1352
+ width="min(1024px, 100%)",
1353
+ gap="24px",
1354
+ flex_grow=1,
1355
+ display="flex",
1356
+ flex_wrap="wrap",
1357
+ )
1358
+ ):
1359
+ box_style = me.Style(
1360
+ flex_basis="max(480px, calc(50% - 48px))",
1361
+ background="#fff",
1362
+ border_radius=12,
1363
+ box_shadow="0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f",
1364
+ padding=me.Padding(top=16, left=16, right=16, bottom=16),
1365
+ display="flex",
1366
+ flex_direction="column",
1367
+ )
1368
+ with me.box(style=box_style):
1369
+ me.text("Input", style=me.Style(font_weight=500))
1370
+ me.box(style=me.Style(height=16))
1371
+ me.textarea(
1372
+ key=str(me.state(State).textarea_key),
1373
+ on_input=on_input,
1374
+ rows=5,
1375
+ autosize=True,
1376
+ max_rows=15,
1377
+ style=me.Style(width="100%"),
1378
+ )
1379
+ me.box(style=me.Style(height=12))
1380
+ with me.box(
1381
+ style=me.Style(display="flex", justify_content="space-between")
1382
+ ):
1383
+ me.button(
1384
+ "Clear", color="primary", type="stroked", on_click=on_click_clear
1385
+ )
1386
+ me.button(
1387
+ "Generate",
1388
+ color="primary",
1389
+ type="flat",
1390
+ on_click=on_click_generate,
1391
+ )
1392
+ with me.box(style=box_style):
1393
+ me.text("Output", style=me.Style(font_weight=500))
1394
+ me.markdown(me.state(State).output)
1395
+
1396
+ <example>
1397
+
1398
+ <example>
1399
+ from dataclasses import dataclass
1400
+
1401
+ import mesop as me
1402
+
1403
+ CARD_WIDTH = "320px"
1404
+
1405
+
1406
+ @dataclass
1407
+ class Resource:
1408
+ title: str
1409
+ description: str
1410
+ github_url: str
1411
+ github_username: str
1412
+ img_url: str
1413
+ app_url: str | None = None
1414
+
1415
+
1416
+ @dataclass
1417
+ class Section:
1418
+ name: str
1419
+ resources: list[Resource]
1420
+ icon: str
1421
+
1422
+
1423
+ SECTIONS = [
1424
+ Section(
1425
+ name="Featured",
1426
+ icon="star",
1427
+ resources=[
1428
+ Resource(
1429
+ title="Mesop Duo Chat",
1430
+ description="Chat with multiple models at once",
1431
+ github_url="https://github.com/wwwillchen/mesop-duo-chat",
1432
+ github_username="wwwillchen",
1433
+ app_url="https://huggingface.co/spaces/wwwillchen/mesop-duo-chat",
1434
+ img_url="https://github.com/user-attachments/assets/107afb9c-f08c-4f27-bd00-e122415c069e",
1435
+ ),
1436
+ Resource(
1437
+ title="Mesop Prompt Tuner",
1438
+ description="Prompt tuning app heavily inspired by Anthropic Console Workbench.",
1439
+ app_url="https://huggingface.co/spaces/richard-to/mesop-prompt-tuner",
1440
+ img_url="https://github.com/user-attachments/assets/2ec6cbfb-c28b-4f60-98f9-34bfca1f6938",
1441
+ github_url="https://github.com/richard-to/mesop-prompt-tuner",
1442
+ github_username="richard-to",
1443
+ ),
1444
+ Resource(
1445
+ title="Meta Llama Agentic System",
1446
+ description="Agentic components of the Llama Stack APIs. Chat UI in Mesop.",
1447
+ img_url="https://github.com/meta-llama/llama-agentic-system/raw/main/demo.png",
1448
+ github_url="https://github.com/meta-llama/llama-agentic-system",
1449
+ github_username="meta-llama",
1450
+ ),
1451
+ ],
1452
+ ),
1453
+ Section(
1454
+ name="Apps",
1455
+ icon="computer",
1456
+ resources=[
1457
+ Resource(
1458
+ title="Mesop App Maker",
1459
+ description="Generate apps with Mesop using LLMs",
1460
+ img_url="https://github.com/user-attachments/assets/1a826d44-c87b-4c79-aeaf-29bc8da3b1c0",
1461
+ github_url="https://github.com/richard-to/mesop-app-maker",
1462
+ github_username="richard-to",
1463
+ ),
1464
+ Resource(
1465
+ title="Mesop Jeopardy",
1466
+ description="A simple jeopardy game built using Mesop",
1467
+ img_url="https://github.com/richard-to/mesop-jeopardy/assets/539889/bc27447d-129f-47ae-b0b1-8f5c546762ed",
1468
+ github_url="https://github.com/richard-to/mesop-jeopardy",
1469
+ github_username="richard-to",
1470
+ ),
1471
+ ],
1472
+ ),
1473
+ Section(
1474
+ name="Web components",
1475
+ icon="code_blocks",
1476
+ resources=[
1477
+ Resource(
1478
+ title="Mesop Markmap",
1479
+ description="Mesop web component for the Markmap library",
1480
+ img_url="https://github.com/user-attachments/assets/6aa40ca3-d98a-42b2-adea-3f49b134445d",
1481
+ github_url="https://github.com/lianggecm/mesop_markmap",
1482
+ app_url="https://colab.research.google.com/drive/17gXlsXPDeo6hcFl1oOyrZ58FTozviN45?usp=sharing",
1483
+ github_username="lianggecm",
1484
+ ),
1485
+ ],
1486
+ ),
1487
+ Section(
1488
+ name="Notebooks",
1489
+ icon="description",
1490
+ resources=[
1491
+ Resource(
1492
+ title="Mesop Getting Started Colab",
1493
+ description="Get started with Mesop in Colab",
1494
+ img_url="https://github.com/user-attachments/assets/37efbe69-ac97-4d26-8fda-d1b7b2b4976a",
1495
+ github_url="https://github.com/google/mesop/blob/main/notebooks/mesop_colab_getting_started.ipynb",
1496
+ app_url="https://colab.research.google.com/github/google/mesop/blob/main/notebooks/mesop_colab_getting_started.ipynb",
1497
+ github_username="google",
1498
+ ),
1499
+ Resource(
1500
+ title="Gemma with Mesop Notebook",
1501
+ description="Use Gemma with Mesop in Colab",
1502
+ img_url="https://github.com/user-attachments/assets/a52ebf01-7f24-469b-9ad9-b271fdb19e37",
1503
+ github_url="https://github.com/google-gemini/gemma-cookbook/blob/main/Gemma/Integrate_with_Mesop.ipynb",
1504
+ app_url="https://colab.research.google.com/github/google-gemini/gemma-cookbook/blob/main/Gemma/Integrate_with_Mesop.ipynb",
1505
+ github_username="google-gemini",
1506
+ ),
1507
+ Resource(
1508
+ title="PaliGemma with Mesop Notebook",
1509
+ description="Use PaliGemma with Mesop in Colab",
1510
+ img_url="https://github.com/user-attachments/assets/8cb456a1-f7be-4187-9a3f-f6b48bde73e9",
1511
+ github_url="https://github.com/google-gemini/gemma-cookbook/blob/main/PaliGemma/Integrate_PaliGemma_with_Mesop.ipynb",
1512
+ app_url="https://colab.research.google.com/github/google-gemini/gemma-cookbook/blob/main/PaliGemma/Integrate_PaliGemma_with_Mesop.ipynb",
1513
+ github_username="google-gemini",
1514
+ ),
1515
+ ],
1516
+ ),
1517
+ ]
1518
+
1519
+
1520
+ def scroll_to_section(e: me.ClickEvent):
1521
+ me.scroll_into_view(key="section-" + e.key)
1522
+ me.state(State).sidenav_menu_open = False
1523
+
1524
+
1525
+ def toggle_theme(e: me.ClickEvent):
1526
+ if me.theme_brightness() == "light":
1527
+ me.set_theme_mode("dark")
1528
+ else:
1529
+ me.set_theme_mode("light")
1530
+
1531
+
1532
+ def on_load(e: me.LoadEvent):
1533
+ me.set_theme_mode("system")
1534
+
1535
+
1536
+ @me.stateclass
1537
+ class State:
1538
+ sidenav_menu_open: bool
1539
+
1540
+
1541
+ def toggle_menu_button(e: me.ClickEvent):
1542
+ s = me.state(State)
1543
+ s.sidenav_menu_open = not s.sidenav_menu_open
1544
+
1545
+
1546
+ def is_mobile():
1547
+ return me.viewport_size().width < 640
1548
+
1549
+
1550
+ @me.page(
1551
+ title="Mesop Showcase",
1552
+ on_load=on_load,
1553
+ security_policy=me.SecurityPolicy(
1554
+ allowed_iframe_parents=["https://huggingface.co"],
1555
+ ),
1556
+ )
1557
+ def page():
1558
+ with me.box(style=me.Style(display="flex", height="100%")):
1559
+ if is_mobile():
1560
+ with me.content_button(
1561
+ type="icon",
1562
+ style=me.Style(top=6, left=8, position="absolute", z_index=9),
1563
+ on_click=toggle_menu_button,
1564
+ ):
1565
+ me.icon("menu")
1566
+ with me.sidenav(
1567
+ opened=me.state(State).sidenav_menu_open,
1568
+ style=me.Style(
1569
+ background=me.theme_var("surface-container-low"),
1570
+ ),
1571
+ ):
1572
+ sidenav()
1573
+ else:
1574
+ sidenav()
1575
+ with me.box(
1576
+ style=me.Style(
1577
+ background=me.theme_var("surface-container-low"),
1578
+ display="flex",
1579
+ flex_direction="column",
1580
+ flex_grow=1,
1581
+ )
1582
+ ):
1583
+ with me.box(
1584
+ style=me.Style(
1585
+ height=240,
1586
+ width="100%",
1587
+ padding=me.Padding.all(16),
1588
+ display="flex",
1589
+ align_items="center",
1590
+ ),
1591
+ ):
1592
+ me.text(
1593
+ "Mesop Showcase",
1594
+ style=me.Style(
1595
+ color=me.theme_var("on-background"),
1596
+ font_size=22,
1597
+ font_weight=500,
1598
+ letter_spacing="0.8px",
1599
+ padding=me.Padding(left=36) if is_mobile() else None,
1600
+ ),
1601
+ )
1602
+
1603
+ with me.content_button(
1604
+ type="icon",
1605
+ style=me.Style(position="absolute", right=4, top=8),
1606
+ on_click=toggle_theme,
1607
+ ):
1608
+ me.icon(
1609
+ "light_mode" if me.theme_brightness() == "dark" else "dark_mode"
1610
+ )
1611
+ with me.box(
1612
+ style=me.Style(
1613
+ background=me.theme_var("background"),
1614
+ flex_grow=1,
1615
+ padding=me.Padding(
1616
+ left=32,
1617
+ right=32,
1618
+ bottom=64,
1619
+ ),
1620
+ border_radius=16,
1621
+ overflow_y="auto",
1622
+ )
1623
+ ):
1624
+ for section in SECTIONS:
1625
+ me.text(
1626
+ section.name,
1627
+ style=me.Style(
1628
+ font_size=18,
1629
+ font_weight=500,
1630
+ padding=me.Padding(top=32, bottom=16),
1631
+ ),
1632
+ key="section-" + section.name,
1633
+ )
1634
+ with me.box(
1635
+ style=me.Style(
1636
+ display="grid",
1637
+ grid_template_columns=f"repeat(auto-fit, minmax({CARD_WIDTH}, 1fr))",
1638
+ gap=24,
1639
+ margin=me.Margin(
1640
+ bottom=24,
1641
+ ),
1642
+ )
1643
+ ):
1644
+ for resource in section.resources:
1645
+ card(resource)
1646
+ with me.box(
1647
+ on_click=lambda e: me.navigate(
1648
+ "https://github.com/google/mesop/issues/new/choose"
1649
+ ),
1650
+ style=me.Style(
1651
+ cursor="pointer",
1652
+ max_width=300,
1653
+ background=me.theme_var("surface-container-lowest"),
1654
+ box_shadow="0 2px 4px rgba(0, 0, 0, 0.1)",
1655
+ # margin=me.Margin.symmetric(horizontal="auto"),
1656
+ display="flex",
1657
+ justify_content="center",
1658
+ align_items="center",
1659
+ border_radius=16,
1660
+ height=120,
1661
+ ),
1662
+ ):
1663
+ me.icon(
1664
+ "add_circle",
1665
+ style=me.Style(
1666
+ color=me.theme_var("primary"),
1667
+ font_size=24,
1668
+ margin=me.Margin(right=4, top=2),
1669
+ ),
1670
+ )
1671
+ me.link(
1672
+ text="Submit your showcase",
1673
+ url="https://github.com/google/mesop/issues/new/choose",
1674
+ style=me.Style(
1675
+ font_size=24,
1676
+ color=me.theme_var("on-background"),
1677
+ text_decoration="none",
1678
+ ),
1679
+ )
1680
+
1681
+
1682
+ def sidenav():
1683
+ with me.box(
1684
+ style=me.Style(
1685
+ width=216,
1686
+ height="100%",
1687
+ background=me.theme_var("surface-container-low"),
1688
+ padding=me.Padding.all(16),
1689
+ )
1690
+ ):
1691
+ with me.box(
1692
+ style=me.Style(
1693
+ display="flex", flex_direction="column", margin=me.Margin(top=48)
1694
+ )
1695
+ ):
1696
+ with me.content_button(
1697
+ type="icon",
1698
+ on_click=lambda e: me.navigate("https://google.github.io/mesop/"),
1699
+ ):
1700
+ with me.box(
1701
+ style=me.Style(display="flex", align_items="center", gap=12)
1702
+ ):
1703
+ me.icon("home")
1704
+ me.text(
1705
+ "Home",
1706
+ style=me.Style(
1707
+ font_size=16,
1708
+ margin=me.Margin(bottom=4),
1709
+ ),
1710
+ )
1711
+ with me.content_button(
1712
+ type="icon",
1713
+ on_click=lambda e: me.navigate("https://google.github.io/mesop/demo/"),
1714
+ ):
1715
+ with me.box(
1716
+ style=me.Style(
1717
+ display="flex",
1718
+ align_items="center",
1719
+ gap=8,
1720
+ )
1721
+ ):
1722
+ me.icon("gallery_thumbnail")
1723
+ me.text(
1724
+ "Demos",
1725
+ style=me.Style(
1726
+ font_size=16,
1727
+ margin=me.Margin(bottom=6, left=4),
1728
+ ),
1729
+ )
1730
+ with me.box(
1731
+ style=me.Style(
1732
+ padding=me.Padding(top=24),
1733
+ display="flex",
1734
+ flex_direction="column",
1735
+ gap=8,
1736
+ ),
1737
+ ):
1738
+ me.text(
1739
+ "Categories",
1740
+ style=me.Style(
1741
+ font_weight=500,
1742
+ letter_spacing="0.4px",
1743
+ padding=me.Padding(left=12),
1744
+ ),
1745
+ )
1746
+ for section in SECTIONS:
1747
+ with me.box(
1748
+ style=me.Style(
1749
+ display="flex",
1750
+ align_items="center",
1751
+ cursor="pointer",
1752
+ ),
1753
+ on_click=scroll_to_section,
1754
+ key=section.name,
1755
+ ):
1756
+ with me.content_button(type="icon"):
1757
+ me.icon(section.icon)
1758
+ me.text(section.name)
1759
+ with me.box(
1760
+ style=me.Style(
1761
+ display="flex",
1762
+ align_items="center",
1763
+ cursor="pointer",
1764
+ padding=me.Padding(top=16),
1765
+ ),
1766
+ on_click=lambda e: me.navigate(
1767
+ "https://github.com/google/mesop/issues/new/choose"
1768
+ ),
1769
+ ):
1770
+ with me.content_button(type="icon"):
1771
+ me.icon("add_circle")
1772
+ me.text("Submit your showcase")
1773
+
1774
+
1775
+ def card(resource: Resource):
1776
+ with me.box(
1777
+ style=me.Style(
1778
+ display="flex",
1779
+ flex_direction="column",
1780
+ gap=12,
1781
+ box_shadow="0 2px 4px rgba(0, 0, 0, 0.1)",
1782
+ border_radius=16,
1783
+ min_width=CARD_WIDTH,
1784
+ max_width=480,
1785
+ background=me.theme_var("surface-container-lowest"),
1786
+ )
1787
+ ):
1788
+ me.box(
1789
+ style=me.Style(
1790
+ background=f"url('{resource.img_url}') center/cover no-repeat",
1791
+ cursor="pointer",
1792
+ height=200,
1793
+ width="100%",
1794
+ border_radius=16,
1795
+ margin=me.Margin(bottom=8),
1796
+ ),
1797
+ key=resource.app_url or resource.github_url,
1798
+ on_click=lambda e: me.navigate(e.key),
1799
+ )
1800
+ with me.box(
1801
+ style=me.Style(
1802
+ padding=me.Padding(left=16),
1803
+ display="flex",
1804
+ flex_direction="column",
1805
+ gap=8,
1806
+ )
1807
+ ):
1808
+ me.text(resource.title, style=me.Style(font_weight="bold"))
1809
+ with me.box(
1810
+ style=me.Style(
1811
+ display="flex",
1812
+ flex_direction="row",
1813
+ align_items="center",
1814
+ gap=8,
1815
+ cursor="pointer",
1816
+ ),
1817
+ key="https://github.com/" + resource.github_username,
1818
+ on_click=lambda e: me.navigate(e.key),
1819
+ ):
1820
+ me.image(
1821
+ src="https://avatars.githubusercontent.com/"
1822
+ + resource.github_username,
1823
+ style=me.Style(height=32, width=32, border_radius=16),
1824
+ )
1825
+ me.text(
1826
+ resource.github_username,
1827
+ style=me.Style(
1828
+ letter_spacing="0.2px",
1829
+ ),
1830
+ )
1831
+ me.text(resource.description, style=me.Style(height=40))
1832
+ with me.box(
1833
+ style=me.Style(
1834
+ display="flex",
1835
+ justify_content="space-between",
1836
+ padding=me.Padding(left=8, right=8, bottom=8),
1837
+ )
1838
+ ):
1839
+ if resource.github_url:
1840
+ me.button(
1841
+ "Open repo",
1842
+ on_click=lambda e: me.navigate(e.key),
1843
+ key=resource.github_url,
1844
+ )
1845
+ if resource.app_url:
1846
+ me.button(
1847
+ "Open app",
1848
+ on_click=lambda e: me.navigate(e.key),
1849
+ key=resource.app_url,
1850
+ )
1851
+
1852
+ <example>
1853
+
prompt/base_system_instructions.txt ADDED
The diff for this file is too large to render. See raw diff
 
prompt/chat_elements_examples.txt ADDED
@@ -0,0 +1,1180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This section provides examples of elements that can be used for composing chat UIs in Mesop. Each example is wrapped in <example> tags.
2
+
3
+ <example>
4
+ # This file provides examples for creating sidebars.
5
+
6
+ import mesop as me
7
+
8
+
9
+ # This is an example of a sidebar on the left. It is compact and uses icons.
10
+ @me.page(
11
+ security_policy=me.SecurityPolicy(
12
+ allowed_iframe_parents=["https://google.github.io"]
13
+ ),
14
+ path="/ai/sidebar-example-1",
15
+ )
16
+ def sidebar_example_1():
17
+ # Create a two column grid. Since the sidebar is the on left, we make that column
18
+ # very thin using a 1fr to 50fr ratio.
19
+ with me.box(
20
+ style=me.Style(
21
+ display="grid", grid_template_columns="1fr 50fr", height="100vh"
22
+ )
23
+ ):
24
+ # This block is the code for the sidebar
25
+ with me.box(
26
+ style=me.Style(
27
+ background=me.theme_var("surface-container"),
28
+ border=me.Border.symmetric(
29
+ horizontal=me.BorderSide(
30
+ width=1, style="solid", color=me.theme_var("outline-variant")
31
+ )
32
+ ),
33
+ height="100%",
34
+ )
35
+ ):
36
+ # This block is code for the sidebar menu item.
37
+ # It needs click event handlers to add interaction.
38
+ with me.box(
39
+ style=me.Style(
40
+ border=me.Border(
41
+ bottom=me.BorderSide(
42
+ width=1, color=me.theme_var("outline-variant"), style="solid"
43
+ )
44
+ ),
45
+ cursor="pointer",
46
+ padding=me.Padding.all(15),
47
+ ),
48
+ ):
49
+ with me.tooltip(message="Home tooltip"):
50
+ me.icon("home")
51
+
52
+ # This block is code for the sidebar menu item.
53
+ # It needs click event handlers to add interaction.
54
+ with me.box(
55
+ style=me.Style(
56
+ border=me.Border(
57
+ bottom=me.BorderSide(
58
+ width=1, color=me.theme_var("outline-variant"), style="solid"
59
+ )
60
+ ),
61
+ cursor="pointer",
62
+ padding=me.Padding.all(15),
63
+ ),
64
+ ):
65
+ with me.tooltip(message="Search tooltip"):
66
+ me.icon("search")
67
+
68
+ # This block is for the main content to be added.
69
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
70
+ me.text("Main body content goes here")
71
+
72
+
73
+ # This is an example of a sidebar on the right. This sidebar is less compact and uses
74
+ # icon and text labels.
75
+ @me.page(
76
+ security_policy=me.SecurityPolicy(
77
+ allowed_iframe_parents=["https://google.github.io"]
78
+ ),
79
+ path="/ai/sidebar-example-2",
80
+ )
81
+ def sidebar_example_2():
82
+ # Create a two column grid. Since the sidebar is the on right, we make the right column
83
+ # very thin using a 50fr to 1fr ratio.
84
+ with me.box(
85
+ style=me.Style(
86
+ display="grid", grid_template_columns="50fr 1fr", height="100vh"
87
+ )
88
+ ):
89
+ # This block is for the main content to be added.
90
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
91
+ me.text("Main body content goes here")
92
+
93
+ # This block is the code for the sidebar
94
+ with me.box(
95
+ style=me.Style(
96
+ background=me.theme_var("surface-container"),
97
+ border=me.Border.symmetric(
98
+ horizontal=me.BorderSide(
99
+ width=1, style="solid", color=me.theme_var("outline-variant")
100
+ )
101
+ ),
102
+ height="100%",
103
+ )
104
+ ):
105
+ # This block is code for the sidebar menu item.
106
+ # It needs click event handlers to add interaction.
107
+ with me.box(
108
+ style=me.Style(
109
+ border=me.Border(
110
+ bottom=me.BorderSide(
111
+ width=1, color=me.theme_var("outline-variant"), style="solid"
112
+ )
113
+ ),
114
+ cursor="pointer",
115
+ padding=me.Padding.all(15),
116
+ ),
117
+ ):
118
+ with me.box(
119
+ style=me.Style(
120
+ display="flex",
121
+ align_items="center",
122
+ gap=5,
123
+ )
124
+ ):
125
+ me.icon("home")
126
+ me.text("Home")
127
+
128
+ # This block is code for the sidebar menu item.
129
+ # It needs click event handlers to add interaction.
130
+ with me.box(
131
+ style=me.Style(
132
+ border=me.Border(
133
+ bottom=me.BorderSide(
134
+ width=1, color=me.theme_var("outline-variant"), style="solid"
135
+ )
136
+ ),
137
+ cursor="pointer",
138
+ padding=me.Padding.all(15),
139
+ ),
140
+ ):
141
+ with me.box(
142
+ style=me.Style(
143
+ display="flex",
144
+ align_items="center",
145
+ gap=5,
146
+ )
147
+ ):
148
+ me.icon("search")
149
+ me.text("Search")
150
+
151
+
152
+ # This is an example of a sidebar that expands and collapses.
153
+
154
+
155
+ @me.stateclass
156
+ class State:
157
+ # Controls whether the sidebar menu is expanded or collapsed
158
+ sidebar_expanded: bool = True
159
+
160
+
161
+ def on_click_menu_icon(e: me.ClickEvent):
162
+ state = me.state(State)
163
+ state.sidebar_expanded = not state.sidebar_expanded
164
+
165
+
166
+ @me.page(
167
+ security_policy=me.SecurityPolicy(
168
+ allowed_iframe_parents=["https://google.github.io"]
169
+ ),
170
+ path="/ai/sidebar-example-3",
171
+ )
172
+ def sidebar_example_3():
173
+ state = me.state(State)
174
+ with me.box(
175
+ style=me.Style(
176
+ display="grid", grid_template_columns="1fr 50fr", height="100vh"
177
+ )
178
+ ):
179
+ # This block is the code for the sidebar
180
+ with me.box(
181
+ style=me.Style(
182
+ background=me.theme_var("surface-container"),
183
+ border=me.Border.symmetric(
184
+ horizontal=me.BorderSide(
185
+ width=1, style="solid", color=me.theme_var("outline-variant")
186
+ )
187
+ ),
188
+ height="100%",
189
+ )
190
+ ):
191
+ # This block is code for the sidebar menu item.
192
+ # It needs click event handlers to add interaction.
193
+ with me.box(
194
+ # Event handler to expand/collapse the sidebar.
195
+ on_click=on_click_menu_icon,
196
+ style=me.Style(
197
+ border=me.Border(
198
+ bottom=me.BorderSide(
199
+ width=1, color=me.theme_var("outline-variant"), style="solid"
200
+ )
201
+ ),
202
+ cursor="pointer",
203
+ padding=me.Padding.all(15),
204
+ ),
205
+ ):
206
+ me.icon("menu")
207
+
208
+ # This block is code for the sidebar menu item.
209
+ # It needs click event handlers to add interaction.
210
+ with me.box(
211
+ style=me.Style(
212
+ border=me.Border(
213
+ bottom=me.BorderSide(
214
+ width=1, color=me.theme_var("outline-variant"), style="solid"
215
+ )
216
+ ),
217
+ cursor="pointer",
218
+ padding=me.Padding.all(15),
219
+ ),
220
+ ):
221
+ if state.sidebar_expanded:
222
+ with me.box(
223
+ style=me.Style(
224
+ display="flex",
225
+ align_items="center",
226
+ gap=5,
227
+ )
228
+ ):
229
+ me.icon("home")
230
+ me.text("Home")
231
+ else:
232
+ with me.tooltip(message="Home tooltip"):
233
+ me.icon("home")
234
+
235
+ # This block is code for the sidebar menu item.
236
+ with me.box(
237
+ style=me.Style(
238
+ border=me.Border(
239
+ bottom=me.BorderSide(
240
+ width=1, color=me.theme_var("outline-variant"), style="solid"
241
+ )
242
+ ),
243
+ cursor="pointer",
244
+ padding=me.Padding.all(15),
245
+ ),
246
+ ):
247
+ if state.sidebar_expanded:
248
+ with me.box(
249
+ style=me.Style(
250
+ display="flex",
251
+ align_items="center",
252
+ gap=5,
253
+ )
254
+ ):
255
+ me.icon("search")
256
+ me.text("Search")
257
+ else:
258
+ with me.tooltip(message="Search tooltip"):
259
+ me.icon("search")
260
+
261
+ # This block is for the main content to be added.
262
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
263
+ me.text("Main body content goes here")
264
+
265
+ <example>
266
+
267
+ <example>
268
+ # This file provides examples for creating headers.
269
+
270
+ import mesop as me
271
+
272
+
273
+ # This is an example of a two section header a fluid width
274
+ # header with a title on the left and some example buttons
275
+ # on the right.
276
+ @me.page(
277
+ security_policy=me.SecurityPolicy(
278
+ allowed_iframe_parents=["https://google.github.io"]
279
+ ),
280
+ path="/ai/header-example-1",
281
+ )
282
+ def headers_example_1():
283
+ with me.box(
284
+ style=me.Style(
285
+ background=me.theme_var("surface-container"),
286
+ border=me.Border.symmetric(
287
+ vertical=me.BorderSide(
288
+ width=1,
289
+ style="solid",
290
+ color=me.theme_var("outline-variant"),
291
+ )
292
+ ),
293
+ padding=me.Padding.all(10),
294
+ )
295
+ ):
296
+ is_mobile = me.viewport_size().width < 640
297
+ if is_mobile:
298
+ default_flex_style = me.Style(
299
+ align_items="center",
300
+ display="flex",
301
+ gap=5,
302
+ justify_content="space-between",
303
+ )
304
+ else:
305
+ default_flex_style = me.Style(
306
+ align_items="center",
307
+ display="flex",
308
+ gap=5,
309
+ justify_content="space-between",
310
+ )
311
+
312
+ with me.box(style=default_flex_style):
313
+ with me.box(style=me.Style(display="flex", gap=5)):
314
+ me.text(
315
+ "Your Title Here",
316
+ type="headline-6",
317
+ style=me.Style(margin=me.Margin(bottom=0)),
318
+ )
319
+
320
+ with me.box(style=me.Style(display="flex", gap=5)):
321
+ me.button("Home")
322
+ me.button("About")
323
+ me.button("FAQ")
324
+
325
+
326
+ # This is an example of a three section header a fluid width
327
+ # header with a title on the left, some example buttons
328
+ # on the center, and a example button on the right.
329
+ @me.page(
330
+ security_policy=me.SecurityPolicy(
331
+ allowed_iframe_parents=["https://google.github.io"]
332
+ ),
333
+ path="/ai/header-example-2",
334
+ )
335
+ def headers_example_2():
336
+ with me.box(
337
+ style=me.Style(
338
+ background=me.theme_var("surface-container"),
339
+ border=me.Border.symmetric(
340
+ vertical=me.BorderSide(
341
+ width=1,
342
+ style="solid",
343
+ color=me.theme_var("outline-variant"),
344
+ )
345
+ ),
346
+ padding=me.Padding.all(10),
347
+ )
348
+ ):
349
+ is_mobile = me.viewport_size().width < 640
350
+ if is_mobile:
351
+ default_flex_style = me.Style(
352
+ align_items="center",
353
+ display="flex",
354
+ gap=5,
355
+ justify_content="space-between",
356
+ )
357
+ else:
358
+ default_flex_style = me.Style(
359
+ align_items="center",
360
+ display="flex",
361
+ gap=5,
362
+ justify_content="space-between",
363
+ )
364
+
365
+ with me.box(style=default_flex_style):
366
+ with me.box(style=me.Style(display="flex", gap=5)):
367
+ me.text(
368
+ "Your Title Here",
369
+ type="headline-6",
370
+ style=me.Style(margin=me.Margin(bottom=0)),
371
+ )
372
+
373
+ with me.box(style=me.Style(display="flex", gap=5)):
374
+ me.button("Home")
375
+ me.button("About")
376
+ me.button("FAQ")
377
+
378
+ with me.box(style=me.Style(display="flex", gap=5)):
379
+ me.button("Login", type="flat")
380
+
381
+
382
+ # This is an example of a centered header with no title and
383
+ # custom icon buttons.
384
+ @me.page(
385
+ security_policy=me.SecurityPolicy(
386
+ allowed_iframe_parents=["https://google.github.io"]
387
+ ),
388
+ path="/ai/header-example-3",
389
+ )
390
+ def headers_example_3():
391
+ with me.box(
392
+ style=me.Style(
393
+ background=me.theme_var("surface-container"),
394
+ border=me.Border.symmetric(
395
+ vertical=me.BorderSide(
396
+ width=1,
397
+ style="solid",
398
+ color=me.theme_var("outline-variant"),
399
+ )
400
+ ),
401
+ padding=me.Padding.all(10),
402
+ )
403
+ ):
404
+ is_mobile = me.viewport_size().width < 640
405
+ if is_mobile:
406
+ default_flex_style = me.Style(
407
+ align_items="center",
408
+ display="flex",
409
+ gap=5,
410
+ justify_content="center",
411
+ )
412
+ else:
413
+ default_flex_style = me.Style(
414
+ align_items="center",
415
+ display="flex",
416
+ gap=5,
417
+ justify_content="center",
418
+ )
419
+
420
+ with me.box(style=default_flex_style):
421
+ with me.box(style=me.Style(display="flex", gap=5)):
422
+ with me.content_button(
423
+ style=me.Style(
424
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
425
+ )
426
+ ):
427
+ me.icon("home")
428
+ me.text("Home")
429
+ with me.content_button(
430
+ style=me.Style(
431
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
432
+ )
433
+ ):
434
+ me.icon("info")
435
+ me.text("About")
436
+ with me.content_button(
437
+ style=me.Style(
438
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
439
+ )
440
+ ):
441
+ me.icon("contact_support")
442
+ me.text("FAQ")
443
+ with me.content_button(
444
+ style=me.Style(
445
+ padding=me.Padding.symmetric(vertical=30, horizontal=25)
446
+ )
447
+ ):
448
+ me.icon("login")
449
+ me.text("Login")
450
+
451
+ <example>
452
+
453
+ <example>
454
+ # This file provides examples for creating layouts.
455
+
456
+ import mesop as me
457
+
458
+
459
+ # This is an example of a layout with a header and sidebar that is below the header.
460
+ # There are also two columns
461
+ @me.page(
462
+ security_policy=me.SecurityPolicy(
463
+ allowed_iframe_parents=["https://google.github.io"]
464
+ ),
465
+ path="/ai/layout-example-1",
466
+ )
467
+ def layout_example_1():
468
+ # This is the grid that manages the layout.
469
+ with me.box(
470
+ style=me.Style(
471
+ display="grid",
472
+ # First column is for the sidebar which is why we use 1fr.
473
+ # The other two columns are equal width.
474
+ grid_template_columns="1fr 50fr 50fr",
475
+ # First row is for the header which is why we use 1fr.
476
+ grid_template_rows="1fr 50fr",
477
+ height="100vh",
478
+ )
479
+ ):
480
+ with me.box(
481
+ style=me.Style(
482
+ # Since the grid defines three columns, we need to span the header across all
483
+ # columns.
484
+ grid_column="1 / -1",
485
+ background=me.theme_var("surface-container"),
486
+ border=me.Border.symmetric(
487
+ vertical=me.BorderSide(
488
+ width=1,
489
+ style="solid",
490
+ color=me.theme_var("outline-variant"),
491
+ )
492
+ ),
493
+ padding=me.Padding.all(10),
494
+ )
495
+ ):
496
+ is_mobile = me.viewport_size().width < 640
497
+ if is_mobile:
498
+ default_flex_style = me.Style(
499
+ align_items="center",
500
+ display="flex",
501
+ gap=5,
502
+ justify_content="space-between",
503
+ )
504
+ else:
505
+ default_flex_style = me.Style(
506
+ align_items="center",
507
+ display="flex",
508
+ gap=5,
509
+ justify_content="space-between",
510
+ )
511
+
512
+ with me.box(style=default_flex_style):
513
+ with me.box(style=me.Style(display="flex", gap=5)):
514
+ me.text(
515
+ "Your Title Here",
516
+ type="headline-6",
517
+ style=me.Style(margin=me.Margin(bottom=0)),
518
+ )
519
+
520
+ with me.box(style=me.Style(display="flex", gap=5)):
521
+ me.button("Home")
522
+ me.button("About")
523
+ me.button("FAQ")
524
+
525
+ # This block is the code for the sidebar
526
+ with me.box(
527
+ style=me.Style(
528
+ background=me.theme_var("surface-container"),
529
+ border=me.Border.symmetric(
530
+ horizontal=me.BorderSide(
531
+ width=1, style="solid", color=me.theme_var("outline-variant")
532
+ )
533
+ ),
534
+ height="100%",
535
+ )
536
+ ):
537
+ # This block is code for the sidebar menu item.
538
+ # It needs click event handlers to add interaction.
539
+ with me.box(
540
+ style=me.Style(
541
+ border=me.Border(
542
+ bottom=me.BorderSide(
543
+ width=1, color=me.theme_var("outline-variant"), style="solid"
544
+ )
545
+ ),
546
+ cursor="pointer",
547
+ padding=me.Padding.all(15),
548
+ ),
549
+ ):
550
+ with me.tooltip(message="Home tooltip"):
551
+ me.icon("home")
552
+
553
+ # This block is code for the sidebar menu item.
554
+ # It needs click event handlers to add interaction.
555
+ with me.box(
556
+ style=me.Style(
557
+ border=me.Border(
558
+ bottom=me.BorderSide(
559
+ width=1, color=me.theme_var("outline-variant"), style="solid"
560
+ )
561
+ ),
562
+ cursor="pointer",
563
+ padding=me.Padding.all(15),
564
+ ),
565
+ ):
566
+ with me.tooltip(message="Search tooltip"):
567
+ me.icon("search")
568
+
569
+ # This block is for the first column content to be added.
570
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
571
+ me.text("First column body content goes here")
572
+
573
+ # This block is for the second column content to be added.
574
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
575
+ me.text("Second column body content goes here")
576
+
577
+
578
+ # This is an example of a layout with header and sidebar at the same level.
579
+ # The sidebar also expands and collapses.
580
+
581
+
582
+ @me.stateclass
583
+ class State:
584
+ # Controls whether the sidebar menu is expanded or collapsed
585
+ sidebar_expanded: bool = True
586
+
587
+
588
+ def on_click_menu_icon(e: me.ClickEvent):
589
+ state = me.state(State)
590
+ state.sidebar_expanded = not state.sidebar_expanded
591
+
592
+
593
+ @me.page(
594
+ security_policy=me.SecurityPolicy(
595
+ allowed_iframe_parents=["https://google.github.io"]
596
+ ),
597
+ path="/ai/layout-example-2",
598
+ )
599
+ def layout_example_2():
600
+ state = me.state(State)
601
+ # This is the grid that manages the layout.
602
+ with me.box(
603
+ style=me.Style(
604
+ display="grid",
605
+ # First column is for the sidebar which is why we use 1fr.
606
+ grid_template_columns="1fr 50fr",
607
+ # First row is for the header which is why we use 1fr.
608
+ grid_template_rows="1fr 50fr",
609
+ height="100vh",
610
+ )
611
+ ):
612
+ # This block is the code for the sidebar
613
+ with me.box(
614
+ style=me.Style(
615
+ grid_row="1 / -1",
616
+ background=me.theme_var("surface-container"),
617
+ border=me.Border.symmetric(
618
+ horizontal=me.BorderSide(
619
+ width=1, style="solid", color=me.theme_var("outline-variant")
620
+ )
621
+ ),
622
+ height="100%",
623
+ )
624
+ ):
625
+ # This block is code for the sidebar menu item.
626
+ # It needs click event handlers to add interaction.
627
+ with me.box(
628
+ # Event handler to expand/collapse the sidebar.
629
+ on_click=on_click_menu_icon,
630
+ style=me.Style(
631
+ cursor="pointer",
632
+ padding=me.Padding.all(15),
633
+ ),
634
+ ):
635
+ me.icon("menu")
636
+
637
+ # This block is code for the sidebar menu item.
638
+ # It needs click event handlers to add interaction.
639
+ with me.box(
640
+ style=me.Style(
641
+ cursor="pointer",
642
+ padding=me.Padding.all(15),
643
+ ),
644
+ ):
645
+ if state.sidebar_expanded:
646
+ with me.box(
647
+ style=me.Style(
648
+ display="flex",
649
+ align_items="center",
650
+ gap=5,
651
+ )
652
+ ):
653
+ me.icon("home")
654
+ me.text("Home")
655
+ else:
656
+ with me.tooltip(message="Home tooltip"):
657
+ me.icon("home")
658
+
659
+ # This block is code for the sidebar menu item.
660
+ # It needs click event handlers to add interaction.
661
+ with me.box(
662
+ style=me.Style(
663
+ cursor="pointer",
664
+ padding=me.Padding.all(15),
665
+ ),
666
+ ):
667
+ if state.sidebar_expanded:
668
+ with me.box(
669
+ style=me.Style(
670
+ display="flex",
671
+ align_items="center",
672
+ gap=5,
673
+ )
674
+ ):
675
+ me.icon("search")
676
+ me.text("Search")
677
+ else:
678
+ with me.tooltip(message="Search tooltip"):
679
+ me.icon("search")
680
+
681
+ with me.box(
682
+ style=me.Style(
683
+ background=me.theme_var("surface-container"),
684
+ padding=me.Padding.all(10),
685
+ border=me.Border(
686
+ bottom=me.BorderSide(
687
+ width=1, style="solid", color=me.theme_var("outline-variant")
688
+ )
689
+ ),
690
+ )
691
+ ):
692
+ is_mobile = me.viewport_size().width < 640
693
+ if is_mobile:
694
+ default_flex_style = me.Style(
695
+ align_items="center",
696
+ display="flex",
697
+ gap=5,
698
+ justify_content="space-between",
699
+ )
700
+ else:
701
+ default_flex_style = me.Style(
702
+ align_items="center",
703
+ display="flex",
704
+ gap=5,
705
+ justify_content="space-between",
706
+ )
707
+
708
+ with me.box(style=default_flex_style):
709
+ with me.box(style=me.Style(display="flex", gap=5)):
710
+ me.text(
711
+ "Your Title Here",
712
+ type="headline-6",
713
+ style=me.Style(margin=me.Margin(bottom=0)),
714
+ )
715
+
716
+ with me.box(style=me.Style(display="flex", gap=5)):
717
+ me.button("Home")
718
+ me.button("About")
719
+ me.button("FAQ")
720
+
721
+ # This block is for the main content to be added.
722
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
723
+ me.text("Main body content goes here")
724
+
725
+
726
+ # This is an example of a layout with a header, no sidebar, and four content columns
727
+ @me.page(
728
+ security_policy=me.SecurityPolicy(
729
+ allowed_iframe_parents=["https://google.github.io"]
730
+ ),
731
+ path="/ai/layout-example-3",
732
+ )
733
+ def layout_example_3():
734
+ # This is the grid that manages the layout.
735
+ with me.box(
736
+ style=me.Style(
737
+ display="grid",
738
+ # Three columns
739
+ grid_template_columns="1fr 1fr 1fr 1fr",
740
+ # First row is for the header which is why we use 1fr to 50fr.
741
+ grid_template_rows="1fr 50fr",
742
+ height="100vh",
743
+ )
744
+ ):
745
+ with me.box(
746
+ style=me.Style(
747
+ # Since the grid defines four columns, we need to span the header across all
748
+ # columns.
749
+ grid_column="1 / -1",
750
+ background=me.theme_var("surface-container"),
751
+ border=me.Border.symmetric(
752
+ vertical=me.BorderSide(
753
+ width=1,
754
+ style="solid",
755
+ color=me.theme_var("outline-variant"),
756
+ )
757
+ ),
758
+ padding=me.Padding.all(10),
759
+ )
760
+ ):
761
+ is_mobile = me.viewport_size().width < 640
762
+ if is_mobile:
763
+ default_flex_style = me.Style(
764
+ align_items="center",
765
+ display="flex",
766
+ gap=5,
767
+ justify_content="space-between",
768
+ )
769
+ else:
770
+ default_flex_style = me.Style(
771
+ align_items="center",
772
+ display="flex",
773
+ gap=5,
774
+ justify_content="space-between",
775
+ )
776
+
777
+ with me.box(style=default_flex_style):
778
+ with me.box(style=me.Style(display="flex", gap=5)):
779
+ me.text(
780
+ "Your Title Here",
781
+ type="headline-6",
782
+ style=me.Style(margin=me.Margin(bottom=0)),
783
+ )
784
+
785
+ with me.box(style=me.Style(display="flex", gap=5)):
786
+ me.button("Home")
787
+ me.button("About")
788
+ me.button("FAQ")
789
+
790
+ # This block is for the first column content to be added.
791
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
792
+ me.text("First column body content goes here")
793
+
794
+ # This block is for the second column content to be added.
795
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
796
+ me.text("Second column body content goes here")
797
+
798
+ # This block is for the third column content to be added.
799
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
800
+ me.text("Third column body content goes here")
801
+
802
+ # This block is for the fourth column content to be added.
803
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
804
+ me.text("Fourth column body content goes here")
805
+
806
+ <example>
807
+
808
+ <example>
809
+ # Examples of chat messages for chat bot interfaces
810
+ import mesop as me
811
+
812
+ USER_PLACEHOLDER_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In et lectus nec lorem pretium euismod?"
813
+ BOT_PLACEHOLDER_TEXT = """
814
+ # Fusce in dui mi. Cras enim metus
815
+
816
+ Maximus sit amet ultrices at, *viverra* vitae ante. Curabitur auctor ut eros id commodo.
817
+ Sed ultricies ornare lectus dictum facilisis. Aenean malesuada sed nisi id tempor.
818
+
819
+ - Mauris aliquet volutpat pretium.
820
+ - Nulla dapibus nibh id nisi mollis efficitur.
821
+
822
+ Vivamus ac **commodo** elit. Nunc dictum mauris sit amet mollis posuere. Nulla fringilla,
823
+ nunc sed varius posuere, augue nibh imperdiet lorem, rutrum lobortis leo leo sit amet sem.
824
+ """.strip()
825
+
826
+
827
+ # Example of a minimalist chat messages. Each row has the user indicator on the left and
828
+ # the corresponding markdown message on the right side. The message use the default
829
+ # background.
830
+ @me.page(
831
+ security_policy=me.SecurityPolicy(
832
+ allowed_iframe_parents=["https://google.github.io"]
833
+ ),
834
+ path="/ai/chat-msg-example-1",
835
+ )
836
+ def chat_msg_example_1():
837
+ # User chat message
838
+ with me.box(
839
+ style=me.Style(
840
+ color=me.theme_var("on-surface"),
841
+ background=me.theme_var("surface-container-lowest"),
842
+ )
843
+ ):
844
+ with me.box(
845
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
846
+ ):
847
+ # User avatar/icon box
848
+ me.text(
849
+ "U",
850
+ style=me.Style(
851
+ background=me.theme_var("primary"),
852
+ border_radius="50%",
853
+ color=me.theme_var("on-primary"),
854
+ font_size=20,
855
+ height=40,
856
+ width=40,
857
+ text_align="center",
858
+ line_height="1",
859
+ padding=me.Padding(top=10),
860
+ margin=me.Margin(top=16),
861
+ ),
862
+ )
863
+ # User query
864
+ me.markdown(USER_PLACEHOLDER_TEXT)
865
+
866
+ # Bot chat message
867
+ with me.box(
868
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
869
+ ):
870
+ # Bot avatar/icon box
871
+ me.text(
872
+ "B",
873
+ style=me.Style(
874
+ background=me.theme_var("secondary"),
875
+ border_radius="50%",
876
+ color=me.theme_var("on-secondary"),
877
+ font_size=20,
878
+ height=40,
879
+ width=40,
880
+ text_align="center",
881
+ line_height="1",
882
+ padding=me.Padding(top=10),
883
+ margin=me.Margin(top=16),
884
+ ),
885
+ )
886
+ # Bot message response
887
+ me.markdown(
888
+ BOT_PLACEHOLDER_TEXT, style=me.Style(color=me.theme_var("on-surface"))
889
+ )
890
+
891
+
892
+ # Example of alternating bubble chat messages.
893
+ @me.page(
894
+ security_policy=me.SecurityPolicy(
895
+ allowed_iframe_parents=["https://google.github.io"]
896
+ ),
897
+ path="/ai/chat-msg-example-2",
898
+ )
899
+ def chat_msg_example_2():
900
+ with me.box(
901
+ style=me.Style(
902
+ color=me.theme_var("on-surface"),
903
+ background=me.theme_var("surface-container-lowest"),
904
+ )
905
+ ):
906
+ # User chat message
907
+ with me.box(
908
+ style=me.Style(
909
+ display="flex", justify_content="end", gap=15, margin=me.Margin.all(20)
910
+ )
911
+ ):
912
+ # Add right aligned message bubble
913
+ with me.box(
914
+ style=me.Style(
915
+ border_radius=15,
916
+ background=me.theme_var("primary-container"),
917
+ color=me.theme_var("on-primary-container"),
918
+ padding=me.Padding.all(10),
919
+ width="66%",
920
+ )
921
+ ):
922
+ me.markdown(USER_PLACEHOLDER_TEXT)
923
+
924
+ # Bot chat message
925
+ with me.box(style=me.Style(margin=me.Margin.all(20))):
926
+ # Bot name label
927
+ me.text(
928
+ "Bot",
929
+ style=me.Style(
930
+ font_weight="bold", font_size=16, margin=me.Margin(left=15)
931
+ ),
932
+ )
933
+ # Add left aligned message bubble
934
+ with me.box(
935
+ style=me.Style(
936
+ border_radius=15,
937
+ background=me.theme_var("secondary-container"),
938
+ color=me.theme_var("on-secondary-container"),
939
+ padding=me.Padding.all(10),
940
+ width="66%",
941
+ )
942
+ ):
943
+ me.markdown(
944
+ BOT_PLACEHOLDER_TEXT,
945
+ style=me.Style(color=me.theme_var("on-secondary-container")),
946
+ )
947
+
948
+
949
+ # Example of a chat messages that also returns an image.
950
+ # background.
951
+ @me.page(
952
+ security_policy=me.SecurityPolicy(
953
+ allowed_iframe_parents=["https://google.github.io"]
954
+ ),
955
+ path="/ai/chat-msg-example-3",
956
+ )
957
+ def chat_msg_example_3():
958
+ # User chat message
959
+ with me.box(
960
+ style=me.Style(
961
+ color=me.theme_var("on-surface"),
962
+ background=me.theme_var("surface-container-lowest"),
963
+ )
964
+ ):
965
+ with me.box(
966
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
967
+ ):
968
+ # User avatar/icon box
969
+ me.text(
970
+ "U",
971
+ style=me.Style(
972
+ background=me.theme_var("primary"),
973
+ border_radius="50%",
974
+ color=me.theme_var("on-primary"),
975
+ font_size=20,
976
+ height=40,
977
+ width=40,
978
+ text_align="center",
979
+ line_height="1",
980
+ padding=me.Padding(top=10),
981
+ margin=me.Margin(top=16),
982
+ ),
983
+ )
984
+ # User query
985
+ me.markdown(USER_PLACEHOLDER_TEXT)
986
+
987
+ # Bot chat message
988
+ with me.box(
989
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
990
+ ):
991
+ # Bot avatar/icon box
992
+ me.text(
993
+ "B",
994
+ style=me.Style(
995
+ background=me.theme_var("secondary"),
996
+ border_radius="50%",
997
+ color=me.theme_var("on-secondary"),
998
+ font_size=20,
999
+ height=40,
1000
+ width=40,
1001
+ text_align="center",
1002
+ line_height="1",
1003
+ padding=me.Padding(top=10),
1004
+ margin=me.Margin(top=16),
1005
+ ),
1006
+ )
1007
+ # Bot image response
1008
+ me.image(
1009
+ src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
1010
+ alt="Grapefruit",
1011
+ style=me.Style(width="100%"),
1012
+ )
1013
+
1014
+ <example>
1015
+
1016
+ <example>
1017
+ # Examples of inputs for chat bot interfaces
1018
+ import mesop as me
1019
+
1020
+
1021
+ # Example using a normal textarea and button
1022
+ @me.page(
1023
+ security_policy=me.SecurityPolicy(
1024
+ allowed_iframe_parents=["https://google.github.io"]
1025
+ ),
1026
+ path="/ai/chat-inputs-example-1",
1027
+ )
1028
+ def chat_input_1():
1029
+ with me.box(style=me.Style(display="flex", width="100%", gap=10)):
1030
+ with me.box(style=me.Style(flex_grow=1)):
1031
+ me.textarea(
1032
+ placeholder="Default chat input",
1033
+ style=me.Style(width="100%"),
1034
+ rows=2,
1035
+ )
1036
+ me.button("Send", type="flat")
1037
+
1038
+
1039
+ # Example using a subtly styled textarea and buttons
1040
+ @me.page(
1041
+ security_policy=me.SecurityPolicy(
1042
+ allowed_iframe_parents=["https://google.github.io"]
1043
+ ),
1044
+ path="/ai/chat-inputs-example-2",
1045
+ )
1046
+ def chat_input_2():
1047
+ with me.box(
1048
+ style=me.Style(
1049
+ color=me.theme_var("on-surface-variant"),
1050
+ border_radius=16,
1051
+ padding=me.Padding.all(8),
1052
+ background=me.theme_var("surface-container-low"),
1053
+ display="flex",
1054
+ width="100%",
1055
+ )
1056
+ ):
1057
+ with me.box(
1058
+ style=me.Style(
1059
+ flex_grow=1,
1060
+ )
1061
+ ):
1062
+ me.native_textarea(
1063
+ autosize=True,
1064
+ min_rows=4,
1065
+ placeholder="Subtle chat input",
1066
+ style=me.Style(
1067
+ color=me.theme_var("on-surface-variant"),
1068
+ padding=me.Padding(top=16, left=16),
1069
+ background=me.theme_var("surface-container-low"),
1070
+ outline="none",
1071
+ width="100%",
1072
+ overflow_y="auto",
1073
+ border=me.Border.all(
1074
+ me.BorderSide(style="none"),
1075
+ ),
1076
+ ),
1077
+ )
1078
+ with me.content_button(type="icon"):
1079
+ me.icon("upload")
1080
+ with me.content_button(type="icon"):
1081
+ me.icon("photo")
1082
+ with me.content_button(type="icon"):
1083
+ me.icon("send")
1084
+
1085
+
1086
+ # Example using a elevated styled textarea and buttons
1087
+ @me.page(
1088
+ security_policy=me.SecurityPolicy(
1089
+ allowed_iframe_parents=["https://google.github.io"]
1090
+ ),
1091
+ path="/ai/chat-inputs-example-3",
1092
+ )
1093
+ def chat_input_3():
1094
+ with me.box(
1095
+ style=me.Style(
1096
+ padding=me.Padding.all(8),
1097
+ background=me.theme_var("surface-container-lowest"),
1098
+ display="flex",
1099
+ width="100%",
1100
+ border=me.Border.all(
1101
+ me.BorderSide(width=0, style="solid", color=me.theme_var("outline"))
1102
+ ),
1103
+ box_shadow="0 10px 20px #0000000a, 0 2px 6px #0000000a, 0 0 1px #0000000a",
1104
+ )
1105
+ ):
1106
+ with me.box(
1107
+ style=me.Style(
1108
+ flex_grow=1,
1109
+ )
1110
+ ):
1111
+ me.native_textarea(
1112
+ autosize=True,
1113
+ min_rows=4,
1114
+ placeholder="Elevated chat input",
1115
+ style=me.Style(
1116
+ color=me.theme_var("on-surface-variant"),
1117
+ font_family="monospace",
1118
+ padding=me.Padding(top=16, left=16),
1119
+ background=me.theme_var("surface-container-lowest"),
1120
+ outline="none",
1121
+ width="100%",
1122
+ overflow_y="auto",
1123
+ border=me.Border.all(
1124
+ me.BorderSide(style="none"),
1125
+ ),
1126
+ ),
1127
+ )
1128
+ with me.content_button(type="icon"):
1129
+ me.icon("upload")
1130
+ with me.content_button(type="icon"):
1131
+ me.icon("photo")
1132
+ with me.content_button(type="icon"):
1133
+ me.icon("send")
1134
+
1135
+ <example>
1136
+
1137
+ <example>
1138
+ # Examples of icon buttons that may be useful for chat UIs.
1139
+ # Note: These buttons event handler functions.
1140
+ import mesop as me
1141
+
1142
+
1143
+ @me.page(
1144
+ security_policy=me.SecurityPolicy(
1145
+ allowed_iframe_parents=["https://google.github.io"]
1146
+ ),
1147
+ path="/ai/chat-buttons-example-1",
1148
+ )
1149
+ def chat_buttons_examples():
1150
+ # The thumb_up icon is good for rating responses positively
1151
+ # This example also shows how to add a tooltip around the the icon button
1152
+ with me.tooltip(message="Good response", position="above"):
1153
+ with me.content_button(type="icon"):
1154
+ me.icon("thumb_up")
1155
+
1156
+ # The thumb_down icon is good for rating responses negatively
1157
+ with me.content_button(type="icon"):
1158
+ me.icon("thumb_down")
1159
+
1160
+ # The add icon is good for starting/resetting a chat
1161
+ with me.content_button(type="icon"):
1162
+ me.icon("add")
1163
+
1164
+ # The restart_alt icon is good for regenerating a messsage
1165
+ with me.content_button(type="icon"):
1166
+ me.icon("restart_alt")
1167
+
1168
+ # The dark_mode icon is good for dark mode
1169
+ with me.content_button(type="icon"):
1170
+ me.icon("dark_mode")
1171
+
1172
+ # The light_mode icon is good for light mode
1173
+ with me.content_button(type="icon"):
1174
+ me.icon("light_mode")
1175
+
1176
+ # The send icon is good for a button that submits user queries
1177
+ with me.content_button(type="icon"):
1178
+ me.icon("send")
1179
+
1180
+ <example>
prompt/chat_examples.txt ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This section provides examples of writing chat UIs in Mesop. Each example is wrapped in <example> tags.
2
+
3
+ <example>
4
+ # This is an example of a basic chat app.
5
+ import random
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Literal
9
+
10
+ import mesop as me
11
+
12
+ Role = Literal["user", "bot"]
13
+
14
+
15
+ @dataclass(kw_only=True)
16
+ class ChatMessage:
17
+ """Chat message metadata."""
18
+
19
+ role: Role = "user"
20
+ content: str = ""
21
+ edited: bool = False
22
+
23
+
24
+ @me.stateclass
25
+ class State:
26
+ input: str
27
+ output: list[ChatMessage]
28
+ in_progress: bool
29
+
30
+
31
+ @me.page()
32
+ def basic_chat():
33
+ state = me.state(State)
34
+ with me.box(
35
+ style=me.Style(
36
+ color=me.theme_var("on-surface"),
37
+ background=me.theme_var("surface-container-lowest"),
38
+ display="flex",
39
+ flex_direction="column",
40
+ height="100%",
41
+ padding=me.Padding.all(15),
42
+ )
43
+ ):
44
+ # This contains the chat messages that have been recorded. This takes 50fr.
45
+ # This section can be replaced with other types of chat messages.
46
+
47
+ # We set overflow to scroll so that the chat input will be fixed at the bottom.
48
+ with me.box(style=me.Style(overflow_y="scroll", flex_grow=1)):
49
+ for msg in state.output:
50
+ # User chat message
51
+ if msg.role == "user":
52
+ with me.box(
53
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
54
+ ):
55
+ # User avatar/icon box
56
+ me.text(
57
+ "U",
58
+ style=me.Style(
59
+ background=me.theme_var("primary"),
60
+ border_radius="50%",
61
+ color=me.theme_var("on-primary"),
62
+ font_size=20,
63
+ height=40,
64
+ width=40,
65
+ text_align="center",
66
+ line_height="1",
67
+ padding=me.Padding(top=10),
68
+ margin=me.Margin(top=16),
69
+ ),
70
+ )
71
+ # User query
72
+ me.markdown(msg.content)
73
+ else:
74
+ # Bot chat message
75
+ with me.box(
76
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
77
+ ):
78
+ # Bot avatar/icon box
79
+ me.text(
80
+ "B",
81
+ style=me.Style(
82
+ background=me.theme_var("secondary"),
83
+ border_radius="50%",
84
+ color=me.theme_var("on-secondary"),
85
+ font_size=20,
86
+ height=40,
87
+ width="40px",
88
+ text_align="center",
89
+ line_height="1",
90
+ padding=me.Padding(top=10),
91
+ margin=me.Margin(top=16),
92
+ ),
93
+ )
94
+ # Bot message response
95
+ me.markdown(
96
+ msg.content,
97
+ style=me.Style(color=me.theme_var("on-surface")),
98
+ )
99
+
100
+ # This is for the basic chat input. This is the second row at 1fr.
101
+ # This section can be replaced with other types of chat inputs.
102
+ with me.box(
103
+ style=me.Style(
104
+ border_radius=16,
105
+ padding=me.Padding.all(8),
106
+ background=me.theme_var("surface-container-low"),
107
+ display="flex",
108
+ width="100%",
109
+ )
110
+ ):
111
+ with me.box(
112
+ style=me.Style(
113
+ flex_grow=1,
114
+ )
115
+ ):
116
+ me.native_textarea(
117
+ key="chat_input",
118
+ value=state.input,
119
+ on_blur=on_chat_input,
120
+ autosize=True,
121
+ min_rows=4,
122
+ placeholder="Subtle chat input",
123
+ style=me.Style(
124
+ color=me.theme_var("on-surface-variant"),
125
+ padding=me.Padding(top=16, left=16),
126
+ background=me.theme_var("surface-container-low"),
127
+ outline="none",
128
+ width="100%",
129
+ overflow_y="auto",
130
+ border=me.Border.all(
131
+ me.BorderSide(style="none"),
132
+ ),
133
+ ),
134
+ )
135
+ with me.content_button(
136
+ type="icon",
137
+ on_click=on_click_submit_chat_msg,
138
+ # If we're processing a message prevent new queries from being sent
139
+ disabled=state.in_progress,
140
+ ):
141
+ me.icon("send")
142
+
143
+
144
+ def on_chat_input(e: me.InputBlurEvent):
145
+ """Capture chat text input on blur."""
146
+ state = me.state(State)
147
+ state.input = e.value
148
+
149
+
150
+ def on_click_submit_chat_msg(e: me.ClickEvent):
151
+ """Handles submitting a chat message."""
152
+ state = me.state(State)
153
+ if state.in_progress or not state.input:
154
+ return
155
+ input = state.input
156
+ # Clear the text input.
157
+ state.input = ""
158
+ yield
159
+
160
+ output = state.output
161
+ if output is None:
162
+ output = []
163
+ output.append(ChatMessage(role="user", content=input))
164
+ state.in_progress = True
165
+ yield
166
+
167
+ start_time = time.time()
168
+ # Send user input and chat history to get the bot response.
169
+ output_message = respond_to_chat(input, state.output)
170
+ assistant_message = ChatMessage(role="bot")
171
+ output.append(assistant_message)
172
+ state.output = output
173
+ for content in output_message:
174
+ assistant_message.content += content
175
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
176
+ if (time.time() - start_time) >= 0.25:
177
+ start_time = time.time()
178
+ yield
179
+
180
+ state.in_progress = False
181
+ me.focus_component(key="chat_input")
182
+ yield
183
+
184
+
185
+ def respond_to_chat(input: str, history: list[ChatMessage]):
186
+ """Displays random canned text.
187
+
188
+ Edit this function to process messages with a real chatbot/LLM.
189
+ """
190
+ lines = [
191
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
192
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
193
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
194
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
195
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
196
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
197
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
198
+ ]
199
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
200
+ yield line + " "
201
+
202
+ <example>
203
+
204
+ <example>
205
+ # This is an example of an advanced chat app.
206
+ #
207
+ # Features
208
+ #
209
+ # - Header with title
210
+ # - Sidebar
211
+ # - Start new chat
212
+ # - Focus input
213
+ # - Dark mode
214
+ # - Rate responses
215
+ # - Regenerate responses
216
+ # - Examples
217
+ import random
218
+ import time
219
+ from dataclasses import dataclass
220
+ from typing import Literal
221
+
222
+ import mesop as me
223
+
224
+ Role = Literal["user", "bot"]
225
+ # Render me.markdown if markdown
226
+ # Render me.image if image
227
+ MessageType = Literal["markdown", "image"]
228
+
229
+
230
+ EXAMPLE_USER_QUERIES = (
231
+ "Create a basic chat UI.",
232
+ "Create a chat UI that renders content side by side from multiple LLMs.",
233
+ "Create an advanced chat UI with a header and sidebar.",
234
+ )
235
+
236
+
237
+ @dataclass(kw_only=True)
238
+ class ChatMessage:
239
+ """Chat message metadata."""
240
+
241
+ role: Role = "user"
242
+ content: str = ""
243
+ edited: bool = False
244
+ # 1 is positive
245
+ # -1 is negative
246
+ # 0 is no rating
247
+ rating: int = 0
248
+ message_type: MessageType = "markdown"
249
+
250
+
251
+ @me.stateclass
252
+ class State:
253
+ input: str
254
+ output: list[ChatMessage]
255
+ in_progress: bool
256
+ sidebar_expanded: bool = False
257
+
258
+
259
+ @me.page()
260
+ def advanced_chat():
261
+ is_mobile = me.viewport_size().width < 640
262
+ state = me.state(State)
263
+ # This is the grid that manages the layout.
264
+ with me.box(
265
+ style=me.Style(
266
+ display="grid",
267
+ # First column is for the sidebar
268
+ # We use 1fr when the sidebar is not expanded and 7fr when it is expanded. This
269
+ # is because the sidebar takes up more space.
270
+ grid_template_columns="7fr 50fr"
271
+ if state.sidebar_expanded and not is_mobile
272
+ else "1fr 50fr",
273
+ # First row is for the header which is why we use 1fr.
274
+ grid_template_rows="1fr 50fr",
275
+ height="100%",
276
+ )
277
+ ):
278
+ # This block is the code for the sidebar
279
+ with me.box(
280
+ style=me.Style(
281
+ grid_row="1 / -1",
282
+ background=me.theme_var("surface-container"),
283
+ border=me.Border.symmetric(
284
+ horizontal=me.BorderSide(
285
+ width=1, style="solid", color=me.theme_var("outline-variant")
286
+ )
287
+ ),
288
+ height="100%",
289
+ )
290
+ ):
291
+ # This block is code for the sidebar menu item.
292
+ # It needs click event handlers to add interaction.
293
+ with me.box(
294
+ # Event handler to expand/collapse the sidebar.
295
+ on_click=on_click_menu_icon,
296
+ style=me.Style(
297
+ cursor="pointer",
298
+ padding=me.Padding.all(15),
299
+ ),
300
+ ):
301
+ me.icon("menu")
302
+
303
+ # This block is code for the sidebar menu item.
304
+ # It needs click event handlers to add interaction.
305
+ with me.box(
306
+ on_click=on_click_new_chat,
307
+ style=me.Style(
308
+ cursor="pointer",
309
+ padding=me.Padding.all(15),
310
+ ),
311
+ ):
312
+ if state.sidebar_expanded and not is_mobile:
313
+ with me.box(
314
+ style=me.Style(
315
+ display="flex",
316
+ align_items="center",
317
+ gap=5,
318
+ )
319
+ ):
320
+ me.icon("add")
321
+ me.text("New chat")
322
+ else:
323
+ with me.tooltip(message="New chat"):
324
+ me.icon("add")
325
+
326
+ # This block is code for the sidebar menu item.
327
+ # It needs click event handlers to add interaction.
328
+ with me.box(
329
+ on_click=on_click_theme_brightness,
330
+ style=me.Style(
331
+ cursor="pointer",
332
+ padding=me.Padding.all(15),
333
+ ),
334
+ ):
335
+ if state.sidebar_expanded and not is_mobile:
336
+ with me.box(
337
+ style=me.Style(
338
+ display="flex",
339
+ align_items="center",
340
+ gap=5,
341
+ )
342
+ ):
343
+ me.icon(
344
+ "light_mode" if me.theme_brightness() == "dark" else "dark_mode",
345
+ )
346
+ me.text(
347
+ "Light mode" if me.theme_brightness() == "dark" else "Dark mode"
348
+ )
349
+ else:
350
+ with me.tooltip(
351
+ message="Light mode"
352
+ if me.theme_brightness() == "dark"
353
+ else "Dark mode"
354
+ ):
355
+ me.icon(
356
+ "light_mode" if me.theme_brightness() == "dark" else "dark_mode"
357
+ )
358
+
359
+ with me.box(
360
+ style=me.Style(
361
+ background=me.theme_var("surface-container"),
362
+ padding=me.Padding.all(10),
363
+ border=me.Border(
364
+ bottom=me.BorderSide(
365
+ width=1, style="solid", color=me.theme_var("outline-variant")
366
+ )
367
+ ),
368
+ )
369
+ ):
370
+ if is_mobile:
371
+ default_flex_style = me.Style(
372
+ align_items="center",
373
+ display="flex",
374
+ gap=5,
375
+ justify_content="space-between",
376
+ )
377
+ else:
378
+ default_flex_style = me.Style(
379
+ align_items="center",
380
+ display="flex",
381
+ gap=5,
382
+ justify_content="space-between",
383
+ )
384
+
385
+ with me.box(style=default_flex_style):
386
+ with me.box(style=me.Style(display="flex", gap=5)):
387
+ me.text(
388
+ "Advanced Chat UI",
389
+ type="headline-6",
390
+ style=me.Style(margin=me.Margin(bottom=0)),
391
+ )
392
+
393
+ # Only show the "Made with Mesop" message on non-mobile screens.
394
+ if not is_mobile:
395
+ with me.box(style=me.Style(display="flex", gap=5)):
396
+ me.text(
397
+ "Made with ",
398
+ style=me.Style(
399
+ color=me.theme_var("on-surface-variant"),
400
+ font_weight="bold",
401
+ font_size=13,
402
+ ),
403
+ )
404
+ me.link(
405
+ text="Mesop",
406
+ url="https://google.github.io/mesop/",
407
+ open_in_new_tab=True,
408
+ style=me.Style(
409
+ color=me.theme_var("primary"),
410
+ text_decoration="none",
411
+ font_weight="bold",
412
+ font_size=13,
413
+ ),
414
+ )
415
+
416
+ # This block is for the main content to be added.
417
+ with me.box(style=me.Style(overflow_y="hidden")):
418
+ with me.box(
419
+ style=me.Style(
420
+ color=me.theme_var("on-surface"),
421
+ background=me.theme_var("surface-container-lowest"),
422
+ display="flex",
423
+ flex_direction="column",
424
+ padding=me.Padding.all(15),
425
+ height="100%",
426
+ )
427
+ ):
428
+ # If there are no messages yet, show some example queries for the user to get
429
+ # started with.
430
+ if not state.output:
431
+ with me.box(
432
+ style=me.Style(
433
+ overflow_y="scroll",
434
+ flex_grow=1,
435
+ color=me.theme_var("on-surface-variant"),
436
+ )
437
+ ):
438
+ with me.box(style=me.Style(margin=me.Margin(top=25), font_size=20)):
439
+ me.text("Get started with an example below")
440
+ with me.box(
441
+ style=me.Style(display="flex", gap=20, margin=me.Margin(top=25))
442
+ ):
443
+ for index, query in enumerate(EXAMPLE_USER_QUERIES):
444
+ with me.box(
445
+ key=f"query-{index}",
446
+ on_click=on_click_example_user_query,
447
+ style=me.Style(
448
+ background=me.theme_var("surface-container-highest"),
449
+ border_radius=15,
450
+ padding=me.Padding.all(20),
451
+ cursor="pointer",
452
+ ),
453
+ ):
454
+ me.text(query)
455
+
456
+ else:
457
+ # This contains the chat messages that have been recorded. This takes 50fr.
458
+ # This section can be replaced with other types of chat messages.
459
+
460
+ # We set overflow to scroll so that the chat input will be fixed at the bottom.
461
+ with me.box(style=me.Style(overflow_y="scroll", flex_grow=1)):
462
+ for index, msg in enumerate(state.output):
463
+ # User chat message
464
+ if msg.role == "user":
465
+ with me.box(
466
+ style=me.Style(
467
+ display="flex",
468
+ justify_content="end",
469
+ gap=15,
470
+ margin=me.Margin.all(20),
471
+ )
472
+ ):
473
+ with me.box(
474
+ style=me.Style(
475
+ border_radius=10,
476
+ background=me.theme_var("surface-container"),
477
+ color=me.theme_var("on-surface-variant"),
478
+ padding=me.Padding.symmetric(vertical=0, horizontal=10),
479
+ width="66%",
480
+ )
481
+ ):
482
+ # User query
483
+ me.markdown(msg.content)
484
+ else:
485
+ # Bot chat message
486
+ with me.box(
487
+ style=me.Style(
488
+ display="flex", gap=15, margin=me.Margin.all(20)
489
+ )
490
+ ):
491
+ # Bot avatar/icon box
492
+ me.text(
493
+ "M",
494
+ style=me.Style(
495
+ background=me.theme_var("primary"),
496
+ border_radius="50%",
497
+ color=me.theme_var("on-primary"),
498
+ font_size=20,
499
+ height=40,
500
+ width="40px",
501
+ text_align="center",
502
+ line_height="1",
503
+ padding=me.Padding(top=10),
504
+ margin=me.Margin(top=16),
505
+ ),
506
+ )
507
+ # Bot message response
508
+ with me.box():
509
+ me.markdown(
510
+ msg.content,
511
+ style=me.Style(color=me.theme_var("on-surface")),
512
+ )
513
+
514
+ rated_style = me.Style(
515
+ background=me.theme_var("surface-container-low"),
516
+ color=me.theme_var("on-surface-variant"),
517
+ )
518
+ with me.tooltip(message="Good response", position="above"):
519
+ with me.content_button(
520
+ type="icon",
521
+ key=f"thumb_up-{index}",
522
+ on_click=on_click_thumb_up,
523
+ style=rated_style if msg.rating == 1 else None,
524
+ ):
525
+ me.icon("thumb_up")
526
+
527
+ with me.tooltip(message="Bad response", position="above"):
528
+ with me.content_button(
529
+ type="icon",
530
+ key=f"thumb_down-{index}",
531
+ on_click=on_click_thumb_down,
532
+ style=rated_style if msg.rating == -1 else None,
533
+ ):
534
+ me.icon("thumb_down")
535
+
536
+ with me.tooltip(
537
+ message="Regenerate answer", position="above"
538
+ ):
539
+ with me.content_button(
540
+ type="icon",
541
+ key=f"restart-{index}",
542
+ on_click=on_click_regenerate,
543
+ ):
544
+ me.icon("restart_alt")
545
+
546
+ # This is for the basic chat input. This is the second row at 1fr.
547
+ # This section can be replaced with other types of chat inputs.
548
+ with me.box(
549
+ style=me.Style(
550
+ border_radius=16,
551
+ padding=me.Padding.all(8),
552
+ background=me.theme_var("surface-container-low"),
553
+ display="flex",
554
+ )
555
+ ):
556
+ with me.box(
557
+ style=me.Style(
558
+ flex_grow=1,
559
+ )
560
+ ):
561
+ me.native_textarea(
562
+ key="chat_input",
563
+ value=state.input,
564
+ on_blur=on_chat_input,
565
+ autosize=True,
566
+ min_rows=4,
567
+ placeholder="Subtle chat input",
568
+ style=me.Style(
569
+ color=me.theme_var("on-surface-variant"),
570
+ padding=me.Padding(top=16, left=16),
571
+ background=me.theme_var("surface-container-low"),
572
+ outline="none",
573
+ width="100%",
574
+ overflow_y="auto",
575
+ border=me.Border.all(
576
+ me.BorderSide(style="none"),
577
+ ),
578
+ ),
579
+ )
580
+ with me.content_button(
581
+ type="icon",
582
+ on_click=on_click_submit_chat_msg,
583
+ # If we're processing a message prevent new queries from being sent
584
+ disabled=state.in_progress,
585
+ ):
586
+ me.icon("send")
587
+
588
+
589
+ def on_click_example_user_query(e: me.ClickEvent):
590
+ """Populates the user input with the example query"""
591
+ state = me.state(State)
592
+ # Get the example index from the key
593
+ _, example_index = e.key.split("-")
594
+ state.input = EXAMPLE_USER_QUERIES[int(example_index)]
595
+
596
+
597
+ def on_click_thumb_up(e: me.ClickEvent):
598
+ """Gives the message a positive rating"""
599
+ state = me.state(State)
600
+ # Get the message index from the key
601
+ _, msg_index = e.key.split("-")
602
+ msg_index = int(msg_index)
603
+ # Give a positive rating
604
+ state.output[msg_index].rating = 1
605
+
606
+
607
+ def on_click_thumb_down(e: me.ClickEvent):
608
+ """Gives the message a negative rating"""
609
+ state = me.state(State)
610
+ # Get the message index from the key
611
+ _, msg_index = e.key.split("-")
612
+ msg_index = int(msg_index)
613
+ # Give a negative rating
614
+ state.output[msg_index].rating = -1
615
+
616
+
617
+ def on_click_new_chat(e: me.ClickEvent):
618
+ """Resets messages."""
619
+ state = me.state(State)
620
+ state.output = []
621
+ me.focus_component(key="chat_input")
622
+
623
+
624
+ def on_click_theme_brightness(e: me.ClickEvent):
625
+ """Toggles dark mode."""
626
+ if me.theme_brightness() == "light":
627
+ me.set_theme_mode("dark")
628
+ else:
629
+ me.set_theme_mode("light")
630
+
631
+
632
+ def on_click_menu_icon(e: me.ClickEvent):
633
+ """Expands and collapses sidebar menu."""
634
+ state = me.state(State)
635
+ state.sidebar_expanded = not state.sidebar_expanded
636
+
637
+
638
+ def on_chat_input(e: me.InputBlurEvent):
639
+ """Capture chat text input on blur."""
640
+ state = me.state(State)
641
+ state.input = e.value
642
+
643
+
644
+ def on_click_regenerate(e: me.ClickEvent):
645
+ """Regenerates response from an existing message"""
646
+ state = me.state(State)
647
+ # Get message index from key
648
+ _, msg_index = e.key.split("-")
649
+ msg_index = int(msg_index)
650
+
651
+ # Get the user message which is the previous message
652
+ user_message = state.output[msg_index - 1]
653
+ # Get bot message to be regenerated
654
+ assistant_message = state.output[msg_index]
655
+ assistant_message.content = ""
656
+ state.in_progress = True
657
+ yield
658
+
659
+ start_time = time.time()
660
+ # Send in the old user input and chat history to get the bot response.
661
+ # We make sure to only pass in the chat history up to this message.
662
+ output_message = respond_to_chat(
663
+ user_message.content, state.output[:msg_index]
664
+ )
665
+ for content in output_message:
666
+ assistant_message.content += content
667
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
668
+ if (time.time() - start_time) >= 0.25:
669
+ start_time = time.time()
670
+ yield
671
+
672
+ state.in_progress = False
673
+ me.focus_component(key="chat_input")
674
+ yield
675
+
676
+
677
+ def on_click_submit_chat_msg(e: me.ClickEvent):
678
+ """Handles submitting a chat message."""
679
+ state = me.state(State)
680
+ if state.in_progress or not state.input:
681
+ return
682
+ input = state.input
683
+ # Clear the text input.
684
+ state.input = ""
685
+ yield
686
+
687
+ output = state.output
688
+ if output is None:
689
+ output = []
690
+ output.append(ChatMessage(role="user", content=input))
691
+ state.in_progress = True
692
+ yield
693
+
694
+ start_time = time.time()
695
+ # Send user input and chat history to get the bot response.
696
+ output_message = respond_to_chat(input, state.output)
697
+ assistant_message = ChatMessage(role="bot")
698
+ output.append(assistant_message)
699
+ state.output = output
700
+ for content in output_message:
701
+ assistant_message.content += content
702
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
703
+ if (time.time() - start_time) >= 0.25:
704
+ start_time = time.time()
705
+ yield
706
+
707
+ state.in_progress = False
708
+ me.focus_component(key="chat_input")
709
+ yield
710
+
711
+
712
+ def respond_to_chat(input: str, history: list[ChatMessage]):
713
+ """Displays random canned text.
714
+
715
+ Edit this function to process messages with a real chatbot/LLM.
716
+ """
717
+ lines = [
718
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
719
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
720
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
721
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
722
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
723
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
724
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
725
+ ]
726
+
727
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
728
+ yield line + " "
729
+
730
+ <example>
state.py CHANGED
@@ -20,11 +20,15 @@ class State:
20
 
21
  # Generate prompt panel
22
  prompt_mode: str = "Generate"
 
23
  prompt_placeholder: str
24
  prompt: str
25
 
 
 
 
26
  # Prompt history panel
27
- prompt_history: list[dict] # Format: {"prompt", "code", "index", "mode"}
28
 
29
  # Code editor
30
  code_placeholder: str = c.EXAMPLE_PROGRAM
@@ -46,6 +50,7 @@ class State:
46
  show_prompt_history_panel: bool = False
47
  show_status_snackbar: bool = False
48
  show_help_dialog: bool = bool(int(os.getenv("MESOP_APP_MAKER_SHOW_HELP", "0")))
 
49
 
50
  # Async action
51
  async_action_name: str
 
20
 
21
  # Generate prompt panel
22
  prompt_mode: str = "Generate"
23
+ prompt_app_type: str = "general"
24
  prompt_placeholder: str
25
  prompt: str
26
 
27
+ # New template dialog
28
+ select_index: int
29
+
30
  # Prompt history panel
31
+ prompt_history: list[dict] # Format: {"prompt", "code", "index", "mode", "app_type"}
32
 
33
  # Code editor
34
  code_placeholder: str = c.EXAMPLE_PROGRAM
 
50
  show_prompt_history_panel: bool = False
51
  show_status_snackbar: bool = False
52
  show_help_dialog: bool = bool(int(os.getenv("MESOP_APP_MAKER_SHOW_HELP", "0")))
53
+ show_new_dialog: bool = False
54
 
55
  # Async action
56
  async_action_name: str
templates/advanced_chat.txt ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import time
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ import mesop as me
7
+
8
+ Role = Literal["user", "bot"]
9
+ # Render me.markdown if markdown
10
+ # Render me.image if image
11
+ MessageType = Literal["markdown", "image"]
12
+
13
+
14
+ EXAMPLE_USER_QUERIES = (
15
+ "Create a basic chat UI.",
16
+ "Create a chat UI that renders content side by side from multiple LLMs.",
17
+ "Create an advanced chat UI with a header and sidebar.",
18
+ )
19
+
20
+
21
+ @dataclass(kw_only=True)
22
+ class ChatMessage:
23
+ """Chat message metadata."""
24
+
25
+ role: Role = "user"
26
+ content: str = ""
27
+ edited: bool = False
28
+ # 1 is positive
29
+ # -1 is negative
30
+ # 0 is no rating
31
+ rating: int = 0
32
+ message_type: MessageType = "markdown"
33
+
34
+
35
+ @me.stateclass
36
+ class State:
37
+ input: str
38
+ output: list[ChatMessage]
39
+ in_progress: bool
40
+ sidebar_expanded: bool = False
41
+
42
+
43
+ @me.page()
44
+ def advanced_chat():
45
+ is_mobile = me.viewport_size().width < 640
46
+ state = me.state(State)
47
+ # This is the grid that manages the layout.
48
+ with me.box(
49
+ style=me.Style(
50
+ display="grid",
51
+ # First column is for the sidebar
52
+ # We use 1fr when the sidebar is not expanded and 7fr when it is expanded. This
53
+ # is because the sidebar takes up more space.
54
+ grid_template_columns="7fr 50fr"
55
+ if state.sidebar_expanded and not is_mobile
56
+ else "1fr 50fr",
57
+ # First row is for the header which is why we use 1fr.
58
+ grid_template_rows="1fr 50fr",
59
+ height="100%",
60
+ )
61
+ ):
62
+ # This block is the code for the sidebar
63
+ with me.box(
64
+ style=me.Style(
65
+ grid_row="1 / -1",
66
+ background=me.theme_var("surface-container"),
67
+ border=me.Border.symmetric(
68
+ horizontal=me.BorderSide(
69
+ width=1, style="solid", color=me.theme_var("outline-variant")
70
+ )
71
+ ),
72
+ height="100%",
73
+ )
74
+ ):
75
+ # This block is code for the sidebar menu item.
76
+ # It needs click event handlers to add interaction.
77
+ with me.box(
78
+ # Event handler to expand/collapse the sidebar.
79
+ on_click=on_click_menu_icon,
80
+ style=me.Style(
81
+ cursor="pointer",
82
+ padding=me.Padding.all(15),
83
+ ),
84
+ ):
85
+ me.icon("menu")
86
+
87
+ # This block is code for the sidebar menu item.
88
+ # It needs click event handlers to add interaction.
89
+ with me.box(
90
+ on_click=on_click_new_chat,
91
+ style=me.Style(
92
+ cursor="pointer",
93
+ padding=me.Padding.all(15),
94
+ ),
95
+ ):
96
+ if state.sidebar_expanded and not is_mobile:
97
+ with me.box(
98
+ style=me.Style(
99
+ display="flex",
100
+ align_items="center",
101
+ gap=5,
102
+ )
103
+ ):
104
+ me.icon("add")
105
+ me.text("New chat")
106
+ else:
107
+ with me.tooltip(message="New chat"):
108
+ me.icon("add")
109
+
110
+ # This block is code for the sidebar menu item.
111
+ # It needs click event handlers to add interaction.
112
+ with me.box(
113
+ on_click=on_click_theme_brightness,
114
+ style=me.Style(
115
+ cursor="pointer",
116
+ padding=me.Padding.all(15),
117
+ ),
118
+ ):
119
+ if state.sidebar_expanded and not is_mobile:
120
+ with me.box(
121
+ style=me.Style(
122
+ display="flex",
123
+ align_items="center",
124
+ gap=5,
125
+ )
126
+ ):
127
+ me.icon(
128
+ "light_mode" if me.theme_brightness() == "dark" else "dark_mode",
129
+ )
130
+ me.text(
131
+ "Light mode" if me.theme_brightness() == "dark" else "Dark mode"
132
+ )
133
+ else:
134
+ with me.tooltip(
135
+ message="Light mode"
136
+ if me.theme_brightness() == "dark"
137
+ else "Dark mode"
138
+ ):
139
+ me.icon(
140
+ "light_mode" if me.theme_brightness() == "dark" else "dark_mode"
141
+ )
142
+
143
+ with me.box(
144
+ style=me.Style(
145
+ background=me.theme_var("surface-container"),
146
+ padding=me.Padding.all(10),
147
+ border=me.Border(
148
+ bottom=me.BorderSide(
149
+ width=1, style="solid", color=me.theme_var("outline-variant")
150
+ )
151
+ ),
152
+ )
153
+ ):
154
+ if is_mobile:
155
+ default_flex_style = me.Style(
156
+ align_items="center",
157
+ display="flex",
158
+ gap=5,
159
+ justify_content="space-between",
160
+ )
161
+ else:
162
+ default_flex_style = me.Style(
163
+ align_items="center",
164
+ display="flex",
165
+ gap=5,
166
+ justify_content="space-between",
167
+ )
168
+
169
+ with me.box(style=default_flex_style):
170
+ with me.box(style=me.Style(display="flex", gap=5)):
171
+ me.text(
172
+ "Advanced Chat UI",
173
+ type="headline-6",
174
+ style=me.Style(margin=me.Margin(bottom=0)),
175
+ )
176
+
177
+ # Only show the "Made with Mesop" message on non-mobile screens.
178
+ if not is_mobile:
179
+ with me.box(style=me.Style(display="flex", gap=5)):
180
+ me.text(
181
+ "Made with ",
182
+ style=me.Style(
183
+ color=me.theme_var("on-surface-variant"),
184
+ font_weight="bold",
185
+ font_size=13,
186
+ ),
187
+ )
188
+ me.link(
189
+ text="Mesop",
190
+ url="https://google.github.io/mesop/",
191
+ open_in_new_tab=True,
192
+ style=me.Style(
193
+ color=me.theme_var("primary"),
194
+ text_decoration="none",
195
+ font_weight="bold",
196
+ font_size=13,
197
+ ),
198
+ )
199
+
200
+ # This block is for the main content to be added.
201
+ with me.box(style=me.Style(overflow_y="hidden")):
202
+ with me.box(
203
+ style=me.Style(
204
+ color=me.theme_var("on-surface"),
205
+ background=me.theme_var("surface-container-lowest"),
206
+ display="flex",
207
+ flex_direction="column",
208
+ padding=me.Padding.all(15),
209
+ height="100%",
210
+ )
211
+ ):
212
+ # If there are no messages yet, show some example queries for the user to get
213
+ # started with.
214
+ if not state.output:
215
+ with me.box(
216
+ style=me.Style(
217
+ overflow_y="scroll",
218
+ flex_grow=1,
219
+ color=me.theme_var("on-surface-variant"),
220
+ )
221
+ ):
222
+ with me.box(style=me.Style(margin=me.Margin(top=25), font_size=20)):
223
+ me.text("Get started with an example below")
224
+ with me.box(
225
+ style=me.Style(display="flex", gap=20, margin=me.Margin(top=25))
226
+ ):
227
+ for index, query in enumerate(EXAMPLE_USER_QUERIES):
228
+ with me.box(
229
+ key=f"query-{index}",
230
+ on_click=on_click_example_user_query,
231
+ style=me.Style(
232
+ background=me.theme_var("surface-container-highest"),
233
+ border_radius=15,
234
+ padding=me.Padding.all(20),
235
+ cursor="pointer",
236
+ ),
237
+ ):
238
+ me.text(query)
239
+
240
+ else:
241
+ # This contains the chat messages that have been recorded. This takes 50fr.
242
+ # This section can be replaced with other types of chat messages.
243
+
244
+ # We set overflow to scroll so that the chat input will be fixed at the bottom.
245
+ with me.box(style=me.Style(overflow_y="scroll", flex_grow=1)):
246
+ for index, msg in enumerate(state.output):
247
+ # User chat message
248
+ if msg.role == "user":
249
+ with me.box(
250
+ style=me.Style(
251
+ display="flex",
252
+ justify_content="end",
253
+ gap=15,
254
+ margin=me.Margin.all(20),
255
+ )
256
+ ):
257
+ with me.box(
258
+ style=me.Style(
259
+ border_radius=10,
260
+ background=me.theme_var("surface-container"),
261
+ color=me.theme_var("on-surface-variant"),
262
+ padding=me.Padding.symmetric(vertical=0, horizontal=10),
263
+ width="66%",
264
+ )
265
+ ):
266
+ # User query
267
+ me.markdown(msg.content)
268
+ else:
269
+ # Bot chat message
270
+ with me.box(
271
+ style=me.Style(
272
+ display="flex", gap=15, margin=me.Margin.all(20)
273
+ )
274
+ ):
275
+ # Bot avatar/icon box
276
+ me.text(
277
+ "M",
278
+ style=me.Style(
279
+ background=me.theme_var("primary"),
280
+ border_radius="50%",
281
+ color=me.theme_var("on-primary"),
282
+ font_size=20,
283
+ height=40,
284
+ width="40px",
285
+ text_align="center",
286
+ line_height="1",
287
+ padding=me.Padding(top=10),
288
+ margin=me.Margin(top=16),
289
+ ),
290
+ )
291
+ # Bot message response
292
+ with me.box():
293
+ me.markdown(
294
+ msg.content,
295
+ style=me.Style(color=me.theme_var("on-surface")),
296
+ )
297
+
298
+ rated_style = me.Style(
299
+ background=me.theme_var("surface-container-low"),
300
+ color=me.theme_var("on-surface-variant"),
301
+ )
302
+ with me.tooltip(message="Good response", position="above"):
303
+ with me.content_button(
304
+ type="icon",
305
+ key=f"thumb_up-{index}",
306
+ on_click=on_click_thumb_up,
307
+ style=rated_style if msg.rating == 1 else None,
308
+ ):
309
+ me.icon("thumb_up")
310
+
311
+ with me.tooltip(message="Bad response", position="above"):
312
+ with me.content_button(
313
+ type="icon",
314
+ key=f"thumb_down-{index}",
315
+ on_click=on_click_thumb_down,
316
+ style=rated_style if msg.rating == -1 else None,
317
+ ):
318
+ me.icon("thumb_down")
319
+
320
+ with me.tooltip(
321
+ message="Regenerate answer", position="above"
322
+ ):
323
+ with me.content_button(
324
+ type="icon",
325
+ key=f"restart-{index}",
326
+ on_click=on_click_regenerate,
327
+ ):
328
+ me.icon("restart_alt")
329
+
330
+ # This is for the basic chat input. This is the second row at 1fr.
331
+ # This section can be replaced with other types of chat inputs.
332
+ with me.box(
333
+ style=me.Style(
334
+ border_radius=16,
335
+ padding=me.Padding.all(8),
336
+ background=me.theme_var("surface-container-low"),
337
+ display="flex",
338
+ )
339
+ ):
340
+ with me.box(
341
+ style=me.Style(
342
+ flex_grow=1,
343
+ )
344
+ ):
345
+ me.native_textarea(
346
+ key="chat_input",
347
+ value=state.input,
348
+ on_blur=on_chat_input,
349
+ autosize=True,
350
+ min_rows=4,
351
+ placeholder="Subtle chat input",
352
+ style=me.Style(
353
+ color=me.theme_var("on-surface-variant"),
354
+ padding=me.Padding(top=16, left=16),
355
+ background=me.theme_var("surface-container-low"),
356
+ outline="none",
357
+ width="100%",
358
+ overflow_y="auto",
359
+ border=me.Border.all(
360
+ me.BorderSide(style="none"),
361
+ ),
362
+ ),
363
+ )
364
+ with me.content_button(
365
+ type="icon",
366
+ on_click=on_click_submit_chat_msg,
367
+ # If we're processing a message prevent new queries from being sent
368
+ disabled=state.in_progress,
369
+ ):
370
+ me.icon("send")
371
+
372
+
373
+ def on_click_example_user_query(e: me.ClickEvent):
374
+ """Populates the user input with the example query"""
375
+ state = me.state(State)
376
+ # Get the example index from the key
377
+ _, example_index = e.key.split("-")
378
+ state.input = EXAMPLE_USER_QUERIES[int(example_index)]
379
+
380
+
381
+ def on_click_thumb_up(e: me.ClickEvent):
382
+ """Gives the message a positive rating"""
383
+ state = me.state(State)
384
+ # Get the message index from the key
385
+ _, msg_index = e.key.split("-")
386
+ msg_index = int(msg_index)
387
+ # Give a positive rating
388
+ state.output[msg_index].rating = 1
389
+
390
+
391
+ def on_click_thumb_down(e: me.ClickEvent):
392
+ """Gives the message a negative rating"""
393
+ state = me.state(State)
394
+ # Get the message index from the key
395
+ _, msg_index = e.key.split("-")
396
+ msg_index = int(msg_index)
397
+ # Give a negative rating
398
+ state.output[msg_index].rating = -1
399
+
400
+
401
+ def on_click_new_chat(e: me.ClickEvent):
402
+ """Resets messages."""
403
+ state = me.state(State)
404
+ state.output = []
405
+ me.focus_component(key="chat_input")
406
+
407
+
408
+ def on_click_theme_brightness(e: me.ClickEvent):
409
+ """Toggles dark mode."""
410
+ if me.theme_brightness() == "light":
411
+ me.set_theme_mode("dark")
412
+ else:
413
+ me.set_theme_mode("light")
414
+
415
+
416
+ def on_click_menu_icon(e: me.ClickEvent):
417
+ """Expands and collapses sidebar menu."""
418
+ state = me.state(State)
419
+ state.sidebar_expanded = not state.sidebar_expanded
420
+
421
+
422
+ def on_chat_input(e: me.InputBlurEvent):
423
+ """Capture chat text input on blur."""
424
+ state = me.state(State)
425
+ state.input = e.value
426
+
427
+
428
+ def on_click_regenerate(e: me.ClickEvent):
429
+ """Regenerates response from an existing message"""
430
+ state = me.state(State)
431
+ # Get message index from key
432
+ _, msg_index = e.key.split("-")
433
+ msg_index = int(msg_index)
434
+
435
+ # Get the user message which is the previous message
436
+ user_message = state.output[msg_index - 1]
437
+ # Get bot message to be regenerated
438
+ assistant_message = state.output[msg_index]
439
+ assistant_message.content = ""
440
+ state.in_progress = True
441
+ yield
442
+
443
+ start_time = time.time()
444
+ # Send in the old user input and chat history to get the bot response.
445
+ # We make sure to only pass in the chat history up to this message.
446
+ output_message = respond_to_chat(
447
+ user_message.content, state.output[:msg_index]
448
+ )
449
+ for content in output_message:
450
+ assistant_message.content += content
451
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
452
+ if (time.time() - start_time) >= 0.25:
453
+ start_time = time.time()
454
+ yield
455
+
456
+ state.in_progress = False
457
+ me.focus_component(key="chat_input")
458
+ yield
459
+
460
+
461
+ def on_click_submit_chat_msg(e: me.ClickEvent):
462
+ """Handles submitting a chat message."""
463
+ state = me.state(State)
464
+ if state.in_progress or not state.input:
465
+ return
466
+ input = state.input
467
+ # Clear the text input.
468
+ state.input = ""
469
+ yield
470
+
471
+ output = state.output
472
+ if output is None:
473
+ output = []
474
+ output.append(ChatMessage(role="user", content=input))
475
+ state.in_progress = True
476
+ yield
477
+
478
+ start_time = time.time()
479
+ # Send user input and chat history to get the bot response.
480
+ output_message = respond_to_chat(input, state.output)
481
+ assistant_message = ChatMessage(role="bot")
482
+ output.append(assistant_message)
483
+ state.output = output
484
+ for content in output_message:
485
+ assistant_message.content += content
486
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
487
+ if (time.time() - start_time) >= 0.25:
488
+ start_time = time.time()
489
+ yield
490
+
491
+ state.in_progress = False
492
+ me.focus_component(key="chat_input")
493
+ yield
494
+
495
+
496
+ def respond_to_chat(input: str, history: list[ChatMessage]):
497
+ """Displays random canned text.
498
+
499
+ Edit this function to process messages with a real chatbot/LLM.
500
+ """
501
+ lines = [
502
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
503
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
504
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
505
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
506
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
507
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
508
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
509
+ ]
510
+
511
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
512
+ yield line + " "
templates/basic_chat.txt ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import time
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ import mesop as me
7
+
8
+ Role = Literal["user", "bot"]
9
+
10
+
11
+ @dataclass(kw_only=True)
12
+ class ChatMessage:
13
+ """Chat message metadata."""
14
+
15
+ role: Role = "user"
16
+ content: str = ""
17
+ edited: bool = False
18
+
19
+
20
+ @me.stateclass
21
+ class State:
22
+ input: str
23
+ output: list[ChatMessage]
24
+ in_progress: bool
25
+
26
+
27
+ @me.page()
28
+ def basic_chat():
29
+ state = me.state(State)
30
+ with me.box(
31
+ style=me.Style(
32
+ color=me.theme_var("on-surface"),
33
+ background=me.theme_var("surface-container-lowest"),
34
+ display="flex",
35
+ flex_direction="column",
36
+ height="100%",
37
+ padding=me.Padding.all(15),
38
+ )
39
+ ):
40
+ # This contains the chat messages that have been recorded. This takes 50fr.
41
+ # This section can be replaced with other types of chat messages.
42
+
43
+ # We set overflow to scroll so that the chat input will be fixed at the bottom.
44
+ with me.box(style=me.Style(overflow_y="scroll", flex_grow=1)):
45
+ for msg in state.output:
46
+ # User chat message
47
+ if msg.role == "user":
48
+ with me.box(
49
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
50
+ ):
51
+ # User avatar/icon box
52
+ me.text(
53
+ "U",
54
+ style=me.Style(
55
+ background=me.theme_var("primary"),
56
+ border_radius="50%",
57
+ color=me.theme_var("on-primary"),
58
+ font_size=20,
59
+ height=40,
60
+ width=40,
61
+ text_align="center",
62
+ line_height="1",
63
+ padding=me.Padding(top=10),
64
+ margin=me.Margin(top=16),
65
+ ),
66
+ )
67
+ # User query
68
+ me.markdown(msg.content)
69
+ else:
70
+ # Bot chat message
71
+ with me.box(
72
+ style=me.Style(display="flex", gap=15, margin=me.Margin.all(20))
73
+ ):
74
+ # Bot avatar/icon box
75
+ me.text(
76
+ "B",
77
+ style=me.Style(
78
+ background=me.theme_var("secondary"),
79
+ border_radius="50%",
80
+ color=me.theme_var("on-secondary"),
81
+ font_size=20,
82
+ height=40,
83
+ width="40px",
84
+ text_align="center",
85
+ line_height="1",
86
+ padding=me.Padding(top=10),
87
+ margin=me.Margin(top=16),
88
+ ),
89
+ )
90
+ # Bot message response
91
+ me.markdown(
92
+ msg.content,
93
+ style=me.Style(color=me.theme_var("on-surface")),
94
+ )
95
+
96
+ # This is for the basic chat input. This is the second row at 1fr.
97
+ # This section can be replaced with other types of chat inputs.
98
+ with me.box(
99
+ style=me.Style(
100
+ border_radius=16,
101
+ padding=me.Padding.all(8),
102
+ background=me.theme_var("surface-container-low"),
103
+ display="flex",
104
+ width="100%",
105
+ )
106
+ ):
107
+ with me.box(
108
+ style=me.Style(
109
+ flex_grow=1,
110
+ )
111
+ ):
112
+ me.native_textarea(
113
+ key="chat_input",
114
+ value=state.input,
115
+ on_blur=on_chat_input,
116
+ autosize=True,
117
+ min_rows=4,
118
+ placeholder="Subtle chat input",
119
+ style=me.Style(
120
+ color=me.theme_var("on-surface-variant"),
121
+ padding=me.Padding(top=16, left=16),
122
+ background=me.theme_var("surface-container-low"),
123
+ outline="none",
124
+ width="100%",
125
+ overflow_y="auto",
126
+ border=me.Border.all(
127
+ me.BorderSide(style="none"),
128
+ ),
129
+ ),
130
+ )
131
+ with me.content_button(
132
+ type="icon",
133
+ on_click=on_click_submit_chat_msg,
134
+ # If we're processing a message prevent new queries from being sent
135
+ disabled=state.in_progress,
136
+ ):
137
+ me.icon("send")
138
+
139
+
140
+ def on_chat_input(e: me.InputBlurEvent):
141
+ """Capture chat text input on blur."""
142
+ state = me.state(State)
143
+ state.input = e.value
144
+
145
+
146
+ def on_click_submit_chat_msg(e: me.ClickEvent):
147
+ """Handles submitting a chat message."""
148
+ state = me.state(State)
149
+ if state.in_progress or not state.input:
150
+ return
151
+ input = state.input
152
+ # Clear the text input.
153
+ state.input = ""
154
+ yield
155
+
156
+ output = state.output
157
+ if output is None:
158
+ output = []
159
+ output.append(ChatMessage(role="user", content=input))
160
+ state.in_progress = True
161
+ yield
162
+
163
+ start_time = time.time()
164
+ # Send user input and chat history to get the bot response.
165
+ output_message = respond_to_chat(input, state.output)
166
+ assistant_message = ChatMessage(role="bot")
167
+ output.append(assistant_message)
168
+ state.output = output
169
+ for content in output_message:
170
+ assistant_message.content += content
171
+ # TODO: 0.25 is an abitrary choice. In the future, consider making this adjustable.
172
+ if (time.time() - start_time) >= 0.25:
173
+ start_time = time.time()
174
+ yield
175
+
176
+ state.in_progress = False
177
+ me.focus_component(key="chat_input")
178
+ yield
179
+
180
+
181
+ def respond_to_chat(input: str, history: list[ChatMessage]):
182
+ """Displays random canned text.
183
+
184
+ Edit this function to process messages with a real chatbot/LLM.
185
+ """
186
+ lines = [
187
+ "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
188
+ "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
189
+ "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
190
+ "Deployment is straightforward, utilizing standard HTTP technologies.",
191
+ "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
192
+ "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
193
+ "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
194
+ ]
195
+ for line in random.sample(lines, random.randint(3, len(lines) - 1)):
196
+ yield line + " "
templates/default.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import mesop as me
2
+
3
+ @me.page()
4
+ def app():
5
+ me.text("Hello World")