diff --git a/gooey_ui/components/__init__.py b/gooey_ui/components/__init__.py deleted file mode 100644 index 2c27edd1d..000000000 --- a/gooey_ui/components/__init__.py +++ /dev/null @@ -1,1009 +0,0 @@ -import base64 -import html as html_lib -import math -import textwrap -import typing -from datetime import datetime, timezone - -import numpy as np -from furl import furl - -from daras_ai.image_input import resize_img_scale -from gooey_ui import state -from gooey_ui.pubsub import md5_values - -T = typing.TypeVar("T") -LabelVisibility = typing.Literal["visible", "collapsed"] - -BLANK_OPTION = "———" - - -def _default_format(value: typing.Any) -> str: - if value is None: - return BLANK_OPTION - return str(value) - - -def dummy(*args, **kwargs): - return state.NestingCtx() - - -spinner = dummy -set_page_config = dummy -form = dummy -dataframe = dummy - - -def countdown_timer( - end_time: datetime, - delay_text: str, -) -> state.NestingCtx: - return _node( - "countdown-timer", - endTime=end_time.astimezone(timezone.utc).isoformat(), - delayText=delay_text, - ) - - -def nav_tabs(): - return _node("nav-tabs") - - -def nav_item(href: str, *, active: bool): - return _node("nav-item", to=href, active="true" if active else None) - - -def nav_tab_content(): - return _node("nav-tab-content") - - -def div(**props) -> state.NestingCtx: - return tag("div", **props) - - -def link(*, to: str, **props) -> state.NestingCtx: - return _node("Link", to=to, **props) - - -def tag(tag_name: str, **props) -> state.NestingCtx: - props["__reactjsxelement"] = tag_name - return _node("tag", **props) - - -def html(body: str, **props): - props["className"] = props.get("className", "") + " gui-html-container" - return _node("html", body=body, **props) - - -def write(*objs: typing.Any, line_clamp: int = None, unsafe_allow_html=False, **props): - for obj in objs: - markdown( - obj if isinstance(obj, str) else repr(obj), - line_clamp=line_clamp, - unsafe_allow_html=unsafe_allow_html, - **props, - ) - - -def center(direction="flex-column", className="") -> state.NestingCtx: - return div( - className=f"d-flex justify-content-center align-items-center text-center {direction} {className}" - ) - - -def newline(): - html("
") - - -def markdown( - body: str | None, *, line_clamp: int = None, unsafe_allow_html=False, **props -): - if body is None: - return _node("markdown", body="", **props) - if not unsafe_allow_html: - body = html_lib.escape(body) - props["className"] = ( - props.get("className", "") + " gui-html-container gui-md-container" - ) - return _node("markdown", body=dedent(body).strip(), lineClamp=line_clamp, **props) - - -def _node(name: str, **props): - node = state.RenderTreeNode(name=name, props=props) - node.mount() - return state.NestingCtx(node) - - -def text(body: str, **props): - state.RenderTreeNode( - name="pre", - props=dict(body=dedent(body), **props), - ).mount() - - -def error( - body: str, - icon: str = "🔥", - *, - unsafe_allow_html=False, - color="rgba(255, 108, 108, 0.2)", - **props, -): - if not isinstance(body, str): - body = repr(body) - with div( - style=dict( - backgroundColor=color, - padding="1rem", - paddingBottom="0", - marginBottom="0.5rem", - borderRadius="0.25rem", - display="flex", - gap="0.5rem", - ) - ): - markdown(icon) - with div(): - markdown(dedent(body), unsafe_allow_html=unsafe_allow_html, **props) - - -def success(body: str, icon: str = "✅", *, unsafe_allow_html=False): - if not isinstance(body, str): - body = repr(body) - with div( - style=dict( - backgroundColor="rgba(108, 255, 108, 0.2)", - padding="1rem", - paddingBottom="0", - marginBottom="0.5rem", - borderRadius="0.25rem", - display="flex", - gap="0.5rem", - ) - ): - markdown(icon) - markdown(dedent(body), unsafe_allow_html=unsafe_allow_html) - - -def caption(body: str, className: str = None, **props): - className = className or "text-muted" - markdown(body, className=className, **props) - - -def tabs(labels: list[str]) -> list[state.NestingCtx]: - parent = state.RenderTreeNode( - name="tabs", - children=[ - state.RenderTreeNode( - name="tab", - props=dict(label=dedent(label)), - ) - for label in labels - ], - ).mount() - return [state.NestingCtx(tab) for tab in parent.children] - - -def controllable_tabs( - labels: list[str], key: str -) -> tuple[list[state.NestingCtx], int]: - index = state.session_state.get(key, 0) - for i, label in enumerate(labels): - if button( - label, - key=f"tab-{i}", - type="primary", - className="replicate-nav", - style={ - "background": "black" if i == index else "white", - "color": "white" if i == index else "black", - }, - ): - state.session_state[key] = index = i - state.experimental_rerun() - ctxs = [] - for i, label in enumerate(labels): - if i == index: - ctxs += [div(className="tab-content")] - else: - ctxs += [div(className="tab-content", style={"display": "none"})] - return ctxs, index - - -def columns( - spec, - *, - gap: str = None, - responsive: bool = True, - column_props: dict = {}, - **props, -) -> tuple[state.NestingCtx, ...]: - if isinstance(spec, int): - spec = [1] * spec - total_weight = sum(spec) - props.setdefault("className", "row") - with div(**props): - return tuple( - div( - className=f"col-lg-{p} {'col-12' if responsive else f'col-{p}'}", - **column_props, - ) - for w in spec - if (p := f"{round(w / total_weight * 12)}") - ) - - -def image( - src: str | np.ndarray, - caption: str = None, - alt: str = None, - href: str = None, - show_download_button: bool = False, - **props, -): - if isinstance(src, np.ndarray): - from daras_ai.image_input import cv2_img_to_bytes - - if not src.shape: - return - # ensure image is not too large - data = resize_img_scale(cv2_img_to_bytes(src), (128, 128)) - # convert to base64 - b64 = base64.b64encode(data).decode("utf-8") - src = "data:image/png;base64," + b64 - if not src: - return - state.RenderTreeNode( - name="img", - props=dict( - src=src, - caption=dedent(caption), - alt=alt or caption, - href=href, - **props, - ), - ).mount() - if show_download_button: - download_button( - label=' Download', url=src - ) - - -def video( - src: str, - caption: str = None, - autoplay: bool = False, - show_download_button: bool = False, -): - autoplay_props = {} - if autoplay: - autoplay_props = { - "preload": "auto", - "controls": True, - "autoPlay": True, - "loop": True, - "muted": True, - "playsInline": True, - } - - if not src: - return - if isinstance(src, str): - # https://muffinman.io/blog/hack-for-ios-safari-to-display-html-video-thumbnail/ - f = furl(src) - f.fragment.args["t"] = "0.001" - src = f.url - state.RenderTreeNode( - name="video", - props=dict(src=src, caption=dedent(caption), **autoplay_props), - ).mount() - if show_download_button: - download_button( - label=' Download', url=src - ) - - -def audio(src: str, caption: str = None, show_download_button: bool = False): - if not src: - return - state.RenderTreeNode( - name="audio", - props=dict(src=src, caption=dedent(caption)), - ).mount() - if show_download_button: - download_button( - label=' Download', url=src - ) - - -def text_area( - label: str, - value: str = "", - height: int = 500, - key: str = None, - help: str = None, - placeholder: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> str: - style = props.setdefault("style", {}) - # if key: - # assert not value, "only one of value or key can be provided" - # else: - if not key: - key = md5_values( - "textarea", - label, - height, - help, - placeholder, - label_visibility, - not disabled or value, - ) - value = str(state.session_state.setdefault(key, value) or "") - if label_visibility != "visible": - label = None - if disabled: - max_height = f"{height}px" - rows = nrows_for_text(value, height) - else: - max_height = "50vh" - rows = nrows_for_text(value, height) - style.setdefault("maxHeight", max_height) - props.setdefault("rows", rows) - state.RenderTreeNode( - name="textarea", - props=dict( - name=key, - label=dedent(label), - defaultValue=value, - help=help, - placeholder=placeholder, - disabled=disabled, - **props, - ), - ).mount() - return value or "" - - -def nrows_for_text( - text: str, - max_height_px: int, - min_rows: int = 1, - row_height_px: int = 30, - row_width_px: int = 70, -) -> int: - max_rows = max_height_px // row_height_px - nrows = math.ceil( - sum( - math.ceil(len(line) / row_width_px) - for line in (text or "").splitlines(keepends=True) - ) - ) - nrows = min(max(nrows, min_rows), max_rows) - return nrows - - -def multiselect( - label: str, - options: typing.Sequence[T], - format_func: typing.Callable[[T], typing.Any] = _default_format, - key: str = None, - help: str = None, - allow_none: bool = False, - *, - disabled: bool = False, -) -> list[T]: - if not options: - return [] - options = list(options) - if not key: - key = md5_values("multiselect", label, options, help) - value = state.session_state.get(key) or [] - if not isinstance(value, list): - value = [value] - value = [o for o in value if o in options] - if not allow_none and not value: - value = [options[0]] - state.session_state[key] = value - state.RenderTreeNode( - name="select", - props=dict( - name=key, - label=dedent(label), - help=help, - isDisabled=disabled, - isMulti=True, - defaultValue=value, - allow_none=allow_none, - options=[ - {"value": option, "label": str(format_func(option))} - for option in options - ], - ), - ).mount() - return value - - -def selectbox( - label: str, - options: typing.Iterable[T], - format_func: typing.Callable[[T], typing.Any] = _default_format, - key: str = None, - help: str = None, - *, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - value: T = None, - allow_none: bool = False, - **props, -) -> T | None: - if not options: - return None - if label_visibility != "visible": - label = None - options = list(options) - if allow_none: - options.insert(0, None) - if not key: - key = md5_values("select", label, options, help, label_visibility) - value = state.session_state.setdefault(key, value) - if value not in options: - value = state.session_state[key] = options[0] - state.RenderTreeNode( - name="select", - props=dict( - name=key, - label=dedent(label), - help=help, - isDisabled=disabled, - defaultValue=value, - options=[ - {"value": option, "label": str(format_func(option))} - for option in options - ], - **props, - ), - ).mount() - return value - - -def download_button( - label: str, - url: str, - key: str = None, - help: str = None, - *, - type: typing.Literal["primary", "secondary", "tertiary", "link"] = "secondary", - disabled: bool = False, - **props, -) -> bool: - url = furl(url).remove(fragment=True).url - return button( - component="download-button", - url=url, - label=label, - key=key, - help=help, - type=type, - disabled=disabled, - **props, - ) - - -def button( - label: str, - key: str = None, - help: str = None, - *, - type: typing.Literal["primary", "secondary", "tertiary", "link"] = "secondary", - disabled: bool = False, - component: typing.Literal["download-button", "gui-button"] = "gui-button", - **props, -) -> bool: - """ - Example: - st.button("Primary", key="test0", type="primary") - st.button("Secondary", key="test1") - st.button("Tertiary", key="test3", type="tertiary") - st.button("Link Button", key="test3", type="link") - """ - if not key: - key = md5_values("button", label, help, type, props) - className = f"btn-{type} " + props.pop("className", "") - state.RenderTreeNode( - name=component, - props=dict( - type="submit", - value="yes", - name=key, - label=dedent(label), - help=help, - disabled=disabled, - className=className, - **props, - ), - ).mount() - return bool(state.session_state.pop(key, False)) - - -def anchor( - label: str, - href: str, - *, - type: typing.Literal["primary", "secondary", "tertiary", "link"] = "secondary", - disabled: bool = False, - unsafe_allow_html: bool = False, - new_tab: bool = False, - **props, -): - className = f"btn btn-theme btn-{type} " + props.pop("className", "") - style = props.pop("style", {}) - if disabled: - style["pointerEvents"] = "none" - if new_tab: - props["target"] = "_blank" - with tag("a", href=href, className=className, style=style, **props): - markdown(dedent(label), unsafe_allow_html=unsafe_allow_html) - - -form_submit_button = button - - -def expander(label: str, *, expanded: bool = False, key: str = None, **props): - node = state.RenderTreeNode( - name="expander", - props=dict( - label=dedent(label), - open=expanded, - name=key or md5_values(label, expanded, props), - **props, - ), - ) - node.mount() - return state.NestingCtx(node) - - -def file_uploader( - label: str, - accept: list[str] = None, - accept_multiple_files=False, - key: str = None, - value: str | list[str] = None, - upload_key: str = None, - help: str = None, - *, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - upload_meta: dict = None, - optional: bool = False, -) -> str | list[str] | None: - if label_visibility != "visible": - label = None - key = upload_key or key - if not key: - key = md5_values( - "file_uploader", - label, - accept, - accept_multiple_files, - help, - label_visibility, - ) - if optional: - if not checkbox( - label, value=bool(state.session_state.get(key, value)), disabled=disabled - ): - state.session_state.pop(key, None) - return None - label = None - value = state.session_state.setdefault(key, value) - if not value: - if accept_multiple_files: - value = [] - else: - value = None - state.session_state[key] = value - state.RenderTreeNode( - name="input", - props=dict( - type="file", - name=key, - label=dedent(label), - help=help, - disabled=disabled, - accept=accept, - multiple=accept_multiple_files, - defaultValue=value, - uploadMeta=upload_meta, - ), - ).mount() - return value - - -def json(value: typing.Any, expanded: bool = False, depth: int = 1): - state.RenderTreeNode( - name="json", - props=dict( - value=value, - expanded=expanded, - defaultInspectDepth=3 if expanded else depth, - ), - ).mount() - - -def data_table(file_url_or_cells: str | list): - if isinstance(file_url_or_cells, str): - file_url = file_url_or_cells - return _node("data-table", fileUrl=file_url) - else: - cells = file_url_or_cells - return _node("data-table-raw", cells=cells) - - -def table(df: "pd.DataFrame"): - with tag("table", className="table table-striped table-sm"): - with tag("thead"): - with tag("tr"): - for col in df.columns: - with tag("th", scope="col"): - html(dedent(col)) - with tag("tbody"): - for row in df.itertuples(index=False): - with tag("tr"): - for value in row: - with tag("td"): - html(dedent(str(value))) - - -def raw_table(header: list[str], className: str = "", **props) -> state.NestingCtx: - className = "table " + className - with tag("table", className=className, **props): - if header: - with tag("thead"), tag("tr"): - for col in header: - with tag("th", scope="col"): - html(dedent(col)) - - return tag("tbody") - - -def table_row(values: list[str], **props): - row = tag("tr", **props) - with row: - for v in values: - with tag("td"): - html(html_lib.escape(v)) - return row - - -def horizontal_radio( - label: str, - options: typing.Sequence[T], - format_func: typing.Callable[[T], typing.Any] = _default_format, - *, - key: str = None, - help: str = None, - value: T = None, - disabled: bool = False, - checked_by_default: bool = True, - label_visibility: LabelVisibility = "visible", - **button_props, -) -> T | None: - if not options: - return None - options = list(options) - if not key: - key = md5_values("horizontal_radio", label, options, help, label_visibility) - value = state.session_state.setdefault(key, value) - if value not in options and checked_by_default: - value = state.session_state[key] = options[0] - if label_visibility != "visible": - label = None - markdown(label) - for option in options: - if button( - format_func(option), - key=f"tab-{key}-{option}", - type="primary", - className="replicate-nav " + ("active" if value == option else ""), - disabled=disabled, - **button_props, - ): - state.session_state[key] = value = option - state.experimental_rerun() - return value - - -def radio( - label: str, - options: typing.Sequence[T], - format_func: typing.Callable[[T], typing.Any] = _default_format, - key: str = None, - value: T = None, - help: str = None, - *, - disabled: bool = False, - checked_by_default: bool = True, - label_visibility: LabelVisibility = "visible", -) -> T | None: - if not options: - return None - options = list(options) - if not key: - key = md5_values("radio", label, options, help, label_visibility) - value = state.session_state.setdefault(key, value) - if value not in options and checked_by_default: - value = state.session_state[key] = options[0] - if label_visibility != "visible": - label = None - markdown(label) - for option in options: - state.RenderTreeNode( - name="input", - props=dict( - type="radio", - name=key, - label=dedent(str(format_func(option))), - value=option, - defaultChecked=bool(value == option), - help=help, - disabled=disabled, - ), - ).mount() - return value - - -def text_input( - label: str, - value: str = "", - max_chars: str = None, - key: str = None, - help: str = None, - *, - placeholder: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> str: - value = _input_widget( - input_type="text", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - maxLength=max_chars, - placeholder=placeholder, - **props, - ) - return value or "" - - -def date_input( - label: str, - value: str | None = None, - key: str = None, - help: str = None, - *, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> datetime | None: - value = _input_widget( - input_type="date", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - style=dict( - border="1px solid hsl(0, 0%, 80%)", - padding="0.375rem 0.75rem", - borderRadius="0.25rem", - margin="0 0.5rem 0 0.5rem", - ), - **props, - ) - try: - return datetime.strptime(value, "%Y-%m-%d") if value else None - except ValueError: - return None - - -def password_input( - label: str, - value: str = "", - max_chars: str = None, - key: str = None, - help: str = None, - *, - placeholder: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> str: - value = _input_widget( - input_type="password", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - maxLength=max_chars, - placeholder=placeholder, - **props, - ) - return value or "" - - -def slider( - label: str, - min_value: float = None, - max_value: float = None, - value: float = None, - step: float = None, - key: str = None, - help: str = None, - *, - disabled: bool = False, -) -> float: - value = _input_widget( - input_type="range", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - min=min_value, - max=max_value, - step=_step_value(min_value, max_value, step), - ) - return value or 0 - - -def number_input( - label: str, - min_value: float = None, - max_value: float = None, - value: float = None, - step: float = None, - key: str = None, - help: str = None, - *, - disabled: bool = False, -) -> float: - value = _input_widget( - input_type="number", - inputMode="decimal", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - min=min_value, - max=max_value, - step=_step_value(min_value, max_value, step), - ) - return value or 0 - - -def _step_value( - min_value: float | None, max_value: float | None, step: float | None -) -> float: - if step: - return step - elif isinstance(min_value, float) or isinstance(max_value, float): - return 0.1 - else: - return 1 - - -def checkbox( - label: str, - value: bool = False, - key: str = None, - help: str = None, - *, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> bool: - value = _input_widget( - input_type="checkbox", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - default_value_attr="defaultChecked", - **props, - ) - return bool(value) - - -def _input_widget( - *, - input_type: str, - label: str, - value: typing.Any = None, - key: str = None, - help: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - default_value_attr: str = "defaultValue", - **kwargs, -) -> typing.Any: - # if key: - # assert not value, "only one of value or key can be provided" - # else: - if not key: - key = md5_values("input", input_type, label, help, label_visibility) - value = state.session_state.setdefault(key, value) - if label_visibility != "visible": - label = None - state.RenderTreeNode( - name="input", - props={ - "type": input_type, - "name": key, - "label": dedent(label), - default_value_attr: value, - "help": help, - "disabled": disabled, - **kwargs, - }, - ).mount() - return value - - -def breadcrumbs(divider: str = "/", **props) -> state.NestingCtx: - style = props.pop("style", {}) | {"--bs-breadcrumb-divider": f"'{divider}'"} - with tag("nav", style=style, **props): - return tag("ol", className="breadcrumb mb-0") - - -def breadcrumb_item(inner_html: str, link_to: str | None = None, **props): - className = "breadcrumb-item " + props.pop("className", "") - with tag("li", className=className, **props): - if link_to: - with tag("a", href=link_to): - html(inner_html) - else: - html(inner_html) - - -def plotly_chart(figure_or_data, **kwargs): - data = ( - figure_or_data.to_plotly_json() - if hasattr(figure_or_data, "to_plotly_json") - else figure_or_data - ) - state.RenderTreeNode( - name="plotly-chart", - props=dict( - chart=data, - args=kwargs, - ), - ).mount() - - -def dedent(text: str | None) -> str | None: - if not text: - return text - return textwrap.dedent(text) - - -def js(src: str, **kwargs): - state.RenderTreeNode( - name="script", - props=dict( - src=src, - args=kwargs, - ), - ).mount() diff --git a/gooey_ui/components/modal.py b/gooey_ui/components/modal.py deleted file mode 100644 index 72e951fc8..000000000 --- a/gooey_ui/components/modal.py +++ /dev/null @@ -1,97 +0,0 @@ -from contextlib import contextmanager - -import gooey_ui as st -from gooey_ui import experimental_rerun as rerun - - -class Modal: - def __init__(self, title, key, padding=20, max_width=744): - """ - :param title: title of the Modal shown in the h1 - :param key: unique key identifying this modal instance - :param padding: padding of the content within the modal - :param max_width: maximum width this modal should use - """ - self.title = title - self.padding = padding - self.max_width = str(max_width) + "px" - self.key = key - - self._container = None - - def is_open(self): - return st.session_state.get(f"{self.key}-opened", False) - - def open(self): - st.session_state[f"{self.key}-opened"] = True - rerun() - - def close(self, rerun_condition=True): - st.session_state[f"{self.key}-opened"] = False - if rerun_condition: - rerun() - - def empty(self): - if self._container: - self._container.empty() - - @contextmanager - def container(self, **props): - st.html( - f""" - - """ - ) - - with st.div(className="blur-background"): - with st.div(className="modal-parent"): - container_class = "modal-container " + props.pop("className", "") - self._container = st.div(className=container_class, **props) - - with self._container: - with st.div(className="d-flex justify-content-between align-items-center"): - if self.title: - st.markdown(f"### {self.title}") - else: - st.div() - - close_ = st.button( - "✖", - type="tertiary", - key=f"{self.key}-close", - style={"padding": "0.375rem 0.75rem"}, - ) - if close_: - self.close() - yield self._container