From 398f9f9b020db7b6471b9167fecf37d541dd5377 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 9 Sep 2024 19:43:31 +0200 Subject: [PATCH 01/14] adding demo app for dashboard ui --- .../{dashboard_ui => chart_ui}/Dockerfile | 0 .../{dashboard_ui => chart_ui}/README.md | 0 vizro-ai/examples/chart_ui/_utils.py | 8 + vizro-ai/examples/chart_ui/actions.py | 129 +++++ vizro-ai/examples/chart_ui/app.py | 199 +++++++ .../examples/chart_ui/assets/custom_css.css | 159 ++++++ vizro-ai/examples/chart_ui/assets/logo.svg | 3 + vizro-ai/examples/chart_ui/components.py | 252 +++++++++ vizro-ai/examples/chart_ui/requirements.in | 4 + vizro-ai/examples/chart_ui/requirements.txt | 507 ++++++++++++++++++ vizro-ai/examples/dashboard_ui/actions.py | 75 +-- vizro-ai/examples/dashboard_ui/app.py | 181 +++---- .../dashboard_ui/assets/custom_css.css | 41 +- vizro-ai/examples/dashboard_ui/components.py | 8 +- .../examples/dashboard_ui/run_vizro_ai.py | 52 ++ 15 files changed, 1435 insertions(+), 183 deletions(-) rename vizro-ai/examples/{dashboard_ui => chart_ui}/Dockerfile (100%) rename vizro-ai/examples/{dashboard_ui => chart_ui}/README.md (100%) create mode 100644 vizro-ai/examples/chart_ui/_utils.py create mode 100644 vizro-ai/examples/chart_ui/actions.py create mode 100644 vizro-ai/examples/chart_ui/app.py create mode 100644 vizro-ai/examples/chart_ui/assets/custom_css.css create mode 100644 vizro-ai/examples/chart_ui/assets/logo.svg create mode 100644 vizro-ai/examples/chart_ui/components.py create mode 100644 vizro-ai/examples/chart_ui/requirements.in create mode 100644 vizro-ai/examples/chart_ui/requirements.txt create mode 100644 vizro-ai/examples/dashboard_ui/run_vizro_ai.py diff --git a/vizro-ai/examples/dashboard_ui/Dockerfile b/vizro-ai/examples/chart_ui/Dockerfile similarity index 100% rename from vizro-ai/examples/dashboard_ui/Dockerfile rename to vizro-ai/examples/chart_ui/Dockerfile diff --git a/vizro-ai/examples/dashboard_ui/README.md b/vizro-ai/examples/chart_ui/README.md similarity index 100% rename from vizro-ai/examples/dashboard_ui/README.md rename to vizro-ai/examples/chart_ui/README.md diff --git a/vizro-ai/examples/chart_ui/_utils.py b/vizro-ai/examples/chart_ui/_utils.py new file mode 100644 index 000000000..534e24552 --- /dev/null +++ b/vizro-ai/examples/chart_ui/_utils.py @@ -0,0 +1,8 @@ +"""Utils file.""" + + +def check_file_extension(filename): + filename = filename.lower() + + # Check if the filename ends with .csv or .xls + return filename.endswith(".csv") or filename.endswith(".xls") or filename.endswith(".xlsx") diff --git a/vizro-ai/examples/chart_ui/actions.py b/vizro-ai/examples/chart_ui/actions.py new file mode 100644 index 000000000..01e907e53 --- /dev/null +++ b/vizro-ai/examples/chart_ui/actions.py @@ -0,0 +1,129 @@ +"""Custom actions used within a dashboard.""" + +import base64 +import io +import logging + +import black +import pandas as pd +from _utils import check_file_extension +from dash.exceptions import PreventUpdate +from langchain_openai import ChatOpenAI +from plotly import graph_objects as go +from vizro.models.types import capture +from vizro_ai import VizroAI + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) # TODO: remove manual setting and make centrally controlled + +SUPPORTED_VENDORS = {"OpenAI": ChatOpenAI} + + +def get_vizro_ai_plot(user_prompt, df, model, api_key, api_base, vendor_input): # noqa: PLR0913 + """VizroAi plot configuration.""" + vendor = SUPPORTED_VENDORS[vendor_input] + llm = vendor(model_name=model, openai_api_key=api_key, openai_api_base=api_base) + vizro_ai = VizroAI(model=llm) + ai_outputs = vizro_ai.plot(df, user_prompt, explain=False, return_elements=True) + + return ai_outputs + + +@capture("action") +def run_vizro_ai(user_prompt, n_clicks, data, model, api_key, api_base, vendor_input): # noqa: PLR0913 + """Gets the AI response and adds it to the text window.""" + + def create_response(ai_response, figure, user_prompt, filename): + plotly_fig = figure.to_json() + return ( + ai_response, + figure, + {"ai_response": ai_response, "figure": plotly_fig, "prompt": user_prompt, "filename": filename}, + ) + + if not n_clicks: + raise PreventUpdate + + if not data: + ai_response = "Please upload data to proceed!" + figure = go.Figure() + return create_response(ai_response, figure, user_prompt, None) + + if not api_key: + ai_response = "API key not found. Make sure you enter your API key!" + figure = go.Figure() + return create_response(ai_response, figure, user_prompt, data["filename"]) + + if api_key.startswith('"'): + ai_response = "Make sure you enter your API key without quotes!" + figure = go.Figure() + return create_response(ai_response, figure, user_prompt, data["filename"]) + + if api_base is not None and api_base.startswith('"'): + ai_response = "Make sure you enter your API base without quotes!" + figure = go.Figure() + return create_response(ai_response, figure, user_prompt, data["filename"]) + + try: + logger.info("Attempting chart code.") + df = pd.DataFrame(data["data"]) + ai_outputs = get_vizro_ai_plot( + user_prompt=user_prompt, + df=df, + model=model, + api_key=api_key, + api_base=api_base, + vendor_input=vendor_input, + ) + ai_code = ai_outputs.code + figure = ai_outputs.figure + formatted_code = black.format_str(ai_code, mode=black.Mode(line_length=100)) + + ai_response = "\n".join(["```python", formatted_code, "```"]) + logger.info("Successful query produced.") + return create_response(ai_response, figure, user_prompt, data["filename"]) + + except Exception as exc: + logger.debug(exc) + logger.info("Chart creation failed.") + ai_response = f"Sorry, I can't do that. Following Error occurred: {exc}" + figure = go.Figure() + return create_response(ai_response, figure, user_prompt, data["filename"]) + + +@capture("action") +def data_upload_action(contents, filename): + """Custom data upload action.""" + if not contents: + raise PreventUpdate + + if not check_file_extension(filename=filename): + return {"error_message": "Unsupported file extension.. Make sure to upload either csv or an excel file."} + + content_type, content_string = contents.split(",") + + try: + decoded = base64.b64decode(content_string) + if filename.endswith(".csv"): + # Handle CSV file + df = pd.read_csv(io.StringIO(decoded.decode("utf-8"))) + else: + # Handle Excel file + df = pd.read_excel(io.BytesIO(decoded)) + + data = df.to_dict("records") + return {"data": data, "filename": filename} + + except Exception as e: + logger.debug(e) + return {"error_message": "There was an error processing this file."} + + +@capture("action") +def display_filename(data): + """Custom action to display uploaded filename.""" + if data is None: + raise PreventUpdate + + display_message = data.get("filename") or data.get("error_message") + return f"Uploaded file name: '{display_message}'" if "filename" in data else display_message diff --git a/vizro-ai/examples/chart_ui/app.py b/vizro-ai/examples/chart_ui/app.py new file mode 100644 index 000000000..3c46e40d5 --- /dev/null +++ b/vizro-ai/examples/chart_ui/app.py @@ -0,0 +1,199 @@ +"""VizroAI UI dashboard configuration.""" + +import json + +import dash_bootstrap_components as dbc +import pandas as pd +import vizro.models as vm +import vizro.plotly.express as px +from actions import data_upload_action, display_filename, run_vizro_ai +from components import ( + CodeClipboard, + CustomDashboard, + Icon, + Modal, + MyDropdown, + MyPage, + OffCanvas, + UserPromptTextArea, + UserUpload, +) +from dash import Input, Output, State, callback, get_asset_url, html +from dash.exceptions import PreventUpdate +from vizro import Vizro + +vm.Container.add_type("components", UserUpload) +vm.Container.add_type("components", MyDropdown) +vm.Container.add_type("components", OffCanvas) +vm.Container.add_type("components", CodeClipboard) +vm.Container.add_type("components", Icon) +vm.Container.add_type("components", Modal) + +MyPage.add_type("components", UserPromptTextArea) +MyPage.add_type("components", UserUpload) +MyPage.add_type("components", MyDropdown) +MyPage.add_type("components", OffCanvas) +MyPage.add_type("components", CodeClipboard) +MyPage.add_type("components", Icon) +MyPage.add_type("components", Modal) + + +SUPPORTED_MODELS = [ + "gpt-4o-mini", + "gpt-4", + "gpt-4-turbo", + "gpt-3.5-turbo", + "gpt-4o", +] + + +plot_page = MyPage( + id="vizro_ai_plot_page", + title="Vizro-AI - effortlessly create interactive charts with Plotly", + layout=vm.Layout( + grid=[ + [3, 3, -1, 5], + [1, 1, 2, 2], + [4, 4, 2, 2], + *[[0, 0, 2, 2]] * 6, + ] + ), + components=[ + vm.Container(title="", components=[CodeClipboard(id="plot")]), + UserPromptTextArea( + id="text-area-id", + ), + vm.Graph(id="graph-id", figure=px.scatter(pd.DataFrame())), + vm.Container( + title="", + layout=vm.Layout(grid=[[1], [0]], row_gap="0px"), + components=[ + UserUpload( + id="data-upload-id", + actions=[ + vm.Action( + function=data_upload_action(), + inputs=["data-upload-id.contents", "data-upload-id.filename"], + outputs=["data-store-id.data"], + ), + vm.Action( + function=display_filename(), + inputs=["data-store-id.data"], + outputs=["upload-message-id.children"], + ), + ], + ), + vm.Card(id="upload-message-id", text="Upload your data file (csv or excel)"), + ], + ), + vm.Container( + title="", + layout=vm.Layout(grid=[[2, -1, -1, -1, -1, 1, 1, 0, 0]], row_gap="0px", col_gap="4px"), + components=[ + vm.Button( + id="trigger-button-id", + text="Run VizroAI", + actions=[ + vm.Action( + function=run_vizro_ai(), + inputs=[ + "text-area-id.value", + "trigger-button-id.n_clicks", + "data-store-id.data", + "model-dropdown-id.value", + "settings-api-key.value", + "settings-api-base.value", + "settings-dropdown.value", + ], + outputs=["plot-code-markdown.children", "graph-id.figure", "outputs-store-id.data"], + ), + ], + ), + MyDropdown(options=SUPPORTED_MODELS, value="gpt-4o-mini", multi=False, id="model-dropdown-id"), + OffCanvas(id="settings", options=["OpenAI"], value="OpenAI"), + # Modal(id="modal"), + ], + ), + Icon(id="open-settings-id"), + ], +) + + +dashboard = CustomDashboard(pages=[plot_page]) + + +# pure dash callbacks +@callback( + [ + Output("plot-code-markdown", "children", allow_duplicate=True), + Output("graph-id", "figure", allow_duplicate=True), + Output("text-area-id", "value"), + Output("upload-message-id", "children"), + ], + [Input("on_page_load_action_trigger_vizro_ai_plot_page", "data")], + [State("outputs-store-id", "data")], + prevent_initial_call="initial_duplicate", +) +def update_data(page_data, outputs_data): + """Callback for retrieving latest vizro-ai output from dcc store.""" + if not outputs_data: + raise PreventUpdate + + ai_response = outputs_data["ai_response"] + fig = json.loads(outputs_data["figure"]) + filename = f"File uploaded: '{outputs_data['filename']}'" + prompt = outputs_data["prompt"] + + return ai_response, fig, prompt, filename + + +@callback( + Output("settings", "is_open"), + Input("open-settings-id", "n_clicks"), + [State("settings", "is_open")], +) +def open_settings(n_clicks, is_open): + """Callback for opening and closing offcanvas settings component.""" + return not is_open if n_clicks else is_open + + +@callback( + Output("settings-api-key", "type"), + Input("settings-api-key-toggle", "value"), +) +def show_api_key(value): + """Callback to show api key.""" + return "text" if value else "password" + + +@callback( + Output("settings-api-base", "type"), + Input("settings-api-base-toggle", "value"), +) +def show_api_base(value): + """Callback to show api base.""" + return "text" if value else "password" + + +app = Vizro().build(dashboard) +app.dash.layout.children.append( + html.Div( + [ + dbc.NavLink("Contact Vizro", href="https://github.com/mckinsey/vizro/issues"), + dbc.NavLink("GitHub", href="https://github.com/mckinsey/vizro"), + dbc.NavLink("Docs", href="https://vizro.readthedocs.io/projects/vizro-ai/"), + html.Div( + [ + "Made using ", + html.Img(src=get_asset_url("logo.svg"), id="banner", alt="Vizro logo"), + "vizro", + ], + ), + ], + className="anchor-container", + ) +) + +server = app.dash.server +if __name__ == "__main__": + app.run() diff --git a/vizro-ai/examples/chart_ui/assets/custom_css.css b/vizro-ai/examples/chart_ui/assets/custom_css.css new file mode 100644 index 000000000..02dcbe0b8 --- /dev/null +++ b/vizro-ai/examples/chart_ui/assets/custom_css.css @@ -0,0 +1,159 @@ +#page-header { + display: none; +} + +.card-body { + color: var(--text-light-mode-secondary); +} + +.textbox { + border-radius: 24px; + font-size: var(--text-size-02); + margin-bottom: 20px; + max-width: 60%; + padding: 4px 12px; + width: max-content; +} + +.user_input:focus { + background: var(--field-enabled); + box-shadow: 0 0 0 2px var(--focus-focus) inset; + color: var(--text-primary); + outline-width: 0; +} + +#text-area-id { + background-color: inherit; + border: 1px solid var(--border-subtle-alpha-01); + color: var(--text-primary); + min-height: 90px; + padding: 8px; + width: 100%; +} + +#code-clipboard { + padding: 8px; +} + +.code-clipboard { + font-size: 20px; + position: absolute; + right: 14px; + top: 12px; +} + +.code-clipboard-container { + background: var(--surfaces-bg-card); + font-family: monospace; + height: 500px; + max-height: 500px; + overflow: auto; + padding: 1rem; + position: relative; +} + +.code-clipboard-container::-webkit-scrollbar-thumb { + border-color: var(--surfaces-bg-card); +} + +#model-dropdown .Select--single .Select-value { + background-color: inherit; + font-size: 12px; +} + +#model-dropdown .Select-control { + background-color: inherit; + font-size: 12px; +} + +#model-dropdown-id .Select-menu-outer { + font-size: 12px; + + /* top: 0; */ + + /* transform: translateY(3px) translateY(-100%); */ +} + +#model-dropdow-idn .dash-dropdown { + background-color: inherit; + font-size: 12px; +} + +#trigger-button-id { + width: 100%; +} + +#dashboard-container .dash-dropdown { + background-color: inherit; +} + +#model-dropdown-id .Select-clear { + display: none; +} + +#save-button-id { + width: 50%; +} + +.card:has(#upload-message-id) { + background-color: inherit; + box-shadow: none; + font-size: 12px; + overflow: hidden; + padding-bottom: 0; + padding-left: 0; + padding-top: 8px; +} + +.card:has(#settings-card-id) { + background-color: inherit; + box-shadow: none; + font-size: 12px; + overflow: hidden; + padding-bottom: 0; + padding-left: 0; +} + +.settings-div { + display: flex; + justify-content: end; + padding-right: 2px; + width: 100%; +} + +#data-upload-id { + border: 1px dashed var(--border-subtle-alpha-01); + border-radius: 5px; + color: var(--text-primary); + height: 46px; + line-height: 46px; + text-align: center; +} + +#settings-api-key-toggle .form-check-input { + border-radius: 8px; +} + +#settings-api-base-toggle .form-check-input { + border-radius: 8px; +} + +#toggle-div-api-base, +#toggle-div-api-key { + align-items: center; + display: flex; + gap: 4px; + justify-content: center; +} + +.anchor-container { + background: #060a17; + bottom: 0; + display: flex; + font-weight: 600; + gap: 2rem; + padding: 4px; + place-content: baseline center; + position: fixed; + width: 100%; +} diff --git a/vizro-ai/examples/chart_ui/assets/logo.svg b/vizro-ai/examples/chart_ui/assets/logo.svg new file mode 100644 index 000000000..bdf73e278 --- /dev/null +++ b/vizro-ai/examples/chart_ui/assets/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/vizro-ai/examples/chart_ui/components.py b/vizro-ai/examples/chart_ui/components.py new file mode 100644 index 000000000..b34a6a25e --- /dev/null +++ b/vizro-ai/examples/chart_ui/components.py @@ -0,0 +1,252 @@ +"""Contains custom components used within a dashboard.""" + +from typing import List, Literal + +import black +import dash_bootstrap_components as dbc +import vizro.models as vm +from dash import dcc, html +from pydantic import PrivateAttr +from vizro.models import Action +from vizro.models._action._actions_chain import _action_validator_factory +from vizro.models._models_utils import _log_call + + +class UserPromptTextArea(vm.VizroBaseModel): + """Input component `UserPromptTextArea`. + + Based on the underlying [`dcc.Input`](https://dash.plotly.com/dash-core-components/input). + + Args: + type (Literal["user_input"]): Defaults to `"user_text_area"`. + title (str): Title to be displayed. Defaults to `""`. + placeholder (str): Default text to display in input field. Defaults to `""`. + actions (Optional[List[Action]]): Defaults to `[]`. + + """ + + type: Literal["user_text_area"] = "user_text_area" + actions: List[Action] = [] # noqa: RUF012 + + _set_actions = _action_validator_factory("value") + + @_log_call + def build(self): + """Returns the text area component to display vizro-ai code output.""" + return html.Div( + children=[ + dcc.Textarea( + id=self.id, + placeholder="Describe the chart you want to create, e.g. " + "'Visualize the life expectancy per continent.'", + ) + ] + ) + + +class UserUpload(vm.VizroBaseModel): + """Component enabling data upload. + + Args: + type (Literal["upload"]): Defaults to `"upload"`. + title (str): Title to be displayed. + actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. + + """ + + type: Literal["upload"] = "upload" + actions: List[Action] = [] # noqa: RUF012 + + # 'contents' property is input to custom action callback + _input_property: str = PrivateAttr("contents") + # change in 'contents' property of Upload component triggers the actions + _set_actions = _action_validator_factory("contents") + + def build(self): + """Returns the upload component for data upload.""" + return html.Div( + [ + dcc.Upload( + id=self.id, + children=html.Div( + ["Drag and Drop or ", html.A("Select Files")], style={"fontColor": "rgba(255, 255, 255, 0.6)"} + ), + ), + ] + ) + + +class CodeClipboard(vm.VizroBaseModel): + """Code snippet with a copy to clipboard button.""" + + type: Literal["code_clipboard"] = "code_clipboard" + code: str = "" + language: str = "python" + + def build(self): + """Returns the code clipboard component inside a output text area.""" + code = black.format_str(self.code, mode=black.Mode(line_length=120)) + code = code.strip("'\"") + + markdown_code = "\n".join(["```python", code, "```"]) + + return html.Div( + [ + dcc.Clipboard(target_id=f"{self.id}-code-markdown", className="code-clipboard"), + dcc.Markdown(markdown_code, id=f"{self.id}-code-markdown"), + ], + className="code-clipboard-container", + ) + + +class MyDropdown(vm.Dropdown): + """Custom dropdown component.""" + + type: Literal["my_dropdown"] = "my_dropdown" + + def build(self): + """Returns custom dropdown component that cannot be cleared.""" + dropdown_build_obj = super().build() + dropdown_build_obj.id = f"{self.id}_outer_div" + dropdown_build_obj.children[1].clearable = False + + return dropdown_build_obj + + +class Modal(vm.VizroBaseModel): + """Modal to convey warning message.""" + + type: Literal["modal"] = "modal" + + def build(self): + """Returns the modal component.""" + return dbc.Modal( + # id=self.id, + children=[ + dbc.ModalHeader(children=dcc.Markdown("""# Warning""")), + dbc.ModalBody( + children=dcc.Markdown( + """### Do NOT upload any sensitive or personally identifying data. + +#### Reasoning: +This space is hosted publicly running one server in a single container. Further this UI executes dynamically created +code on the server. It thus cannot guarantee the security of your data. In addition it sends the user query and the +data to the chosen LLM vendor API. This is not an exhaustive list. + +#### Alternatives: +If sending your query and data to a LLM is acceptable, you can pull and run this image locally. This will avoid sharing +an instance with others. You can do so by clicking the three dots in the top right of the HuggingFace banner +and click `Run with Docker`. + +Always exercise caution when sharing data online and understand your responsibilities regarding data privacy +and security. +""" + ) + ), + ], + size="l", + is_open=True, + ) + + +class OffCanvas(vm.VizroBaseModel): + """OffCanvas component for settings.""" + + type: Literal["offcanvas"] = "offcanvas" + options: List[str] + value: str + + def build(self): + """Returns the off canvas component for settings.""" + input_groups = html.Div( + [ + dbc.InputGroup( + [ + dbc.InputGroupText("API Key"), + dbc.Input(placeholder="API key", type="password", id=f"{self.id}-api-key"), + html.Div( + dbc.Checklist( + id=f"{self.id}-api-key-toggle", + options=[{"label": "", "value": False}], + switch=True, + inline=True, + ), + id="toggle-div-api-key", + ), + ], + className="mb-3", + ), + dbc.InputGroup( + [ + dbc.InputGroupText("API base"), + dbc.Input(placeholder="(optional) API base", type="password", id=f"{self.id}-api-base"), + html.Div( + dbc.Checklist( + id=f"{self.id}-api-base-toggle", + options=[{"label": "", "value": False}], + switch=True, + inline=True, + ), + id="toggle-div-api-base", + ), + ], + className="mb-3", + ), + dbc.InputGroup( + [ + dbc.InputGroupText("Choose your vendor"), + dbc.Select(options=self.options, value=self.value, id=f"{self.id}-dropdown"), + ], + className="mb-3", + ), + ], + className="mb-3", + ) + + offcanvas = dbc.Offcanvas( + id=self.id, + children=[ + html.Div( + children=[ + input_groups, + ] + ) + ], + title="Settings", + is_open=True, + ) + return offcanvas + + +class MyPage(vm.Page): + """Custom page.""" + + type: Literal["my_page"] = "my_page" + + def pre_build(self): + """Overwriting pre_build.""" + pass + + +class Icon(vm.VizroBaseModel): + """Icon component for settings.""" + + type: Literal["icon"] = "icon" + + def build(self): + """Returns the icon for api settings.""" + return html.Div( + children=[html.Span("settings", className="material-symbols-outlined", id=self.id)], + className="settings-div", + ) + + +class CustomDashboard(vm.Dashboard): + """Custom Dashboard model.""" + + def build(self): + """Returns custom dashboard.""" + dashboard_build_obj = super().build() + dashboard_build_obj.children.append(dcc.Store(id="data-store-id", storage_type="session")) + dashboard_build_obj.children.append(dcc.Store(id="outputs-store-id", storage_type="session")) + return dashboard_build_obj diff --git a/vizro-ai/examples/chart_ui/requirements.in b/vizro-ai/examples/chart_ui/requirements.in new file mode 100644 index 000000000..2d12186e6 --- /dev/null +++ b/vizro-ai/examples/chart_ui/requirements.in @@ -0,0 +1,4 @@ +gunicorn +vizro-ai>=0.2.1 +black +jupyter diff --git a/vizro-ai/examples/chart_ui/requirements.txt b/vizro-ai/examples/chart_ui/requirements.txt new file mode 100644 index 000000000..4a932b3b0 --- /dev/null +++ b/vizro-ai/examples/chart_ui/requirements.txt @@ -0,0 +1,507 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt +aiohappyeyeballs==2.4.0 + # via aiohttp +aiohttp==3.10.5 + # via langchain +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via + # httpx + # jupyter-server + # openai +appnope==0.1.4 + # via ipykernel +argon2-cffi==23.1.0 + # via jupyter-server +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +arrow==1.3.0 + # via isoduration +asttokens==2.4.1 + # via stack-data +async-lru==2.0.4 + # via jupyterlab +attrs==24.2.0 + # via + # aiohttp + # jsonschema + # referencing +babel==2.16.0 + # via jupyterlab-server +beautifulsoup4==4.12.3 + # via nbconvert +black==24.8.0 + # via -r requirements.in +bleach==6.1.0 + # via nbconvert +blinker==1.8.2 + # via flask +cachelib==0.9.0 + # via flask-caching +certifi==2024.7.4 + # via + # httpcore + # httpx + # requests +cffi==1.17.0 + # via argon2-cffi-bindings +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # black + # flask +comm==0.2.2 + # via + # ipykernel + # ipywidgets +dash==2.17.1 + # via + # dash-ag-grid + # dash-bootstrap-components + # vizro +dash-ag-grid==31.2.0 + # via vizro +dash-bootstrap-components==1.6.0 + # via vizro +dash-core-components==2.0.0 + # via dash +dash-html-components==2.0.0 + # via dash +dash-mantine-components==0.12.1 + # via vizro +dash-table==5.0.0 + # via dash +debugpy==1.8.5 + # via ipykernel +decorator==5.1.1 + # via ipython +defusedxml==0.7.1 + # via nbconvert +distro==1.9.0 + # via openai +executing==2.0.1 + # via stack-data +fastjsonschema==2.20.0 + # via nbformat +flask==3.0.3 + # via + # dash + # flask-caching +flask-caching==2.3.0 + # via vizro +fqdn==1.5.1 + # via jsonschema +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +gunicorn==23.0.0 + # via -r requirements.in +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.2 + # via + # jupyterlab + # langsmith + # openai +idna==3.8 + # via + # anyio + # httpx + # jsonschema + # requests + # yarl +importlib-metadata==8.4.0 + # via dash +ipykernel==6.29.5 + # via + # jupyter + # jupyter-console + # jupyterlab + # qtconsole +ipython==8.26.0 + # via + # ipykernel + # ipywidgets + # jupyter-console +ipywidgets==8.1.5 + # via jupyter +isoduration==20.11.0 + # via jsonschema +itsdangerous==2.2.0 + # via flask +jedi==0.19.1 + # via ipython +jinja2==3.1.4 + # via + # flask + # jupyter-server + # jupyterlab + # jupyterlab-server + # nbconvert +jiter==0.5.0 + # via openai +json5==0.9.25 + # via jupyterlab-server +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.0.0 + # via + # jsonpatch + # jsonschema +jsonschema==4.23.0 + # via + # jupyter-events + # jupyterlab-server + # nbformat +jsonschema-specifications==2023.12.1 + # via jsonschema +jupyter==1.0.0 + # via -r requirements.in +jupyter-client==8.6.2 + # via + # ipykernel + # jupyter-console + # jupyter-server + # nbclient + # qtconsole +jupyter-console==6.6.3 + # via jupyter +jupyter-core==5.7.2 + # via + # ipykernel + # jupyter-client + # jupyter-console + # jupyter-server + # jupyterlab + # nbclient + # nbconvert + # nbformat + # qtconsole +jupyter-events==0.10.0 + # via jupyter-server +jupyter-lsp==2.2.5 + # via jupyterlab +jupyter-server==2.14.2 + # via + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # notebook + # notebook-shim +jupyter-server-terminals==0.5.3 + # via jupyter-server +jupyterlab==4.2.5 + # via notebook +jupyterlab-pygments==0.3.0 + # via nbconvert +jupyterlab-server==2.27.3 + # via + # jupyterlab + # notebook +jupyterlab-widgets==3.0.13 + # via ipywidgets +langchain==0.2.15 + # via vizro-ai +langchain-core==0.2.36 + # via + # langchain + # langchain-openai + # langchain-text-splitters + # langgraph + # langgraph-checkpoint +langchain-openai==0.1.23 + # via vizro-ai +langchain-text-splitters==0.2.2 + # via langchain +langgraph==0.2.14 + # via vizro-ai +langgraph-checkpoint==1.0.6 + # via langgraph +langsmith==0.1.106 + # via + # langchain + # langchain-core +markupsafe==2.1.5 + # via + # jinja2 + # nbconvert + # werkzeug +matplotlib-inline==0.1.7 + # via + # ipykernel + # ipython +mistune==3.0.2 + # via nbconvert +multidict==6.0.5 + # via + # aiohttp + # yarl +mypy-extensions==1.0.0 + # via black +nbclient==0.10.0 + # via nbconvert +nbconvert==7.16.4 + # via + # jupyter + # jupyter-server +nbformat==5.10.4 + # via + # jupyter-server + # nbclient + # nbconvert +nest-asyncio==1.6.0 + # via + # dash + # ipykernel +notebook==7.2.2 + # via jupyter +notebook-shim==0.2.4 + # via + # jupyterlab + # notebook +numpy==1.26.4 + # via + # langchain + # pandas +openai==1.42.0 + # via + # langchain-openai + # vizro-ai +orjson==3.10.7 + # via langsmith +overrides==7.7.0 + # via jupyter-server +packaging==24.1 + # via + # black + # gunicorn + # ipykernel + # jupyter-server + # jupyterlab + # jupyterlab-server + # langchain-core + # nbconvert + # plotly + # qtconsole + # qtpy +pandas==2.2.2 + # via + # vizro + # vizro-ai +pandocfilters==1.5.1 + # via nbconvert +parso==0.8.4 + # via jedi +pathspec==0.12.1 + # via black +pexpect==4.9.0 + # via ipython +platformdirs==4.2.2 + # via + # black + # jupyter-core +plotly==5.23.0 + # via dash +prometheus-client==0.20.0 + # via jupyter-server +prompt-toolkit==3.0.47 + # via + # ipython + # jupyter-console +psutil==6.0.0 + # via ipykernel +ptyprocess==0.7.0 + # via + # pexpect + # terminado +pure-eval==0.2.3 + # via stack-data +pycparser==2.22 + # via cffi +pydantic==2.8.2 + # via + # langchain + # langchain-core + # langsmith + # openai + # vizro +pydantic-core==2.20.1 + # via pydantic +pygments==2.18.0 + # via + # ipython + # jupyter-console + # nbconvert + # qtconsole +python-dateutil==2.9.0.post0 + # via + # arrow + # jupyter-client + # pandas +python-dotenv==1.0.1 + # via vizro-ai +python-json-logger==2.0.7 + # via jupyter-events +pytz==2024.1 + # via pandas +pyyaml==6.0.2 + # via + # jupyter-events + # langchain + # langchain-core +pyzmq==26.2.0 + # via + # ipykernel + # jupyter-client + # jupyter-console + # jupyter-server +qtconsole==5.6.0 + # via jupyter +qtpy==2.4.1 + # via qtconsole +referencing==0.35.1 + # via + # jsonschema + # jsonschema-specifications + # jupyter-events +regex==2024.7.24 + # via tiktoken +requests==2.32.3 + # via + # dash + # jupyterlab-server + # langchain + # langsmith + # tiktoken +retrying==1.3.4 + # via dash +rfc3339-validator==0.1.4 + # via + # jsonschema + # jupyter-events +rfc3986-validator==0.1.1 + # via + # jsonschema + # jupyter-events +rpds-py==0.20.0 + # via + # jsonschema + # referencing +ruff==0.6.2 + # via vizro +send2trash==1.8.3 + # via jupyter-server +setuptools==74.0.0 + # via + # dash + # jupyterlab +six==1.16.0 + # via + # asttokens + # bleach + # python-dateutil + # retrying + # rfc3339-validator +sniffio==1.3.1 + # via + # anyio + # httpx + # openai +soupsieve==2.6 + # via beautifulsoup4 +sqlalchemy==2.0.32 + # via langchain +stack-data==0.6.3 + # via ipython +tabulate==0.9.0 + # via vizro-ai +tenacity==8.5.0 + # via + # langchain + # langchain-core + # plotly +terminado==0.18.1 + # via + # jupyter-server + # jupyter-server-terminals +tiktoken==0.7.0 + # via langchain-openai +tinycss2==1.3.0 + # via nbconvert +tornado==6.4.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # notebook + # terminado +tqdm==4.66.5 + # via openai +traitlets==5.14.3 + # via + # comm + # ipykernel + # ipython + # ipywidgets + # jupyter-client + # jupyter-console + # jupyter-core + # jupyter-events + # jupyter-server + # jupyterlab + # matplotlib-inline + # nbclient + # nbconvert + # nbformat + # qtconsole +types-python-dateutil==2.9.0.20240821 + # via arrow +typing-extensions==4.12.2 + # via + # dash + # langchain-core + # openai + # pydantic + # pydantic-core + # sqlalchemy +tzdata==2024.1 + # via pandas +uri-template==1.3.0 + # via jsonschema +urllib3==2.2.2 + # via requests +vizro==0.1.21 + # via vizro-ai +vizro-ai==0.2.1 + # via -r requirements.in +wcwidth==0.2.13 + # via prompt-toolkit +webcolors==24.8.0 + # via jsonschema +webencodings==0.5.1 + # via + # bleach + # tinycss2 +websocket-client==1.8.0 + # via jupyter-server +werkzeug==3.0.4 + # via + # dash + # flask +widgetsnbextension==4.0.13 + # via ipywidgets +wrapt==1.16.0 + # via vizro +yarl==1.9.4 + # via aiohttp +zipp==3.20.1 + # via importlib-metadata diff --git a/vizro-ai/examples/dashboard_ui/actions.py b/vizro-ai/examples/dashboard_ui/actions.py index 01e907e53..15627e693 100644 --- a/vizro-ai/examples/dashboard_ui/actions.py +++ b/vizro-ai/examples/dashboard_ui/actions.py @@ -4,93 +4,26 @@ import io import logging -import black import pandas as pd from _utils import check_file_extension from dash.exceptions import PreventUpdate from langchain_openai import ChatOpenAI -from plotly import graph_objects as go from vizro.models.types import capture from vizro_ai import VizroAI -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) # TODO: remove manual setting and make centrally controlled - SUPPORTED_VENDORS = {"OpenAI": ChatOpenAI} -def get_vizro_ai_plot(user_prompt, df, model, api_key, api_base, vendor_input): # noqa: PLR0913 - """VizroAi plot configuration.""" +def get_vizro_ai_dashboard(user_prompt, dfs, model, api_key, api_base, vendor_input): # noqa: PLR0913 + """VizroAi dashboard configuration.""" vendor = SUPPORTED_VENDORS[vendor_input] llm = vendor(model_name=model, openai_api_key=api_key, openai_api_base=api_base) vizro_ai = VizroAI(model=llm) - ai_outputs = vizro_ai.plot(df, user_prompt, explain=False, return_elements=True) + ai_outputs = vizro_ai._dashboard([dfs], user_prompt, return_elements=True) return ai_outputs -@capture("action") -def run_vizro_ai(user_prompt, n_clicks, data, model, api_key, api_base, vendor_input): # noqa: PLR0913 - """Gets the AI response and adds it to the text window.""" - - def create_response(ai_response, figure, user_prompt, filename): - plotly_fig = figure.to_json() - return ( - ai_response, - figure, - {"ai_response": ai_response, "figure": plotly_fig, "prompt": user_prompt, "filename": filename}, - ) - - if not n_clicks: - raise PreventUpdate - - if not data: - ai_response = "Please upload data to proceed!" - figure = go.Figure() - return create_response(ai_response, figure, user_prompt, None) - - if not api_key: - ai_response = "API key not found. Make sure you enter your API key!" - figure = go.Figure() - return create_response(ai_response, figure, user_prompt, data["filename"]) - - if api_key.startswith('"'): - ai_response = "Make sure you enter your API key without quotes!" - figure = go.Figure() - return create_response(ai_response, figure, user_prompt, data["filename"]) - - if api_base is not None and api_base.startswith('"'): - ai_response = "Make sure you enter your API base without quotes!" - figure = go.Figure() - return create_response(ai_response, figure, user_prompt, data["filename"]) - - try: - logger.info("Attempting chart code.") - df = pd.DataFrame(data["data"]) - ai_outputs = get_vizro_ai_plot( - user_prompt=user_prompt, - df=df, - model=model, - api_key=api_key, - api_base=api_base, - vendor_input=vendor_input, - ) - ai_code = ai_outputs.code - figure = ai_outputs.figure - formatted_code = black.format_str(ai_code, mode=black.Mode(line_length=100)) - - ai_response = "\n".join(["```python", formatted_code, "```"]) - logger.info("Successful query produced.") - return create_response(ai_response, figure, user_prompt, data["filename"]) - - except Exception as exc: - logger.debug(exc) - logger.info("Chart creation failed.") - ai_response = f"Sorry, I can't do that. Following Error occurred: {exc}" - figure = go.Figure() - return create_response(ai_response, figure, user_prompt, data["filename"]) - - @capture("action") def data_upload_action(contents, filename): """Custom data upload action.""" @@ -115,7 +48,7 @@ def data_upload_action(contents, filename): return {"data": data, "filename": filename} except Exception as e: - logger.debug(e) + logging.exception(e) return {"error_message": "There was an error processing this file."} diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 3c46e40d5..8db9d3ca4 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -1,12 +1,9 @@ -"""VizroAI UI dashboard configuration.""" - import json +import subprocess import dash_bootstrap_components as dbc -import pandas as pd import vizro.models as vm -import vizro.plotly.express as px -from actions import data_upload_action, display_filename, run_vizro_ai +from actions import data_upload_action, display_filename from components import ( CodeClipboard, CustomDashboard, @@ -22,6 +19,13 @@ from dash.exceptions import PreventUpdate from vizro import Vizro +SUPPORTED_MODELS = [ + "gpt-4o-mini", + "gpt-4", + "gpt-4-turbo", + "gpt-3.5-turbo", + "gpt-4o", +] vm.Container.add_type("components", UserUpload) vm.Container.add_type("components", MyDropdown) vm.Container.add_type("components", OffCanvas) @@ -35,151 +39,134 @@ MyPage.add_type("components", OffCanvas) MyPage.add_type("components", CodeClipboard) MyPage.add_type("components", Icon) -MyPage.add_type("components", Modal) - - -SUPPORTED_MODELS = [ - "gpt-4o-mini", - "gpt-4", - "gpt-4-turbo", - "gpt-3.5-turbo", - "gpt-4o", -] - +vm.Container.add_type("components", Modal) -plot_page = MyPage( - id="vizro_ai_plot_page", - title="Vizro-AI - effortlessly create interactive charts with Plotly", +dashboard_page = MyPage( + id="vizro_ai_dashboard_page", + title="Vizro AI - Dashboard", layout=vm.Layout( - grid=[ - [3, 3, -1, 5], - [1, 1, 2, 2], - [4, 4, 2, 2], - *[[0, 0, 2, 2]] * 6, - ] + grid=[[2, 2, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [3, 3, 0, 0, 0]] ), components=[ - vm.Container(title="", components=[CodeClipboard(id="plot")]), - UserPromptTextArea( - id="text-area-id", - ), - vm.Graph(id="graph-id", figure=px.scatter(pd.DataFrame())), + vm.Container(title="", components=[CodeClipboard(id="dashboard")]), + UserPromptTextArea(id="dashboard-text-area", placeholder="Describe the dashboard you want to create."), vm.Container( title="", - layout=vm.Layout(grid=[[1], [0]], row_gap="0px"), + layout=vm.Layout(grid=[[0], [1]], row_gap="0px"), components=[ + vm.Card(id="dashboard-upload-message-id", text="Upload your data files (csv or excel)"), UserUpload( - id="data-upload-id", + id="dashboard-data-upload", actions=[ vm.Action( function=data_upload_action(), - inputs=["data-upload-id.contents", "data-upload-id.filename"], - outputs=["data-store-id.data"], + inputs=["dashboard-data-upload.contents", "dashboard-data-upload.filename"], + outputs=["dashboard-data-store.data"], ), vm.Action( function=display_filename(), - inputs=["data-store-id.data"], - outputs=["upload-message-id.children"], + inputs=["dashboard-data-store.data"], + outputs=["dashboard-upload-message-id.children"], ), ], ), - vm.Card(id="upload-message-id", text="Upload your data file (csv or excel)"), ], ), vm.Container( title="", - layout=vm.Layout(grid=[[2, -1, -1, -1, -1, 1, 1, 0, 0]], row_gap="0px", col_gap="4px"), + layout=vm.Layout(grid=[[2, 3, -1, -1, -1, -1, 1, 1, 0, 0]], row_gap="0px", col_gap="4px"), components=[ vm.Button( - id="trigger-button-id", + id="dashboard-trigger-button", text="Run VizroAI", - actions=[ - vm.Action( - function=run_vizro_ai(), - inputs=[ - "text-area-id.value", - "trigger-button-id.n_clicks", - "data-store-id.data", - "model-dropdown-id.value", - "settings-api-key.value", - "settings-api-base.value", - "settings-dropdown.value", - ], - outputs=["plot-code-markdown.children", "graph-id.figure", "outputs-store-id.data"], - ), - ], ), - MyDropdown(options=SUPPORTED_MODELS, value="gpt-4o-mini", multi=False, id="model-dropdown-id"), - OffCanvas(id="settings", options=["OpenAI"], value="OpenAI"), + MyDropdown(options=SUPPORTED_MODELS, value="gpt-4o-mini", multi=False, id="dashboard-model-dropdown"), + Icon(id="open-settings-id"), + OffCanvas(id="dashboard-settings", options=["OpenAI"], value="OpenAI"), # Modal(id="modal"), ], ), - Icon(id="open-settings-id"), ], ) - -dashboard = CustomDashboard(pages=[plot_page]) +dashboard = CustomDashboard(pages=[dashboard_page]) -# pure dash callbacks @callback( - [ - Output("plot-code-markdown", "children", allow_duplicate=True), - Output("graph-id", "figure", allow_duplicate=True), - Output("text-area-id", "value"), - Output("upload-message-id", "children"), - ], - [Input("on_page_load_action_trigger_vizro_ai_plot_page", "data")], - [State("outputs-store-id", "data")], - prevent_initial_call="initial_duplicate", + Output("dashboard-settings-api-key", "type"), + Input("dashboard-settings-api-key-toggle", "value"), ) -def update_data(page_data, outputs_data): - """Callback for retrieving latest vizro-ai output from dcc store.""" - if not outputs_data: - raise PreventUpdate +def show_api_key(value): + """Callback to show api key.""" + return "text" if value else "password" - ai_response = outputs_data["ai_response"] - fig = json.loads(outputs_data["figure"]) - filename = f"File uploaded: '{outputs_data['filename']}'" - prompt = outputs_data["prompt"] - return ai_response, fig, prompt, filename +@callback( + Output("dashboard-settings-api-base", "type"), + Input("dashboard-settings-api-base-toggle", "value"), +) +def show_api_base(value): + """Callback to show api base.""" + return "text" if value else "password" @callback( - Output("settings", "is_open"), + Output("dashboard-settings", "is_open"), Input("open-settings-id", "n_clicks"), - [State("settings", "is_open")], + [State("dashboard-settings", "is_open")], ) def open_settings(n_clicks, is_open): - """Callback for opening and closing offcanvas settings component.""" return not is_open if n_clicks else is_open @callback( - Output("settings-api-key", "type"), - Input("settings-api-key-toggle", "value"), -) -def show_api_key(value): - """Callback to show api key.""" - return "text" if value else "password" - - -@callback( - Output("settings-api-base", "type"), - Input("settings-api-base-toggle", "value"), + Output("dashboard-code-markdown", "children"), + [ + Input("dashboard-text-area", "value"), + Input("dashboard-model-dropdown", "value"), + Input("dashboard-settings-api-key", "value"), + Input("dashboard-settings-api-base", "value"), + Input("dashboard-trigger-button", "n_clicks"), + Input("dashboard-data-store", "data"), + ], ) -def show_api_base(value): - """Callback to show api base.""" - return "text" if value else "password" +def run_script(user_prompt, model, api_key, api_base, n_clicks, data): + data = json.dumps(data) + if n_clicks is None: + raise PreventUpdate + else: + result = subprocess.run( + [ + "python", + "run_vizro_ai.py", + "--arg1", + f"{user_prompt}", + "--arg2", + f"{model}", + "--arg3", + f"{api_key}", + "--arg4", + f"{api_base}", + "--arg5", + f"{n_clicks}", + "--arg6", + data, + ], + capture_output=True, + text=True, + ) + if result.returncode == 0: + start_index = result.stdout.find("```") + output = result.stdout[start_index:] + return output + return result.stderr app = Vizro().build(dashboard) app.dash.layout.children.append( html.Div( [ - dbc.NavLink("Contact Vizro", href="https://github.com/mckinsey/vizro/issues"), + dbc.NavLink("Contact us", href="https://github.com/mckinsey/vizro/issues"), dbc.NavLink("GitHub", href="https://github.com/mckinsey/vizro"), dbc.NavLink("Docs", href="https://vizro.readthedocs.io/projects/vizro-ai/"), html.Div( @@ -194,6 +181,6 @@ def show_api_base(value): ) ) -server = app.dash.server + if __name__ == "__main__": app.run() diff --git a/vizro-ai/examples/dashboard_ui/assets/custom_css.css b/vizro-ai/examples/dashboard_ui/assets/custom_css.css index 02dcbe0b8..3840aed5e 100644 --- a/vizro-ai/examples/dashboard_ui/assets/custom_css.css +++ b/vizro-ai/examples/dashboard_ui/assets/custom_css.css @@ -22,11 +22,11 @@ outline-width: 0; } -#text-area-id { +#dashboard-text-area { background-color: inherit; border: 1px solid var(--border-subtle-alpha-01); color: var(--text-primary); - min-height: 90px; + height: 500px; padding: 8px; width: 100%; } @@ -45,8 +45,7 @@ .code-clipboard-container { background: var(--surfaces-bg-card); font-family: monospace; - height: 500px; - max-height: 500px; + height: 100%; overflow: auto; padding: 1rem; position: relative; @@ -66,15 +65,13 @@ font-size: 12px; } -#model-dropdown-id .Select-menu-outer { +#dashboard-model-dropdown .Select-menu-outer { font-size: 12px; - - /* top: 0; */ - - /* transform: translateY(3px) translateY(-100%); */ + top: 0; + transform: translateY(3px) translateY(-100%); } -#model-dropdow-idn .dash-dropdown { +#dashboard-model-dropdown .dash-dropdown { background-color: inherit; font-size: 12px; } @@ -83,6 +80,10 @@ width: 100%; } +#dashboard-trigger-button { + width: 100%; +} + #dashboard-container .dash-dropdown { background-color: inherit; } @@ -105,6 +106,16 @@ padding-top: 8px; } +.card:has(#dashboard-upload-message-id) { + background-color: inherit; + box-shadow: none; + font-size: 12px; + overflow: hidden; + padding-bottom: 0; + padding-left: 0; + padding-top: 8px; +} + .card:has(#settings-card-id) { background-color: inherit; box-shadow: none; @@ -121,7 +132,7 @@ width: 100%; } -#data-upload-id { +#dashboard-data-upload { border: 1px dashed var(--border-subtle-alpha-01); border-radius: 5px; color: var(--text-primary); @@ -138,6 +149,14 @@ border-radius: 8px; } +#dashboard-settings-api-key-toggle .form-check-input { + border-radius: 8px; +} + +#dashboard-settings-api-base-toggle .form-check-input { + border-radius: 8px; +} + #toggle-div-api-base, #toggle-div-api-key { align-items: center; diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index b34a6a25e..d5b7cae97 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -27,6 +27,7 @@ class UserPromptTextArea(vm.VizroBaseModel): type: Literal["user_text_area"] = "user_text_area" actions: List[Action] = [] # noqa: RUF012 + placeholder: str _set_actions = _action_validator_factory("value") @@ -37,8 +38,7 @@ def build(self): children=[ dcc.Textarea( id=self.id, - placeholder="Describe the chart you want to create, e.g. " - "'Visualize the life expectancy per continent.'", + placeholder=self.placeholder, ) ] ) @@ -247,6 +247,6 @@ class CustomDashboard(vm.Dashboard): def build(self): """Returns custom dashboard.""" dashboard_build_obj = super().build() - dashboard_build_obj.children.append(dcc.Store(id="data-store-id", storage_type="session")) - dashboard_build_obj.children.append(dcc.Store(id="outputs-store-id", storage_type="session")) + dashboard_build_obj.children.append(dcc.Store(id="dashboard-data-store", storage_type="session")) + # dashboard_build_obj.children.append(dcc.Store(id="outputs-store-id", storage_type="session")) return dashboard_build_obj diff --git a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py new file mode 100644 index 000000000..5eb3ac48d --- /dev/null +++ b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py @@ -0,0 +1,52 @@ +import argparse +import json +import logging + +import black +import pandas as pd +from actions import get_vizro_ai_dashboard +from dash.exceptions import PreventUpdate + + +def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data): + """Gets the AI response.""" + data = json.loads(data) + + if not n_clicks: + raise PreventUpdate + + if not data: + return "Please upload data to proceed!" + + if not api_key: + return "API key not found. Make sure you enter your API key!" + + try: + df = pd.DataFrame(data["data"]) + ai_outputs = get_vizro_ai_dashboard( + user_prompt=user_prompt, dfs=df, model=model, api_key=api_key, api_base=api_base + ) + ai_code = ai_outputs.code + formatted_code = black.format_str(ai_code, mode=black.Mode(line_length=90)) + + ai_response = "\n".join(["```python", formatted_code, "```"]) + return ai_response + + except Exception as exc: + logging.exception(exc) + ai_response = f"Sorry, I can't do that. Following Error occurred: {exc}" + return ai_response + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Process some arguments.") + parser.add_argument("--arg1", required=True, help="User prompt") + parser.add_argument("--arg2", required=True, help="Model") + parser.add_argument("--arg3", required=True, help="API key") + parser.add_argument("--arg4", required=True, help="API base") + parser.add_argument("--arg5", required=True, help="n_clicks") + parser.add_argument("--arg6", required=True, help="Data") + + args = parser.parse_args() + + print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6)) From 13bb8b0317b45b5b5f9c32a6420ad739eaf94b84 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 9 Sep 2024 19:57:27 +0200 Subject: [PATCH 02/14] adding dashboard ui --- vizro-ai/examples/chart_ui/app.py | 2 +- vizro-ai/examples/dashboard_ui/app.py | 10 +++++++++- vizro-ai/examples/dashboard_ui/run_vizro_ai.py | 13 +++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/vizro-ai/examples/chart_ui/app.py b/vizro-ai/examples/chart_ui/app.py index 3c46e40d5..bcfbd6d3e 100644 --- a/vizro-ai/examples/chart_ui/app.py +++ b/vizro-ai/examples/chart_ui/app.py @@ -1,4 +1,4 @@ -"""VizroAI UI dashboard configuration.""" +"""VizroAI chart UI configuration.""" import json diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 8db9d3ca4..d3fa163d1 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -1,3 +1,5 @@ +"""VizroAI dashboard UI configuration.""" + import json import subprocess @@ -116,6 +118,7 @@ def show_api_base(value): [State("dashboard-settings", "is_open")], ) def open_settings(n_clicks, is_open): + """Callback for opening and closing offcanvas settings component.""" return not is_open if n_clicks else is_open @@ -128,9 +131,11 @@ def open_settings(n_clicks, is_open): Input("dashboard-settings-api-base", "value"), Input("dashboard-trigger-button", "n_clicks"), Input("dashboard-data-store", "data"), + Input("dashboard-settings-dropdown", "value"), ], ) -def run_script(user_prompt, model, api_key, api_base, n_clicks, data): +def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): # noqa: PLR0913 + """Callback for triggering subprocess that run vizro-ai.""" data = json.dumps(data) if n_clicks is None: raise PreventUpdate @@ -151,9 +156,12 @@ def run_script(user_prompt, model, api_key, api_base, n_clicks, data): f"{n_clicks}", "--arg6", data, + "--arg7", + f"{vendor}", ], capture_output=True, text=True, + check=False, ) if result.returncode == 0: start_index = result.stdout.find("```") diff --git a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py index 5eb3ac48d..25712795b 100644 --- a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py +++ b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py @@ -1,3 +1,5 @@ +"""File running Vizro AI.""" + import argparse import json import logging @@ -8,8 +10,8 @@ from dash.exceptions import PreventUpdate -def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data): - """Gets the AI response.""" +def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data, vendor): # noqa: PLR0913 + """Function for running VizroAI.""" data = json.loads(data) if not n_clicks: @@ -24,7 +26,7 @@ def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data try: df = pd.DataFrame(data["data"]) ai_outputs = get_vizro_ai_dashboard( - user_prompt=user_prompt, dfs=df, model=model, api_key=api_key, api_base=api_base + user_prompt=user_prompt, dfs=df, model=model, api_key=api_key, api_base=api_base, vendor_input=vendor ) ai_code = ai_outputs.code formatted_code = black.format_str(ai_code, mode=black.Mode(line_length=90)) @@ -46,7 +48,10 @@ def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data parser.add_argument("--arg4", required=True, help="API base") parser.add_argument("--arg5", required=True, help="n_clicks") parser.add_argument("--arg6", required=True, help="Data") + parser.add_argument("--arg7", required=True, help="Vendor") args = parser.parse_args() - print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6)) + print( + run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6, args.arg7) + ) # noqa: T201 From 85af56aa333fa8ce259442a298d527f401ad15fb Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 9 Sep 2024 20:09:59 +0200 Subject: [PATCH 03/14] adjusting settings button possition --- vizro-ai/examples/chart_ui/assets/custom_css.css | 1 + vizro-ai/examples/dashboard_ui/app.py | 9 ++++++++- vizro-ai/examples/dashboard_ui/components.py | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/vizro-ai/examples/chart_ui/assets/custom_css.css b/vizro-ai/examples/chart_ui/assets/custom_css.css index 02dcbe0b8..c9279680a 100644 --- a/vizro-ai/examples/chart_ui/assets/custom_css.css +++ b/vizro-ai/examples/chart_ui/assets/custom_css.css @@ -119,6 +119,7 @@ justify-content: end; padding-right: 2px; width: 100%; + align-items: center; } #data-upload-id { diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index d3fa163d1..fc8fb3b4d 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -76,7 +76,14 @@ ), vm.Container( title="", - layout=vm.Layout(grid=[[2, 3, -1, -1, -1, -1, 1, 1, 0, 0]], row_gap="0px", col_gap="4px"), + layout=vm.Layout( + grid=[ + [3, -1, -1, -1, -1, -1, 1, 1, 0, 0], + [-1, -1, -1, -1, -1, -1, -1, -1, 2, 2] + ], + row_gap="0px", + col_gap="4px" + ), components=[ vm.Button( id="dashboard-trigger-button", diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index d5b7cae97..323a06f4b 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -236,7 +236,8 @@ class Icon(vm.VizroBaseModel): def build(self): """Returns the icon for api settings.""" return html.Div( - children=[html.Span("settings", className="material-symbols-outlined", id=self.id)], + children=[html.P("Settings", style={"display": "flex", "justifyContent": "center", "alignItems": "center", "paddingTop": "4px", "paddingRight": "8px"}), + html.Span("settings", className="material-symbols-outlined", id=self.id)], className="settings-div", ) From f8e780f85228737ca006306bdbcefe7a8ab0e513 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 9 Sep 2024 20:15:17 +0200 Subject: [PATCH 04/14] fix linting --- vizro-ai/examples/chart_ui/assets/custom_css.css | 2 +- vizro-ai/examples/dashboard_ui/app.py | 7 ++----- vizro-ai/examples/dashboard_ui/components.py | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/vizro-ai/examples/chart_ui/assets/custom_css.css b/vizro-ai/examples/chart_ui/assets/custom_css.css index c9279680a..1d6b16ebb 100644 --- a/vizro-ai/examples/chart_ui/assets/custom_css.css +++ b/vizro-ai/examples/chart_ui/assets/custom_css.css @@ -115,11 +115,11 @@ } .settings-div { + align-items: center; display: flex; justify-content: end; padding-right: 2px; width: 100%; - align-items: center; } #data-upload-id { diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index fc8fb3b4d..3cb09c486 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -77,12 +77,9 @@ vm.Container( title="", layout=vm.Layout( - grid=[ - [3, -1, -1, -1, -1, -1, 1, 1, 0, 0], - [-1, -1, -1, -1, -1, -1, -1, -1, 2, 2] - ], + grid=[[3, -1, -1, -1, -1, -1, 1, 1, 0, 0], [-1, -1, -1, -1, -1, -1, -1, -1, 2, 2]], row_gap="0px", - col_gap="4px" + col_gap="4px", ), components=[ vm.Button( diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index 323a06f4b..feec1295c 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -236,8 +236,19 @@ class Icon(vm.VizroBaseModel): def build(self): """Returns the icon for api settings.""" return html.Div( - children=[html.P("Settings", style={"display": "flex", "justifyContent": "center", "alignItems": "center", "paddingTop": "4px", "paddingRight": "8px"}), - html.Span("settings", className="material-symbols-outlined", id=self.id)], + children=[ + html.P( + "Settings", + style={ + "display": "flex", + "justifyContent": "center", + "alignItems": "center", + "paddingTop": "4px", + "paddingRight": "8px", + }, + ), + html.Span("settings", className="material-symbols-outlined", id=self.id), + ], className="settings-div", ) From 442d3786c98cff7408778bd6ca5ddf28a50474f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:16:15 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- vizro-ai/examples/dashboard_ui/run_vizro_ai.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py index 25712795b..7ae262d6d 100644 --- a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py +++ b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py @@ -52,6 +52,4 @@ def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data args = parser.parse_args() - print( - run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6, args.arg7) - ) # noqa: T201 + print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6, args.arg7)) From c2d653ae7b4d25b6c6616107e4d0a2482004b3f4 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 9 Sep 2024 20:18:40 +0200 Subject: [PATCH 06/14] adding changelog file --- ..._nadija_ratkusic_graca_add_dashboard_ui.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 vizro-ai/changelog.d/20240909_201831_nadija_ratkusic_graca_add_dashboard_ui.md diff --git a/vizro-ai/changelog.d/20240909_201831_nadija_ratkusic_graca_add_dashboard_ui.md b/vizro-ai/changelog.d/20240909_201831_nadija_ratkusic_graca_add_dashboard_ui.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-ai/changelog.d/20240909_201831_nadija_ratkusic_graca_add_dashboard_ui.md @@ -0,0 +1,48 @@ + + + + + + + + + From 94ea3bcc8f889375ec638b217ce9ea279e35d409 Mon Sep 17 00:00:00 2001 From: Lingyi Zhang Date: Tue, 10 Sep 2024 00:45:21 -0400 Subject: [PATCH 07/14] add dockerfile --- vizro-ai/examples/chart_ui/Dockerfile | 2 ++ vizro-ai/examples/dashboard_ui/Dockerfile | 20 ++++++++++++++++++++ vizro-ai/examples/dashboard_ui/app.py | 1 + 3 files changed, 23 insertions(+) create mode 100644 vizro-ai/examples/dashboard_ui/Dockerfile diff --git a/vizro-ai/examples/chart_ui/Dockerfile b/vizro-ai/examples/chart_ui/Dockerfile index a09ba3aef..1215eadda 100644 --- a/vizro-ai/examples/chart_ui/Dockerfile +++ b/vizro-ai/examples/chart_ui/Dockerfile @@ -15,4 +15,6 @@ COPY --chown=user . /app EXPOSE 7860 +USER user + CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "app:server"] diff --git a/vizro-ai/examples/dashboard_ui/Dockerfile b/vizro-ai/examples/dashboard_ui/Dockerfile new file mode 100644 index 000000000..aed92b90c --- /dev/null +++ b/vizro-ai/examples/dashboard_ui/Dockerfile @@ -0,0 +1,20 @@ +# read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker +# you will also find guides on how best to write your Dockerfile + +FROM python:3.12 + +RUN useradd -m -u 1000 user + +WORKDIR /app + +COPY --chown=user ./requirements.txt requirements.txt + +RUN pip install --no-cache-dir --upgrade -r requirements.txt + +COPY --chown=user . /app + +EXPOSE 7868 + +USER user + +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7868", "app:server"] diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 3cb09c486..fe2fc43a1 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -194,5 +194,6 @@ def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): ) +server = app.dash.server if __name__ == "__main__": app.run() From e4d566ff10bc8058abdeb9410c5d8dfa81607905 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Tue, 10 Sep 2024 17:32:54 +0200 Subject: [PATCH 08/14] add ability to upload multiple files --- vizro-ai/examples/dashboard_ui/_utils.py | 30 +++++++++++ vizro-ai/examples/dashboard_ui/actions.py | 51 ++++++++----------- vizro-ai/examples/dashboard_ui/app.py | 14 ++--- .../dashboard_ui/assets/custom_css.css | 5 ++ vizro-ai/examples/dashboard_ui/components.py | 7 +-- .../examples/dashboard_ui/run_vizro_ai.py | 11 ++-- 6 files changed, 71 insertions(+), 47 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/_utils.py b/vizro-ai/examples/dashboard_ui/_utils.py index 534e24552..a5e5f5015 100644 --- a/vizro-ai/examples/dashboard_ui/_utils.py +++ b/vizro-ai/examples/dashboard_ui/_utils.py @@ -1,8 +1,38 @@ """Utils file.""" +import base64 +import io +import logging + +import pandas as pd + def check_file_extension(filename): filename = filename.lower() # Check if the filename ends with .csv or .xls return filename.endswith(".csv") or filename.endswith(".xls") or filename.endswith(".xlsx") + + +def process_file(contents, filename): + """Process the uploaded file content based on its extension.""" + if not check_file_extension(filename=filename): + return {"error_message": "Unsupported file extension.. Make sure to upload either csv or an excel file."} + + try: + content_type, content_string = contents.split(",") + decoded = base64.b64decode(content_string) + if filename.endswith(".csv"): + # Handle CSV file + df = pd.read_csv(io.StringIO(decoded.decode("utf-8"))) + else: + # Handle Excel file + df = pd.read_excel(io.BytesIO(decoded)) + + data = df.to_dict("records") + return data + + except Exception as e: + logging.exception(f"Error processing the file '{filename}': {e}") + logging.exception(e) + return {"error_message": f"There was an error processing the file '{filename}'."} diff --git a/vizro-ai/examples/dashboard_ui/actions.py b/vizro-ai/examples/dashboard_ui/actions.py index 15627e693..96793ee51 100644 --- a/vizro-ai/examples/dashboard_ui/actions.py +++ b/vizro-ai/examples/dashboard_ui/actions.py @@ -1,11 +1,6 @@ """Custom actions used within a dashboard.""" -import base64 -import io -import logging - -import pandas as pd -from _utils import check_file_extension +from _utils import process_file from dash.exceptions import PreventUpdate from langchain_openai import ChatOpenAI from vizro.models.types import capture @@ -19,44 +14,40 @@ def get_vizro_ai_dashboard(user_prompt, dfs, model, api_key, api_base, vendor_in vendor = SUPPORTED_VENDORS[vendor_input] llm = vendor(model_name=model, openai_api_key=api_key, openai_api_base=api_base) vizro_ai = VizroAI(model=llm) - ai_outputs = vizro_ai._dashboard([dfs], user_prompt, return_elements=True) + ai_outputs = vizro_ai._dashboard(dfs, user_prompt, return_elements=True) return ai_outputs @capture("action") def data_upload_action(contents, filename): - """Custom data upload action.""" + """Custom action to handle data upload for single or multiple files.""" if not contents: raise PreventUpdate - if not check_file_extension(filename=filename): - return {"error_message": "Unsupported file extension.. Make sure to upload either csv or an excel file."} - - content_type, content_string = contents.split(",") + uploaded_data = {} + if isinstance(filename, str): + data = process_file(filename=filename, contents=contents) + uploaded_data[filename] = data - try: - decoded = base64.b64decode(content_string) - if filename.endswith(".csv"): - # Handle CSV file - df = pd.read_csv(io.StringIO(decoded.decode("utf-8"))) - else: - # Handle Excel file - df = pd.read_excel(io.BytesIO(decoded)) + if isinstance(filename, list): + for index, item in enumerate(filename): + data = process_file(filename=item, contents=contents[index]) + uploaded_data[item] = data - data = df.to_dict("records") - return {"data": data, "filename": filename} - - except Exception as e: - logging.exception(e) - return {"error_message": "There was an error processing this file."} + return uploaded_data @capture("action") def display_filename(data): - """Custom action to display uploaded filename.""" - if data is None: + """Custom action to display the uploaded filename.""" + if not data: raise PreventUpdate + # Check for any error message in the data + error_message = data.get("error_message") + + if error_message: + return error_message - display_message = data.get("filename") or data.get("error_message") - return f"Uploaded file name: '{display_message}'" if "filename" in data else display_message + filenames = ", ".join(data.keys()) + return f"Uploaded file name: '{filenames}'" diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index fe2fc43a1..5e4326475 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -50,7 +50,7 @@ grid=[[2, 2, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [3, 3, 0, 0, 0]] ), components=[ - vm.Container(title="", components=[CodeClipboard(id="dashboard")]), + vm.Container(title="", components=[CodeClipboard(id="dashboard")], id="clipboard-container"), UserPromptTextArea(id="dashboard-text-area", placeholder="Describe the dashboard you want to create."), vm.Container( title="", @@ -129,13 +129,13 @@ def open_settings(n_clicks, is_open): @callback( Output("dashboard-code-markdown", "children"), [ - Input("dashboard-text-area", "value"), - Input("dashboard-model-dropdown", "value"), - Input("dashboard-settings-api-key", "value"), - Input("dashboard-settings-api-base", "value"), + State("dashboard-text-area", "value"), + State("dashboard-model-dropdown", "value"), + State("dashboard-settings-api-key", "value"), + State("dashboard-settings-api-base", "value"), Input("dashboard-trigger-button", "n_clicks"), - Input("dashboard-data-store", "data"), - Input("dashboard-settings-dropdown", "value"), + State("dashboard-data-store", "data"), + State("dashboard-settings-dropdown", "value"), ], ) def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): # noqa: PLR0913 diff --git a/vizro-ai/examples/dashboard_ui/assets/custom_css.css b/vizro-ai/examples/dashboard_ui/assets/custom_css.css index 3840aed5e..33e17a9dc 100644 --- a/vizro-ai/examples/dashboard_ui/assets/custom_css.css +++ b/vizro-ai/examples/dashboard_ui/assets/custom_css.css @@ -49,6 +49,7 @@ overflow: auto; padding: 1rem; position: relative; + width: 100%; } .code-clipboard-container::-webkit-scrollbar-thumb { @@ -176,3 +177,7 @@ position: fixed; width: 100%; } + +#clipboard-container { + overflow: hidden; +} diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index feec1295c..733f42e1d 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -71,6 +71,7 @@ def build(self): children=html.Div( ["Drag and Drop or ", html.A("Select Files")], style={"fontColor": "rgba(255, 255, 255, 0.6)"} ), + multiple=True, ), ] ) @@ -85,17 +86,17 @@ class CodeClipboard(vm.VizroBaseModel): def build(self): """Returns the code clipboard component inside a output text area.""" - code = black.format_str(self.code, mode=black.Mode(line_length=120)) + code = black.format_str(self.code, mode=black.Mode(line_length=100)) code = code.strip("'\"") markdown_code = "\n".join(["```python", code, "```"]) - return html.Div( + return dcc.Loading( [ dcc.Clipboard(target_id=f"{self.id}-code-markdown", className="code-clipboard"), dcc.Markdown(markdown_code, id=f"{self.id}-code-markdown"), ], - className="code-clipboard-container", + parent_className="code-clipboard-container", ) diff --git a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py index 7ae262d6d..2c348d74e 100644 --- a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py +++ b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py @@ -11,25 +11,22 @@ def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data, vendor): # noqa: PLR0913 - """Function for running VizroAI.""" - data = json.loads(data) - + """Function to run the VizroAI dashboard based on user inputs and API configurations.""" if not n_clicks: raise PreventUpdate if not data: return "Please upload data to proceed!" - if not api_key: return "API key not found. Make sure you enter your API key!" try: - df = pd.DataFrame(data["data"]) + dfs = [pd.DataFrame(item) for item in json.loads(data).values()] ai_outputs = get_vizro_ai_dashboard( - user_prompt=user_prompt, dfs=df, model=model, api_key=api_key, api_base=api_base, vendor_input=vendor + user_prompt=user_prompt, dfs=dfs, model=model, api_key=api_key, api_base=api_base, vendor_input=vendor ) ai_code = ai_outputs.code - formatted_code = black.format_str(ai_code, mode=black.Mode(line_length=90)) + formatted_code = black.format_str(ai_code, mode=black.Mode(line_length=100)) ai_response = "\n".join(["```python", formatted_code, "```"]) return ai_response From f4bfa8d3917d73ff616522b474394ade4522a094 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Thu, 12 Sep 2024 13:48:59 +0200 Subject: [PATCH 09/14] adjusting subprocess data flow --- vizro-ai/examples/dashboard_ui/app.py | 19 +++++++++---------- .../examples/dashboard_ui/run_vizro_ai.py | 10 ++++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 5e4326475..9a796bafe 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -144,7 +144,7 @@ def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): if n_clicks is None: raise PreventUpdate else: - result = subprocess.run( + process = subprocess.Popen( [ "python", "run_vizro_ai.py", @@ -159,19 +159,18 @@ def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): "--arg5", f"{n_clicks}", "--arg6", - data, - "--arg7", f"{vendor}", ], - capture_output=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, - check=False, ) - if result.returncode == 0: - start_index = result.stdout.find("```") - output = result.stdout[start_index:] - return output - return result.stderr + stdout_data, stderr_data = process.communicate(input=data) + if stdout_data: + start_index = stdout_data.find("```") + return stdout_data[start_index:] + return stderr_data app = Vizro().build(dashboard) diff --git a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py index 2c348d74e..d52d4660b 100644 --- a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py +++ b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py @@ -3,6 +3,7 @@ import argparse import json import logging +import sys import black import pandas as pd @@ -10,8 +11,10 @@ from dash.exceptions import PreventUpdate -def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data, vendor): # noqa: PLR0913 +def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, vendor): # noqa: PLR0913 """Function to run the VizroAI dashboard based on user inputs and API configurations.""" + data = sys.stdin.read() + if not n_clicks: raise PreventUpdate @@ -44,9 +47,8 @@ def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, data parser.add_argument("--arg3", required=True, help="API key") parser.add_argument("--arg4", required=True, help="API base") parser.add_argument("--arg5", required=True, help="n_clicks") - parser.add_argument("--arg6", required=True, help="Data") - parser.add_argument("--arg7", required=True, help="Vendor") + parser.add_argument("--arg6", required=True, help="Vendor") args = parser.parse_args() - print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6, args.arg7)) + print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6)) From 699f8cb1b4345429f2266bf13c6c1738f6c15c31 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 16 Sep 2024 17:04:06 +0200 Subject: [PATCH 10/14] adding iframe --- vizro-ai/examples/dashboard_ui/_utils.py | 46 ++++++++ vizro-ai/examples/dashboard_ui/actions.py | 25 ++++ vizro-ai/examples/dashboard_ui/app.py | 110 +++++++++++++++++- vizro-ai/examples/dashboard_ui/components.py | 22 ++++ .../output_files/run_vizro_ai_output.py | 0 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py diff --git a/vizro-ai/examples/dashboard_ui/_utils.py b/vizro-ai/examples/dashboard_ui/_utils.py index a5e5f5015..8bad612d6 100644 --- a/vizro-ai/examples/dashboard_ui/_utils.py +++ b/vizro-ai/examples/dashboard_ui/_utils.py @@ -36,3 +36,49 @@ def process_file(contents, filename): logging.exception(f"Error processing the file '{filename}': {e}") logging.exception(e) return {"error_message": f"There was an error processing the file '{filename}'."} + + +def format_output(generated_code): + generated_code = generated_code.replace("```python", "") + generated_code = generated_code.replace("```", "") + + code_lines = generated_code.split("\n") + + for i, line in enumerate(code_lines): + if line.startswith("import vizro.plotly.express"): + # Insert the additional import statement right after the first import + code_lines.insert(i + 1, "from vizro import Vizro") + code_lines.insert(i + 2, "import os") + code_lines.insert(i + 3, "import pandas as pd") + + break # Ensure only one insertion after the first import + + for i, line in enumerate(code_lines): + if line.startswith("####### Function definitions ######"): + code_lines.insert(i - 1, "script_directory = os.path.dirname(os.path.abspath(__file__))") + break + + for i, line in enumerate(code_lines): + if line.startswith("script_directory"): + code_lines.insert(i + 1, "all_files = os.listdir(script_directory)") + code_lines.insert(i + 2, "csv_files = [file for file in all_files if file.endswith('.csv')]") + code_lines.insert(i + 3, "csv_file = csv_files[0]") + code_lines.insert(i + 4, "file_path = os.path.join(script_directory, csv_file)") + code_lines.insert(i + 5, "df = pd.read_csv(file_path)") + + for i, line in enumerate(code_lines): + if line.startswith("# from vizro.managers "): + code_lines[i] = line.lstrip("#").strip() + if line.startswith("# data_manager"): + code_lines[i] = line.lstrip("#").strip() + + for i, line in enumerate(code_lines): + if line.startswith("data_manager"): + code_lines[i] = line.replace("===> Fill in here <===", "df") + + generated_code = "\n".join(code_lines) + generated_code += "\napp = Vizro().build(model)\n" + generated_code += '\nif __name__ == "__main__":\n' + generated_code += " app.run(port=8051)\n" + + return generated_code diff --git a/vizro-ai/examples/dashboard_ui/actions.py b/vizro-ai/examples/dashboard_ui/actions.py index 96793ee51..d1b18e464 100644 --- a/vizro-ai/examples/dashboard_ui/actions.py +++ b/vizro-ai/examples/dashboard_ui/actions.py @@ -1,5 +1,8 @@ """Custom actions used within a dashboard.""" +import base64 +import os + from _utils import process_file from dash.exceptions import PreventUpdate from langchain_openai import ChatOpenAI @@ -51,3 +54,25 @@ def display_filename(data): filenames = ", ".join(data.keys()) return f"Uploaded file name: '{filenames}'" + + +@capture("action") +def save_files(contents, filenames, last_modified): + """Custom action to save the uploaded filenames.""" + if contents is not None: + + project_folder = "./output_files" + if not os.path.exists(project_folder): + os.makedirs(project_folder) + + for content, filename in zip(contents, filenames): + # Decode base64 encoded data + content_type, content_string = content.split(",") + decoded = base64.b64decode(content_string) + + # Save each file in the desired folder + file_path = os.path.join(project_folder, filename) + with open(file_path, "wb") as f: + f.write(decoded) + + return last_modified diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 9a796bafe..9de4ecaad 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -5,11 +5,14 @@ import dash_bootstrap_components as dbc import vizro.models as vm -from actions import data_upload_action, display_filename +from _utils import format_output +from actions import data_upload_action, display_filename, save_files from components import ( CodeClipboard, + CustomButton, CustomDashboard, Icon, + IframeComponent, Modal, MyDropdown, MyPage, @@ -34,6 +37,8 @@ vm.Container.add_type("components", CodeClipboard) vm.Container.add_type("components", Icon) vm.Container.add_type("components", Modal) +vm.Container.add_type("components", CustomButton) +vm.Container.add_type("components", IframeComponent) MyPage.add_type("components", UserPromptTextArea) MyPage.add_type("components", UserUpload) @@ -50,7 +55,58 @@ grid=[[2, 2, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [3, 3, 0, 0, 0]] ), components=[ - vm.Container(title="", components=[CodeClipboard(id="dashboard")], id="clipboard-container"), + # vm.Container( + # id="clipboard-container", + # title="", + # components=[ + # CodeClipboard(id="dashboard"), + # CustomButton(text="Run dashboard", id="run-dashboard-button"), + # ], + # layout=vm.Layout( + # grid=[ + # *[[0, 0, 0, 0, 0, 0]] * 11, + # [-1, -1, -1, -1, -1, 1] + # ], + # col_gap="20px" + # ) + # ), + vm.Tabs( + tabs=[ + vm.Container( + title="Code", + components=[ + vm.Container( + title="", + components=[ + vm.Container( + id="clipboard-container", + title="", + components=[ + CodeClipboard(id="dashboard"), + CustomButton(text="Run dashboard", id="run-dashboard-button"), + ], + layout=vm.Layout( + grid=[*[[0, 0, 0, 0, 0, 0]] * 10, [-1, -1, -1, -1, -1, 1]], col_gap="20px" + ), + ) + ], + id="clipboard-tab", + ), + ], + ), + vm.Container( + title="Dashboard", + components=[ + IframeComponent( + id="embedded_dashboard", + # src="http://localhost:7868/", + src="http://localhost:8051/", + height="600px", + ) + ], + ), + ], + ), UserPromptTextArea(id="dashboard-text-area", placeholder="Describe the dashboard you want to create."), vm.Container( title="", @@ -70,6 +126,15 @@ inputs=["dashboard-data-store.data"], outputs=["dashboard-upload-message-id.children"], ), + vm.Action( + function=save_files(), + inputs=[ + "dashboard-data-upload.contents", + "dashboard-data-upload.filename", + "dashboard-data-upload.last_modified", + ], + outputs=["dashboard-data-store.modified_timestamp"], + ), ], ), ], @@ -173,6 +238,47 @@ def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): return stderr_data +@callback( + Output("dashboard-code-markdown", "style"), + Input("dashboard-code-markdown", "children"), +) +def save_to_file(generated_code): + gen_ai_file = "output_files/run_vizro_ai_output.py" + + # format code + generated_code = format_output(generated_code) + + if generated_code: + with open(gen_ai_file, "w") as f: + f.write(generated_code) + + return {} + + +@callback( + Output("run-dashboard-button", "style"), + Input("dashboard-code-markdown", "children"), +) +def show_button(ai_response): + if ai_response: + return {"minWidth": "100%"} + + +# @callback( +# Output("embedded_dashboard", "src"), +# Input("run-dashboard-button", "n_clicks"), +# ) +# def run_generated_dashboard(n_clicks): +# if not n_clicks: +# raise PreventUpdate +# else: +# subprocess.Popen( +# ['python', 'output_files/run_vizro_ai_output.py'], +# capture_output=True +# ) +# return "http://localhost:8051/" + + app = Vizro().build(dashboard) app.dash.layout.children.append( html.Div( diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index 733f42e1d..58ae9027f 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -263,3 +263,25 @@ def build(self): dashboard_build_obj.children.append(dcc.Store(id="dashboard-data-store", storage_type="session")) # dashboard_build_obj.children.append(dcc.Store(id="outputs-store-id", storage_type="session")) return dashboard_build_obj + + +class CustomButton(vm.Button): + type: Literal["custom_button"] = "custom_button" + + def build(self): + button_build_obj = super().build() + # button_build_obj.disabled = True + button_build_obj.style = {"minWidth": "100%", "display": "none"} + return button_build_obj + + +class IframeComponent(vm.VizroBaseModel): + """Custom component to embed an iframe in Vizro.""" + + type: Literal["iframe"] = "iframe" + src: str = "" + width: str = "100%" + height: str = "600px" + + def build(self): + return html.Iframe(src=self.src, style={"width": self.width, "height": self.height, "border": "none"}) diff --git a/vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py b/vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py new file mode 100644 index 000000000..e69de29bb From 61f34d8e01a88cd18c1146baac558ebef86199b0 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Mon, 23 Sep 2024 12:19:39 +0200 Subject: [PATCH 11/14] updating iframe --- vizro-ai/examples/dashboard_ui/app.py | 39 ++++++++----------- vizro-ai/examples/dashboard_ui/components.py | 15 ++----- .../output_files/run_vizro_ai_output.py | 1 + 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 9de4ecaad..248bc7716 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -12,7 +12,6 @@ CustomButton, CustomDashboard, Icon, - IframeComponent, Modal, MyDropdown, MyPage, @@ -38,7 +37,6 @@ vm.Container.add_type("components", Icon) vm.Container.add_type("components", Modal) vm.Container.add_type("components", CustomButton) -vm.Container.add_type("components", IframeComponent) MyPage.add_type("components", UserPromptTextArea) MyPage.add_type("components", UserUpload) @@ -95,15 +93,9 @@ ], ), vm.Container( + id="embedded-dashboard", title="Dashboard", - components=[ - IframeComponent( - id="embedded_dashboard", - # src="http://localhost:7868/", - src="http://localhost:8051/", - height="600px", - ) - ], + components=[vm.Card(text="VizroAI generated dashboard placeholder")], ), ], ), @@ -243,6 +235,7 @@ def run_script(user_prompt, model, api_key, api_base, n_clicks, data, vendor): Input("dashboard-code-markdown", "children"), ) def save_to_file(generated_code): + """Saves vizro-ai generated dashboard code to a file.""" gen_ai_file = "output_files/run_vizro_ai_output.py" # format code @@ -260,23 +253,23 @@ def save_to_file(generated_code): Input("dashboard-code-markdown", "children"), ) def show_button(ai_response): + """Displays a button to launch the dashboard in a subprocess.""" if ai_response: return {"minWidth": "100%"} -# @callback( -# Output("embedded_dashboard", "src"), -# Input("run-dashboard-button", "n_clicks"), -# ) -# def run_generated_dashboard(n_clicks): -# if not n_clicks: -# raise PreventUpdate -# else: -# subprocess.Popen( -# ['python', 'output_files/run_vizro_ai_output.py'], -# capture_output=True -# ) -# return "http://localhost:8051/" +@callback( + [Output("run-dashboard-button", "disabled"), Output("embedded-dashboard", "children")], + Input("run-dashboard-button", "n_clicks"), +) +def run_generated_dashboard(n_clicks): + """Runs vizro-ai generated dashboard in an iframe window.""" + if not n_clicks: + raise PreventUpdate + else: + subprocess.Popen(["python", "output_files/run_vizro_ai_output.py"]) + iframe = html.Iframe(src="http://localhost:8051/", height="600px") + return True, iframe app = Vizro().build(dashboard) diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index 58ae9027f..7638699d7 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -266,22 +266,13 @@ def build(self): class CustomButton(vm.Button): + """Custom Button model.""" + type: Literal["custom_button"] = "custom_button" def build(self): + """Returns custom button.""" button_build_obj = super().build() # button_build_obj.disabled = True button_build_obj.style = {"minWidth": "100%", "display": "none"} return button_build_obj - - -class IframeComponent(vm.VizroBaseModel): - """Custom component to embed an iframe in Vizro.""" - - type: Literal["iframe"] = "iframe" - src: str = "" - width: str = "100%" - height: str = "600px" - - def build(self): - return html.Iframe(src=self.src, style={"width": self.width, "height": self.height, "border": "none"}) diff --git a/vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py b/vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py index e69de29bb..b7130c3e0 100644 --- a/vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py +++ b/vizro-ai/examples/dashboard_ui/output_files/run_vizro_ai_output.py @@ -0,0 +1 @@ +"""Vizro-ai generated dashboard code.""" From c8d4401d69024ca65e7b6a2a18841a3be84eb189 Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Thu, 26 Sep 2024 11:53:58 +0200 Subject: [PATCH 12/14] adding check port functionality --- vizro-ai/examples/dashboard_ui/_utils.py | 12 ++++++++++++ vizro-ai/examples/dashboard_ui/app.py | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/_utils.py b/vizro-ai/examples/dashboard_ui/_utils.py index 8bad612d6..d889aa0de 100644 --- a/vizro-ai/examples/dashboard_ui/_utils.py +++ b/vizro-ai/examples/dashboard_ui/_utils.py @@ -3,6 +3,7 @@ import base64 import io import logging +import socket import pandas as pd @@ -82,3 +83,14 @@ def format_output(generated_code): generated_code += " app.run(port=8051)\n" return generated_code + + +def check_available_port(port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sk: + return sk.connect_ex(('127.0.0.1', port)) != 0 + + +def find_available_port(base_port=8051): + while not check_available_port(base_port): + base_port += 1 + return base_port diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 248bc7716..8db4482e3 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -5,7 +5,7 @@ import dash_bootstrap_components as dbc import vizro.models as vm -from _utils import format_output +from _utils import format_output, find_available_port from actions import data_upload_action, display_filename, save_files from components import ( CodeClipboard, @@ -264,10 +264,11 @@ def show_button(ai_response): ) def run_generated_dashboard(n_clicks): """Runs vizro-ai generated dashboard in an iframe window.""" + port = find_available_port() if not n_clicks: raise PreventUpdate else: - subprocess.Popen(["python", "output_files/run_vizro_ai_output.py"]) + subprocess.Popen(["python", "output_files/run_vizro_ai_output.py", str(port)]) iframe = html.Iframe(src="http://localhost:8051/", height="600px") return True, iframe From a0750d608a4881417d5c72f3cdbe12866941310b Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Wed, 6 Nov 2024 10:56:54 +0100 Subject: [PATCH 13/14] removing iframe component --- vizro-ai/examples/dashboard_ui/_utils.py | 2 +- vizro-ai/examples/dashboard_ui/app.py | 115 +++++++++--------- .../dashboard_ui/assets/custom_css.css | 67 +++++++++- vizro-ai/examples/dashboard_ui/components.py | 44 ++++++- 4 files changed, 164 insertions(+), 64 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/_utils.py b/vizro-ai/examples/dashboard_ui/_utils.py index d889aa0de..06b459fd0 100644 --- a/vizro-ai/examples/dashboard_ui/_utils.py +++ b/vizro-ai/examples/dashboard_ui/_utils.py @@ -87,7 +87,7 @@ def format_output(generated_code): def check_available_port(port): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sk: - return sk.connect_ex(('127.0.0.1', port)) != 0 + return sk.connect_ex(("127.0.0.1", port)) != 0 def find_available_port(base_port=8051): diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 8db4482e3..7a938aec8 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -5,12 +5,13 @@ import dash_bootstrap_components as dbc import vizro.models as vm -from _utils import format_output, find_available_port +from _utils import find_available_port, format_output from actions import data_upload_action, display_filename, save_files from components import ( CodeClipboard, CustomButton, CustomDashboard, + HeaderComponent, Icon, Modal, MyDropdown, @@ -37,6 +38,7 @@ vm.Container.add_type("components", Icon) vm.Container.add_type("components", Modal) vm.Container.add_type("components", CustomButton) +vm.Container.add_type("components", HeaderComponent) MyPage.add_type("components", UserPromptTextArea) MyPage.add_type("components", UserUpload) @@ -50,58 +52,48 @@ id="vizro_ai_dashboard_page", title="Vizro AI - Dashboard", layout=vm.Layout( - grid=[[2, 2, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [3, 3, 0, 0, 0]] + grid=[ + [4, 4, 4, 4, 4], + [2, 2, 0, 0, 0], + [2, 2, 0, 0, 0], + [3, 3, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + ] ), components=[ - # vm.Container( - # id="clipboard-container", - # title="", - # components=[ - # CodeClipboard(id="dashboard"), - # CustomButton(text="Run dashboard", id="run-dashboard-button"), - # ], - # layout=vm.Layout( - # grid=[ - # *[[0, 0, 0, 0, 0, 0]] * 11, - # [-1, -1, -1, -1, -1, 1] - # ], - # col_gap="20px" - # ) - # ), - vm.Tabs( - tabs=[ + vm.Container( + title="Code", + components=[ vm.Container( - title="Code", + title="", components=[ vm.Container( + id="clipboard-container", title="", components=[ - vm.Container( - id="clipboard-container", - title="", - components=[ - CodeClipboard(id="dashboard"), - CustomButton(text="Run dashboard", id="run-dashboard-button"), - ], - layout=vm.Layout( - grid=[*[[0, 0, 0, 0, 0, 0]] * 10, [-1, -1, -1, -1, -1, 1]], col_gap="20px" - ), - ) + CodeClipboard(id="dashboard"), + CustomButton(id="run-dashboard"), ], - id="clipboard-tab", - ), + layout=vm.Layout( + grid=[*[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] * 11, [-1, -1, -1, -1, -1, -1, -1, -1, 1, 1]], + col_gap="20px", + ), + ) ], - ), - vm.Container( - id="embedded-dashboard", - title="Dashboard", - components=[vm.Card(text="VizroAI generated dashboard placeholder")], + id="clipboard-tab", ), ], ), UserPromptTextArea(id="dashboard-text-area", placeholder="Describe the dashboard you want to create."), vm.Container( - title="", + id="upload-container", + title="Turn your data into visuals — just upload, describe, and see your dashboard in action", layout=vm.Layout(grid=[[0], [1]], row_gap="0px"), components=[ vm.Card(id="dashboard-upload-message-id", text="Upload your data files (csv or excel)"), @@ -134,7 +126,9 @@ vm.Container( title="", layout=vm.Layout( - grid=[[3, -1, -1, -1, -1, -1, 1, 1, 0, 0], [-1, -1, -1, -1, -1, -1, -1, -1, 2, 2]], + grid=[ + [2, -1, -1, -1, -1, -1, 1, 1, 0, 0], + ], row_gap="0px", col_gap="4px", ), @@ -144,11 +138,14 @@ text="Run VizroAI", ), MyDropdown(options=SUPPORTED_MODELS, value="gpt-4o-mini", multi=False, id="dashboard-model-dropdown"), - Icon(id="open-settings-id"), OffCanvas(id="dashboard-settings", options=["OpenAI"], value="OpenAI"), # Modal(id="modal"), ], ), + vm.Container( + title="", + components=[HeaderComponent()], + ), ], ) @@ -249,28 +246,32 @@ def save_to_file(generated_code): @callback( - Output("run-dashboard-button", "style"), + Output("run-dashboard-navlink", "style"), Input("dashboard-code-markdown", "children"), ) def show_button(ai_response): """Displays a button to launch the dashboard in a subprocess.""" - if ai_response: - return {"minWidth": "100%"} + if not ai_response: + raise PreventUpdate + port = find_available_port() + subprocess.Popen(["python", "output_files/run_vizro_ai_output.py", str(port)]) + return {} -@callback( - [Output("run-dashboard-button", "disabled"), Output("embedded-dashboard", "children")], - Input("run-dashboard-button", "n_clicks"), -) -def run_generated_dashboard(n_clicks): - """Runs vizro-ai generated dashboard in an iframe window.""" - port = find_available_port() - if not n_clicks: - raise PreventUpdate - else: - subprocess.Popen(["python", "output_files/run_vizro_ai_output.py", str(port)]) - iframe = html.Iframe(src="http://localhost:8051/", height="600px") - return True, iframe +# +# @callback( +# [Output("run-dashboard-button", "disabled"), Output("embedded-dashboard", "children")], +# Input("run-dashboard-button", "n_clicks"), +# ) +# def run_generated_dashboard(n_clicks): +# """Runs vizro-ai generated dashboard in an iframe window.""" +# port = find_available_port() +# if not n_clicks: +# raise PreventUpdate +# else: +# subprocess.Popen(["python", "output_files/run_vizro_ai_output.py", str(port)]) +# iframe = html.Iframe(src="http://localhost:8051/", height="600px") +# return True, iframe app = Vizro().build(dashboard) diff --git a/vizro-ai/examples/dashboard_ui/assets/custom_css.css b/vizro-ai/examples/dashboard_ui/assets/custom_css.css index 33e17a9dc..a3fcce0ab 100644 --- a/vizro-ai/examples/dashboard_ui/assets/custom_css.css +++ b/vizro-ai/examples/dashboard_ui/assets/custom_css.css @@ -130,7 +130,6 @@ display: flex; justify-content: end; padding-right: 2px; - width: 100%; } #dashboard-data-upload { @@ -181,3 +180,69 @@ #clipboard-container { overflow: hidden; } + +#right-header { + display: none; +} + +.custom_header { + align-items: center; + border-bottom: 1px solid var(--border-subtle-alpha-01); + display: flex; + flex-direction: row; + height: 60px; + justify-content: space-between; + min-height: 0; + width: 100%; +} + +#right-side { + padding-top: 0; +} + +.header-logo { + width: 48px; +} + +#custom-header-div { + display: flex; + flex-direction: row; + gap: 8px; + justify-content: center; + width: 100%; +} + +#custom-header-title { + align-items: center; + display: flex; + font-size: 28px; + font-weight: 400; +} + +#data-upload { + color: var(--text-secondary); +} + +#open-settings-id:hover { + cursor: pointer; +} + +.container__title { + font-size: 14px; + padding-top: 4px; +} + +.navlink-button { + background: var(--fill-active); + border: 0 solid transparent; + box-shadow: var(--elevation-0); + color: var(--text-primary-inverted); + font-size: 14px; + font-stretch: normal; + font-style: normal; + font-weight: 600; + letter-spacing: -0.056px; + line-height: 16px; + text-decoration: none; + transition: box-shadow 0.2s; +} diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index 7638699d7..e442e17a7 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -5,7 +5,7 @@ import black import dash_bootstrap_components as dbc import vizro.models as vm -from dash import dcc, html +from dash import dcc, get_asset_url, html from pydantic import PrivateAttr from vizro.models import Action from vizro.models._action._actions_chain import _action_validator_factory @@ -272,7 +272,41 @@ class CustomButton(vm.Button): def build(self): """Returns custom button.""" - button_build_obj = super().build() - # button_build_obj.disabled = True - button_build_obj.style = {"minWidth": "100%", "display": "none"} - return button_build_obj + button = dbc.Button( + id=self.id, + children=[ + dbc.NavLink( + "View your dashboard", + href="http://localhost:8051/", + target="_blank", + external_link=True, + id=f"{self.id}-navlink", + ), + ], + style={"width": "12rem"}, + className="navlink-button", + ) + return button + + +class HeaderComponent(vm.VizroBaseModel): + """Custom header component.""" + + type: Literal["header"] = "header" + + def build(self): + """Returns custom header component.""" + title = html.Header("Vizro", id="custom-header-title") + header = html.Div( + children=[html.Img(src=get_asset_url("logo.svg"), alt="Vizro logo", className="header-logo"), title], + id="custom-header-div", + ) + icon = html.Div( + children=[ + html.Span("settings", className="material-symbols-outlined", id="open-settings-id"), + dbc.Tooltip("Settings", target="open-settings-id"), + ], + className="settings-div", + ) + + return html.Div(children=[header, icon], className="custom_header") From fbfd6e99695bf90e1538db347b7cc0ea30c3f2ed Mon Sep 17 00:00:00 2001 From: nadijagraca Date: Fri, 8 Nov 2024 10:08:06 +0100 Subject: [PATCH 14/14] minor fixes --- vizro-ai/examples/dashboard_ui/app.py | 56 +++++++------------ .../dashboard_ui/assets/custom_css.css | 10 +++- vizro-ai/examples/dashboard_ui/components.py | 15 +---- .../examples/dashboard_ui/run_vizro_ai.py | 2 +- 4 files changed, 30 insertions(+), 53 deletions(-) diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 7a938aec8..2cafc8fa6 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -15,7 +15,6 @@ Icon, Modal, MyDropdown, - MyPage, OffCanvas, UserPromptTextArea, UserUpload, @@ -39,16 +38,17 @@ vm.Container.add_type("components", Modal) vm.Container.add_type("components", CustomButton) vm.Container.add_type("components", HeaderComponent) - -MyPage.add_type("components", UserPromptTextArea) -MyPage.add_type("components", UserUpload) -MyPage.add_type("components", MyDropdown) -MyPage.add_type("components", OffCanvas) -MyPage.add_type("components", CodeClipboard) -MyPage.add_type("components", Icon) vm.Container.add_type("components", Modal) -dashboard_page = MyPage( +vm.Page.add_type("components", UserPromptTextArea) +vm.Page.add_type("components", UserUpload) +vm.Page.add_type("components", MyDropdown) +vm.Page.add_type("components", OffCanvas) +vm.Page.add_type("components", CodeClipboard) +vm.Page.add_type("components", Icon) + + +dashboard_page = vm.Page( id="vizro_ai_dashboard_page", title="Vizro AI - Dashboard", layout=vm.Layout( @@ -56,7 +56,6 @@ [4, 4, 4, 4, 4], [2, 2, 0, 0, 0], [2, 2, 0, 0, 0], - [3, 3, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], @@ -64,11 +63,13 @@ [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 0, 0, 0], - ] + [3, 3, 0, 0, 0], + ], + row_min_height="50px", ), components=[ vm.Container( - title="Code", + title="", components=[ vm.Container( title="", @@ -82,7 +83,7 @@ ], layout=vm.Layout( grid=[*[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] * 11, [-1, -1, -1, -1, -1, -1, -1, -1, 1, 1]], - col_gap="20px", + row_min_height="30px", ), ) ], @@ -129,8 +130,6 @@ grid=[ [2, -1, -1, -1, -1, -1, 1, 1, 0, 0], ], - row_gap="0px", - col_gap="4px", ), components=[ vm.Button( @@ -246,7 +245,7 @@ def save_to_file(generated_code): @callback( - Output("run-dashboard-navlink", "style"), + [Output("run-dashboard", "style"), Output("run-dashboard-navlink", "href")], Input("dashboard-code-markdown", "children"), ) def show_button(ai_response): @@ -255,38 +254,21 @@ def show_button(ai_response): raise PreventUpdate port = find_available_port() subprocess.Popen(["python", "output_files/run_vizro_ai_output.py", str(port)]) - return {} - - -# -# @callback( -# [Output("run-dashboard-button", "disabled"), Output("embedded-dashboard", "children")], -# Input("run-dashboard-button", "n_clicks"), -# ) -# def run_generated_dashboard(n_clicks): -# """Runs vizro-ai generated dashboard in an iframe window.""" -# port = find_available_port() -# if not n_clicks: -# raise PreventUpdate -# else: -# subprocess.Popen(["python", "output_files/run_vizro_ai_output.py", str(port)]) -# iframe = html.Iframe(src="http://localhost:8051/", height="600px") -# return True, iframe + href = f"http://localhost:{port}/" + return {}, href app = Vizro().build(dashboard) app.dash.layout.children.append( html.Div( [ - dbc.NavLink("Contact us", href="https://github.com/mckinsey/vizro/issues"), - dbc.NavLink("GitHub", href="https://github.com/mckinsey/vizro"), - dbc.NavLink("Docs", href="https://vizro.readthedocs.io/projects/vizro-ai/"), html.Div( [ "Made using ", html.Img(src=get_asset_url("logo.svg"), id="banner", alt="Vizro logo"), - "vizro", + dbc.NavLink("vizro", href="https://github.com/mckinsey/vizro"), ], + style={"display": "flex", "flexDirection": "row"}, ), ], className="anchor-container", diff --git a/vizro-ai/examples/dashboard_ui/assets/custom_css.css b/vizro-ai/examples/dashboard_ui/assets/custom_css.css index a3fcce0ab..3cac139f9 100644 --- a/vizro-ai/examples/dashboard_ui/assets/custom_css.css +++ b/vizro-ai/examples/dashboard_ui/assets/custom_css.css @@ -26,7 +26,7 @@ background-color: inherit; border: 1px solid var(--border-subtle-alpha-01); color: var(--text-primary); - height: 500px; + height: 62vh; padding: 8px; width: 100%; } @@ -46,6 +46,7 @@ background: var(--surfaces-bg-card); font-family: monospace; height: 100%; + max-height: 670px; overflow: auto; padding: 1rem; position: relative; @@ -136,7 +137,7 @@ border: 1px dashed var(--border-subtle-alpha-01); border-radius: 5px; color: var(--text-primary); - height: 46px; + height: 100%; line-height: 46px; text-align: center; } @@ -245,4 +246,9 @@ line-height: 16px; text-decoration: none; transition: box-shadow 0.2s; + width: 100%; +} + +#run-dashboard-navlink.nav-link { + color: var(--text-primary-inverted); } diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index e442e17a7..1b4751194 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -219,16 +219,6 @@ def build(self): return offcanvas -class MyPage(vm.Page): - """Custom page.""" - - type: Literal["my_page"] = "my_page" - - def pre_build(self): - """Overwriting pre_build.""" - pass - - class Icon(vm.VizroBaseModel): """Icon component for settings.""" @@ -261,7 +251,6 @@ def build(self): """Returns custom dashboard.""" dashboard_build_obj = super().build() dashboard_build_obj.children.append(dcc.Store(id="dashboard-data-store", storage_type="session")) - # dashboard_build_obj.children.append(dcc.Store(id="outputs-store-id", storage_type="session")) return dashboard_build_obj @@ -283,7 +272,7 @@ def build(self): id=f"{self.id}-navlink", ), ], - style={"width": "12rem"}, + style={"display": "none"}, className="navlink-button", ) return button @@ -296,7 +285,7 @@ class HeaderComponent(vm.VizroBaseModel): def build(self): """Returns custom header component.""" - title = html.Header("Vizro", id="custom-header-title") + title = html.Header("Vizro-AI dashboard generator", id="custom-header-title") header = html.Div( children=[html.Img(src=get_asset_url("logo.svg"), alt="Vizro logo", className="header-logo"), title], id="custom-header-div", diff --git a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py index d52d4660b..81f8be3ec 100644 --- a/vizro-ai/examples/dashboard_ui/run_vizro_ai.py +++ b/vizro-ai/examples/dashboard_ui/run_vizro_ai.py @@ -51,4 +51,4 @@ def run_vizro_ai_dashboard(user_prompt, model, api_key, api_base, n_clicks, vend args = parser.parse_args() - print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6)) + print(run_vizro_ai_dashboard(args.arg1, args.arg2, args.arg3, args.arg4, args.arg5, args.arg6)) # noqa: T201