diff --git a/examples/.DS_Store b/examples/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 Binary files a/examples/.DS_Store and /dev/null differ diff --git a/examples/.python-version b/examples/.python-version deleted file mode 100644 index d20cc2bf020ea4d4e6b4237229024d03130d0203..0000000000000000000000000000000000000000 --- a/examples/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.8.10 diff --git a/examples/__pycache__/__init__.cpython-37.pyc b/examples/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index a206cd01abc541956ced0329dd3b0f11b766e224..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/__init__.cpython-39.pyc b/examples/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 9c93051bc147e18348d4354c1be1686a6c0deabc..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/audio_annotator.cpython-39.pyc b/examples/__pycache__/audio_annotator.cpython-39.pyc deleted file mode 100644 index 0f9ffb0e90ac05dd2fee348ff8e2fdb2f470cb4e..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/audio_annotator.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/button.cpython-39.pyc b/examples/__pycache__/button.cpython-39.pyc deleted file mode 100644 index 4830e00c0942b7593b5df0c9a821d845fa120dfc..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/button.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/chatbot.cpython-38.pyc b/examples/__pycache__/chatbot.cpython-38.pyc deleted file mode 100644 index 52c9f50a706aac0ad30e14e6b7e5e4844b88d00a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/chatbot.cpython-38.pyc and /dev/null differ diff --git a/examples/__pycache__/chatbot.cpython-39.pyc b/examples/__pycache__/chatbot.cpython-39.pyc deleted file mode 100644 index 14e1612709ad07f01a325347bf66056ab057b405..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/chatbot.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/chatbot_stream.cpython-39.pyc b/examples/__pycache__/chatbot_stream.cpython-39.pyc deleted file mode 100644 index 8647ed19a6d8836be33de1b8eff749618447cb39..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/chatbot_stream.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/choice_group.cpython-39.pyc b/examples/__pycache__/choice_group.cpython-39.pyc deleted file mode 100644 index e4539373f2cfe7c3d703b0c35435e62bdaef4b7e..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/choice_group.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/color_picker.cpython-39.pyc b/examples/__pycache__/color_picker.cpython-39.pyc deleted file mode 100644 index 6688d15dbb1c4cc67b3a8df3426676f80bfaab11..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/color_picker.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/combobox.cpython-37.pyc b/examples/__pycache__/combobox.cpython-37.pyc deleted file mode 100644 index a82e1bbd5dae2c29b377f2f720ba0ae913f08c48..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/combobox.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/combobox.cpython-39.pyc b/examples/__pycache__/combobox.cpython-39.pyc deleted file mode 100644 index 46df061462ec3f06f276a91f5fb5e9c5edc5401f..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/combobox.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/copyable_text.cpython-39.pyc b/examples/__pycache__/copyable_text.cpython-39.pyc deleted file mode 100644 index 5f3e0923f95cc52181ac81c0d1d07721a82b2ceb..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/copyable_text.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/counter_broadcast.cpython-39.pyc b/examples/__pycache__/counter_broadcast.cpython-39.pyc deleted file mode 100644 index 821a771fdbc8d4baf876ac49cf7a93700dd26287..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/counter_broadcast.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/counter_multicast.cpython-39.pyc b/examples/__pycache__/counter_multicast.cpython-39.pyc deleted file mode 100644 index a372e3c645a976540739ec87dabf52f9ff051914..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/counter_multicast.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/counter_unicast.cpython-39.pyc b/examples/__pycache__/counter_unicast.cpython-39.pyc deleted file mode 100644 index 69bbe09a7d7967a8d75cb9445c94d33a0dce4021..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/counter_unicast.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/demo.cpython-39.pyc b/examples/__pycache__/demo.cpython-39.pyc deleted file mode 100644 index b91bdcfdb4ed0c2b1ed4040acfd2043167ca469a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/demo.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/dropdown.cpython-37.pyc b/examples/__pycache__/dropdown.cpython-37.pyc deleted file mode 100644 index 6a790f322358ef17e7060ef91814bd980224635d..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/dropdown.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/dropdown.cpython-39.pyc b/examples/__pycache__/dropdown.cpython-39.pyc deleted file mode 100644 index 733e75eb2088b6d244ab9a7b3f6fee6cfc07391c..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/dropdown.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/file_stream.cpython-39.pyc b/examples/__pycache__/file_stream.cpython-39.pyc deleted file mode 100644 index 3c0285d42b0697a32e98c01715e5194a69046dc9..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/file_stream.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/file_upload.cpython-37.pyc b/examples/__pycache__/file_upload.cpython-37.pyc deleted file mode 100644 index 1299c15cd7f8aa947c823334db24d7ade8da96df..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/file_upload.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/file_upload.cpython-39.pyc b/examples/__pycache__/file_upload.cpython-39.pyc deleted file mode 100644 index 1fd7cdc5f288e8be9e551037b283bd336f6631d0..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/file_upload.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/file_upload_compact.cpython-39.pyc b/examples/__pycache__/file_upload_compact.cpython-39.pyc deleted file mode 100644 index bf78bbfdb13f28e4b17f3e1ea2508b43e7001da3..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/file_upload_compact.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/form_menu.cpython-39.pyc b/examples/__pycache__/form_menu.cpython-39.pyc deleted file mode 100644 index cdd947556c04a2b723d9909b5124e8c59b8330c3..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/form_menu.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/form_visibility.cpython-39.pyc b/examples/__pycache__/form_visibility.cpython-39.pyc deleted file mode 100644 index 228cb6d3d88f2cdaf71884517140beb75e42c77a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/form_visibility.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/graphics_path.cpython-39.pyc b/examples/__pycache__/graphics_path.cpython-39.pyc deleted file mode 100644 index 1375e7384c528c903716af55962b7851563b6031..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/graphics_path.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/hash_routing.cpython-39.pyc b/examples/__pycache__/hash_routing.cpython-39.pyc deleted file mode 100644 index 9e74d50e32935eef932c4685fa66ea6482151a14..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/hash_routing.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/header.cpython-39.pyc b/examples/__pycache__/header.cpython-39.pyc deleted file mode 100644 index 0edab5f657efacd58fc5414bcf2d9d13757922a8..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/header.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/http_client.cpython-39.pyc b/examples/__pycache__/http_client.cpython-39.pyc deleted file mode 100644 index 898189481900b01cae45657bf968a997b72ca9ff..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/http_client.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/image_annotator.cpython-37.pyc b/examples/__pycache__/image_annotator.cpython-37.pyc deleted file mode 100644 index 540cc49efa9b60a6ac4d734851a3767974d9401a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/image_annotator.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/image_annotator.cpython-39.pyc b/examples/__pycache__/image_annotator.cpython-39.pyc deleted file mode 100644 index c931e02cc6355b810caabcab359244b612ed5627..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/image_annotator.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/image_popup.cpython-39.pyc b/examples/__pycache__/image_popup.cpython-39.pyc deleted file mode 100644 index 28f4a4f7cb50383a24c346739cd0602e02803dc9..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/image_popup.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/inline.cpython-39.pyc b/examples/__pycache__/inline.cpython-39.pyc deleted file mode 100644 index 634ef91c3d29ddae9129473652dae8d07aed25ba..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/inline.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/issue_tracker.cpython-39.pyc b/examples/__pycache__/issue_tracker.cpython-39.pyc deleted file mode 100644 index 2b61b19dfee97c8b60ec1e9b020956f13ff3e3fd..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/issue_tracker.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/link.cpython-39.pyc b/examples/__pycache__/link.cpython-39.pyc deleted file mode 100644 index 40fb2acfede3ccc09621c77ed329281ae61f26d0..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/link.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/markdown_submit_text.cpython-39.pyc b/examples/__pycache__/markdown_submit_text.cpython-39.pyc deleted file mode 100644 index 447a1c3ea469419cd303866ad37d7840fa98979f..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/markdown_submit_text.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/menu.cpython-37.pyc b/examples/__pycache__/menu.cpython-37.pyc deleted file mode 100644 index 8c32837701e0a44b8d5bbaf5b0b6ab7bdd79ca9c..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/menu.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/menu.cpython-39.pyc b/examples/__pycache__/menu.cpython-39.pyc deleted file mode 100644 index 24a00e62cfac62852eb231c3c6a73f79b60bcc8a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/menu.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/meta_dialog.cpython-39.pyc b/examples/__pycache__/meta_dialog.cpython-39.pyc deleted file mode 100644 index b07fe337086c5214ad035f129f4350824366a20a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/meta_dialog.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/meta_dialog_closable.cpython-39.pyc b/examples/__pycache__/meta_dialog_closable.cpython-39.pyc deleted file mode 100644 index 926ad89df7bbbbdc03306a6e076eb40a965e27e2..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/meta_dialog_closable.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/meta_notification_bar.cpython-39.pyc b/examples/__pycache__/meta_notification_bar.cpython-39.pyc deleted file mode 100644 index c9a4a8d4d1bd8552a6566429e9f8e698540f30e5..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/meta_notification_bar.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/meta_notification_bar_closable.cpython-39.pyc b/examples/__pycache__/meta_notification_bar_closable.cpython-39.pyc deleted file mode 100644 index 5a60e5e279f00432bdecb78c31108a7acf67e090..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/meta_notification_bar_closable.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/meta_theme.cpython-39.pyc b/examples/__pycache__/meta_theme.cpython-39.pyc deleted file mode 100644 index bea88592bca23fe479cce1760157818225ec4c2e..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/meta_theme.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/nav.cpython-39.pyc b/examples/__pycache__/nav.cpython-39.pyc deleted file mode 100644 index 6da5a0ed9492a9ad9417e24d8047e56515bd1a13..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/nav.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/persona.cpython-39.pyc b/examples/__pycache__/persona.cpython-39.pyc deleted file mode 100644 index b966350f4cf8bd92da2f2563dc63f02f53c2afee..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/persona.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_app.cpython-39.pyc b/examples/__pycache__/plot_app.cpython-39.pyc deleted file mode 100644 index 1d382fe965dbebd482d12bf4ca5bee0f3ca703b3..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_app.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_events.cpython-39.pyc b/examples/__pycache__/plot_events.cpython-39.pyc deleted file mode 100644 index 22edbeb54e9e13e2442a9a0f21625636cffea41f..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_events.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_form.cpython-39.pyc b/examples/__pycache__/plot_form.cpython-39.pyc deleted file mode 100644 index 828f4ad6412b55f35c2fa120e608fa13a9f2b907..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_form.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_line.cpython-39.pyc b/examples/__pycache__/plot_line.cpython-39.pyc deleted file mode 100644 index a355dd0c36d5ab052752c9cdf658c07b2d84d57b..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_line.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_matplotlib.cpython-37.pyc b/examples/__pycache__/plot_matplotlib.cpython-37.pyc deleted file mode 100644 index 2e3335f3431b06a99cfbf55f3efdec3cc91dece4..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_matplotlib.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_plotly.cpython-39.pyc b/examples/__pycache__/plot_plotly.cpython-39.pyc deleted file mode 100644 index 7d67da54e35dd23b25c25e70d1ca9ef2f77beb6a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_plotly.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_theme.cpython-39.pyc b/examples/__pycache__/plot_theme.cpython-39.pyc deleted file mode 100644 index 3ab7fe60fb47a5ec5cb296649d97f945dea82897..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_theme.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/plot_vegalite_update.cpython-39.pyc b/examples/__pycache__/plot_vegalite_update.cpython-39.pyc deleted file mode 100644 index 08537deab13c276cbe6a88096b2601b6b0b2eed9..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/plot_vegalite_update.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/pycon.cpython-39.pyc b/examples/__pycache__/pycon.cpython-39.pyc deleted file mode 100644 index 249d5d6e30468362eafcd972b74fa1be81abd4a3..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/pycon.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/site_async.cpython-39.pyc b/examples/__pycache__/site_async.cpython-39.pyc deleted file mode 100644 index d7437ce43c31af8d2b9858ebcc5ee5b75e315fb5..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/site_async.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/slider.cpython-39.pyc b/examples/__pycache__/slider.cpython-39.pyc deleted file mode 100644 index 2f8809e798e70f361eb73de97a482a86ad940dbf..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/slider.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/spinbox.cpython-39.pyc b/examples/__pycache__/spinbox.cpython-39.pyc deleted file mode 100644 index 5cb22952ff848de2c670132843da260b77c2d642..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/spinbox.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/stepper.cpython-37.pyc b/examples/__pycache__/stepper.cpython-37.pyc deleted file mode 100644 index df2d1c4d7ddce1be236bbb6bd730e7f48b2f406d..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/stepper.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/swatch_picker.cpython-39.pyc b/examples/__pycache__/swatch_picker.cpython-39.pyc deleted file mode 100644 index c71383f77ecc6307af0867ea68d600e3871d1dc5..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/swatch_picker.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/synth.cpython-37.pyc b/examples/__pycache__/synth.cpython-37.pyc deleted file mode 100644 index 7c11ee1231409eeaa9df315e50319e3d0aecaacf..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/synth.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/synth.cpython-39.pyc b/examples/__pycache__/synth.cpython-39.pyc deleted file mode 100644 index b9cc2d1b60b721c5213470ec0f765932c3d614c8..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/synth.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/tab.cpython-39.pyc b/examples/__pycache__/tab.cpython-39.pyc deleted file mode 100644 index a2bbe658e0152e38a2fdfeee593af541ae5eabcb..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/tab.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table.cpython-37.pyc b/examples/__pycache__/table.cpython-37.pyc deleted file mode 100644 index 15e4196178122a8081793b789db11737be7b1564..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/table.cpython-39.pyc b/examples/__pycache__/table.cpython-39.pyc deleted file mode 100644 index 577105b9c8560d455d82aa7c94656268b0bd4f8a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_column_alignment.cpython-39.pyc b/examples/__pycache__/table_column_alignment.cpython-39.pyc deleted file mode 100644 index 3c34bd15dd5d7726fb7132c51379ee19fa53a6c8..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_column_alignment.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_download.cpython-39.pyc b/examples/__pycache__/table_download.cpython-39.pyc deleted file mode 100644 index 33146e91c3656bfe4e2ba79215e5aa1ecd3cf8c5..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_download.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_filter.cpython-39.pyc b/examples/__pycache__/table_filter.cpython-39.pyc deleted file mode 100644 index a05c340e5b3cadd58223b49b25d2915316f24aac..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_filter.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_groupby.cpython-39.pyc b/examples/__pycache__/table_groupby.cpython-39.pyc deleted file mode 100644 index a17f99373d2bab65c80644a4abcb4052d0fa611b..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_groupby.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_markdown.cpython-37.pyc b/examples/__pycache__/table_markdown.cpython-37.pyc deleted file mode 100644 index b896623607da04c9bb1b219447f98f4b23ad14e0..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_markdown.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/table_markdown.cpython-39.pyc b/examples/__pycache__/table_markdown.cpython-39.pyc deleted file mode 100644 index 26715bdda9478d4bd7e65a36829d3e29312ccf3c..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_markdown.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_markdown_overflow.cpython-39.pyc b/examples/__pycache__/table_markdown_overflow.cpython-39.pyc deleted file mode 100644 index 5fa06e38f573a3ffb63b80e2cb81bfdf1689c138..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_markdown_overflow.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_menu.cpython-39.pyc b/examples/__pycache__/table_menu.cpython-39.pyc deleted file mode 100644 index 762a7b03a2c4fa2ebf71c56a39b11eddb7a7bcac..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_menu.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_pagination.cpython-39.pyc b/examples/__pycache__/table_pagination.cpython-39.pyc deleted file mode 100644 index ca9555c861ffefa80c0ea09ca9944acb27e1b25c..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_pagination.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_pagination_download.cpython-39.pyc b/examples/__pycache__/table_pagination_download.cpython-39.pyc deleted file mode 100644 index 2cdad0136ee8a53c4c3a744da75b216b403063d2..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_pagination_download.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_pagination_groups.cpython-39.pyc b/examples/__pycache__/table_pagination_groups.cpython-39.pyc deleted file mode 100644 index a53939c172b98672484c63bea06760047cbb2e26..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_pagination_groups.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_pagination_wavedb.cpython-37.pyc b/examples/__pycache__/table_pagination_wavedb.cpython-37.pyc deleted file mode 100644 index f22d71fdba8c08381bac22ee5c6fcf2ee1eb4a65..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_pagination_wavedb.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/table_search.cpython-39.pyc b/examples/__pycache__/table_search.cpython-39.pyc deleted file mode 100644 index a5a896df9caed1d23d01e4318a511fbe02c44c5c..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_search.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_search_groupable.cpython-39.pyc b/examples/__pycache__/table_search_groupable.cpython-39.pyc deleted file mode 100644 index db19233fd156d48120a36b95589f7cf225fc4292..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_search_groupable.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_select_single.cpython-39.pyc b/examples/__pycache__/table_select_single.cpython-39.pyc deleted file mode 100644 index 4976ddc53030ba2354cd3508ba74a45e58c2401f..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_select_single.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/table_tags.cpython-39.pyc b/examples/__pycache__/table_tags.cpython-39.pyc deleted file mode 100644 index e08be0bfc225098a63acded5a7f40443b29fb6a9..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/table_tags.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/tabs.cpython-39.pyc b/examples/__pycache__/tabs.cpython-39.pyc deleted file mode 100644 index 6dd7dfc34131180dd3067a30e876c7fc6604ebca..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/tabs.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/test.cpython-39.pyc b/examples/__pycache__/test.cpython-39.pyc deleted file mode 100644 index db99fdcccb837d4bed4faa3e3d3e5d8ad9326638..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/test.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/text_annotator.cpython-37.pyc b/examples/__pycache__/text_annotator.cpython-37.pyc deleted file mode 100644 index 56bfaa9ab4f5f3e5444d34e04f8816fb2eabb9e3..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/text_annotator.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/text_annotator.cpython-39.pyc b/examples/__pycache__/text_annotator.cpython-39.pyc deleted file mode 100644 index da6eb77cef27a8c97f1342501b8bc1d586a2f679..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/text_annotator.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/textbox.cpython-37.pyc b/examples/__pycache__/textbox.cpython-37.pyc deleted file mode 100644 index 6959599451913042c748b53d65f4e7a9c87003ba..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/textbox.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/textbox.cpython-39.pyc b/examples/__pycache__/textbox.cpython-39.pyc deleted file mode 100644 index b3951b0620e676a962b5a5c6e5b6862891793cbf..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/textbox.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/theme_generator.cpython-39.pyc b/examples/__pycache__/theme_generator.cpython-39.pyc deleted file mode 100644 index 15f8de1539ac315a0ddb2975cfac135eb18b7f75..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/theme_generator.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/time_picker.cpython-37.pyc b/examples/__pycache__/time_picker.cpython-37.pyc deleted file mode 100644 index dfd7020b5d0e16d9b8ab14676a8d73d6531c5d63..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/time_picker.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/time_picker.cpython-39.pyc b/examples/__pycache__/time_picker.cpython-39.pyc deleted file mode 100644 index b1e325388d2675f6d0fac14d7ff13342dd5059f7..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/time_picker.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/todo.cpython-39.pyc b/examples/__pycache__/todo.cpython-39.pyc deleted file mode 100644 index 38865d8038d74c3530942b25bf4c45a29fa69a24..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/todo.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/token.cpython-39.pyc b/examples/__pycache__/token.cpython-39.pyc deleted file mode 100644 index c6d148bfa86a58a46c6f12f29b06eb4a32575754..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/token.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/tour.cpython-37.pyc b/examples/__pycache__/tour.cpython-37.pyc deleted file mode 100644 index c656205eb289b665b6fa926234cc7b01c6ad4655..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/tour.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/tour.cpython-39.pyc b/examples/__pycache__/tour.cpython-39.pyc deleted file mode 100644 index fb0464c22f7dd3f6e5d39bacd3f5a83c08d6ffd4..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/tour.cpython-39.pyc and /dev/null differ diff --git a/examples/__pycache__/wizard.cpython-37.pyc b/examples/__pycache__/wizard.cpython-37.pyc deleted file mode 100644 index cafce89d7ae7ae65b2333864e32d447d907c6174..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/wizard.cpython-37.pyc and /dev/null differ diff --git a/examples/__pycache__/wizard.cpython-38.pyc b/examples/__pycache__/wizard.cpython-38.pyc deleted file mode 100644 index 8e53355c0af89cbb0e8c5901d923669398707f9a..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/wizard.cpython-38.pyc and /dev/null differ diff --git a/examples/__pycache__/wizard.cpython-39.pyc b/examples/__pycache__/wizard.cpython-39.pyc deleted file mode 100644 index 18f717a176f5a15a074f7ed99d0d7ee9440cd736..0000000000000000000000000000000000000000 Binary files a/examples/__pycache__/wizard.cpython-39.pyc and /dev/null differ diff --git a/examples/background_progress.py b/examples/background_progress.py index f418828ca442257efaf2d2f8878ba6ad3dfba229..f4afb75bcfc416c0e5470a467b57808058f570a3 100644 --- a/examples/background_progress.py +++ b/examples/background_progress.py @@ -2,54 +2,70 @@ # Execute background functions while incrementing a #progress bar. # #background_tasks # --- -import time import asyncio +import time import concurrent.futures +from threading import Event from h2o_wave import main, app, Q, ui -# A long-running that performs a blocking operation, in this case time.sleep() -def blocking_function(secs) -> str: - time.sleep(secs) # Blocks! - return 'Download completed!' +# This takes a lot of time (compute heavy). +def blocking_function(q: Q, loop: asyncio.AbstractEventLoop): + count = 0 + total = 10 + future = None + while count < total: + # Check if cancelled. + if q.client.event.is_set(): + asyncio.ensure_future(show_cancel(q), loop=loop) + return + # This blocks the main thread and prevents any other execution. + # This would be the compute in the real world. + time.sleep(1) + count += 1 + # If future is not done yet, skip the update to keep the correct order. + if not future or future.done(): + # Assume you are able to emit some kind of progress. + future = asyncio.ensure_future(update_ui(q, count / total), loop=loop) + +async def show_cancel(q: Q): + q.page['form'].progress.caption = 'Cancelled' + await q.page.save() -# An async function that displays a progress bar -async def display_progress_bar(q: Q, form, seconds: int): - for i in range(seconds): - progress_value = (i + 1.0) / seconds - form.items = [ - ui.progress( - label='Downloading the interwebs...', - caption=f'{int(progress_value * 100)}%', - value=progress_value, - ) - ] - await q.page.save() - await q.sleep(1) + +async def update_ui(q: Q, value: int): + q.page['form'].progress.value = value + await q.page.save() @app('/demo') async def serve(q: Q): - if q.args.start: # The button named 'start' was clicked - seconds = 5 - - # Grab a reference to the form - form = q.page['form'] + # Unimportant, draw initial UI. + if not q.client.initialized: + q.page['form'] = ui.form_card(box='1 1 3 2', items=[ + ui.inline([ + ui.button(name='start_job', label='Start job'), + ui.button(name='cancel', label='Cancel') + ]), + ui.progress(name='progress', label='Progress', value=0), + ]) + q.client.initialized = True - # Start incrementing the progress bar in the background - future = asyncio.ensure_future(display_progress_bar(q, form, seconds)) + # Handle start job button click. + if q.args.start_job: + # Do not run like this - will block the whole thread - freeze the app. + # blocking_function(q, loop) - # Execute our long-running function in the background + # Get the current event loop - will be used for + # running async functions within the blocking. + loop = asyncio.get_event_loop() + # Create an event to use for cancellation. + q.client.event = Event() with concurrent.futures.ThreadPoolExecutor() as pool: - message = await q.exec(pool, blocking_function, seconds) + await q.exec(pool, blocking_function, q, loop) - # Stop the progress bar (optional unless we used a infinite while loop in display_progress_bar()). - future.cancel() + if q.args.cancel: + q.client.event.set() - # At this point, we're done with the long-running function; so display a completion message - form.items = [ui.message_bar('info', message)] - await q.page.save() - else: - q.page['form'] = ui.form_card(box='1 1 2 2', items=[ui.button(name='start', label='Start')]) - await q.page.save() + await q.page.save() diff --git a/examples/chatbot.py b/examples/chatbot.py index 23fdb5e0584c41668ff3ee51f9d0cb607ecbf538..ae83225a39359c3a1707d62ae34c620f100807c0 100644 --- a/examples/chatbot.py +++ b/examples/chatbot.py @@ -1,27 +1,22 @@ # Chatbot -# Use this card for chat interactions. -# #chat +# Use this card for chatbot interactions. +# #chatbot # --- from h2o_wave import main, app, Q, ui, data -MAX_MESSAGES = 500 - - @app('/demo') async def serve(q: Q): if not q.client.initialized: - # Cyclic buffer drops oldest messages when full. Must have exactly 2 columns - msg and fromUser. - cyclic_buffer = data(fields='msg fromUser', size=-MAX_MESSAGES) - q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=cyclic_buffer, name='chatbot') - q.page['meta'] = ui.meta_card(box='', theme='h2o-dark') + # List buffer is a dynamic array. Cyclic buffer can also be used. Must have exactly 2 fields - content and from_user. + q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=data('content from_user', t='list'), name='chatbot') q.client.initialized = True # A new message arrived. if q.args.chatbot: # Append user message. - q.page['example'].data[-1] = [q.args.chatbot, True] + q.page['example'].data += [q.args.chatbot, True] # Append bot response. - q.page['example'].data[-1] = ['I am a fake chatbot. Sorry, I cannot help you.', False] + q.page['example'].data += ['I am a fake chatbot. Sorry, I cannot help you.', False] await q.page.save() diff --git a/examples/chatbot_events_scroll.py b/examples/chatbot_events_scroll.py new file mode 100644 index 0000000000000000000000000000000000000000..5da4ca2af1880f0ac263da87ab4a636328ede1d8 --- /dev/null +++ b/examples/chatbot_events_scroll.py @@ -0,0 +1,51 @@ +# Chatbot / Events/ Scroll +# Infinite scroll for previous messages. +# #chatbot #infinite #scroll +# --- +from h2o_wave import main, app, Q, ui, data + +prev_messages = [{'content': f'Message {i}', 'from_user': i % 2 == 0} for i in range(100)] +LOAD_SIZE = 10 + + +@app('/demo') +async def serve(q: Q): + if not q.client.initialized: + q.client.current_load_page = len(prev_messages) + # Use list buffer to allow easy streaming. Must have exactly 2 fields - msg and from_user. + q.page['example'] = ui.chatbot_card( + box='1 1 5 5', + data=data(fields='content from_user', t='list', rows=[ + ['Hello', True], + ['Hi', False], + ['Hello', True], + ['Hi', False], + ['Hello', True], + ['Hi', False], + ['Hello', True], + ['Hi', False], + ]), + events=['scroll_up'], + name='chatbot' + ) + q.client.initialized = True + + # A new message arrived. + if q.args.chatbot: + # Append user message. + q.page['example'].data += [q.args.chatbot, True] + # Append bot response. + q.page['example'].data += ['I am a fake chatbot. Sorry, I cannot help you.', False] + + # User scrolled up, load chat history. + if q.events.chatbot and q.events.chatbot.scroll_up: + if q.client.current_load_page == 0: + # If we reached the end, signal it to Wave by setting prev_items to empty list. + q.page['example'].prev_items = [] + else: + end = q.client.current_load_page - LOAD_SIZE + q.page['example'].prev_items = prev_messages[end:q.client.current_load_page] + q.client.current_load_page = end + await q.sleep(0.5) # Simulate network latency. + + await q.page.save() diff --git a/examples/chatbot_events_stop.py b/examples/chatbot_events_stop.py new file mode 100644 index 0000000000000000000000000000000000000000..9aef9639bd8048b3bba2b1d12fb013582f046715 --- /dev/null +++ b/examples/chatbot_events_stop.py @@ -0,0 +1,48 @@ +# Chatbot / Events/ Stop +# Use this card for chatbot interactions. Streaming can be stopped by the user. +# #chatbot #events #stop +# --- +from h2o_wave import main, app, Q, ui, data +import asyncio + + +async def stream_message(q): + stream = '' + q.page['example'].data += [stream, False] + # Show the "Stop generating" button + q.page['example'].generating = True + for w in 'I am a fake chatbot. Sorry, I cannot help you.'.split(): + await asyncio.sleep(0.3) + stream += w + ' ' + q.page['example'].data[-1] = [stream, False] + await q.page.save() + # Hide the "Stop generating" button + q.page['example'].generating = False + await q.page.save() + + +@app('/demo') +async def serve(q: Q): + if not q.client.initialized: + q.page['example'] = ui.chatbot_card( + box='1 1 5 5', + data=data(fields='content from_user', t='list'), + name='chatbot', + events=['stop'] + ) + q.client.initialized = True + + # Handle the stop event. + if q.events.chatbot and q.events.chatbot.stop: + # Cancel the streaming task. + q.client.task.cancel() + # Hide the "Stop generating" button. + q.page['example'].generating = False + # A new message arrived. + elif q.args.chatbot: + # Append user message. + q.page['example'].data += [q.args.chatbot, True] + # Run the streaming within cancelable asyncio task. + q.client.task = asyncio.create_task(stream_message(q)) + + await q.page.save() diff --git a/examples/chatbot_stream.py b/examples/chatbot_stream.py new file mode 100644 index 0000000000000000000000000000000000000000..8631e85f63a72cb2d37b39d0698067f6843361d7 --- /dev/null +++ b/examples/chatbot_stream.py @@ -0,0 +1,30 @@ +# Chatbot / Stream +# Use this card for chatbot interactions, supports text streaming. +# #chatbot #stream +# --- +from h2o_wave import main, app, Q, ui, data + + +@app('/demo') +async def serve(q: Q): + if not q.client.initialized: + # Use list buffer to allow easy streaming. Must have exactly 2 fields - content and from_user. + q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=data(fields='content from_user', t='list'), name='chatbot') + q.client.initialized = True + + # A new message arrived. + if q.args.chatbot: + # Append user message. + q.page['example'].data += [q.args.chatbot, True] + # Append bot response. + q.page['example'].data += ['', False] + + # Stream bot response. + stream = '' + for w in 'I am a fake chatbot. Sorry, I cannot help you.'.split(): + await q.sleep(0.1) + stream += w + ' ' + q.page['example'].data[-1] = [stream, False] + await q.page.save() + + await q.page.save() diff --git a/examples/component-snippets.json b/examples/component-snippets.json index 48d02540fb5a86b9c2d8db2bdad719b4ddd6fada..ca8f71532cb7c9778fb3f5267fd88c8b8b5d7014 100644 --- a/examples/component-snippets.json +++ b/examples/component-snippets.json @@ -62,10 +62,17 @@ ], "description": "Create a minimal Wave ChatCard." }, + "Wave ChatbotMessage": { + "prefix": "w_chatbot_message", + "body": [ + "ui.chatbot_message(content='$1', from_user=${2:False}),$0" + ], + "description": "Create a minimal Wave ChatbotMessage." + }, "Wave Chatbot": { "prefix": "w_chatbot", "body": [ - "ui.chatbot(name='$1', data=[\n\t\t$2\t\t\n]),$0" + "ui.chatbot(name='$1', data=$2),$0" ], "description": "Create a minimal Wave Chatbot." }, @@ -1000,17 +1007,24 @@ ], "description": "Create a full Wave ChatCard." }, + "Wave Full ChatbotMessage": { + "prefix": "w_full_chatbot_message", + "body": [ + "ui.chatbot_message(content='$1', from_user=${2:False}),$0" + ], + "description": "Create a full Wave ChatbotMessage." + }, "Wave Full Chatbot": { "prefix": "w_full_chatbot", "body": [ - "ui.chatbot(name='$1', placeholder='$2', data=[\n\t\t$3\t\t\n]),$0" + "ui.chatbot(name='$1', data=$2, placeholder='$3', generating=${4:False}, events=[\n\t\t$5\t\t\n], prev_items=[\n\t\t$6\t\t\n]),$0" ], "description": "Create a full Wave Chatbot." }, "Wave Full ChatbotCard": { "prefix": "w_full_chatbot_card", "body": [ - "ui.chatbot_card(box='$1', name='$2', data=$3, placeholder='$4', commands=[\n\t\t$5\t\t\n])$0" + "ui.chatbot_card(box='$1', name='$2', data=$3, placeholder='$4', generating=${5:False}, events=[\n\t\t$6\t\t\n], commands=[\n\t\t$7\t\t\n])$0" ], "description": "Create a full Wave ChatbotCard." }, @@ -1129,7 +1143,7 @@ "Wave Full Component": { "prefix": "w_full_component", "body": [ - "ui.component(text=${1:None}, text_xl=${2:None}, text_l=${3:None}, text_m=${4:None}, text_s=${5:None}, text_xs=${6:None}, label=${7:None}, separator=${8:None}, progress=${9:None}, message_bar=${10:None}, textbox=${11:None}, checkbox=${12:None}, toggle=${13:None}, choice_group=${14:None}, checklist=${15:None}, dropdown=${16:None}, combobox=${17:None}, slider=${18:None}, spinbox=${19:None}, date_picker=${20:None}, color_picker=${21:None}, button=${22:None}, buttons=${23:None}, mini_button=${24:None}, mini_buttons=${25:None}, file_upload=${26:None}, table=${27:None}, link=${28:None}, links=${29:None}, tabs=${30:None}, expander=${31:None}, frame=${32:None}, markup=${33:None}, template=${34:None}, picker=${35:None}, range_slider=${36:None}, stepper=${37:None}, visualization=${38:None}, vega_visualization=${39:None}, stats=${40:None}, inline=${41:None}, image=${42:None}, persona=${43:None}, text_annotator=${44:None}, image_annotator=${45:None}, facepile=${46:None}, copyable_text=${47:None}, menu=${48:None}, tags=${49:None}, time_picker=${50:None}, chatbot=${51:None}),$0" + "ui.component(text=${1:None}, text_xl=${2:None}, text_l=${3:None}, text_m=${4:None}, text_s=${5:None}, text_xs=${6:None}, label=${7:None}, separator=${8:None}, progress=${9:None}, message_bar=${10:None}, textbox=${11:None}, checkbox=${12:None}, toggle=${13:None}, choice_group=${14:None}, checklist=${15:None}, dropdown=${16:None}, combobox=${17:None}, slider=${18:None}, spinbox=${19:None}, date_picker=${20:None}, color_picker=${21:None}, button=${22:None}, buttons=${23:None}, mini_button=${24:None}, mini_buttons=${25:None}, file_upload=${26:None}, table=${27:None}, link=${28:None}, links=${29:None}, tabs=${30:None}, expander=${31:None}, frame=${32:None}, markup=${33:None}, template=${34:None}, picker=${35:None}, range_slider=${36:None}, stepper=${37:None}, visualization=${38:None}, vega_visualization=${39:None}, stats=${40:None}, inline=${41:None}, image=${42:None}, persona=${43:None}, text_annotator=${44:None}, image_annotator=${45:None}, facepile=${46:None}, copyable_text=${47:None}, menu=${48:None}, tags=${49:None}, time_picker=${50:None}),$0" ], "description": "Create a full Wave Component." }, @@ -1248,7 +1262,7 @@ "Wave Full ImageAnnotator": { "prefix": "w_full_image_annotator", "body": [ - "ui.image_annotator(name='$1', image='$2', title='$3', trigger=${4:False}, image_height='$5', tags=[\n\t\t$6\t\t\n], items=[\n\t\t$7\t\t\n], allowed_shapes=[\n\t\t$8\t\t\n]),$0" + "ui.image_annotator(name='$1', image='$2', title='$3', trigger=${4:False}, image_height='$5', tags=[\n\t\t$6\t\t\n], items=[\n\t\t$7\t\t\n], allowed_shapes=[\n\t\t$8\t\t\n], events=[\n\t\t$9\t\t\n]),$0" ], "description": "Create a full Wave ImageAnnotator." }, diff --git a/examples/image_annotator_events_click.py b/examples/image_annotator_events_click.py new file mode 100644 index 0000000000000000000000000000000000000000..338977fa2ad60bde56880d4982868c98659f4a42 --- /dev/null +++ b/examples/image_annotator_events_click.py @@ -0,0 +1,43 @@ +# Form / Image Annotator / Events / Click +# Register the `click` #event to emit Wave event with cursor coordinates when the image is clicked. +# #form #annotator #image #events +# --- +from h2o_wave import main, app, Q, ui + +vehicle = {'x1': 657, 'y1': 273, 'x2': 848, 'y2': 440} + + +@app('/demo') +async def serve(q: Q): + if not q.client.initialized or q.args.back: + q.page['example'] = ui.form_card(box='1 1 5 8', items=[ + ui.image_annotator( + name='annotator', + title='Click on the car and see what happens!', + image='https://images.unsplash.com/photo-1535082623926-b39352a03fb7?auto=compress&cs=tinysrgb&w=1260&h=750&q=80', + image_height='450px', + allowed_shapes=['rect'], + tags=[ui.image_annotator_tag(name='v', label='Vehicle', color='$cyan')], + items=[], + events=['click'], + ), + ui.button(name='submit', label='Submit', primary=True) + ]) + q.client.initialized = True + if q.args.submit: + q.page['example'].items = [ + ui.text(f'annotator={q.args.annotator}'), + ui.button(name='back', label='Back', primary=True), + ] + if q.events.annotator and q.events.annotator.click: + x, y = q.events.annotator.click.values() + # If the click is within the vehicle's bounding box, show the vehicle. + if vehicle['x1'] <= x <= vehicle['x2'] and vehicle['y1'] <= y <= vehicle['y2']: + q.page['example'].annotator.items = [ + ui.image_annotator_item(shape=ui.image_annotator_rect(x1=657, y1=273, x2=848, y2=440), tag='v') + ] + q.page['example'].annotator.title = 'You got it!' + else: + q.page['example'].annotator.title = 'You missed the car!' + + await q.page.save() \ No newline at end of file diff --git a/examples/image_annotator_events_tool_change.py b/examples/image_annotator_events_tool_change.py new file mode 100644 index 0000000000000000000000000000000000000000..3a540a30a562c791a83f9fa9041145bb6d697664 --- /dev/null +++ b/examples/image_annotator_events_tool_change.py @@ -0,0 +1,34 @@ +# Form / Image Annotator / Events / Tool Change +# Register a `tool_change` #event to emit Wave event when the drawing function is changed. +# #form #annotator #image #events +# --- +from h2o_wave import main, app, Q, ui + + +@app('/demo') +async def serve(q: Q): + if not q.client.initialized: + q.client.initialized = True + q.page['example'] = ui.form_card(box='1 1 5 7', items=[ + ui.image_annotator( + name='annotator', + title='Drag to annotate', + image='https://images.unsplash.com/photo-1535082623926-b39352a03fb7?auto=compress&cs=tinysrgb&w=1260&h=750&q=80', + image_height='450px', + tags=[ + ui.image_annotator_tag(name='v', label='Vehicle', color='$cyan'), + ui.image_annotator_tag(name='a', label='Animal', color='$blue'), + ], + items=[], + events=['tool_change'] + ), + ]) + q.page['details'] = ui.markdown_card( + box='1 8 5 2', + title='Selected tool', + content='The active tool is "rect".', + ) + if q.events.annotator and q.events.annotator.tool_change: + q.page['details'].content = f'The active tool is "{q.events.annotator.tool_change}".' + + await q.page.save() \ No newline at end of file diff --git a/examples/layout_responsive.py b/examples/layout_responsive.py index b97e0f0becb57c79d05a5bd9e1d24bffdd4e081d..3ba8157b24b0cf848b0ca8d03d37f87af3f9fc6a 100644 --- a/examples/layout_responsive.py +++ b/examples/layout_responsive.py @@ -6,7 +6,7 @@ from h2o_wave import site, ui page = site['/demo'] page.drop() -content = '![Wave University](https://raw.githubusercontent.com/h2oai/wave/master/assets/brand/wave-university-wide.png)' +content = '![Wave University](https://raw.githubusercontent.com/h2oai/wave/main/assets/brand/wave-university-wide.png)' # The meta card's 'zones' attribute defines placeholder zones to lay out cards for different viewport sizes. # We define three layout schemes here. diff --git a/examples/markdown.py b/examples/markdown.py index 8ef9b34da15072226cb72f84fee55f3a652a0035..fd03d4b4792543e59fdd4fcacbf548ec37f5adc4 100644 --- a/examples/markdown.py +++ b/examples/markdown.py @@ -43,4 +43,26 @@ page['example'] = ui.markdown_card( title='I was made using markdown!', content=sample_markdown, ) +page['example1'] = ui.markdown_card( + box='4 1 3 10', + title='I was made using markdown!', + content=''' +```py +from h2o_wave import main, app, Q, ui + + +@app('/') +async def serve(q: Q): + # Display a Hello, world! message. + q.page['hello'] = ui.markdown_card( + box='1 1 4 4', + title='Hello', + content='Hello, world!' + ) + + await q.page`.save() +``` +''', +) + page.save() diff --git a/examples/markdown_code_theme.py b/examples/markdown_code_theme.py new file mode 100644 index 0000000000000000000000000000000000000000..e7d1f8384f153aaeaec57591da7691b15e6c0e98 --- /dev/null +++ b/examples/markdown_code_theme.py @@ -0,0 +1,32 @@ +# Markdown Code Theme +# Pick from more than 100 themes for your code blocks. +# --- +from h2o_wave import main, app, Q, ui + + +@app('/demo') +async def serve(q: Q): + q.page['meta'] = ui.meta_card( + box='', + stylesheets=[ui.stylesheet(path='https://highlightjs.org/static/demo/styles/atom-one-dark.css')] + ) + q.page['example'] = ui.markdown_card( + box='1 1 3 4', + title='I was made using markdown!', + content=''' +```py +from h2o_wave import main, app, Q, ui + + +@app('/') +async def serve(q: Q): + # Display a Hello, world! message. + q.page['hello'] = ui.markdown_card( + box='1 1 4 4', + title='Hello', + content='Hello, world!' + ) + + await q.page`.save() + ''') + await q.page.save() diff --git a/examples/meta_dialog.py b/examples/meta_dialog.py index 298f9afa01e7bf188a126fcd0ee72a30d48aead5..2d177517827ffcf19df86798cdb768ad7fef54ee 100644 --- a/examples/meta_dialog.py +++ b/examples/meta_dialog.py @@ -12,26 +12,26 @@ async def serve(q: Q): ui.button(name='show_dialog', label='Order donuts', primary=True) ]) q.client.initialized = True - else: - if q.args.show_dialog: - q.page['meta'].dialog = ui.dialog(title='Order Donuts', items=[ - ui.text('Donuts cost $1.99. Proceed?'), - ui.buttons([ui.button(name='next_step', label='Next', primary=True)]) + + if q.args.show_dialog: + q.page['meta'].dialog = ui.dialog(title='Order Donuts', items=[ + ui.text('Donuts cost $1.99. Proceed?'), + ui.buttons([ui.button(name='next_step', label='Next', primary=True)]) + ]) + elif q.args.next_step: + q.page['meta'].dialog.items = [ + ui.text('You will be charged $1.99. Proceed?'), + ui.buttons([ + ui.button(name='cancel', label='Back to safety'), + ui.button(name='submit', label='Place order', primary=True), ]) - elif q.args.next_step: - q.page['meta'].dialog.items = [ - ui.text('You will be charged $1.99. Proceed?'), - ui.buttons([ - ui.button(name='cancel', label='Back to safety'), - ui.button(name='submit', label='Place order', primary=True), - ]) - ] - elif q.args.submit: - q.page['example'].items = [ui.message_bar('success', 'Order placed!')] - q.page['meta'].dialog = None + ] + elif q.args.submit: + q.page['example'].items = [ui.message_bar('success', 'Order placed!')] + q.page['meta'].dialog = None - elif q.args.cancel: - q.page['example'].items = [ui.message_bar('info', 'Order canceled!')] - q.page['meta'].dialog = None + elif q.args.cancel: + q.page['example'].items = [ui.message_bar('info', 'Order canceled!')] + q.page['meta'].dialog = None await q.page.save() diff --git a/examples/meta_side_panel.py b/examples/meta_side_panel.py index 7c551f5cdac362d3d14c315fa621bb4b65ec905b..a1b627e82ed6eef921a33090ef6b36e4505c7f12 100644 --- a/examples/meta_side_panel.py +++ b/examples/meta_side_panel.py @@ -12,26 +12,26 @@ async def serve(q: Q): ui.button(name='show_side_panel', label='Order donuts', primary=True) ]) q.client.initialized = True - else: - if q.args.show_side_panel: - q.page['meta'].side_panel = ui.side_panel(title='Welcome to store', items=[ - ui.text('Donuts cost $1.99. Proceed?'), - ui.buttons([ui.button(name='next_step', label='Next', primary=True)]) + + if q.args.show_side_panel: + q.page['meta'].side_panel = ui.side_panel(title='Welcome to store', items=[ + ui.text('Donuts cost $1.99. Proceed?'), + ui.buttons([ui.button(name='next_step', label='Next', primary=True)]) + ]) + elif q.args.next_step: + q.page['meta'].side_panel.items = [ + ui.text('You will be charged $1.99. Proceed?'), + ui.buttons([ + ui.button(name='cancel', label='Back to safety'), + ui.button(name='submit', label='Place order', primary=True), ]) - elif q.args.next_step: - q.page['meta'].side_panel.items = [ - ui.text('You will be charged $1.99. Proceed?'), - ui.buttons([ - ui.button(name='cancel', label='Back to safety'), - ui.button(name='submit', label='Place order', primary=True), - ]) - ] - elif q.args.submit: - q.page['example'].items = [ui.message_bar('success', 'Order placed!')] - q.page['meta'].side_panel = None + ] + elif q.args.submit: + q.page['example'].items = [ui.message_bar('success', 'Order placed!')] + q.page['meta'].side_panel = None - elif q.args.cancel: - q.page['example'].items = [ui.message_bar('info', 'Order canceled!')] - q.page['meta'].side_panel = None + elif q.args.cancel: + q.page['example'].items = [ui.message_bar('info', 'Order canceled!')] + q.page['meta'].side_panel = None await q.page.save() diff --git a/examples/tour_autocomplete_parser.py b/examples/parser.py similarity index 100% rename from examples/tour_autocomplete_parser.py rename to examples/parser.py diff --git a/examples/requirements.txt b/examples/requirements.txt index e97dba6611f72964b4211ea326461c5bf9d604a2..db07d850c324713f13cf74f3239fee4b2117f359 100644 --- a/examples/requirements.txt +++ b/examples/requirements.txt @@ -8,7 +8,6 @@ numpy==1.22.2 opencv-python==4.5.5.64 pandas==1.3.5 plotly==5.7.0 -pygments==2.11.2 -scikit-learn==1.0.2 +scikit-learn==1.2.2 toml==0.10.2 vega-datasets==0.9.0 \ No newline at end of file diff --git a/examples/theme_generator.py b/examples/theme_generator.py index 9db081e701c3f5c56138987af400cbf331f8b9a1..51cd922cf7f1070b00837267c958cc5d2a6aa619 100644 --- a/examples/theme_generator.py +++ b/examples/theme_generator.py @@ -2,18 +2,10 @@ # Use theme generator to quickly generate custom color schemes for your app. # #theme_generator # --- -from typing import Tuple -from h2o_wave import main, app, Q, ui, data - -from pygments import highlight -from pygments.formatters.html import HtmlFormatter -from pygments.lexers import get_lexer_by_name -from pygments.style import Style -from pygments.token import Name, String, Operator, Punctuation - import math +from typing import Tuple -py_lexer = get_lexer_by_name('python') +from h2o_wave import Q, app, data, main, ui def to_grayscale(color: float) -> float: @@ -33,7 +25,7 @@ def hex_to_rgb(hex_color: str) -> Tuple[int, ...]: # Source: https://stackoverflow.com/questions/9733288/how-to-programmatically-calculate-the-contrast-ratio-between-two-colors. # noqa -def update_contrast_check(color1: str, color2: str, q:Q, min_contrast=4.5): +def update_contrast_check(color1: str, color2: str, q: Q, min_contrast=4.5): rgb1 = hex_to_rgb(q.client[color1].lstrip('#')) rgb2 = hex_to_rgb(q.client[color2].lstrip('#')) lum1 = get_luminance(rgb1[0], rgb1[1], rgb1[2]) @@ -41,16 +33,19 @@ def update_contrast_check(color1: str, color2: str, q:Q, min_contrast=4.5): brightest = max(lum1, lum2) darkest = min(lum1, lum2) contrast = (brightest + 0.05) / (darkest + 0.05) + message_bar_mobile = q.page['meta'][f'{color1}_{color2}'] + message_bar = q.page['form'][f'{color1}_{color2}'] if contrast < min_contrast: - q.page['form'][f'{color1}_{color2}'].type = 'error' - q.page['form'][f'{color1}_{color2}'].text = f'Improve contrast between **{color1}** and **{color2}**.' + message_bar.type = message_bar_mobile.type = 'error' + message_bar.text = message_bar_mobile.text = f'Improve contrast between **{color1}** and **{color2}**.' else: - q.page['form'][f'{color1}_{color2}'].type = 'success' - q.page['form'][f'{color1}_{color2}'].text = f'Contrast between **{color1}** and **{color2}** is great!' + message_bar.type = message_bar_mobile.type = 'success' + message_bar.text = message_bar_mobile.text = f'Contrast between **{color1}** and **{color2}** is great!' def get_theme_code(q: Q): - contents = f''' + return f''' +```py ui.theme( name='', primary='{q.client.primary}', @@ -58,29 +53,154 @@ ui.theme( card='{q.client.card}', page='{q.client.page}', ) +``` ''' - # Reference: http://svn.python.org/projects/external/Pygments-0.10/docs/build/styles.html - class CustomStyle(Style): - styles = { - Name: q.client.text, - Operator: q.client.text, - Punctuation: q.client.text, - String: q.client.primary - } - html_formatter = HtmlFormatter(full=True, style=CustomStyle, nobackground=True) - html = highlight(contents, py_lexer, html_formatter) - html = html.replace('

