From d2afaf5bb3f86ec3e81d63ec6eb6d44a22fc19e8 Mon Sep 17 00:00:00 2001 From: Tom Gotsman <64492814+tgberkeley@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:45:33 -0700 Subject: [PATCH] Add demo app template (#2046) --- reflex/.templates/apps/demo/.gitignore | 4 + .../.templates/apps/demo/assets/favicon.ico | Bin 0 -> 4286 bytes reflex/.templates/apps/demo/assets/github.svg | 10 + reflex/.templates/apps/demo/assets/icon.svg | 37 ++ reflex/.templates/apps/demo/assets/logo.svg | 68 ++++ .../.templates/apps/demo/assets/paneleft.svg | 13 + reflex/.templates/apps/demo/code/__init__.py | 1 + reflex/.templates/apps/demo/code/demo.py | 123 ++++++ .../apps/demo/code/pages/__init__.py | 6 + .../apps/demo/code/pages/chatapp.py | 31 ++ .../apps/demo/code/pages/datatable.py | 359 ++++++++++++++++++ .../.templates/apps/demo/code/pages/forms.py | 254 +++++++++++++ .../apps/demo/code/pages/graphing.py | 252 ++++++++++++ .../.templates/apps/demo/code/pages/home.py | 55 +++ reflex/.templates/apps/demo/code/sidebar.py | 177 +++++++++ reflex/.templates/apps/demo/code/state.py | 22 ++ .../apps/demo/code/states/form_state.py | 40 ++ .../apps/demo/code/states/pie_state.py | 47 +++ reflex/.templates/apps/demo/code/styles.py | 67 ++++ .../apps/demo/code/webui/__init__.py | 0 .../demo/code/webui/components/__init__.py | 4 + .../apps/demo/code/webui/components/chat.py | 118 ++++++ .../code/webui/components/loading_icon.py | 26 ++ .../apps/demo/code/webui/components/modal.py | 56 +++ .../apps/demo/code/webui/components/navbar.py | 68 ++++ .../demo/code/webui/components/sidebar.py | 66 ++++ .../.templates/apps/demo/code/webui/state.py | 145 +++++++ .../.templates/apps/demo/code/webui/styles.py | 88 +++++ 28 files changed, 2137 insertions(+) create mode 100644 reflex/.templates/apps/demo/.gitignore create mode 100644 reflex/.templates/apps/demo/assets/favicon.ico create mode 100644 reflex/.templates/apps/demo/assets/github.svg create mode 100644 reflex/.templates/apps/demo/assets/icon.svg create mode 100644 reflex/.templates/apps/demo/assets/logo.svg create mode 100644 reflex/.templates/apps/demo/assets/paneleft.svg create mode 100644 reflex/.templates/apps/demo/code/__init__.py create mode 100644 reflex/.templates/apps/demo/code/demo.py create mode 100644 reflex/.templates/apps/demo/code/pages/__init__.py create mode 100644 reflex/.templates/apps/demo/code/pages/chatapp.py create mode 100644 reflex/.templates/apps/demo/code/pages/datatable.py create mode 100644 reflex/.templates/apps/demo/code/pages/forms.py create mode 100644 reflex/.templates/apps/demo/code/pages/graphing.py create mode 100644 reflex/.templates/apps/demo/code/pages/home.py create mode 100644 reflex/.templates/apps/demo/code/sidebar.py create mode 100644 reflex/.templates/apps/demo/code/state.py create mode 100644 reflex/.templates/apps/demo/code/states/form_state.py create mode 100644 reflex/.templates/apps/demo/code/states/pie_state.py create mode 100644 reflex/.templates/apps/demo/code/styles.py create mode 100644 reflex/.templates/apps/demo/code/webui/__init__.py create mode 100644 reflex/.templates/apps/demo/code/webui/components/__init__.py create mode 100644 reflex/.templates/apps/demo/code/webui/components/chat.py create mode 100644 reflex/.templates/apps/demo/code/webui/components/loading_icon.py create mode 100644 reflex/.templates/apps/demo/code/webui/components/modal.py create mode 100644 reflex/.templates/apps/demo/code/webui/components/navbar.py create mode 100644 reflex/.templates/apps/demo/code/webui/components/sidebar.py create mode 100644 reflex/.templates/apps/demo/code/webui/state.py create mode 100644 reflex/.templates/apps/demo/code/webui/styles.py diff --git a/reflex/.templates/apps/demo/.gitignore b/reflex/.templates/apps/demo/.gitignore new file mode 100644 index 000000000..eab0d4b05 --- /dev/null +++ b/reflex/.templates/apps/demo/.gitignore @@ -0,0 +1,4 @@ +*.db +*.py[cod] +.web +__pycache__/ \ No newline at end of file diff --git a/reflex/.templates/apps/demo/assets/favicon.ico b/reflex/.templates/apps/demo/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..166ae995eaa63fc96771410a758282dc30e925cf GIT binary patch literal 4286 zcmeHL>rYc>81ELdEe;}zmYd}cUgmJRfwjUwD1`#s5KZP>mMqza#Viv|_7|8f+0+bX zHuqusuw-7Ca`DTu#4U4^o2bjO#K>4%N?Wdi*wZ3Vx%~Ef4}D1`U_EMRg3u z#2#M|V>}}q-@IaO@{9R}d*u7f&~5HfxSkmHVcazU#i30H zAGxQ5Spe!j9`KuGqR@aExK`-}sH1jvqoIp3C7Vm)9Tu=UPE;j^esN~a6^a$ZILngo;^ zGLXl(ZFyY&U!li`6}y-hUQ99v?s`U4O!kgog74FPw-9g+V)qs!jFGEQyvBf><U|E2vRmx|+(VI~S=lT?@~C5pvZOd`x{Q_+3tG6H=gtdWcf z)+7-Zp=UqH^J4sk^>_G-Ufn-2Hz z2mN12|C{5}U`^eCQuFz=F%wp@}SzA1MHEaM^CtJs<{}Tzu$bx2orTKiedgmtVGM{ zdd#vX`&cuiec|My_KW;y{Ryz2kFu9}=~us6hvx1ZqQCk(d+>HP>ks>mmHCjjDh{pe zKQkKpk0SeDX#XMqf$}QV{z=xrN!mQczJAvud@;zFqaU1ocq==Py)qsa=8UKrt!J7r z{RsTo^rgtZo%$rak)DN*D)!(Y^$@yL6Nd=#eu&?unzhH8yq>v{gkt8xcG3S%H)-y_ zqQ1|v|JT$0R~Y}omg2Y+nDvR+K|kzR5i^fmKF>j~N;A35Vr`JWh4yRqKl#P|qlx?` z@|CmBiP}ysYO%m2{eBG6&ix5 zr#u((F2{vb=W4jNmTQh3M^F2o80T49?w>*rv0mt)-o1y!{hRk`E#UVPdna6jnz`rw dKpn)r^--YJZpr;bYU`N~>#v3X5BRU&{{=gv-{1fM literal 0 HcmV?d00001 diff --git a/reflex/.templates/apps/demo/assets/github.svg b/reflex/.templates/apps/demo/assets/github.svg new file mode 100644 index 000000000..61c9d791b --- /dev/null +++ b/reflex/.templates/apps/demo/assets/github.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/reflex/.templates/apps/demo/assets/icon.svg b/reflex/.templates/apps/demo/assets/icon.svg new file mode 100644 index 000000000..b9cc89da9 --- /dev/null +++ b/reflex/.templates/apps/demo/assets/icon.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/reflex/.templates/apps/demo/assets/logo.svg b/reflex/.templates/apps/demo/assets/logo.svg new file mode 100644 index 000000000..94fe1f511 --- /dev/null +++ b/reflex/.templates/apps/demo/assets/logo.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/reflex/.templates/apps/demo/assets/paneleft.svg b/reflex/.templates/apps/demo/assets/paneleft.svg new file mode 100644 index 000000000..ac9c5040a --- /dev/null +++ b/reflex/.templates/apps/demo/assets/paneleft.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/reflex/.templates/apps/demo/code/__init__.py b/reflex/.templates/apps/demo/code/__init__.py new file mode 100644 index 000000000..e1d286346 --- /dev/null +++ b/reflex/.templates/apps/demo/code/__init__.py @@ -0,0 +1 @@ +"""Base template for Reflex.""" diff --git a/reflex/.templates/apps/demo/code/demo.py b/reflex/.templates/apps/demo/code/demo.py new file mode 100644 index 000000000..fe98a31eb --- /dev/null +++ b/reflex/.templates/apps/demo/code/demo.py @@ -0,0 +1,123 @@ +"""Welcome to Reflex! This file outlines the steps to create a basic app.""" +from typing import Callable + +import reflex as rx + +from .pages import chatapp_page, datatable_page, forms_page, graphing_page, home_page +from .sidebar import sidebar +from .state import State +from .styles import * + +meta = [ + { + "name": "viewport", + "content": "width=device-width, shrink-to-fit=no, initial-scale=1", + }, +] + + +def template(main_content: Callable[[], rx.Component]) -> rx.Component: + """The template for each page of the app. + + Args: + main_content (Callable[[], rx.Component]): The main content of the page. + + Returns: + rx.Component: The template for each page of the app. + """ + menu_button = rx.box( + rx.menu( + rx.menu_button( + rx.icon( + tag="hamburger", + size="4em", + color=text_color, + ), + ), + rx.menu_list( + rx.menu_item(rx.link("Home", href="/", width="100%")), + rx.menu_divider(), + rx.menu_item( + rx.link("About", href="https://github.com/reflex-dev", width="100%") + ), + rx.menu_item( + rx.link("Contact", href="mailto:founders@=reflex.dev", width="100%") + ), + ), + ), + position="fixed", + right="1.5em", + top="1.5em", + z_index="500", + ) + + return rx.hstack( + sidebar(), + main_content(), + rx.spacer(), + menu_button, + align_items="flex-start", + transition="left 0.5s, width 0.5s", + position="relative", + left=rx.cond(State.sidebar_displayed, "0px", f"-{sidebar_width}"), + ) + + +@rx.page("/", meta=meta) +@template +def home() -> rx.Component: + """Home page. + + Returns: + rx.Component: The home page. + """ + return home_page() + + +@rx.page("/forms", meta=meta) +@template +def forms() -> rx.Component: + """Forms page. + + Returns: + rx.Component: The settings page. + """ + return forms_page() + + +@rx.page("/graphing", meta=meta) +@template +def graphing() -> rx.Component: + """Graphing page. + + Returns: + rx.Component: The graphing page. + """ + return graphing_page() + + +@rx.page("/datatable", meta=meta) +@template +def datatable() -> rx.Component: + """Data Table page. + + Returns: + rx.Component: The chatapp page. + """ + return datatable_page() + + +@rx.page("/chatapp", meta=meta) +@template +def chatapp() -> rx.Component: + """Chatapp page. + + Returns: + rx.Component: The chatapp page. + """ + return chatapp_page() + + +# Add state and page to the app. +app = rx.App(style=base_style) +app.compile() diff --git a/reflex/.templates/apps/demo/code/pages/__init__.py b/reflex/.templates/apps/demo/code/pages/__init__.py new file mode 100644 index 000000000..7f319d25c --- /dev/null +++ b/reflex/.templates/apps/demo/code/pages/__init__.py @@ -0,0 +1,6 @@ +"""The pages of the app.""" +from .chatapp import chatapp_page +from .datatable import datatable_page +from .forms import forms_page +from .graphing import graphing_page +from .home import home_page diff --git a/reflex/.templates/apps/demo/code/pages/chatapp.py b/reflex/.templates/apps/demo/code/pages/chatapp.py new file mode 100644 index 000000000..5f06cc15d --- /dev/null +++ b/reflex/.templates/apps/demo/code/pages/chatapp.py @@ -0,0 +1,31 @@ +"""The main Chat app.""" + +import reflex as rx + +from ..styles import * +from ..webui import styles +from ..webui.components import chat, modal, navbar, sidebar + + +def chatapp_page() -> rx.Component: + """The main app. + + Returns: + The UI for the main app. + """ + return rx.box( + rx.vstack( + navbar(), + chat.chat(), + chat.action_bar(), + sidebar(), + modal(), + bg=styles.bg_dark_color, + color=styles.text_light_color, + min_h="100vh", + align_items="stretch", + spacing="0", + style=template_content_style, + ), + style=template_page_style, + ) diff --git a/reflex/.templates/apps/demo/code/pages/datatable.py b/reflex/.templates/apps/demo/code/pages/datatable.py new file mode 100644 index 000000000..64bdea0eb --- /dev/null +++ b/reflex/.templates/apps/demo/code/pages/datatable.py @@ -0,0 +1,359 @@ +"""The settings page for the template.""" +from typing import Any + +import reflex as rx +from reflex.components.datadisplay.dataeditor import DataEditorTheme + +from ..styles import * +from ..webui.state import State + + +class DataTableState(State): + """Datatable state.""" + + cols: list[Any] = [ + {"title": "Title", "type": "str"}, + { + "title": "Name", + "type": "str", + "group": "Data", + "width": 300, + }, + { + "title": "Birth", + "type": "str", + "group": "Data", + "width": 150, + }, + { + "title": "Human", + "type": "bool", + "group": "Data", + "width": 80, + }, + { + "title": "House", + "type": "str", + "group": "Data", + }, + { + "title": "Wand", + "type": "str", + "group": "Data", + "width": 250, + }, + { + "title": "Patronus", + "type": "str", + "group": "Data", + }, + { + "title": "Blood status", + "type": "str", + "group": "Data", + "width": 200, + }, + ] + + data = [ + [ + "1", + "Harry James Potter", + "31 July 1980", + True, + "Gryffindor", + "11' Holly phoenix feather", + "Stag", + "Half-blood", + ], + [ + "2", + "Ronald Bilius Weasley", + "1 March 1980", + True, + "Gryffindor", + "12' Ash unicorn tail hair", + "Jack Russell terrier", + "Pure-blood", + ], + [ + "3", + "Hermione Jean Granger", + "19 September, 1979", + True, + "Gryffindor", + "10¾' vine wood dragon heartstring", + "Otter", + "Muggle-born", + ], + [ + "4", + "Albus Percival Wulfric Brian Dumbledore", + "Late August 1881", + True, + "Gryffindor", + "15' Elder Thestral tail hair core", + "Phoenix", + "Half-blood", + ], + [ + "5", + "Rubeus Hagrid", + "6 December 1928", + False, + "Gryffindor", + "16' Oak unknown core", + "None", + "Part-Human (Half-giant)", + ], + [ + "6", + "Fred Weasley", + "1 April, 1978", + True, + "Gryffindor", + "Unknown", + "Unknown", + "Pure-blood", + ], + [ + "7", + "George Weasley", + "1 April, 1978", + True, + "Gryffindor", + "Unknown", + "Unknown", + "Pure-blood", + ], + ] + + +code_show = """rx.hstack( + rx.divider(orientation="vertical", height="100vh", border="solid black 1px"), + rx.vstack( + rx.box( + rx.data_editor( + columns=DataTableState.cols, + data=DataTableState.data, + draw_focus_ring=True, + row_height=50, + smooth_scroll_x=True, + smooth_scroll_y=True, + column_select="single", + # style + theme=DataEditorTheme(**darkTheme), + width="80vw", + height="80vh", + ), + ), + rx.spacer(), + height="100vh", + spacing="25", + ), +)""" + +state_show = """class DataTableState(State): + cols: list[Any] = [ + {"title": "Title", "type": "str"}, + { + "title": "Name", + "type": "str", + "group": "Data", + "width": 300, + }, + { + "title": "Birth", + "type": "str", + "group": "Data", + "width": 150, + }, + { + "title": "Human", + "type": "bool", + "group": "Data", + "width": 80, + }, + { + "title": "House", + "type": "str", + "group": "Data", + }, + { + "title": "Wand", + "type": "str", + "group": "Data", + "width": 250, + }, + { + "title": "Patronus", + "type": "str", + "group": "Data", + }, + { + "title": "Blood status", + "type": "str", + "group": "Data", + "width": 200, + }, + ]""" + +data_show = """[ + ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"], + ["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"], + ["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"], + ["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"], + ["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"], + ["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], + ["7", "George Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], +]""" + + +darkTheme = { + "accent_color": "#8c96ff", + "accent_light": "rgba(202, 206, 255, 0.253)", + "text_dark": "#ffffff", + "text_medium": "#b8b8b8", + "text_light": "#a0a0a0", + "text_bubble": "#ffffff", + "bg_icon_header": "#b8b8b8", + "fg_icon_header": "#000000", + "text_header": "#a1a1a1", + "text_header_selected": "#000000", + "bg_cell": "#16161b", + "bg_cell_medium": "#202027", + "bg_header": "#212121", + "bg_header_has_focus": "#474747", + "bg_header_hovered": "#404040", + "bg_bubble": "#212121", + "bg_bubble_selected": "#000000", + "bg_search_result": "#423c24", + "border_color": "rgba(225,225,225,0.2)", + "drilldown_border": "rgba(225,225,225,0.4)", + "link_color": "#4F5DFF", + "header_font_style": "bold 14px", + "base_font_style": "13px", + "font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", +} + +darkTheme_show = """darkTheme={ + "accent_color": "#8c96ff", + "accent_light": "rgba(202, 206, 255, 0.253)", + "text_dark": "#ffffff", + "text_medium": "#b8b8b8", + "text_light": "#a0a0a0", + "text_bubble": "#ffffff", + "bg_icon_header": "#b8b8b8", + "fg_icon_header": "#000000", + "text_header": "#a1a1a1", + "text_header_selected": "#000000", + "bg_cell": "#16161b", + "bg_cell_medium": "#202027", + "bg_header": "#212121", + "bg_header_has_focus": "#474747", + "bg_header_hovered": "#404040", + "bg_bubble": "#212121", + "bg_bubble_selected": "#000000", + "bg_search_result": "#423c24", + "border_color": "rgba(225,225,225,0.2)", + "drilldown_border": "rgba(225,225,225,0.4)", + "link_color": "#4F5DFF", + "header_font_style": "bold 14px", + "base_font_style": "13px", + "font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", +}""" + + +def datatable_page() -> rx.Component: + """The UI for the settings page. + + Returns: + rx.Component: The UI for the settings page. + """ + return rx.box( + rx.vstack( + rx.heading( + "Data Table Demo", + font_size="3em", + ), + rx.hstack( + rx.vstack( + rx.box( + rx.data_editor( + columns=DataTableState.cols, + data=DataTableState.data, + draw_focus_ring=True, + row_height=50, + smooth_scroll_x=True, + smooth_scroll_y=True, + column_select="single", + # style + theme=DataEditorTheme(**darkTheme), + width="80vw", + ), + ), + rx.spacer(), + spacing="25", + ), + ), + rx.tabs( + rx.tab_list( + rx.tab("Code", style=tab_style), + rx.tab("Data", style=tab_style), + rx.tab("State", style=tab_style), + rx.tab("Styling", style=tab_style), + padding_x=0, + ), + rx.tab_panels( + rx.tab_panel( + rx.code_block( + code_show, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + data_show, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + state_show, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + darkTheme_show, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + width="100%", + ), + variant="unstyled", + color_scheme="purple", + align="end", + width="100%", + padding_top=".5em", + ), + style=template_content_style, + ), + style=template_page_style, + ) diff --git a/reflex/.templates/apps/demo/code/pages/forms.py b/reflex/.templates/apps/demo/code/pages/forms.py new file mode 100644 index 000000000..904f003cd --- /dev/null +++ b/reflex/.templates/apps/demo/code/pages/forms.py @@ -0,0 +1,254 @@ +"""The settings page for the template.""" +import reflex as rx + +from ..states.form_state import FormState, UploadState +from ..styles import * + +forms_1_code = """rx.vstack( + rx.form( + rx.vstack( + rx.input( + placeholder="First Name", + id="first_name", + ), + rx.input( + placeholder="Last Name", id="last_name" + ), + rx.hstack( + rx.checkbox("Checked", id="check"), + rx.switch("Switched", id="switch"), + ), + rx.button("Submit", + type_="submit", + bg="#ecfdf5", + color="#047857", + border_radius="lg", + ), + ), + on_submit=FormState.handle_submit, + ), + rx.divider(), + rx.heading("Results"), + rx.text(FormState.form_data.to_string()), + width="100%", +)""" + +color = "rgb(107,99,246)" + +forms_1_state = """class FormState(rx.State): + + form_data: dict = {} + + def handle_submit(self, form_data: dict): + "Handle the form submit." + self.form_data = form_data""" + + +forms_2_code = """rx.vstack( + rx.upload( + rx.vstack( + rx.button( + "Select File", + color=color, + bg="white", + border=f"1px solid {color}", + ), + rx.text( + "Drag and drop files here or click to select files" + ), + ), + border=f"1px dotted {color}", + padding="5em", + ), + rx.hstack(rx.foreach(rx.selected_files, rx.text)), + rx.button( + "Upload", + on_click=lambda: UploadState.handle_upload( + rx.upload_files() + ), + ), + rx.button( + "Clear", + on_click=rx.clear_selected_files, + ), + rx.foreach( + UploadState.img, lambda img: rx.image(src=img, width="20%", height="auto",) + ), + padding="5em", + width="100%", +)""" + +forms_2_state = """class UploadState(State): + "The app state." + + # The images to show. + img: list[str] + + async def handle_upload( + self, files: list[rx.UploadFile] + ): + "Handle the upload of file(s). + + Args: + files: The uploaded files. + " + for file in files: + upload_data = await file.read() + outfile = rx.get_asset_path(file.filename) + # Save the file. + with open(outfile, "wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img.append(f"/{file.filename}")""" + + +def forms_page() -> rx.Component: + """The UI for the settings page. + + Returns: + rx.Component: The UI for the settings page. + """ + return rx.box( + rx.vstack( + rx.heading( + "Forms Demo", + font_size="3em", + ), + rx.vstack( + rx.form( + rx.vstack( + rx.input( + placeholder="First Name", + id="first_name", + ), + rx.input(placeholder="Last Name", id="last_name"), + rx.hstack( + rx.checkbox("Checked", id="check"), + rx.switch("Switched", id="switch"), + ), + rx.button( + "Submit", + type_="submit", + bg="#ecfdf5", + color="#047857", + border_radius="lg", + ), + ), + on_submit=FormState.handle_submit, + ), + rx.divider(), + rx.heading("Results"), + rx.text(FormState.form_data.to_string()), + width="100%", + ), + rx.tabs( + rx.tab_list( + rx.tab("Code", style=tab_style), + rx.tab("State", style=tab_style), + padding_x=0, + ), + rx.tab_panels( + rx.tab_panel( + rx.code_block( + forms_1_code, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + forms_1_state, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + width="100%", + ), + variant="unstyled", + color_scheme="purple", + align="end", + width="100%", + padding_top=".5em", + ), + rx.heading("Upload Example", font_size="3em"), + rx.text("Try uploading some images and see how they look."), + rx.vstack( + rx.upload( + rx.vstack( + rx.button( + "Select File", + color=color, + bg="white", + border=f"1px solid {color}", + ), + rx.text("Drag and drop files here or click to select files"), + ), + border=f"1px dotted {color}", + padding="5em", + ), + rx.hstack(rx.foreach(rx.selected_files, rx.text)), + rx.button( + "Upload", + on_click=lambda: UploadState.handle_upload(rx.upload_files()), + ), + rx.button( + "Clear", + on_click=rx.clear_selected_files, + ), + rx.foreach( + UploadState.img, + lambda img: rx.image( + src=img, + width="20%", + height="auto", + ), + ), + padding="5em", + width="100%", + ), + rx.tabs( + rx.tab_list( + rx.tab("Code", style=tab_style), + rx.tab("State", style=tab_style), + padding_x=0, + ), + rx.tab_panels( + rx.tab_panel( + rx.code_block( + forms_2_code, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + forms_2_state, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + width="100%", + ), + variant="unstyled", + color_scheme="purple", + align="end", + width="100%", + padding_top=".5em", + ), + style=template_content_style, + ), + style=template_page_style, + ) diff --git a/reflex/.templates/apps/demo/code/pages/graphing.py b/reflex/.templates/apps/demo/code/pages/graphing.py new file mode 100644 index 000000000..0accf81a0 --- /dev/null +++ b/reflex/.templates/apps/demo/code/pages/graphing.py @@ -0,0 +1,252 @@ +"""The dashboard page for the template.""" +import reflex as rx + +from ..states.pie_state import PieChartState +from ..styles import * + +data_1 = [ + {"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400}, + {"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210}, + {"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290}, + {"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000}, + {"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181}, + {"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500}, + {"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100}, +] +data_1_show = """[ + {"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400}, + {"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210}, + {"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290}, + {"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000}, + {"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181}, + {"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500}, + {"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100}, +]""" + + +graph_1_code = """rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", bar_size=20, fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", type_="monotone", stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + data=data, +)""" + + +graph_2_code = """rx.recharts.pie_chart( + rx.recharts.pie( + data=PieChartState.resources, + data_key="count", + name_key="type_", + cx="50%", + cy="50%", + start_angle=180, + end_angle=0, + fill="#8884d8", + label=True, + ), + rx.recharts.graphing_tooltip(), +), +rx.vstack( + rx.foreach( + PieChartState.resource_types, + lambda type_, i: rx.hstack( + rx.button( + "-", + on_click=PieChartState.decrement(type_), + ), + rx.text( + type_, + PieChartState.resources[i]["count"], + ), + rx.button( + "+", + on_click=PieChartState.increment(type_), + ), + ), + ), +)""" + +graph_2_state = """class PieChartState(rx.State): + resources: list[dict[str, Any]] = [ + dict(type_="🏆", count=1), + dict(type_="🪵", count=1), + dict(type_="🥑", count=1), + dict(type_="🧱", count=1), + ] + + @rx.cached_var + def resource_types(self) -> list[str]: + return [r["type_"] for r in self.resources] + + def increment(self, type_: str): + for resource in self.resources: + if resource["type_"] == type_: + resource["count"] += 1 + break + + def decrement(self, type_: str): + for resource in self.resources: + if ( + resource["type_"] == type_ + and resource["count"] > 0 + ): + resource["count"] -= 1 + break +""" + + +def graphing_page() -> rx.Component: + """The UI for the dashboard page. + + Returns: + rx.Component: The UI for the dashboard page. + """ + return rx.box( + rx.vstack( + rx.heading( + "Graphing Demo", + font_size="3em", + ), + rx.heading( + "Composed Chart", + font_size="2em", + ), + rx.stack( + rx.recharts.composed_chart( + rx.recharts.area(data_key="uv", stroke="#8884d8", fill="#8884d8"), + rx.recharts.bar(data_key="amt", bar_size=20, fill="#413ea0"), + rx.recharts.line(data_key="pv", type_="monotone", stroke="#ff7300"), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + data=data_1, + # height="15em", + ), + width="100%", + height="20em", + ), + rx.tabs( + rx.tab_list( + rx.tab("Code", style=tab_style), + rx.tab("Data", style=tab_style), + padding_x=0, + ), + rx.tab_panels( + rx.tab_panel( + rx.code_block( + graph_1_code, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + data_1_show, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + width="100%", + ), + variant="unstyled", + color_scheme="purple", + align="end", + width="100%", + padding_top=".5em", + ), + rx.heading("Interactive Example", font_size="2em"), + rx.hstack( + rx.recharts.pie_chart( + rx.recharts.pie( + data=PieChartState.resources, + data_key="count", + name_key="type_", + cx="50%", + cy="50%", + start_angle=180, + end_angle=0, + fill="#8884d8", + label=True, + ), + rx.recharts.graphing_tooltip(), + ), + rx.vstack( + rx.foreach( + PieChartState.resource_types, + lambda type_, i: rx.hstack( + rx.button( + "-", + on_click=PieChartState.decrement(type_), + ), + rx.text( + type_, + PieChartState.resources[i]["count"], + ), + rx.button( + "+", + on_click=PieChartState.increment(type_), + ), + ), + ), + ), + width="100%", + height="15em", + ), + rx.tabs( + rx.tab_list( + rx.tab("Code", style=tab_style), + rx.tab("State", style=tab_style), + padding_x=0, + ), + rx.tab_panels( + rx.tab_panel( + rx.code_block( + graph_2_code, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + rx.tab_panel( + rx.code_block( + graph_2_state, + language="python", + show_line_numbers=True, + ), + width="100%", + padding_x=0, + padding_y=".25em", + ), + width="100%", + ), + variant="unstyled", + color_scheme="purple", + align="end", + width="100%", + padding_top=".5em", + ), + style=template_content_style, + min_h="100vh", + ), + style=template_page_style, + min_h="100vh", + ) diff --git a/reflex/.templates/apps/demo/code/pages/home.py b/reflex/.templates/apps/demo/code/pages/home.py new file mode 100644 index 000000000..3b94dfe0a --- /dev/null +++ b/reflex/.templates/apps/demo/code/pages/home.py @@ -0,0 +1,55 @@ +"""The home page of the app.""" +import reflex as rx + +from ..styles import * + + +def home_page() -> rx.Component: + """The UI for the home page. + + Returns: + rx.Component: The UI for the home page. + """ + return rx.box( + rx.vstack( + rx.heading( + "Welcome to Reflex! 👋", + font_size="3em", + ), + rx.text( + "Reflex is an open-source app framework built specifically to allow you to build web apps in pure python. 👈 Select a demo from the sidebar to see some examples of what Reflex can do!", + ), + rx.heading( + "Things to check out:", + font_size="2em", + ), + rx.unordered_list( + rx.list_item( + "Take a look at ", + rx.link( + "reflex.dev", + href="https://reflex.dev", + color="rgb(107,99,246)", + ), + ), + rx.list_item( + "Check out our ", + rx.link( + "docs", + href="https://reflex.dev/docs/getting-started/introduction/", + color="rgb(107,99,246)", + ), + ), + rx.list_item( + "Ask a question in our ", + rx.link( + "community", + href="https://discord.gg/T5WSbC2YtQ", + color="rgb(107,99,246)", + ), + ), + ), + style=template_content_style, + ), + style=template_page_style, + ) diff --git a/reflex/.templates/apps/demo/code/sidebar.py b/reflex/.templates/apps/demo/code/sidebar.py new file mode 100644 index 000000000..e4cd1672a --- /dev/null +++ b/reflex/.templates/apps/demo/code/sidebar.py @@ -0,0 +1,177 @@ +"""Sidebar component for the app.""" + +import reflex as rx + +from .state import State +from .styles import * + + +def sidebar_header() -> rx.Component: + """Sidebar header. + + Returns: + rx.Component: The sidebar header component. + """ + return rx.hstack( + rx.image( + src="/icon.svg", + height="2em", + ), + rx.spacer(), + rx.link( + rx.center( + rx.image( + src="/github.svg", + height="3em", + padding="0.5em", + ), + box_shadow=box_shadow, + bg="transparent", + border_radius=border_radius, + _hover={ + "bg": accent_color, + }, + ), + href="https://github.com/reflex-dev/reflex", + ), + width="100%", + border_bottom=border, + padding="1em", + ) + + +def sidebar_footer() -> rx.Component: + """Sidebar footer. + + Returns: + rx.Component: The sidebar footer component. + """ + return rx.hstack( + rx.link( + rx.center( + rx.image( + src="/paneleft.svg", + height="2em", + padding="0.5em", + ), + bg="transparent", + border_radius=border_radius, + **hover_accent_bg, + ), + on_click=State.toggle_sidebar_displayed, + transform=rx.cond(~State.sidebar_displayed, "rotate(180deg)", ""), + transition="transform 0.5s, left 0.5s", + position="relative", + left=rx.cond(State.sidebar_displayed, "0px", "20.5em"), + **overlapping_button_style, + ), + rx.spacer(), + rx.link( + rx.text( + "Docs", + ), + href="https://reflex.dev/docs/getting-started/introduction/", + ), + rx.link( + rx.text( + "Blog", + ), + href="https://reflex.dev/blog/", + ), + width="100%", + border_top=border, + padding="1em", + ) + + +def sidebar_item(text: str, icon: str, url: str) -> rx.Component: + """Sidebar item. + + Args: + text (str): The text of the item. + icon (str): The icon of the item. + url (str): The URL of the item. + + Returns: + rx.Component: The sidebar item component. + """ + return rx.link( + rx.hstack( + rx.image( + src=icon, + height="2.5em", + padding="0.5em", + ), + rx.text( + text, + ), + bg=rx.cond( + State.origin_url == f"/{text.lower()}/", + accent_color, + "transparent", + ), + color=rx.cond( + State.origin_url == f"/{text.lower()}/", + accent_text_color, + text_color, + ), + border_radius=border_radius, + box_shadow=box_shadow, + width="100%", + padding_x="1em", + ), + href=url, + width="100%", + ) + + +def sidebar() -> rx.Component: + """Sidebar. + + Returns: + rx.Component: The sidebar component. + """ + return rx.box( + rx.vstack( + sidebar_header(), + rx.vstack( + sidebar_item( + "Welcome", + "/github.svg", + "/", + ), + sidebar_item( + "Graphing Demo", + "/github.svg", + "/graphing", + ), + sidebar_item( + "Data Table Demo", + "/github.svg", + "/datatable", + ), + sidebar_item( + "Forms Demo", + "/github.svg", + "/forms", + ), + sidebar_item( + "Chat App Demo", + "/github.svg", + "/chatapp", + ), + width="100%", + overflow_y="auto", + align_items="flex-start", + padding="1em", + ), + rx.spacer(), + sidebar_footer(), + height="100dvh", + ), + min_width=sidebar_width, + height="100%", + position="sticky", + top="0px", + border_right=border, + ) diff --git a/reflex/.templates/apps/demo/code/state.py b/reflex/.templates/apps/demo/code/state.py new file mode 100644 index 000000000..a5c6f57bd --- /dev/null +++ b/reflex/.templates/apps/demo/code/state.py @@ -0,0 +1,22 @@ +"""Base state for the app.""" + +import reflex as rx + + +class State(rx.State): + """State for the app.""" + + sidebar_displayed: bool = True + + @rx.var + def origin_url(self) -> str: + """Get the url of the current page. + + Returns: + str: The url of the current page. + """ + return self.router_data.get("asPath", "") + + def toggle_sidebar_displayed(self) -> None: + """Toggle the sidebar displayed.""" + self.sidebar_displayed = not self.sidebar_displayed diff --git a/reflex/.templates/apps/demo/code/states/form_state.py b/reflex/.templates/apps/demo/code/states/form_state.py new file mode 100644 index 000000000..2b30e859e --- /dev/null +++ b/reflex/.templates/apps/demo/code/states/form_state.py @@ -0,0 +1,40 @@ +import reflex as rx + +from ..state import State + + +class FormState(State): + """Form state.""" + + form_data: dict = {} + + def handle_submit(self, form_data: dict): + """Handle the form submit. + + Args: + form_data: The form data. + """ + self.form_data = form_data + + +class UploadState(State): + """The app state.""" + + # The images to show. + img: list[str] + + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + for file in files: + upload_data = await file.read() + outfile = rx.get_asset_path(file.filename) + # Save the file. + with open(outfile, "wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img.append(f"/{file.filename}") diff --git a/reflex/.templates/apps/demo/code/states/pie_state.py b/reflex/.templates/apps/demo/code/states/pie_state.py new file mode 100644 index 000000000..1c380c3af --- /dev/null +++ b/reflex/.templates/apps/demo/code/states/pie_state.py @@ -0,0 +1,47 @@ +from typing import Any + +import reflex as rx + +from ..state import State + + +class PieChartState(State): + """Pie Chart State.""" + + resources: list[dict[str, Any]] = [ + dict(type_="🏆", count=1), + dict(type_="🪵", count=1), + dict(type_="🥑", count=1), + dict(type_="🧱", count=1), + ] + + @rx.cached_var + def resource_types(self) -> list[str]: + """Get the resource types. + + Returns: + The resource types. + """ + return [r["type_"] for r in self.resources] + + def increment(self, type_: str): + """Increment the count of a resource type. + + Args: + type_: The type of resource to increment. + """ + for resource in self.resources: + if resource["type_"] == type_: + resource["count"] += 1 + break + + def decrement(self, type_: str): + """Decrement the count of a resource type. + + Args: + type_: The type of resource to decrement. + """ + for resource in self.resources: + if resource["type_"] == type_ and resource["count"] > 0: + resource["count"] -= 1 + break diff --git a/reflex/.templates/apps/demo/code/styles.py b/reflex/.templates/apps/demo/code/styles.py new file mode 100644 index 000000000..934821ad6 --- /dev/null +++ b/reflex/.templates/apps/demo/code/styles.py @@ -0,0 +1,67 @@ +"""Styles for the app.""" +import reflex as rx + +from .state import State + +border_radius = "0.375rem" +box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)" +border = "1px solid #F4F3F6" +text_color = "black" +accent_text_color = "#1A1060" +accent_color = "#F5EFFE" +hover_accent_color = {"_hover": {"color": accent_color}} +hover_accent_bg = {"_hover": {"bg": accent_color}} +content_width_vw = "90vw" +sidebar_width = "20em" + +template_page_style = { + "padding_top": "5em", + "padding_x": "2em", +} + +template_content_style = { + "width": rx.cond( + State.sidebar_displayed, + f"calc({content_width_vw} - {sidebar_width})", + content_width_vw, + ), + "min-width": sidebar_width, + "align_items": "flex-start", + "box_shadow": box_shadow, + "border_radius": border_radius, + "padding": "1em", + "margin_bottom": "2em", +} + +link_style = { + "color": text_color, + "text_decoration": "none", + **hover_accent_color, +} + +overlapping_button_style = { + "background_color": "white", + "border": border, + "border_radius": border_radius, +} + +base_style = { + rx.MenuButton: { + "width": "3em", + "height": "3em", + **overlapping_button_style, + }, + rx.MenuItem: hover_accent_bg, +} + +tab_style = { + "color": "#494369", + "font_weight": 600, + "_selected": { + "color": "#5646ED", + "bg": "#F5EFFE", + "padding_x": "0.5em", + "padding_y": "0.25em", + "border_radius": "8px", + }, +} diff --git a/reflex/.templates/apps/demo/code/webui/__init__.py b/reflex/.templates/apps/demo/code/webui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reflex/.templates/apps/demo/code/webui/components/__init__.py b/reflex/.templates/apps/demo/code/webui/components/__init__.py new file mode 100644 index 000000000..e29eb0ab2 --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/components/__init__.py @@ -0,0 +1,4 @@ +from .loading_icon import loading_icon +from .modal import modal +from .navbar import navbar +from .sidebar import sidebar diff --git a/reflex/.templates/apps/demo/code/webui/components/chat.py b/reflex/.templates/apps/demo/code/webui/components/chat.py new file mode 100644 index 000000000..9ac6ddf9e --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/components/chat.py @@ -0,0 +1,118 @@ +import reflex as rx + +from ...webui import styles +from ...webui.components import loading_icon +from ...webui.state import QA, State + + +def message(qa: QA) -> rx.Component: + """A single question/answer message. + + Args: + qa: The question/answer pair. + + Returns: + A component displaying the question/answer pair. + """ + return rx.box( + rx.box( + rx.text( + qa.question, + bg=styles.border_color, + shadow=styles.shadow_light, + **styles.message_style, + ), + text_align="right", + margin_top="1em", + ), + rx.box( + rx.text( + qa.answer, + bg=styles.accent_color, + shadow=styles.shadow_light, + **styles.message_style, + ), + text_align="left", + padding_top="1em", + ), + width="100%", + ) + + +def chat() -> rx.Component: + """List all the messages in a single conversation. + + Returns: + A component displaying all the messages in a single conversation. + """ + return rx.vstack( + rx.box(rx.foreach(State.chats[State.current_chat], message)), + py="8", + flex="1", + width="100%", + max_w="3xl", + padding_x="4", + align_self="center", + overflow="hidden", + padding_bottom="5em", + **styles.base_style[rx.Vstack], + ) + + +def action_bar() -> rx.Component: + """The action bar to send a new message. + + Returns: + The action bar to send a new message. + """ + return rx.box( + rx.vstack( + rx.form( + rx.form_control( + rx.hstack( + rx.input( + placeholder="Type something...", + value=State.question, + on_change=State.set_question, + _placeholder={"color": "#fffa"}, + _hover={"border_color": styles.accent_color}, + style=styles.input_style, + ), + rx.button( + rx.cond( + State.processing, + loading_icon(height="1em"), + rx.text("Send"), + ), + type_="submit", + _hover={"bg": styles.accent_color}, + style=styles.input_style, + ), + **styles.base_style[rx.Hstack], + ), + is_disabled=State.processing, + ), + on_submit=State.process_question, + width="100%", + ), + rx.text( + "ReflexGPT may return factually incorrect or misleading responses. Use discretion.", + font_size="xs", + color="#fff6", + text_align="center", + ), + width="100%", + max_w="3xl", + mx="auto", + **styles.base_style[rx.Vstack], + ), + position="sticky", + bottom="0", + left="0", + py="4", + backdrop_filter="auto", + backdrop_blur="lg", + border_top=f"1px solid {styles.border_color}", + align_items="stretch", + width="100%", + ) diff --git a/reflex/.templates/apps/demo/code/webui/components/loading_icon.py b/reflex/.templates/apps/demo/code/webui/components/loading_icon.py new file mode 100644 index 000000000..2678e8fa9 --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/components/loading_icon.py @@ -0,0 +1,26 @@ +import reflex as rx + + +class LoadingIcon(rx.Component): + """A custom loading icon component.""" + + library = "react-loading-icons" + tag = "SpinningCircles" + stroke: rx.Var[str] + stroke_opacity: rx.Var[str] + fill: rx.Var[str] + fill_opacity: rx.Var[str] + stroke_width: rx.Var[str] + speed: rx.Var[str] + height: rx.Var[str] + + def get_event_triggers(self) -> dict: + """Get the event triggers that pass the component's value to the handler. + + Returns: + A dict mapping the event trigger to the var that is passed to the handler. + """ + return {"on_change": lambda status: [status]} + + +loading_icon = LoadingIcon.create diff --git a/reflex/.templates/apps/demo/code/webui/components/modal.py b/reflex/.templates/apps/demo/code/webui/components/modal.py new file mode 100644 index 000000000..ce51b9b6b --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/components/modal.py @@ -0,0 +1,56 @@ +import reflex as rx + +from ...webui.state import State + + +def modal() -> rx.Component: + """A modal to create a new chat. + + Returns: + The modal component. + """ + return rx.modal( + rx.modal_overlay( + rx.modal_content( + rx.modal_header( + rx.hstack( + rx.text("Create new chat"), + rx.icon( + tag="close", + font_size="sm", + on_click=State.toggle_modal, + color="#fff8", + _hover={"color": "#fff"}, + cursor="pointer", + ), + align_items="center", + justify_content="space-between", + ) + ), + rx.modal_body( + rx.input( + placeholder="Type something...", + on_blur=State.set_new_chat_name, + bg="#222", + border_color="#fff3", + _placeholder={"color": "#fffa"}, + ), + ), + rx.modal_footer( + rx.button( + "Create", + bg="#5535d4", + box_shadow="md", + px="4", + py="2", + h="auto", + _hover={"bg": "#4c2db3"}, + on_click=State.create_chat, + ), + ), + bg="#222", + color="#fff", + ), + ), + is_open=State.modal_open, + ) diff --git a/reflex/.templates/apps/demo/code/webui/components/navbar.py b/reflex/.templates/apps/demo/code/webui/components/navbar.py new file mode 100644 index 000000000..35c3e2522 --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/components/navbar.py @@ -0,0 +1,68 @@ +import reflex as rx + +from ...webui import styles +from ...webui.state import State + + +def navbar(): + return rx.box( + rx.hstack( + rx.hstack( + rx.icon( + tag="hamburger", + mr=4, + on_click=State.toggle_drawer, + cursor="pointer", + ), + rx.link( + rx.box( + rx.image(src="favicon.ico", width=30, height="auto"), + p="1", + border_radius="6", + bg="#F0F0F0", + mr="2", + ), + href="/", + ), + rx.breadcrumb( + rx.breadcrumb_item( + rx.heading("ReflexGPT", size="sm"), + ), + rx.breadcrumb_item( + rx.text(State.current_chat, size="sm", font_weight="normal"), + ), + ), + ), + rx.hstack( + rx.button( + "+ New chat", + bg=styles.accent_color, + px="4", + py="2", + h="auto", + on_click=State.toggle_modal, + ), + rx.menu( + rx.menu_button( + rx.avatar(name="User", size="md"), + rx.box(), + ), + rx.menu_list( + rx.menu_item("Help"), + rx.menu_divider(), + rx.menu_item("Settings"), + ), + ), + spacing="8", + ), + justify="space-between", + ), + bg=styles.bg_dark_color, + backdrop_filter="auto", + backdrop_blur="lg", + p="4", + border_bottom=f"1px solid {styles.border_color}", + position="sticky", + top="0", + z_index="100", + ) diff --git a/reflex/.templates/apps/demo/code/webui/components/sidebar.py b/reflex/.templates/apps/demo/code/webui/components/sidebar.py new file mode 100644 index 000000000..91b7acd8d --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/components/sidebar.py @@ -0,0 +1,66 @@ +import reflex as rx + +from ...webui import styles +from ...webui.state import State + + +def sidebar_chat(chat: str) -> rx.Component: + """A sidebar chat item. + + Args: + chat: The chat item. + + Returns: + The sidebar chat item. + """ + return rx.hstack( + rx.box( + chat, + on_click=lambda: State.set_chat(chat), + style=styles.sidebar_style, + color=styles.icon_color, + flex="1", + ), + rx.box( + rx.icon( + tag="delete", + style=styles.icon_style, + on_click=State.delete_chat, + ), + style=styles.sidebar_style, + ), + color=styles.text_light_color, + cursor="pointer", + ) + + +def sidebar() -> rx.Component: + """The sidebar component. + + Returns: + The sidebar component. + """ + return rx.drawer( + rx.drawer_overlay( + rx.drawer_content( + rx.drawer_header( + rx.hstack( + rx.text("Chats"), + rx.icon( + tag="close", + on_click=State.toggle_drawer, + style=styles.icon_style, + ), + ) + ), + rx.drawer_body( + rx.vstack( + rx.foreach(State.chat_titles, lambda chat: sidebar_chat(chat)), + align_items="stretch", + ) + ), + ), + ), + placement="left", + is_open=State.drawer_open, + ) diff --git a/reflex/.templates/apps/demo/code/webui/state.py b/reflex/.templates/apps/demo/code/webui/state.py new file mode 100644 index 000000000..4956e6f59 --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/state.py @@ -0,0 +1,145 @@ +import asyncio + +import reflex as rx + +from ..state import State + +# openai.api_key = os.environ["OPENAI_API_KEY"] +# openai.api_base = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1") + + +class QA(rx.Base): + """A question and answer pair.""" + + question: str + answer: str + + +DEFAULT_CHATS = { + "Intros": [], +} + + +class State(State): + """The app state.""" + + # A dict from the chat name to the list of questions and answers. + chats: dict[str, list[QA]] = DEFAULT_CHATS + + # The current chat name. + current_chat = "Intros" + + # The current question. + question: str + + # Whether we are processing the question. + processing: bool = False + + # The name of the new chat. + new_chat_name: str = "" + + # Whether the drawer is open. + drawer_open: bool = False + + # Whether the modal is open. + modal_open: bool = False + + def create_chat(self): + """Create a new chat.""" + # Add the new chat to the list of chats. + self.current_chat = self.new_chat_name + self.chats[self.new_chat_name] = [] + + # Toggle the modal. + self.modal_open = False + + def toggle_modal(self): + """Toggle the new chat modal.""" + self.modal_open = not self.modal_open + + def toggle_drawer(self): + """Toggle the drawer.""" + self.drawer_open = not self.drawer_open + + def delete_chat(self): + """Delete the current chat.""" + del self.chats[self.current_chat] + if len(self.chats) == 0: + self.chats = DEFAULT_CHATS + self.current_chat = list(self.chats.keys())[0] + self.toggle_drawer() + + def set_chat(self, chat_name: str): + """Set the name of the current chat. + + Args: + chat_name: The name of the chat. + """ + self.current_chat = chat_name + self.toggle_drawer() + + @rx.var + def chat_titles(self) -> list[str]: + """Get the list of chat titles. + + Returns: + The list of chat names. + """ + return list(self.chats.keys()) + + async def process_question(self, form_data: dict[str, str]): + """Get the response from the API. + + Args: + form_data: A dict with the current question. + + Yields: + The current question and the response. + """ + # Check if the question is empty + if self.question == "": + return + + # Add the question to the list of questions. + qa = QA(question=self.question, answer="") + self.chats[self.current_chat].append(qa) + + # Clear the input and start the processing. + self.processing = True + self.question = "" + yield + + # # Build the messages. + # messages = [ + # {"role": "system", "content": "You are a friendly chatbot named Reflex."} + # ] + # for qa in self.chats[self.current_chat]: + # messages.append({"role": "user", "content": qa.question}) + # messages.append({"role": "assistant", "content": qa.answer}) + + # # Remove the last mock answer. + # messages = messages[:-1] + + # Start a new session to answer the question. + # session = openai.ChatCompletion.create( + # model=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"), + # messages=messages, + # stream=True, + # ) + + # Stream the results, yielding after every word. + # for item in session: + answer = "I don't know! This Chatbot still needs to add in AI API keys!" + for i in range(len(answer)): + # Pause to show the streaming effect. + await asyncio.sleep(0.1) + # Add one letter at a time to the output. + + # if hasattr(item.choices[0].delta, "content"): + # answer_text = item.choices[0].delta.content + self.chats[self.current_chat][-1].answer += answer[i] + self.chats = self.chats + yield + + # Toggle the processing flag. + self.processing = False diff --git a/reflex/.templates/apps/demo/code/webui/styles.py b/reflex/.templates/apps/demo/code/webui/styles.py new file mode 100644 index 000000000..7abc579fb --- /dev/null +++ b/reflex/.templates/apps/demo/code/webui/styles.py @@ -0,0 +1,88 @@ +import reflex as rx + +bg_dark_color = "#111" +bg_medium_color = "#222" + +border_color = "#fff3" + +accennt_light = "#6649D8" +accent_color = "#5535d4" +accent_dark = "#4c2db3" + +icon_color = "#fff8" + +text_light_color = "#fff" +shadow_light = "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px;" +shadow = "rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;" + +message_style = dict(display="inline-block", p="4", border_radius="xl", max_w="30em") + +input_style = dict( + bg=bg_medium_color, + border_color=border_color, + border_width="1px", + p="4", +) + +icon_style = dict( + font_size="md", + color=icon_color, + _hover=dict(color=text_light_color), + cursor="pointer", + w="8", +) + +sidebar_style = dict( + border="double 1px transparent;", + border_radius="10px;", + background_image=f"linear-gradient({bg_dark_color}, {bg_dark_color}), radial-gradient(circle at top left, {accent_color},{accent_dark});", + background_origin="border-box;", + background_clip="padding-box, border-box;", + p="2", + _hover=dict( + background_image=f"linear-gradient({bg_dark_color}, {bg_dark_color}), radial-gradient(circle at top left, {accent_color},{accennt_light});", + ), +) + +base_style = { + rx.Avatar: { + "shadow": shadow, + "color": text_light_color, + # "bg": border_color, + }, + rx.Button: { + "shadow": shadow, + "color": text_light_color, + "_hover": { + "bg": accent_dark, + }, + }, + rx.Menu: { + "bg": bg_dark_color, + "border": f"red", + }, + rx.MenuList: { + "bg": bg_dark_color, + "border": f"1.5px solid {bg_medium_color}", + }, + rx.MenuDivider: { + "border": f"1px solid {bg_medium_color}", + }, + rx.MenuItem: { + "bg": bg_dark_color, + "color": text_light_color, + }, + rx.DrawerContent: { + "bg": bg_dark_color, + "color": text_light_color, + "opacity": "0.9", + }, + rx.Hstack: { + "align_items": "center", + "justify_content": "space-between", + }, + rx.Vstack: { + "align_items": "stretch", + "justify_content": "space-between", + }, +}