Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GTM-836]Rework Init workflow #4377

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions reflex/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ class Templates(SimpleNamespace):
# The default template
DEFAULT = "blank"

# The AI template
AI = "ai"

# The option for the user to choose a remote template.
CHOOSE_TEMPLATES = "choose-templates"

# The URL to find reflex templates.
REFLEX_TEMPLATES_URL = "https://reflex.dev/templates"

# Demo url for the default template.
DEFAULT_TEMPLATE_URL = "https://blank-template.reflex.run"

# The reflex.build frontend host
REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run"

Expand Down
28 changes: 3 additions & 25 deletions reflex/reflex.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from reflex.config import environment, get_config
from reflex.custom_components.custom_components import custom_components_cli
from reflex.state import reset_disk_state_manager
from reflex.utils import console, redir, telemetry
from reflex.utils import console, telemetry

# Disable typer+rich integration for help panels
typer.core.rich = False # type: ignore
Expand Down Expand Up @@ -89,38 +89,16 @@ def _init(
# Set up the web project.
prerequisites.initialize_frontend_dependencies()

# Integrate with reflex.build.
generation_hash = None
if ai:
if template is None:
# If AI is requested and no template specified, redirect the user to reflex.build.
generation_hash = redir.reflex_build_redirect()
elif prerequisites.is_generation_hash(template):
# Otherwise treat the template as a generation hash.
generation_hash = template
else:
console.error(
"Cannot use `--template` option with `--ai` option. Please remove `--template` option."
)
raise typer.Exit(2)
template = constants.Templates.DEFAULT

# Initialize the app.
template = prerequisites.initialize_app(app_name, template)

# If a reflex.build generation hash is available, download the code and apply it to the main module.
if generation_hash:
prerequisites.initialize_main_module_index_from_generation(
app_name, generation_hash=generation_hash
)
template = prerequisites.initialize_app(app_name, template, ai)

# Initialize the .gitignore.
prerequisites.initialize_gitignore()

# Initialize the requirements.txt.
prerequisites.initialize_requirements_txt()

template_msg = "" if template else f" using the {template} template"
template_msg = f" using the {template} template" if template else ""
# Finish initializing the app.
console.success(f"Initialized {app_name}{template_msg}")

Expand Down
233 changes: 191 additions & 42 deletions reflex/utils/prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from reflex import constants, model
from reflex.compiler import templates
from reflex.config import Config, environment, get_config
from reflex.utils import console, net, path_ops, processes
from reflex.utils import console, net, path_ops, processes, redir
from reflex.utils.exceptions import (
GeneratedCodeHasNoFunctionDefs,
raise_system_package_missing_error,
Expand Down Expand Up @@ -1209,7 +1209,7 @@ def check_schema_up_to_date():
)


def prompt_for_template(templates: list[Template]) -> str:
def prompt_for_template_options(templates: list[Template]) -> str:
"""Prompt the user to specify a template.

Args:
Expand All @@ -1221,9 +1221,14 @@ def prompt_for_template(templates: list[Template]) -> str:
# Show the user the URLs of each template to preview.
console.print("\nGet started with a template:")

def format_demo_url_str(url: str) -> str:
return f" ({url})" if url else ""

# Prompt the user to select a template.
id_to_name = {
str(idx): f"{template.name} ({template.demo_url}) - {template.description}"
str(
idx
): f"{template.name.replace('_', ' ').replace('-', ' ')}{format_demo_url_str(template.demo_url)} - {template.description}"
for idx, template in enumerate(templates)
}
for id in range(len(id_to_name)):
Expand Down Expand Up @@ -1378,15 +1383,138 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
shutil.rmtree(unzip_dir)


def initialize_app(app_name: str, template: str | None = None) -> str | None:
"""Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.
def prompt_for_remote_template_selection(templates: dict[str, Template]) -> str:
"""Prompt the user to input a remote template.

Args:
templates: The available templates.

Returns:
The selected template.
"""
while True:
console.print(
f"Visit {constants.Templates.REFLEX_TEMPLATES_URL} for the complete list of templates."
)
selected_template = console.ask(
"Enter a valid template name", show_choices=False
)
if selected_template not in templates:
console.error("Invalid template name. Please try again.")
else:
return selected_template


def initialize_default_app(app_name: str):
"""Initialize the default app.

Args:
app_name: The name of the app.
template: The name of the template to use.
"""
create_config(app_name)
initialize_app_directory(app_name)


def validate_and_create_app_using_remote_template(app_name, template, templates):
"""Validate and create an app using a remote template.

Args:
app_name: The name of the app.
template: The name of the template.
templates: The available templates.

Raises:
Exit: If template is directly provided in the command flag and is invalid.
Exit: If the template is not found.
"""
# If user selects a template, it needs to exist.
if template in templates:
template_url = templates[template].code_url
else:
# Check if the template is a github repo.
if template.startswith("https://github.com"):
template_url = f"{template.strip('/').replace('.git', '')}/archive/main.zip"
else:
console.error(f"Template `{template}` not found.")
raise typer.Exit(1)

if template_url is None:
return

create_config_init_app_from_remote_template(
app_name=app_name, template_url=template_url
)


def generate_template_using_ai(template: str | None = None) -> str:
"""Generate a template using AI(Flexgen).

Args:
template: The name of the template.

Returns:
The generation hash.

Raises:
Exit: If the template and ai flags are used.
"""
if template is None:
# If AI is requested and no template specified, redirect the user to reflex.build.
return redir.reflex_build_redirect()
elif is_generation_hash(template):
# Otherwise treat the template as a generation hash.
return template
else:
console.error(
"Cannot use `--template` option with `--ai` option. Please remove `--template` option."
)
raise typer.Exit(2)


def fetch_and_prompt_with_remote_templates(
template: str, show_prompt: bool = True
) -> tuple[str, dict[str, Template]]:
"""Fetch the available remote templates and prompt the user for an input.

Args:
template: The name of the template.
show_prompt: Whether to show the prompt.

Returns:
The selected template and the available templates.
"""
available_templates = {}

try:
# Get the available templates
available_templates = fetch_app_templates(constants.Reflex.VERSION)
if not show_prompt and template in available_templates:
return template, available_templates

if not show_prompt and (template not in available_templates):
console.error(f"{template!r} is not a valid template name.")

redir.open_browser(constants.Templates.REFLEX_TEMPLATES_URL)
template = (
prompt_for_remote_template_selection(available_templates)
if available_templates
else constants.Templates.DEFAULT
)
except Exception as e:
console.warn("Failed to fetch templates. Falling back to default template.")
console.debug(f"Error while fetching templates: {e}")

return (template or constants.Templates.DEFAULT), available_templates


def initialize_app(
app_name: str, template: str | None = None, ai: bool = False
) -> str | None:
"""Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.

Args:
app_name: The name of the app.
template: The name of the template to use.
ai: Whether to use AI to generate the template.

Returns:
The name of the template.
Expand All @@ -1399,54 +1527,75 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None:
telemetry.send("reinit")
return

generation_hash = None
if ai:
generation_hash = generate_template_using_ai(template)
template = constants.Templates.DEFAULT

templates: dict[str, Template] = {}

# Don't fetch app templates if the user directly asked for DEFAULT.
if template is None or (template != constants.Templates.DEFAULT):
try:
# Get the available templates
templates = fetch_app_templates(constants.Reflex.VERSION)
if template is None and len(templates) > 0:
template = prompt_for_template(list(templates.values()))
except Exception as e:
console.warn("Failed to fetch templates. Falling back to default template.")
console.debug(f"Error while fetching templates: {e}")
finally:
template = template or constants.Templates.DEFAULT
if template is not None and (template not in (constants.Templates.DEFAULT,)):
template, templates = fetch_and_prompt_with_remote_templates(
template, show_prompt=False
)

if template is None:
template = prompt_for_template_options(get_init_cli_prompt_options())
if template == constants.Templates.AI:
generation_hash = generate_template_using_ai()
# change to the default to allow creation of default app
template = constants.Templates.DEFAULT
elif template == constants.Templates.CHOOSE_TEMPLATES:
template, templates = fetch_and_prompt_with_remote_templates(template)

# If the blank template is selected, create a blank app.
if template == constants.Templates.DEFAULT:
if template in (constants.Templates.DEFAULT,):
# Default app creation behavior: a blank app.
create_config(app_name)
initialize_app_directory(app_name)
initialize_default_app(app_name)
else:
# Fetch App templates from the backend server.
console.debug(f"Available templates: {templates}")

# If user selects a template, it needs to exist.
if template in templates:
template_url = templates[template].code_url
else:
# Check if the template is a github repo.
if template.startswith("https://github.com"):
template_url = (
f"{template.strip('/').replace('.git', '')}/archive/main.zip"
)
else:
console.error(f"Template `{template}` not found.")
raise typer.Exit(1)

if template_url is None:
return

create_config_init_app_from_remote_template(
app_name=app_name, template_url=template_url
validate_and_create_app_using_remote_template(
app_name=app_name, template=template, templates=templates
)

# If a reflex.build generation hash is available, download the code and apply it to the main module.
if generation_hash:
initialize_main_module_index_from_generation(
app_name, generation_hash=generation_hash
)
telemetry.send("init", template=template)

return template


def get_init_cli_prompt_options() -> list[Template]:
"""Get the CLI options for initializing a Reflex app.

Returns:
The CLI options.
"""
return [
Template(
name=constants.Templates.DEFAULT,
description="A blank Reflex app.",
demo_url=constants.Templates.DEFAULT_TEMPLATE_URL,
code_url="",
),
Template(
name=constants.Templates.AI,
description="Generate a template using AI [Experimental]",
demo_url="",
code_url="",
),
Template(
name=constants.Templates.CHOOSE_TEMPLATES,
description="Choose an existing template.",
demo_url="",
code_url="",
),
]


def initialize_main_module_index_from_generation(app_name: str, generation_hash: str):
"""Overwrite the `index` function in the main module with reflex.build generated code.

Expand Down
17 changes: 13 additions & 4 deletions reflex/utils/redir.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
from . import console


def open_browser(target_url: str) -> None:
"""Open a browser window to target_url.

Args:
target_url: The URL to open in the browser.
"""
if not webbrowser.open(target_url):
console.warn(
f"Unable to automatically open the browser. Please navigate to {target_url} in your browser."
)


def open_browser_and_wait(
target_url: str, poll_url: str, interval: int = 2
) -> httpx.Response:
Expand All @@ -23,10 +35,7 @@ def open_browser_and_wait(
Returns:
The response from the poll_url.
"""
if not webbrowser.open(target_url):
console.warn(
f"Unable to automatically open the browser. Please navigate to {target_url} in your browser."
)
open_browser(target_url)
console.info("[b]Complete the workflow in the browser to continue.[/b]")
while True:
try:
Expand Down
Loading