', '') - html = html.replace('', '') - html = html.replace('
', '
')
-    return html
+image = 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&h=750&w=1260'
+mobile_items = [
+    ui.text_xl(content='Sample App to show colors'),
+    ui.text('Click the top right button to change the theme. 👆'),
+    ui.inline(justify='between', items=[
+        ui.persona(title='John Doe', subtitle='Data Scientist', size='s', image=image),
+        ui.toggle(name='toggle', label='Toggle', value=True),
+    ]),
+    ui.inline([
+        ui.stepper(name='stepper', width='500px', items=[
+            ui.step(label='Step 1', icon='MailLowImportance'),
+            ui.step(label='Step 2', icon='TaskManagerMirrored'),
+            ui.step(label='Step 3', icon='Cafe'),
+        ]),
+    ]),
+    ui.progress(label='A progress bar'),
+    ui.inline(justify='between', items=[
+        ui.tabs(name='menu', value='email', items=[
+            ui.tab(name='email', label='Mail', icon='Mail'),
+            ui.tab(name='events', label='Events', icon='Calendar'),
+            ui.tab(name='spam', label='Spam'),
+        ]),
+        ui.link(label='Link'),
+    ]),
+    ui.slider(name='slider', label='Slider', value=70),
+    ui.date_picker(name='date_picker', label='Date picker'),
+    ui.picker(name='picker', label='Picker', choices=[
+        ui.choice('choice1', label='Choice 1'),
+        ui.choice('choice2', label='Choice 2'),
+        ui.choice('choice3', label='Choice 3'),
+    ]),
+    ui.combobox(name='combobox', label='Combobox', choices=['Choice 1', 'Choice 2', 'Choice 3']),
+    ui.checkbox(name='checkbox1', label='Checkbox 1', value=True),
+    ui.checkbox(name='checkbox2', label='Checkbox 2'),
+    ui.checkbox(name='checkbox3', label='Checkbox 3'),
+    ui.inline(direction='column', items=[
+        ui.table(
+            name='table',
+            width='100%',
+            columns=[
+                ui.table_column(name='name', label='Name', min_width='80px'),
+                ui.table_column(name='surname', label='Surname', filterable=True, max_width='90px'),
+                ui.table_column(name='progress', label='Progress', max_width='80px',
+                                cell_type=ui.progress_table_cell_type(color='$themePrimary')),
+            ],
+            rows=[
+                ui.table_row(name='row1', cells=['John', 'Doe', '0.90']),
+                ui.table_row(name='row2', cells=['Ann', 'Doe', '0.75']),
+            ],
+        ),
+        ui.visualization(
+            width='100%',
+            data=data('profession salary', 5, rows=[
+                ('medicine', 23000),
+                ('fire fighting', 18000),
+                ('pedagogy', 24000),
+                ('psychology', 22500),
+                ('computer science', 36000),
+            ], pack=True),
+            plot=ui.plot([ui.mark(type='interval', x='=profession', y='=salary', y_min=0)])
+        ),
+    ]),
+    ui.buttons([
+        ui.button(name='primary_button', label='Primary', primary=True),
+        ui.button(name='standard_button', label='Standard'),
+        ui.button(name='standard_disabled_button', label='Disabled', disabled=True),
+        ui.button(name='icon_button', icon='Heart', caption='Tooltip text'),
+    ]),
+]
+desktop_items = [
+    ui.text_xl(content='Sample App to show colors'),
+    ui.progress(label='A progress bar'),
+    ui.inline([
+        ui.checkbox(name='checkbox1', label='Checkbox 1', value=True),
+        ui.checkbox(name='checkbox2', label='Checkbox 2'),
+        ui.checkbox(name='checkbox3', label='Checkbox 3'),
+        ui.toggle(name='toggle', label='Toggle', value=True),
+    ]),
+    ui.inline([
+        ui.date_picker(name='date_picker', label='Date picker'),
+        ui.picker(name='picker', label='Picker', choices=[
+            ui.choice('choice1', label='Choice 1'),
+            ui.choice('choice2', label='Choice 2'),
+            ui.choice('choice3', label='Choice 3'),
+        ]),
+        ui.combobox(name='combobox', label='Combobox', choices=['Choice 1', 'Choice 2', 'Choice 3']),
+        ui.persona(title='John Doe', subtitle='Data Scientist', size='s', image=image),
+    ]),
+    ui.slider(name='slider', label='Slider', value=70),
+    ui.link(label='Link'),
+    ui.inline(justify='between', items=[
+        ui.stepper(name='stepper', width='500px', items=[
+            ui.step(label='Step 1', icon='MailLowImportance'),
+            ui.step(label='Step 2', icon='TaskManagerMirrored'),
+            ui.step(label='Step 3', icon='Cafe'),
+        ]),
+        ui.tabs(name='menu', value='email', items=[
+            ui.tab(name='email', label='Mail', icon='Mail'),
+            ui.tab(name='events', label='Events', icon='Calendar'),
+            ui.tab(name='spam', label='Spam'),
+        ]),
+    ]),
+    ui.inline(items=[
+        ui.table(
+            name='table',
+            width='50%',
+            columns=[
+                ui.table_column(name='name', label='Name', min_width='80px'),
+                ui.table_column(name='surname', label='Surname', filterable=True),
+                ui.table_column(name='age', label='Age', sortable=True, max_width='80px'),
+                ui.table_column(name='progress', label='Progress',
+                                cell_type=ui.progress_table_cell_type(color='$themePrimary')),
+            ],
+            rows=[
+                ui.table_row(name='row1', cells=['John', 'Doe', '25', '0.90']),
+                ui.table_row(name='row2', cells=['Ann', 'Doe', '35', '0.75']),
+                ui.table_row(name='row3', cells=['Casey', 'Smith', '40', '0.33']),
+            ],
+            height='330px',
+        ),
+        ui.visualization(
+            width='50%',
+            data=data('profession salary', 5, rows=[
+                ('medicine', 23000),
+                ('fire fighting', 18000),
+                ('pedagogy', 24000),
+                ('psychology', 22500),
+                ('computer science', 36000),
+            ], pack=True),
+            plot=ui.plot([ui.mark(type='interval', x='=profession', y='=salary', y_min=0)])
+        ),
+    ]),
+    ui.buttons([
+        ui.button(name='primary_button', label='Primary', primary=True),
+        ui.button(name='standard_button', label='Standard'),
+        ui.button(name='standard_disabled_button', label='Disabled', disabled=True),
+        ui.button(name='icon_button', icon='Heart', caption='Tooltip text'),
+    ]),
+]
 
 
 @app('/demo')
 async def serve(q: Q):
     if not q.client.initialized:
