import json import os import time from inspect import signature import huggingface_hub as hub import gradio as gr import gradio.utils from gradio.sketch.sketchbox import SketchBox from gradio.sketch.utils import ai, get_header, set_kwarg def create(app_file: str, config_file: str): file_name = os.path.basename(app_file) folder_name = os.path.basename(os.path.dirname(app_file)) created_fns_namespace = {} nonconfigurable_params = ["every", "inputs", "render", "key"] default_kwargs_map = { gr.Image: {"type": "filepath"}, gr.Audio: {"type": "filepath"}, gr.Chatbot: {"type": "messages"}, } quick_component_list = [ gr.Textbox, gr.Number, gr.Button, gr.Markdown, gr.State, ] all_component_list = [ gr.AnnotatedImage, # gr.Accordion, gr.Audio, gr.BarPlot, gr.BrowserState, gr.Button, gr.Chatbot, gr.Checkbox, gr.CheckboxGroup, gr.Code, gr.ColorPicker, gr.Dataframe, gr.DateTime, gr.Dropdown, gr.File, gr.Gallery, gr.HighlightedText, gr.HTML, gr.Image, gr.ImageEditor, gr.JSON, gr.Label, gr.LinePlot, gr.Markdown, gr.Model3D, gr.MultimodalTextbox, gr.Number, gr.Radio, gr.Slider, gr.State, gr.Textbox, gr.Timer, gr.Video, ] def get_component_by_name(name): return [ component for component in all_component_list if component.__name__ == name ][0] def get_box(_slot, i, gp=None): parent = _slot target = _slot[i[0]] if isinstance(_slot, list) and i[0] < len(_slot) else None if len(i) > 1: gp, parent, target = get_box(target, i[1:], parent) return gp, parent, target def add_component( component, layout, components, dependencies, add_index, new_component_id ): gp, parent, _ = get_box(layout, add_index) if isinstance(parent, int): parent = [parent] if gp: gp[add_index[-2]] = parent parent.insert(add_index[-1], new_component_id) default_kwargs = default_kwargs_map.get(component, {}).copy() components[new_component_id] = [component.__name__, default_kwargs, ""] component_name = component.__name__.lower() existing_names = [components[i][2] for i in components] var_name = component_name i = 2 while var_name in existing_names: var_name = component_name + "_" + str(i) i += 1 components[new_component_id][2] = var_name return ( layout, components, dependencies, "modify_component", new_component_id, new_component_id + 1, gr.Button(interactive=True), ) def set_hf_token(token): try: hub.login(token) except BaseException as err: raise gr.Error("Invalid Hugging Face token.") from err gr.Success("Token set successfully.", duration=2) return token with gr.Blocks() as demo: _id = gr.State(0) # Below was giving issues, commenting out for now # if os.path.exists(config_file): # with open(config_file) as f: # config = json.load(f) # _layout = config["layout"] # _components = {int(k): v for k, v in config["components"].items()} # _new_component_id = len(_components) # mode = gr.State("default") # else: _layout = [] _components = {} _dependencies = [] _new_component_id = 0 mode = gr.State("add_component") new_component_id = gr.State(_new_component_id) components = gr.State(_components) dependencies = gr.State(_dependencies) layout = gr.State(_layout) add_index = gr.State([_new_component_id]) modify_id = gr.State(None) saved = gr.State(False) hf_token = gr.State(hub.get_token() or os.getenv("HF_TOKEN")) add_fn_btn = gr.Button( "+ Add Function", scale=0, interactive=False, render=False ) with gr.Sidebar() as left_sidebar: @gr.render( [ mode, add_index, new_component_id, components, dependencies, modify_id, hf_token, ], show_progress="hidden", ) def render_sidebar( _mode, _add_index, _new_component_id, _components, _dependencies, _modify_id, _hf_token, ): if _mode == "default" and len(_components) == 0: _mode = "add_component" _add_index = [0] if _mode == "default": gr.Markdown("## Placement") gr.Markdown("Click on a '+' button to add a component.") if _mode == "add_component": gr.Markdown("## Selection") if len(_components) == 0: gr.Markdown("Select first component to place.") else: gr.Markdown("Select component to place in selected area.") for component in quick_component_list: gr.Button(component.__name__, size="md").click( lambda _layout, _component=component: add_component( _component, _layout, _components, _dependencies, _add_index, _new_component_id, ), layout, [ layout, components, dependencies, mode, modify_id, new_component_id, add_fn_btn, ], ) any_component_search = gr.Dropdown( [component.__name__ for component in all_component_list], container=True, label="Other Components...", interactive=True, ) any_component_search.change( lambda _component, _layout: add_component( get_component_by_name(_component), _layout, _components, _dependencies, _add_index, _new_component_id, ), [any_component_search, layout], [ layout, components, dependencies, mode, modify_id, new_component_id, add_fn_btn, ], ) if _mode == "modify_component": component_name, kwargs, var_name = _components[_modify_id] gr.Markdown( "## Configuration\nHover over a component to add new components when done configuring." ) var_name_box = gr.Textbox(var_name, label="Variable Name") def set_var_name(name): _components[_modify_id][2] = name return _components gr.on( [var_name_box.blur, var_name_box.submit], set_var_name, var_name_box, components, ) gr.Markdown( 'Set args below with python syntax, e.g. `True`, `5`, or `["choice1", "choice2"]`.' ) component = get_component_by_name(component_name) arguments = list(signature(component.__init__).parameters.keys())[ 1: ] for arg in arguments: if arg in nonconfigurable_params: continue arg_value = kwargs.get(arg, "") arg_box = gr.Textbox( arg_value, label=arg, info=f"docs", ) def set_arg(value, arg=arg): set_kwarg(_components[_modify_id][1], arg, value) return _components gr.on( [arg_box.blur, arg_box.submit], set_arg, arg_box, components ) if _mode == "modify_function": dep = _dependencies[_modify_id] _triggers, _inputs, _outputs, var_name, _history, _code = dep gr.Markdown("## Event Listeners") function_name_box = gr.Textbox(var_name, label="Function Name") def set_fn_name(name): dep[3] = name return _dependencies gr.on( [function_name_box.blur, function_name_box.submit], set_fn_name, function_name_box, dependencies, ) gr.Markdown( "Mark the components in the diagram as inputs or outputs, and select their triggers. Then use the code generator below." ) if not _hf_token: input_hf_token = gr.Textbox( label="HF Token", info="Needed for code generation. Copy from [HF Token Page](https://huggingface.co/settings/token). Token requires access to inference providers.", type="password", ) submit_token_btn = gr.Button("Submit Token", size="md") submit_token_btn.click(set_hf_token, input_hf_token, hf_token) else: new_prompt_placeholder = "Describe what the function should do." edit_prompt_placeholder = "Describe how to change the code generation. Click 'Reset Code' to start over." history_exists = len(_history) > 0 prompt = gr.Textbox( label="Prompt", lines=3, placeholder=edit_prompt_placeholder if history_exists else new_prompt_placeholder, interactive=True, ) no_components_are_set = ( len(_dependencies[_modify_id][1]) == 0 + len(_dependencies[_modify_id][2]) == 0 ) if no_components_are_set: gr.Markdown( "Set **all inputs and outputs** before generating code." ) new_generate_text = "Generate Code" update_generate_text = "Update Code" generate_code_btn = gr.Button( update_generate_text if history_exists else new_generate_text, size="md", interactive=not no_components_are_set, ) reset_code_btn = gr.Button( "Reset Code", size="md", visible=history_exists ) __inputs = [_components[c][2] for c in _inputs] __outputs = [_components[c][2] for c in _outputs] _code = ( _code if _code is not None else f"""{get_header(var_name, __inputs)} ... return {", ".join(["..." for _ in __outputs])}""" ) fn_code = gr.Code(_code, lines=4, language="python") save_code_btn = gr.Button("Save Code", size="md") history = gr.JSON(_history, visible=False) def generate(_prompt, _history): yield from ai( _history + [[_prompt, None]], _hf_token, var_name, [ ( _components[c][2], get_component_by_name(_components[c][0]), _components[c][1], ) for c in _inputs ], [ ( get_component_by_name(_components[c][0]), _components[c][1], ) for c in _outputs ], ) def append_to_history( history: list[tuple[str, str]], prompt: str, code: str ): history.append((prompt, code)) return ( history, gr.Button(visible=True), gr.Textbox( value="", placeholder=edit_prompt_placeholder ), gr.Button(update_generate_text), ) generate_code_btn.click( generate, [prompt, history], fn_code ).then( append_to_history, [history, prompt, fn_code], [history, reset_code_btn, prompt, generate_code_btn], show_progress="hidden", ) def reset_code(_dependencies, _modify_id): _dependencies[_modify_id][4] = [] _dependencies[_modify_id][5] = None return ( get_header(var_name, __inputs), gr.Button(visible=False), gr.Textbox(placeholder=new_prompt_placeholder), gr.Button(new_generate_text), [], _dependencies, ) reset_code_btn.click( reset_code, [dependencies, modify_id], [ fn_code, reset_code_btn, prompt, generate_code_btn, history, dependencies, ], ) def save_code(_history, _code): try: exec(_code, created_fns_namespace) except BaseException as e: raise gr.Error(f"Error saving function: {e}") from e if var_name not in created_fns_namespace: raise gr.Error( f"Function '{var_name}' not found in code." ) dep[4] = ( [] if len(_history) == 0 else _history[:-1] + [[_history[-1][0], _code]] ) dep[5] = _code gr.Success("Function saved.", duration=2) return _dependencies save_code_btn.click(save_code, [history, fn_code], dependencies) done_function_btn = gr.Button("Done", variant="primary", size="md") done_function_btn.click( lambda: ["default", None], None, [mode, modify_id] ) del_function_btn = gr.Button( "Delete Function", variant="stop", size="md" ) def del_function(): del _dependencies[_modify_id] return _dependencies, "default", None del_function_btn.click( del_function, None, [dependencies, mode, modify_id] ) with gr.Row(): gr.Markdown("## Sketching '" + folder_name + "/" + file_name + "'") add_fn_btn.render() save_btn = gr.Button("Save & Render", variant="primary", scale=0) deploy_to_spaces_btn = gr.Button( "Deploy to Spaces", visible=False, scale=0, min_width=240, icon=gradio.utils.get_icon_path("huggingface-logo.svg"), ) @gr.render( [layout, components, dependencies, saved, modify_id, mode], show_progress="hidden", ) def app(_layout, _components, _dependencies, saved, _modify_id, _mode): boxes = [] rendered_components = {} function_mode = _mode == "modify_function" def render_slot(slot, is_column, index, depth=1): container = gr.Column() if is_column else gr.Row() with container: for i, element in enumerate(slot): this_index = index + [i] if isinstance(element, list): if saved: render_slot( element, not is_column, this_index, depth + 1 ) else: with SketchBox( is_container=True, function_mode=function_mode ) as box: render_slot( element, not is_column, this_index, depth + 1 ) boxes.append((box, this_index)) continue component_name, kwargs, var_name = _components[element] component = get_component_by_name(component_name) if saved: rendered_components[element] = component(**kwargs) else: if function_mode: triggers = [ t for c, t in _dependencies[_modify_id][0] if c == element ] is_input = element in _dependencies[_modify_id][1] is_output = element in _dependencies[_modify_id][2] else: triggers = None is_input = False is_output = False with SketchBox( component_type=component.__name__.lower(), var_name=var_name, active=_modify_id == element and not function_mode, function_mode=function_mode, event_list=component.EVENTS if hasattr(component, "EVENTS") else None, is_input=is_input, is_output=is_output, triggers=triggers, ) as box: component(**kwargs) boxes.append((box, this_index)) render_slot(_layout, True, []) for box, index in boxes: def box_action( _layout, _components, _dependencies, _modify_id, data: gr.SelectData, index=index, ): if data.value in ("up", "down", "left", "right"): if len(index) % 2 == 1: # vertical if data.value == "down": index[-1] += 1 elif data.value == "left": index.append(0) elif data.value == "right": index.append(1) elif data.value == "right": index[-1] += 1 elif data.value == "up": index.append(0) elif data.value == "down": index.append(1) return ( _layout, _components, _dependencies, "add_component", index, None, ) if data.value == "delete": def delete_index(_layout, index): gp, parent, target = get_box(_layout, index) parent.remove(target) if isinstance(target, int): del _components[target] if len(parent) == 0 and len(index) > 1: delete_index(_layout, index[:-1]) elif len(parent) == 1 and gp: gp[index[-2]] = parent[0] delete_index(_layout, index) if len(_layout) == 0: return ( _layout, _components, _dependencies, "add_component", [0], None, ) else: return ( _layout, _components, _dependencies, "default", [], None, ) if data.value == "modify": *_, target = get_box(_layout, index) return ( _layout, _components, _dependencies, "modify_component", None, target, ) if data.value in ["input", "output"]: *_, target = get_box(_layout, index) component_list = _dependencies[_modify_id][ 1 if data.value == "input" else 2 ] if target in component_list: component_list.remove(target) else: component_list.append(target) return ( _layout, _components, _dependencies, "modify_function", None, _modify_id, ) if data.value.startswith("on:"): *_, target = get_box(_layout, index) event = data.value[3:] triggers = _dependencies[_modify_id][0] if (target, event) in triggers: triggers.remove((target, event)) else: triggers.append((target, event)) return ( _layout, _components, _dependencies, "modify_function", None, _modify_id, ) box.select( box_action, [layout, components, dependencies, modify_id], [layout, components, dependencies, mode, add_index, modify_id], ) if saved: for triggers, inputs, outputs, fn_name, *_, code in _dependencies: rendered_triggers = [ getattr(rendered_components[c], t) for c, t in triggers ] rendered_inputs = [rendered_components[c] for c in inputs] rendered_outputs = [rendered_components[c] for c in outputs] if code: try: gr.on( rendered_triggers, created_fns_namespace[fn_name], rendered_inputs, rendered_outputs, ) except Exception: pass else: output_count = len(rendered_outputs) fn_output = ( [gr.skip()] * output_count if output_count > 1 else gr.skip() if output_count == 1 else None ) def sleep(*_): print("sleeping") time.sleep(1) return fn_output gr.on( rendered_triggers, sleep, rendered_inputs, rendered_outputs ) with gr.Sidebar(position="right", open=False) as right_sidebar: gr.Markdown("## Functions") @gr.render([dependencies], show_progress="hidden") def render_deps(_dependencies): for i, dep in enumerate(_dependencies): fn_btn = gr.Button(dep[3], size="md") def load_fn(i=i): return "modify_function", i fn_btn.click(load_fn, outputs=[mode, modify_id]) def add_fn(_dependencies): _dependencies.append( [[], [], [], f"fn_{len(_dependencies) + 1}", [], None] ) return ( _dependencies, "modify_function", len(_dependencies) - 1, gr.Sidebar(open=True), ) add_fn_btn.click( add_fn, dependencies, [dependencies, mode, modify_id, right_sidebar] ) gr.Markdown("## Generated File") code = gr.Code(language="python", interactive=False, show_label=False) @gr.on( inputs=[layout, components, dependencies], outputs=code, show_progress="hidden", ) def render_code(_layout, _components, _dependencies): code_str = "" def render_code_slot(slot, is_column, index, depth=1): nonlocal code_str for i, element in enumerate(slot): this_index = index + [i] if isinstance(element, list): code_str += ( " " * depth + "with gr." + ("Row" if is_column else "Column") + "():\n" ) render_code_slot( element, not is_column, this_index, depth + 1 ) continue component_name, kwargs, var_name = _components[element] code_str += ( " " * depth + var_name + " = gr." + component_name + "(" ) for i, (k, v) in enumerate(kwargs.items()): v = ( f'"{v}"'.replace("\n", "\\n") if isinstance(v, str) else v ) if i != 0: code_str += ", " code_str += f"{k}={v}" code_str += ")\n" render_code_slot(_layout, True, []) for dep in _dependencies: triggers = [_components[c][2] + "." + t for c, t in dep[0]] inputs = [_components[c][2] for c in dep[1]] outputs = [_components[c][2] for c in dep[2]] fn_name = dep[3] if dep[5] is not None: fn_code = dep[5].replace("\n", "\n ") else: fn_code = f"""def {fn_name}({", ".join(inputs)}): ... return {", ".join(["..." for _ in outputs])}""" code_str += f""" @{triggers[0] + "(" if len(triggers) == 1 else "gr.on([" + ", ".join(triggers) + "], "}inputs=[{", ".join(inputs)}], outputs=[{", ".join(outputs)}]) {fn_code} """ code_str = f"""import gradio as gr with gr.Blocks() as demo: {code_str} demo.launch()""" return code_str @save_btn.click( inputs=[saved, code, dependencies], outputs=[ saved, save_btn, add_fn_btn, deploy_to_spaces_btn, mode, left_sidebar, right_sidebar, ], show_progress="hidden", ) def save(saved, code, deps): with open(app_file, "w") as f: f.write(code) with open(config_file, "w") as f: json.dump( { "layout": _layout, "components": _components, }, f, ) return [ not saved, "Save & Render" if saved else "Edit Sketch", gr.Button(visible=saved), gr.Button(visible=not saved), "default", gr.Sidebar(open=saved), gr.Sidebar(open=saved and len(deps) > 0), ] deploy_to_spaces_btn.click( fn=None, inputs=code, js="""(code) => { code = encodeURIComponent(code); url = `https://huggingface.co/new-space?name=new-space&sdk=gradio&files[0][path]=app.py&files[0][content]=${code}` window.open(url, '_blank') }""", ) return demo if __name__ == "__main__": demo = create("app.py", "app.json") demo.launch()