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 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
13 changes: 13 additions & 0 deletions reflex/utils/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,3 +766,16 @@ def format_data_editor_cell(cell: Any):
"kind": Var(_js_expr="GridCellKind.Text"),
"data": cell,
}


def format_template_name(name: str) -> str:
"""Format the template name of remote templates obtained during reflex init.
Args:
name: The name of the template.
Returns:
The formatted template name.
"""
formatted_name = to_kebab_case(name)
return formatted_name.removesuffix("-app").removesuffix("-template")
233 changes: 190 additions & 43 deletions reflex/utils/prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
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,
)
from reflex.utils.format import format_library_name
from reflex.utils.format import format_library_name, format_template_name
from reflex.utils.registry import _get_npm_registry

CURRENTLY_INSTALLING_NODE = False
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 @@ -1223,7 +1223,9 @@ def prompt_for_template(templates: list[Template]) -> str:

# 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('-', ' ')} ({template.demo_url}) - {template.description}"
for idx, template in enumerate(templates)
}
for id in range(len(id_to_name)):
Expand Down Expand Up @@ -1289,6 +1291,7 @@ def get_release_by_tag(tag: str) -> dict | None:

filtered_templates = {}
for tp in templates_data:
tp["name"] = format_template_name(tp["name"])
if tp["hidden"] or tp["code_url"] is None:
continue
known_fields = set(f.name for f in dataclasses.fields(Template))
Expand Down Expand Up @@ -1378,15 +1381,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)
console.info(available_templates)
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.")

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 +1525,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 (Flexgen)",
demo_url=constants.Templates.REFLEX_BUILD_FRONTEND,
code_url="",
),
Template(
name=constants.Templates.CHOOSE_TEMPLATES,
description="Choose an existing template.",
demo_url=constants.Templates.REFLEX_TEMPLATES_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
Loading