-        image = 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&h=750&w=1260'
         q.client.primary = '#000000'
         q.client.page = '#e2e2e2'
         q.client.card = '#ffffff'
@@ -88,106 +208,63 @@ async def serve(q: Q):
         q.page['meta'] = ui.meta_card(box='', theme='custom', layouts=[
             ui.layout(
                 breakpoint='xs',
+                zones=[
+                    ui.zone('mobile_content'),
+                    ui.zone('footer')
+                ]
+            ),
+            ui.layout(
+                breakpoint='817px',
                 zones=[
                     ui.zone('header'),
                     ui.zone('content', direction=ui.ZoneDirection.ROW, zones=[
-                        ui.zone('colors', size='400px'),
-                        ui.zone('preview')
+                        ui.zone('colors', size='30%'),
+                        ui.zone('preview', size='70%')
                     ]),
                     ui.zone('footer')
                 ]
-            )
+            ),
         ])
         q.page['header'] = ui.header_card(box='header', title='Theme generator', subtitle='Color your app easily',
                                           icon='Color', icon_color='$card')
-        q.page['form'] = ui.form_card(box='colors', items=[
-            ui.color_picker(name='primary', label='Primary', trigger=True, alpha=False, inline=True, value=q.client.primary),
+        q.page['mobile_header'] = ui.header_card(
+            box='mobile_content',
+            icon='Color',
+            title='Theme generator',
+            subtitle='Color your app easily',
+            items=[ui.button(name='show_side_panel', label=' ', icon='Color')]
+        )
+        q.client.color_items = [
+            ui.color_picker(name='primary', label='Primary', trigger=True, alpha=False, inline=True,
+                            value=q.client.primary),
             ui.color_picker(name='text', label='Text', trigger=True, alpha=False, inline=True, value=q.client.text),
             ui.color_picker(name='card', label='Card', trigger=True, alpha=False, inline=True, value=q.client.card),
             ui.color_picker(name='page', label='Page', trigger=True, alpha=False, inline=True, value=q.client.page),
             ui.text_xl('Check contrast'),
             ui.message_bar(name='text_card', type='success', text='Contrast between **text** and **card** is great!'),
-            ui.message_bar(name='card_primary', type='success', text='Contrast between **card** and **primary** is great!'),
+            ui.message_bar(name='card_primary', type='success',
+                           text='Contrast between **card** and **primary** is great!'),
             ui.message_bar(name='text_page', type='success', text='Contrast between **text** and **page** is great!'),
-            ui.message_bar(name='page_primary', type='success', text='Contrast between **page** and **primary** is great!'),
+            ui.message_bar(name='page_primary', type='success',
+                           text='Contrast between **page** and **primary** is great!'),
             ui.text_xl('Copy code'),
-            ui.frame(name='frame', content=get_theme_code(q), height='180px'),
-        ])
-        q.page['sample'] = ui.form_card(box='preview', items=[
-            ui.text_xl(content='Sample App to show colors'),
-            ui.progress(label='A progress bar'),
-            ui.inline([
-                ui.checkbox(name='checkbox1', label='A checkbox', value=True),
-                ui.checkbox(name='checkbox2', label='Another checkbox'),
-                ui.checkbox(name='checkbox3', label='Yet another checkbox'),
-                ui.toggle(name='toggle', label='Toggle', value=True),
-            ]),
-            ui.inline([
-                ui.date_picker(name='date_picker', label='Date picker'),
-                ui.picker(name='picker', label='Picker', choices=[
-                    ui.choice('choice1', label='Choice 1'),
-                    ui.choice('choice2', label='Choice 2'),
-                    ui.choice('choice3', label='Choice 3'),
-                ]),
-                ui.combobox(name='combobox', label='Combobox', choices=['Choice 1', 'Choice 2', 'Choice 3']),
-                ui.persona(title='John Doe', subtitle='Data Scientist', size='s', image=image),
-            ]),
-            ui.slider(name='slider', label='Slider', value=70),
-            ui.link(label='Link'),
-            ui.inline(justify='between', items=[
-                ui.stepper(name='stepper', width='500px', items=[
-                    ui.step(label='Step 1', icon='MailLowImportance'),
-                    ui.step(label='Step 2', icon='TaskManagerMirrored'),
-                    ui.step(label='Step 3', icon='Cafe'),
-                ]),
-                ui.tabs(name='menu', value='email', items=[
-                    ui.tab(name='email', label='Mail', icon='Mail'),
-                    ui.tab(name='events', label='Events', icon='Calendar'),
-                    ui.tab(name='spam', label='Spam'),
-                ]),
-            ]),
-            ui.inline(items=[
-                ui.table(
-                    name='table',
-                    width='50%',
-                    columns=[
-                        ui.table_column(name='name', label='Name', min_width='80px'),
-                        ui.table_column(name='surname', label='Surname', filterable=True),
-                        ui.table_column(name='age', label='Age', sortable=True, max_width='80px'),
-                        ui.table_column(name='progress', label='Progress',
-                                        cell_type=ui.progress_table_cell_type(color='$themePrimary')),
-                    ],
-                    rows=[
-                        ui.table_row(name='row1', cells=['John', 'Doe', '25', '0.90']),
-                        ui.table_row(name='row2', cells=['Ann', 'Doe', '35', '0.75']),
-                        ui.table_row(name='row3', cells=['Casey', 'Smith', '40', '0.33']),
-                    ],
-                    height='330px',
-                ),
-                ui.visualization(
-                    width='50%',
-                    data=data('profession salary', 5, rows=[
-                        ('medicine', 23000),
-                        ('fire fighting', 18000),
-                        ('pedagogy', 24000),
-                        ('psychology', 22500),
-                        ('computer science', 36000),
-                    ], pack=True),
-                    plot=ui.plot([ui.mark(type='interval', x='=profession', y='=salary', y_min=0)])
-                ),
-            ]),
-            ui.buttons([
-                ui.button(name='primary_button', label='Primary', primary=True),
-                ui.button(name='standard_button', label='Standard'),
-                ui.button(name='standard_disabled_button', label='Disabled', disabled=True),
-                ui.button(name='icon_button', icon='Heart', caption='Tooltip text'),
-            ]),
-        ])
-        q.page['footer'] = ui.footer_card(box='footer', caption='(c) 2021 H2O.ai. All rights reserved.')
+            ui.text(name='code', content=get_theme_code(q)),
+        ]
+        q.page['form'] = ui.form_card(box='colors', items=q.client.color_items)
+        q.page['sample'] = ui.form_card(box='preview', items=desktop_items)
+        q.page['sample_mobile'] = ui.form_card(box='mobile_content', items=mobile_items)
+        q.page['footer'] = ui.footer_card(box='footer', caption='Made with 💛 by H2O Wave Team.')
         q.client.themes = [ui.theme(name='custom', text=q.client.text, card=q.client.card,
                                     page=q.client.page, primary=q.client.primary)]
         q.client.initialized = True
 
+    if q.args.show_side_panel:
+        q.page['meta'].side_panel = ui.side_panel(
+            title='Adjust theme colors',
+            items=q.client.color_items,
+            closable=True,
+            width='min(75%, 420px)'
+        )
     if q.args.primary:
         q.client.themes[0].primary = q.args.primary
         q.client.primary = q.args.primary
@@ -206,5 +283,5 @@ async def serve(q: Q):
     update_contrast_check('card', 'primary', q)
     update_contrast_check('text', 'page', q)
     update_contrast_check('page', 'primary', q)
-    q.page['form'].frame.content = get_theme_code(q)
+    q.page['form'].code.content = get_theme_code(q)
     await q.page.save()
diff --git a/examples/tour.conf b/examples/tour.conf
index fdd427b07b8ae05c156844b570e936896408d4bd..9646640fde5d9192d46ed4acf5e43980c5bc30da 100644
--- a/examples/tour.conf
+++ b/examples/tour.conf
@@ -32,6 +32,9 @@ layout.py
 layout_size.py
 layout_responsive.py
 chatbot.py
+chatbot_stream.py
+chatbot_events_stop.py
+chatbot_events_scroll.py
 form.py
 form_visibility.py
 text.py
@@ -106,6 +109,8 @@ tags.py
 image.py
 image_popup.py
 image_annotator.py
+image_annotator_events_click.py
+image_annotator_events_tool_change.py
 inline.py
 file_stream.py
 frame.py
@@ -116,6 +121,7 @@ markdown.py
 markdown_table.py
 markdown_data.py
 markdown_submit_text.py
+markdown_code_theme.py
 markup.py
 nav.py
 toolbar.py
diff --git a/examples/tour.py b/examples/tour.py
index 24b55851940c7760d5a2a283c8b2bf455b4788b4..2e3267be00ea8f67d2735b9092930881e84ff6a2 100644
--- a/examples/tour.py
+++ b/examples/tour.py
@@ -4,16 +4,17 @@ import os.path
 import re
 import shutil
 import socket
-import subprocess
 import sys
 import uuid
+from asyncio.subprocess import Process
+from asyncio import create_subprocess_exec
 from contextlib import closing
 from pathlib import Path
 from string import Template
 from typing import Dict, List, Optional, Tuple
 from urllib.parse import urlparse
 
-from h2o_wave import Q, app, main, ui
+from h2o_wave import Q, app, handle_on, main, on, ui
 
 example_dir = os.path.dirname(os.path.realpath(__file__))
 tour_tmp_dir = os.path.join(example_dir, '_tour_apps_tmp')
@@ -26,6 +27,9 @@ vsc_extension_path = os.path.join(example_dir, '..', '..', 'tools', 'vscode-exte
 
 def scan_free_port(port: int):
     while True:
+        # If we run out of ports, wrap around.
+        if port > 60000:
+            port = 10000
         with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
             if sock.connect_ex(('localhost', port)):
                 return port
@@ -41,9 +45,9 @@ class Example:
         self.source = source
         self.previous_example: Optional[Example] = None
         self.next_example: Optional[Example] = None
-        self.process: Optional[subprocess.Popen] = None
+        self.process: Optional[Process] = None
 
-    def start(self, filename: str, is_app: bool, q: Q):
+    async def start(self, filename: str, is_app: bool, q: Q):
         env = os.environ.copy()
         env['H2O_WAVE_BASE_URL'] = _base_url
         env['H2O_WAVE_ADDRESS'] = os.environ.get('H2O_WAVE_ADDRESS', 'http://127.0.0.1:10101')
@@ -54,19 +58,20 @@ class Example:
         if is_app:
             q.app.app_port = scan_free_port(q.app.app_port)
             env['H2O_WAVE_APP_ADDRESS'] = f'http://{_app_address.hostname}:{q.app.app_port}'
-            self.process = subprocess.Popen([
+            cmd = [
                 sys.executable, '-m', 'uvicorn',
                 '--host', '0.0.0.0',
                 '--port', str(q.app.app_port),
                 f'examples.{filename.replace(".py", "")}:main',
-            ], env=env)
+            ]
+            self.process = await create_subprocess_exec(*cmd, env=env)
         else:
-            self.process = subprocess.Popen([sys.executable, os.path.join(example_dir, filename)], env=env)
+            self.process = await create_subprocess_exec(sys.executable, os.path.join(example_dir, filename), env=env)
 
-    def stop(self):
+    async def stop(self):
         if self.process and self.process.returncode is None:
             self.process.terminate()
-            self.process.wait()
+            await self.process.wait()
 
 
 def read_lines(p: str) -> List[str]:
@@ -183,12 +188,18 @@ async def setup_page(q: Q):
     py_content = ''
     parser_path = os.path.join(example_dir, 'tour_autocomplete_parser.py')
     utils_path = os.path.join(example_dir, 'tour_autocomplete_utils.py')
-
+    # In prod.
     if os.path.exists(parser_path) and os.path.exists(utils_path):
         with open(parser_path, 'r') as f:
             py_content = f.read()
         with open(utils_path, 'r') as f:
             py_content += f.read()
+    # When run in development from Wave repo.
+    elif os.path.exists(vsc_extension_path):
+        with open(os.path.join(vsc_extension_path, 'server', 'parser.py'), 'r') as f:
+            py_content = f.read()
+        with open(os.path.join(vsc_extension_path, 'server', 'utils.py'), 'r') as f:
+            py_content += f.read()
 
     if py_content:
         py_content += '''
@@ -307,7 +318,7 @@ async def show_example(q: Q, example: Example):
     filename = os.path.join(tour_tmp_dir, f'{q.client.path}.py')
     code = q.events.editor.change if q.events.editor else example.source
     code = code.replace("`", "\\`")
-    is_app = code.find('@app(') > 0
+    is_app = '@app(' in code
     with open(filename, 'w') as f:
         fixed_path = code
         if is_app:
@@ -321,10 +332,10 @@ async def show_example(q: Q, example: Example):
     # Stop active example, if any.
     active_example = q.client.active_example
     if active_example:
-        active_example.stop()
+        await active_example.stop()
 
     # Start new example
-    example.start(filename, is_app, q)
+    await example.start(filename, is_app, q)
     q.client.active_example = example
 
     # Update example blurb
@@ -370,6 +381,16 @@ async def on_shutdown():
         shutil.rmtree(dirpath)
 
 
+@on("@system.client_disconnect")
+async def client_disconnect(q: Q):
+    demo_page = q.site[f'/{q.client.path}']
+    demo_page.drop()
+    await demo_page.save()
+
+    if q.client.active_example:
+        await q.client.active_example.stop()
+
+
 @app('/', on_startup=on_startup, on_shutdown=on_shutdown)
 async def serve(q: Q):
     if not q.app.initialized:
@@ -377,8 +398,15 @@ async def serve(q: Q):
         q.app.tour_assets, = await q.site.upload_dir(os.path.join(example_dir, 'tour-assets'))
         base_snippets_path = os.path.join(example_dir, 'base-snippets.json')
         component_snippets_path = os.path.join(example_dir, 'component-snippets.json')
+        # Prod.
         if os.path.exists(base_snippets_path) and os.path.exists(component_snippets_path):
             q.app.snippets1, q.app.snippets2, = await q.site.upload([base_snippets_path, component_snippets_path])
+        # When run in development from Wave repo.
+        elif os.path.exists(vsc_extension_path):
+            q.app.snippets1, q.app.snippets2, = await q.site.upload([
+                os.path.join(vsc_extension_path, 'base-snippets.json'),
+                os.path.join(vsc_extension_path, 'component-snippets.json')
+            ])
         q.app.initialized = True
     if not q.client.initialized:
         q.client.initialized = True
@@ -386,6 +414,8 @@ async def serve(q: Q):
         q.client.path = uuid.uuid4()
         await setup_page(q)
 
+    await handle_on(q)
+
     search = q.args[q.args['#'] or default_example_name]
     if search and not q.events.editor:
         q.page['meta'] = ui.meta_card(box='', redirect=f'#{search}')
@@ -395,10 +425,9 @@ async def serve(q: Q):
 
 example_filenames = [line.strip() for line in read_lines(os.path.join(example_dir, 'tour.conf')) if
                      not line.strip().startswith('#')]
-
 catalog = load_examples(example_filenames)
 print('----------------------------------------')
 print(' Welcome to the H2O Wave Interactive Tour!')
 print('')
-print(' Go to http://localhost:10101')
+print(' Go to http://localhost:10101/tour')
 print('----------------------------------------')
diff --git a/examples/tour_autocomplete_utils.py b/examples/utils.py
similarity index 100%
rename from examples/tour_autocomplete_utils.py
rename to examples/utils.py
diff --git a/tour.py b/tour.py
deleted file mode 100644
index f614d4ce24f05d0134f7c48aec33e7d9b6cbbf4a..0000000000000000000000000000000000000000
--- a/tour.py
+++ /dev/null
@@ -1,416 +0,0 @@
-import collections
-import os
-import os.path
-import re
-import shutil
-import socket
-import subprocess
-import sys
-import uuid
-from contextlib import closing
-from pathlib import Path
-from string import Template
-from typing import Dict, List, Optional, Tuple
-from urllib.parse import urlparse
-
-from h2o_wave import Q, app, main, ui
-
-example_dir = os.path.dirname(os.path.realpath(__file__))
-tour_tmp_dir = os.path.join(example_dir, '_tour_apps_tmp')
-
-_base_url = os.environ.get('H2O_WAVE_BASE_URL', '/')
-_app_address = urlparse(os.environ.get('H2O_WAVE_APP_ADDRESS', 'http://127.0.0.1:8000'))
-default_example_name = 'hello_world'
-vsc_extension_path = os.path.join(example_dir, '..', '..', 'tools', 'vscode-extension')
-
-
-def scan_free_port(port: int):
-    while True:
-        with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
-            if sock.connect_ex(('localhost', port)):
-                return port
-        port += 1
-
-
-class Example:
-    def __init__(self, filename: str, title: str, description: str, source: str):
-        self.name = os.path.splitext(filename)[0]
-        self.filename = filename
-        self.title = title
-        self.description = description
-        self.source = source
-        self.previous_example: Optional[Example] = None
-        self.next_example: Optional[Example] = None
-        self.process: Optional[subprocess.Popen] = None
-
-    def start(self, filename: str, is_app: bool, q: Q):
-        env = os.environ.copy()
-        env['H2O_WAVE_BASE_URL'] = _base_url
-        env['H2O_WAVE_ADDRESS'] = os.environ.get('H2O_WAVE_ADDRESS', 'http://127.0.0.1:10101')
-        # The environment passed into Popen must include SYSTEMROOT, otherwise Popen will fail when called
-        # inside python during initialization if %PATH% is configured, but without %SYSTEMROOT%.
-        if sys.platform.lower().startswith('win'):
-            env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
-        if is_app:
-            q.app.app_port = scan_free_port(q.app.app_port)
-            env['H2O_WAVE_APP_ADDRESS'] = f'http://{_app_address.hostname}:{q.app.app_port}'
-            self.process = subprocess.Popen([
-                sys.executable, '-m', 'uvicorn',
-                '--host', '0.0.0.0',
-                '--port', str(q.app.app_port),
-                f'examples.{filename.replace(".py", "")}:main',
-            ], env=env)
-        else:
-            self.process = subprocess.Popen([sys.executable, os.path.join(example_dir, filename)], env=env)
-
-    def stop(self):
-        if self.process and self.process.returncode is None:
-            self.process.terminate()
-            self.process.wait()
-
-
-def read_lines(p: str) -> List[str]:
-    with open(p, encoding='utf-8') as f:
-        return f.readlines()
-
-
-def read_file(p: str) -> str:
-    with open(p, encoding='utf-8') as f:
-        return f.read()
-
-
-def strip_comment(line: str) -> str:
-    """Returns the content of a line without '#' and ' ' characters
-
-    remove leading '#', but preserve '#' that is part of a tag
-    example:
-    >>> '# #hello '.strip('#').strip()
-    '#hello'
-    """
-    return line.strip('#').strip()
-
-
-def parse_tags(description: str) -> Tuple[str, List[str]]:
-    """Creates tags from description.
-
-    Accepts a description containing tags and returns a (new_description, tags) tuple.
-
-    The convention for tags:
-    1. Any valid twitter hashtag
-
-    For example, accept a description in any of the following forms
-
-    1. Use a checklist to group a set of related checkboxes. #form #checkbox #checklist
-
-    2. Use a checklist to group a set of related checkboxes.
-       #form #checkbox #checklist
-
-    3. Use a #checklist to group a set of related checkboxes.
-       #form #checkbox
-
-    and return
-    ('Use a checklist to group a set of related checkboxes.', ['checkbox', 'checklist', 'form']). The list of tags will
-    be sorted and all tags will be converted to lowercase.
-
-    Args:
-        description: Complete description of an example.
-    Returns:
-        A tuple of new_description and a sorted list of tags. new_description is created by removing the '#' characters
-        from the description.
-    """
-    hashtag_regex_pattern = r"(\s+)#(\w*[a-zA-Z]+\w*)\b"
-    pattern = re.compile(hashtag_regex_pattern)
-    matches = pattern.findall(' ' + description)
-
-    # Retrieve tags from the matches
-    tags = sorted(list(set([x[-1].lower() for x in matches])))
-
-    # Remove the '#' before the tags in description
-    new_d = pattern.sub(r'\1\2', ' ' + description)
-
-    # Remove the last line in description if it has only tags
-    *lines, last_line = new_d.strip().splitlines()
-    last_line_has_tags_only = len(last_line.strip()) > 1 and all([x.strip().lower() in tags for x in last_line.split()])
-    if last_line_has_tags_only:
-        # Return all lines except the last line
-        return '\n'.join(lines), tags
-
-    # Remove the last sentence if it has only tags
-    *sentences, last_sentence = last_line.split('. ')
-    last_sentence_has_tags_only = len(last_sentence.strip()) > 1 and all(
-        [x.strip().lower() in tags for x in last_sentence.split()])
-    if last_sentence_has_tags_only:
-        # Return all lines and all sentences in the last line except the last sentence
-        lines.extend(sentences)
-        return '\n'.join(lines) + '.', tags
-
-    # Return the complete description
-    lines.append(last_line)
-    return '\n'.join(lines), tags
-
-
-def load_example(filename: str) -> Example:
-    contents = read_file(os.path.join(example_dir, filename))
-    parts = contents.split('---', maxsplit=1)
-    header, source = parts[0].strip().splitlines(), parts[1].strip()
-    title, description = strip_comment(header[0]), [strip_comment(x) for x in header[1:]]
-    new_description, _ = parse_tags('\n'.join(description))
-    return Example(filename, title, new_description, source)
-
-
-def load_examples(filenames: List[str]) -> Dict[str, Example]:
-    examples = collections.OrderedDict()
-    for filename in filenames:
-        example = load_example(filename)
-        examples[example.name] = example
-    example_list = [e for e in examples.values()]
-    k = len(example_list) - 1
-    for i, e in enumerate(example_list):
-        if i > 0:
-            e.previous_example = example_list[i - 1]
-        if i < k:
-            e.next_example = example_list[i + 1]
-    return examples
-
-
-app_title = 'H2O Wave Tour'
-header_height = 76
-blurb_height = 56
-mobile_blurb_height = 76
-
-
-async def setup_page(q: Q):
-    py_content = ''
-    parser_path = os.path.join(example_dir, 'tour_autocomplete_parser.py')
-    utils_path = os.path.join(example_dir, 'tour_autocomplete_utils.py')
-    # In prod.
-    if os.path.exists(parser_path) and os.path.exists(utils_path):
-        with open(parser_path, 'r') as f:
-            py_content = f.read()
-        with open(utils_path, 'r') as f:
-            py_content += f.read()
-    # When run in development from Wave repo.
-    elif os.path.exists(vsc_extension_path):
-        with open(os.path.join(vsc_extension_path, 'server', 'parser.py'), 'r') as f:
-            py_content = f.read()
-        with open(os.path.join(vsc_extension_path, 'server', 'utils.py'), 'r') as f:
-            py_content += f.read()
-
-    if py_content:
-        py_content += '''
-def get_wave_completions(line, character, file_content):
-    completion_type, leaf_val = get_completion_type(line, character, file_content)
-    if completion_type in ['args', 'events', 'zones', 'client', 'app', 'user']:
-        completion_items = []
-        file_metadata = fill_completion(file_content)
-        if completion_type == 'events' and leaf_val:
-            completion_items = list(getattr(file_metadata, completion_type).get(leaf_val, []))
-        elif completion_type == 'events' and leaf_val is None:
-            completion_items = list(getattr(file_metadata, completion_type).keys())
-        elif leaf_val is None:
-            completion_items = getattr(file_metadata, completion_type)
-        return [{'label': label, 'kind': 6, 'sort_text': '0'} for label in completion_items]
-    elif completion_type == 'themes':
-        return [{'label': theme, 'kind': 13, 'sort_text': '0'} for theme in themes]
-    elif completion_type == 'icons':
-        return [{'label': icon, 'kind': 13, 'sort_text': '0'} for icon in fluent_icons]
-        '''
-    js_code = ''
-    with open(os.path.join(example_dir, 'tour.js'), 'r') as f:
-        js_code = f.read()
-    template = Template(js_code).substitute(
-        tour_assets=q.app.tour_assets,
-        base_url=_base_url,
-        snippets1=q.app.snippets1,
-        snippets2=q.app.snippets2,
-        py_content=py_content
-    )
-    q.page['meta'] = ui.meta_card(
-        box='',
-        title=app_title,
-        scripts=[ui.script(q.app.tour_assets + '/loader.min.js')],
-        script=ui.inline_script(content=template, requires=['require'], targets=['monaco-editor']),
-        layouts=[
-            ui.layout(
-                breakpoint='xs',
-                zones=[
-                    ui.zone('mobile_header'),
-                    ui.zone('main',
-                            zones=[
-                                ui.zone('code', size=f'calc(50vh - {(header_height + mobile_blurb_height) / 2}px)'),
-                                ui.zone('preview', size=f'calc(50vh - {(header_height + mobile_blurb_height) / 2}px)'),
-                            ]),
-                    ui.zone('mobile_blurb')
-                ],
-            ),
-            ui.layout(breakpoint='m', zones=[
-                ui.zone('header'),
-                ui.zone('blurb'),
-                ui.zone('main', size=f'calc(100vh - {header_height + blurb_height}px)', direction=ui.ZoneDirection.ROW,
-                        zones=[
-                            ui.zone('code'),
-                            ui.zone('preview')
-                        ])
-            ]),
-        ])
-    nav_links = [
-        ('docs', 'Wave docs', 'https://wave.h2o.ai/docs/getting-started'),
-        ('discussions', 'Discussions', 'https://github.com/h2oai/wave/discussions'),
-        ('blog', 'Blog', 'https://wave.h2o.ai/blog'),
-        ('cloud', 'H2O AI Cloud', 'https://h2o.ai/platform/ai-cloud/'),
-        ('h2o', 'H2O', 'https://www.h2o.ai/'),
-    ]
-    q.page['header'] = ui.header_card(
-        box='header',
-        title=app_title,
-        subtitle=f'{len(catalog)} Interactive Examples',
-        image=f'{q.app.tour_assets}/h2o-logo.svg',
-        items=[
-            ui.links(inline=True, items=[ui.link(label=link[1], path=link[2], target='_blank') for link in nav_links])
-        ])
-    q.page['mobile_header'] = ui.header_card(
-        box='mobile_header',
-        title=app_title,
-        subtitle=f'{len(catalog)} Interactive Examples',
-        image=f'{q.app.tour_assets}/h2o-logo.svg',
-        nav=[
-            ui.nav_group('Links', items=[ui.nav_item(name=link[0], label=link[1], path=link[2]) for link in nav_links])
-        ])
-    q.page['blurb'] = ui.section_card(box='blurb', title='', subtitle='', items=[])
-    q.page['mobile_blurb'] = ui.form_card(box='mobile_blurb', items=[])
-    q.page['code'] = ui.markup_card(
-        box='code',
-        title='',
-        content='
' - ) - # Put tmp placeholder
to simulate blank screen. - q.page['preview'] = ui.frame_card(box='preview', title='Preview', content='
') - await q.page.save() - - -def make_blurb(q: Q): - example = q.client.active_example - blurb_card = q.page['blurb'] - blurb_card.title = example.title - blurb_card.subtitle = example.description - # HACK: Recreate dropdown every time (by dynamic name) to control value (needed for next / prev btn functionality). - items = [ui.dropdown(name=q.args['#'] or default_example_name, width='300px', value=example.name, trigger=True, - choices=[ui.choice(name=e.name, label=e.title) for e in catalog.values()])] - if example.previous_example: - items.append(ui.button(name=f'#{example.previous_example.name}', label='Prev')) - if example.next_example: - items.append(ui.button(name=f'#{example.next_example.name}', label='Next', primary=True)) - blurb_card.items = items - q.page['mobile_blurb'].items = [ui.inline(direction='row', justify='center', items=items)] - - -async def show_example(q: Q, example: Example): - # Clear demo page - demo_page = q.site[f'/{q.client.path}'] - demo_page.drop() - await demo_page.save() - - filename = os.path.join(tour_tmp_dir, f'{q.client.path}.py') - code = q.events.editor.change if q.events.editor else example.source - code = code.replace("`", "\\`") - is_app = code.find('@app(') > 0 - with open(filename, 'w') as f: - fixed_path = code - if is_app: - fixed_path = fixed_path.replace("@app('/demo')", f"@app('/{q.client.path}')") - else: - fixed_path = fixed_path.replace("site['/demo']", f"site['/{q.client.path}']") - f.write(fixed_path) - if is_app: - filename = '.'.join([tour_tmp_dir, f'{q.client.path}.py']).split(os.sep)[-1] - - # Stop active example, if any. - active_example = q.client.active_example - if active_example: - active_example.stop() - - # Start new example - example.start(filename, is_app, q) - q.client.active_example = example - - # Update example blurb - make_blurb(q) - - # Update preview title - q.page['preview'].title = f'Preview of {example.filename}' - q.page['code'].title = example.filename - await q.page.save() - - if q.client.is_first_load: - # Make sure all the JS has loaded properly. - await q.sleep(1) - q.client.is_first_load = False - - # Update code display - if not q.events.editor: - code = code.replace('$', '\\$') - q.page['meta'].script = ui.inline_script(f'editor.setValue(`{code}`)', requires=['editor']) - await q.page.save() - if q.args['#']: - q.page['meta'].script = ui.inline_script('editor.setScrollPosition({ scrollTop: 0 }); editor.focus()', - requires=['editor']) - - # HACK - # The ?e= appended to the path forces the frame to reload. - # The url param is not actually used. - q.page['preview'].path = f'{_base_url}{q.client.path}?e={example.name}' - await q.page.save() - - -async def on_startup(): - # Clean up previous tmp dir. - await on_shutdown() - os.mkdir(tour_tmp_dir) - shutil.copyfile(os.path.join(example_dir, 'synth.py'), os.path.join(tour_tmp_dir, 'synth.py')) - shutil.copyfile(os.path.join(example_dir, 'plot_d3.js'), os.path.join(tour_tmp_dir, 'plot_d3.js')) - - -async def on_shutdown(): - dirpath = Path(tour_tmp_dir) - if dirpath.exists(): - shutil.rmtree(dirpath) - - -@app('/tour', on_startup=on_startup, on_shutdown=on_shutdown) -async def serve(q: Q): - if not q.app.initialized: - q.app.app_port = 10102 - q.app.tour_assets, = await q.site.upload_dir(os.path.join(example_dir, 'tour-assets')) - base_snippets_path = os.path.join(example_dir, 'base-snippets.json') - component_snippets_path = os.path.join(example_dir, 'component-snippets.json') - # Prod. - if os.path.exists(base_snippets_path) and os.path.exists(component_snippets_path): - q.app.snippets1, q.app.snippets2, = await q.site.upload([base_snippets_path, component_snippets_path]) - # When run in development from Wave repo. - elif os.path.exists(vsc_extension_path): - q.app.snippets1, q.app.snippets2, = await q.site.upload([ - os.path.join(vsc_extension_path, 'base-snippets.json'), - os.path.join(vsc_extension_path, 'component-snippets.json') - ]) - q.app.initialized = True - if not q.client.initialized: - q.client.initialized = True - q.client.is_first_load = True - q.client.path = uuid.uuid4() - await setup_page(q) - - search = q.args[q.args['#'] or default_example_name] - if search and not q.events.editor: - q.page['meta'] = ui.meta_card(box='', redirect=f'#{search}') - - await show_example(q, catalog[q.args['#'] or default_example_name]) - - -example_filenames = [line.strip() for line in read_lines(os.path.join(example_dir, 'tour.conf')) if - not line.strip().startswith('#')] -catalog = load_examples(example_filenames) -print('----------------------------------------') -print(' Welcome to the H2O Wave Interactive Tour!') -print('') -print(' Go to http://localhost:10101/tour') -print('----------------------------------------')