Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 72 additions & 4 deletions daras_ai_v2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from daras_ai_v2.exceptions import InsufficientCredits
from daras_ai_v2.fastapi_tricks import get_route_path
from daras_ai_v2.github_tools import github_url_for_file
from daras_ai_v2.gooey_builder import render_gooey_builder
from daras_ai_v2.gooey_builder import render_gooey_builder, render_gooey_builder_inline
from daras_ai_v2.grid_layout_widget import grid_layout
from daras_ai_v2.html_spinner_widget import html_spinner
from daras_ai_v2.manage_api_keys_widget import manage_api_keys
Expand Down Expand Up @@ -82,6 +82,7 @@
)
from widgets.publish_form import clear_publish_form
from widgets.saved_workflow import render_saved_workflow_preview
from widgets.sidebar import sidebar_layout, use_sidebar
from widgets.workflow_image import (
render_change_notes_input,
render_workflow_photo_uploader,
Expand Down Expand Up @@ -415,6 +416,54 @@ def render(self):
with header_placeholder:
self._render_header()

def render_sidebar(self):
if not self.is_current_user_admin():
return

sidebar_ref = use_sidebar("builder-sidebar", self.request.session)
if self.tab != RecipeTabs.run and self.tab != RecipeTabs.preview:
if sidebar_ref.is_open or sidebar_ref.is_mobile_open:
sidebar_ref.set_open(False)
sidebar_ref.set_mobile_open(False)
raise gui.RerunException()
return

if sidebar_ref.is_open or sidebar_ref.is_mobile_open:
gui.tag(
"button",
type="submit",
name="onCloseGooeyBuilder",
value="yes",
hidden=True,
id="onClose",
) # hidden button to trigger the onClose event passed in the config

if gui.session_state.pop("onCloseGooeyBuilder", None):
sidebar_ref.set_open(False)
raise gui.RerunException()

with gui.div(className="w-100 h-100"):
self._render_gooey_builder()
else:
with gui.styled("& .gooey-builder-open-button:hover { scale: 1.2; }"):
with gui.div(
className="w-100 position-absolute",
style={"bottom": "24px", "left": "16px", "zIndex": "1000"},
):
gooey_builder_open_button = gui.button(
label=f"<img src='{settings.GOOEY_BUILDER_ICON}' style='width: 56px; height: 56px; border-radius: 50%;' />",
className="btn btn-secondary border-0 d-none d-md-block p-0 gooey-builder-open-button",
style={
"width": "56px",
"height": "56px",
"borderRadius": "50%",
"boxShadow": "#0000001a 0 1px 4px, #0003 0 2px 12px",
},
)
if gooey_builder_open_button:
sidebar_ref.set_open(True)
raise gui.RerunException()

Comment on lines 419 to 466
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure sidebar close works for both desktop and mobile states

In render_sidebar, when handling the hidden "onCloseGooeyBuilder" button, only sidebar_ref.set_open(False) is called. If the sidebar was opened via the mobile state (is_mobile_open), this leaves is_mobile_open True in the session, so the sidebar may immediately re‑open or get out of sync on mobile.

Recommend closing both flags when the builder’s onClose fires:

-        if sidebar_ref.is_open or sidebar_ref.is_mobile_open:
+        if sidebar_ref.is_open or sidebar_ref.is_mobile_open:
             gui.tag(
                 "button",
@@
-            if gui.session_state.pop("onCloseGooeyBuilder", None):
-                sidebar_ref.set_open(False)
-                raise gui.RerunException()
+            if gui.session_state.pop("onCloseGooeyBuilder", None):
+                sidebar_ref.set_open(False)
+                sidebar_ref.set_mobile_open(False)
+                raise gui.RerunException()
🤖 Prompt for AI Agents
daras_ai_v2/base.py around lines 419-466: the hidden "onCloseGooeyBuilder"
handler only calls sidebar_ref.set_open(False), leaving
sidebar_ref.is_mobile_open possibly true and causing mobile state to remain
out-of-sync; update the handler (the gui.session_state.pop branch) to also call
sidebar_ref.set_mobile_open(False) before raising gui.RerunException, and ensure
any other close-only branches in this range that call set_open(False) also clear
set_mobile_open(False) so both flags are closed when the builder onClose fires.

def _render_header(self):
from widgets.workflow_image import CIRCLE_IMAGE_WORKFLOWS

Expand Down Expand Up @@ -1172,8 +1221,6 @@ def render_selected_tab(self):
self.render_deleted_output()
return

self._render_gooey_builder()

with gui.styled(INPUT_OUTPUT_COLS_CSS):
input_col, output_col = gui.columns([3, 2], gap="medium")
with input_col:
Expand Down Expand Up @@ -1220,7 +1267,7 @@ def _render_gooey_builder(self):

if not self.is_current_user_admin():
return
render_gooey_builder(
render_gooey_builder_inline(
page_slug=self.slug_versions[-1],
builder_state=dict(
status=dict(
Expand All @@ -1241,6 +1288,27 @@ def _render_gooey_builder(self):
),
)

# render_gooey_builder(
# page_slug=self.slug_versions[-1],
# builder_state=dict(
# status=dict(
# error_msg=gui.session_state.get(StateKeys.error_msg),
# run_status=gui.session_state.get(StateKeys.run_status),
# run_time=gui.session_state.get(StateKeys.run_time),
# ),
# request=extract_model_fields(
# model=self.RequestModel, state=gui.session_state
# ),
# response=extract_model_fields(
# model=self.ResponseModel, state=gui.session_state
# ),
# metadata=dict(
# title=self.current_pr.title,
# description=self.current_pr.notes,
# ),
# ),
# )
Comment on lines +1291 to +1310
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove commented code once the filtered approach is restored.

After applying the fix from the previous review to filter builder_state, remove this commented block to improve code cleanliness.

-
-        # render_gooey_builder(
-        #     page_slug=self.slug_versions[-1],
-        #     builder_state=dict(
-        #         status=dict(
-        #             error_msg=gui.session_state.get(StateKeys.error_msg),
-        #             run_status=gui.session_state.get(StateKeys.run_status),
-        #             run_time=gui.session_state.get(StateKeys.run_time),
-        #         ),
-        #         request=extract_model_fields(
-        #             model=self.RequestModel, state=gui.session_state
-        #         ),
-        #         response=extract_model_fields(
-        #             model=self.ResponseModel, state=gui.session_state
-        #         ),
-        #         metadata=dict(
-        #             title=self.current_pr.title,
-        #             description=self.current_pr.notes,
-        #         ),
-        #     ),
-        # )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# render_gooey_builder(
# page_slug=self.slug_versions[-1],
# builder_state=dict(
# status=dict(
# error_msg=gui.session_state.get(StateKeys.error_msg),
# run_status=gui.session_state.get(StateKeys.run_status),
# run_time=gui.session_state.get(StateKeys.run_time),
# ),
# request=extract_model_fields(
# model=self.RequestModel, state=gui.session_state
# ),
# response=extract_model_fields(
# model=self.ResponseModel, state=gui.session_state
# ),
# metadata=dict(
# title=self.current_pr.title,
# description=self.current_pr.notes,
# ),
# ),
# )
🤖 Prompt for AI Agents
In daras_ai_v2/base.py around lines 1286 to 1305, there is a leftover commented
block calling render_gooey_builder that should be removed now that the filtered
builder_state approach is restored; delete the entire commented block (all lines
between the triple-comment markers) to clean up dead code and ensure no dangling
references remain, then run tests/lint to confirm no unused symbols or
formatting issues were introduced.


def _render_version_history(self):
versions = self.current_pr.versions.all()
first_version = versions[0]
Expand Down
64 changes: 64 additions & 0 deletions daras_ai_v2/gooey_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,70 @@
from daras_ai_v2 import settings


def render_gooey_builder_inline(page_slug: str, builder_state: dict):
if not settings.GOOEY_BUILDER_INTEGRATION_ID:
return

bi = BotIntegration.objects.get(id=settings.GOOEY_BUILDER_INTEGRATION_ID)
config = bi.get_web_widget_config(
hostname="gooey.ai", target="#gooey-builder-embed"
)

config["mode"] = "inline"
config["showRunLink"] = True
branding = config.setdefault("branding", {})
branding["showPoweredByGooey"] = False

config.setdefault("payload", {}).setdefault("variables", {})
# will be added later in the js code
variables = dict(
update_gui_state_params=dict(state=builder_state, page_slug=page_slug),
)

gui.html(
# language=html
f"""
<div id="gooey-builder-embed" style="height: 100%"></div>
<script id="gooey-builder-embed-script" src="{settings.WEB_WIDGET_LIB}"></script>
"""
)
gui.js(
# language=javascript
"""
async function onload() {
await window.waitUntilHydrated;
if (typeof GooeyEmbed === "undefined" ||
document.getElementById("gooey-builder-embed")?.children.length)
return;

// this is a trick to update the variables after the widget is already mounted
GooeyEmbed.setGooeyBuilderVariables = (value) => {
config.payload.variables = value;
};

GooeyEmbed.setGooeyBuilderVariables(variables);

config.onClose = function() {
document.getElementById("onClose").click();
};
GooeyEmbed.mount(config);
}

const script = document.getElementById("gooey-builder-embed-script");
if (script) script.onload = onload;
onload();
window.addEventListener("hydrated", onload);

// if the widget is already mounted, update the variables
if (typeof GooeyEmbed !== "undefined" && GooeyEmbed.setGooeyBuilderVariables) {
GooeyEmbed.setGooeyBuilderVariables(variables);
}
""",
config=config,
variables=variables,
)


def render_gooey_builder(page_slug: str, builder_state: dict):
if not settings.GOOEY_BUILDER_INTEGRATION_ID:
return
Expand Down
5 changes: 5 additions & 0 deletions daras_ai_v2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@
"https://cdn.jsdelivr.net/gh/GooeyAI/gooey-web-widget@2/dist/lib.js",
)

GOOEY_BUILDER_ICON = config(
"GOOEY_BUILDER_ICON",
"https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/63bdb560-b891-11f0-b9bc-02420a00014a/generate-ai-abstract-symbol-artificial-intelligence-colorful-stars-icon-vector%201.jpg",
)

MAX_CONCURRENCY_ANON = config("MAX_CONCURRENCY_ANON", 1, cast=int)
MAX_CONCURRENCY_FREE = config("MAX_CONCURRENCY_FREE", 2, cast=int)
MAX_CONCURRENCY_PAID = config("MAX_CONCURRENCY_PAID", 4, cast=int)
Expand Down
156 changes: 101 additions & 55 deletions routers/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from handles.models import Handle
from routers.custom_api_router import CustomAPIRouter
from routers.static_pages import serve_static_file
from widgets.sidebar import sidebar_layout, use_sidebar
from widgets.workflow_search import SearchFilters, render_search_bar_with_redirect
from workspaces.widgets import global_workspace_selector, workspace_selector_link

Expand Down Expand Up @@ -701,7 +702,7 @@ def render_recipe_page(
if not gui.session_state:
gui.session_state.update(page.current_sr_to_session_state())

with page_wrapper(request):
with page_wrapper(request, page=page, is_recipe_page=True):
page.render()

return dict(
Expand All @@ -720,81 +721,126 @@ def get_og_url_path(request) -> str:
@contextmanager
def page_wrapper(
request: Request,
page: None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect type annotation for page parameter.

The type annotation page: None = None is incorrect. This annotation means the parameter can only accept None as a value, which prevents passing actual page instances. The parameter should accept either None or a page instance.

Apply this diff to fix the type annotation:

 def page_wrapper(
     request: Request,
-    page: None = None,
+    page: typing.Optional["BasePage"] = None,
     className="",
     search_filters: typing.Optional[SearchFilters] = None,
     show_search_bar: bool = True,
 ):

Note: You may need to add from __future__ import annotations at the top of the file if forward reference resolution is needed, or import BasePage directly if circular import is not a concern.

🤖 Prompt for AI Agents
In routers/root.py around line 724, the parameter annotation `page: None = None`
is wrong because it only allows None; change it to accept a page instance or
None by annotating `page` as Optional[BasePage] (e.g., `page: Optional[BasePage]
= None`) or using a union/PEP 604 style (`page: BasePage | None = None`). Add
`from typing import Optional` (or adjust imports for union syntax) and either
import BasePage directly or add `from __future__ import annotations` to avoid
forward-reference issues; update imports accordingly.

className="",
search_filters: typing.Optional[SearchFilters] = None,
show_search_bar: bool = True,
is_recipe_page: bool = False,
):
from routers.account import explore_in_current_workspace

context = {"request": request, "block_incognito": True}

with gui.div(className="d-flex flex-column min-vh-100"):
gui.html(templates.get_template("gtag.html").render(**context))
container = page if page else None
sidebar_ref = use_sidebar("builder-sidebar", request.session, default_open=False)
sidebar_content, pane_content = sidebar_layout(sidebar_ref)

with (
gui.div(className="header"),
gui.div(className="navbar navbar-expand-xl bg-transparent p-0 m-0"),
gui.div(className="container-xxl my-2"),
gui.div(
className="position-relative w-100 d-flex justify-content-between gap-2"
),
):
with (
gui.div(className="d-md-block"),
gui.tag("a", href="/"),
):
gui.tag(
"img",
src=settings.GOOEY_LOGO_IMG,
width="300",
height="142",
className="img-fluid logo d-none d-sm-block",
)
gui.tag(
"img",
src=settings.GOOEY_LOGO_RECT,
width="145",
height="40",
className="img-fluid logo d-sm-none",
)
is_builder_sidebar_open = sidebar_ref.is_open
if not is_recipe_page and (is_builder_sidebar_open):
sidebar_ref.set_open(False)
sidebar_ref.set_mobile_open(False)
raise gui.RerunException()

Comment on lines +734 to 743
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat mobile-open sidebars as “open” and close them on non-recipe pages

is_builder_sidebar_open = sidebar_ref.is_open ignores sidebar_ref.is_mobile_open. If an admin opens the builder via the mobile button on a non-recipe page, you can end up with a visually open but empty sidebar that isn’t auto-closed by the not is_recipe_page guard.

Updating the flag to consider mobile state fixes both the guard and the layout branches that depend on it:

-    is_builder_sidebar_open = sidebar_ref.is_open
-    if not is_recipe_page and (is_builder_sidebar_open):
+    is_builder_sidebar_open = sidebar_ref.is_open or sidebar_ref.is_mobile_open
+    if not is_recipe_page and is_builder_sidebar_open:
         sidebar_ref.set_open(False)
         sidebar_ref.set_mobile_open(False)
         raise gui.RerunException()

This also makes the header and main-content sizing react consistently to mobile-open sidebars.

🤖 Prompt for AI Agents
In routers/root.py around lines 734 to 743, the code sets
is_builder_sidebar_open = sidebar_ref.is_open but ignores
sidebar_ref.is_mobile_open, causing mobile-open sidebars to remain visually open
on non-recipe pages; change the flag to consider both states (e.g.,
is_builder_sidebar_open = sidebar_ref.is_open or sidebar_ref.is_mobile_open) and
update any subsequent branches that rely on is_builder_sidebar_open so they
react to the combined state (also ensure the auto-close logic calls both
sidebar_ref.set_open(False) and sidebar_ref.set_mobile_open(False) as already
present).

if show_search_bar:
_render_mobile_search_button(request, search_filters)
with sidebar_content:
if container:
container.render_sidebar()

with gui.div(
className="d-flex gap-2 justify-content-end flex-wrap align-items-center"
with pane_content:
with gui.div(className="d-flex flex-column min-vh-100 w-100 px-2"):
gui.html(templates.get_template("gtag.html").render(**context))

with (
gui.div(className="header"),
gui.div(className="navbar navbar-expand-xl bg-transparent p-0 m-0"),
gui.div(
className="container-xxl my-2"
if not is_builder_sidebar_open
else "my-2 mx-2 w-100"
),
gui.div(
className="position-relative w-100 d-flex justify-content-between gap-2"
),
):
for url, label in settings.HEADER_LINKS:
render_header_link(
url=url, label=label, icon=settings.HEADER_ICONS.get(url)
with (
gui.div(className="d-md-block"),
gui.tag("a", href="/"),
):
gui.tag(
"img",
src=settings.GOOEY_LOGO_IMG,
width="300",
height="142",
className="img-fluid logo d-none d-sm-block",
)

if request.user and not request.user.is_anonymous:
render_header_link(
url=get_route_path(explore_in_current_workspace),
label="Saved",
icon=icons.save,
gui.tag(
"img",
src=settings.GOOEY_LOGO_RECT,
width="145",
height="40",
className="img-fluid logo d-sm-none",
)

current_workspace = global_workspace_selector(
request.user, request.session
)
else:
current_workspace = None
anonymous_login_container(request, context)
with gui.div(
className="d-flex justify-content-end flex-grow-1 align-items-center"
):
if request.user and request.user.is_admin:
gooey_builder_mobile_open_button = gui.button(
label=f"<img src='{settings.GOOEY_BUILDER_ICON}' style='width: 36px; height: 36px; border-radius: 50%;' />",
className="border-0 m-0 btn btn-secondary rounded-pill d-md-none gooey-builder-open-button p-0",
style={
"width": "36px",
"height": "36px",
"borderRadius": "50%",
},
)
if gooey_builder_mobile_open_button:
sidebar_ref.set_mobile_open(True)
raise gui.RerunException()

if show_search_bar:
_render_mobile_search_button(request, search_filters)

with gui.div(
className="d-flex gap-2 justify-content-end flex-wrap align-items-center"
):
for url, label in settings.HEADER_LINKS:
render_header_link(
url=url,
label=label,
icon=settings.HEADER_ICONS.get(url),
)

if request.user and not request.user.is_anonymous:
render_header_link(
url=get_route_path(explore_in_current_workspace),
label="Saved",
icon=icons.save,
)

current_workspace = global_workspace_selector(
request.user, request.session
)
else:
current_workspace = None
anonymous_login_container(request, context)

gui.html(copy_to_clipboard_scripts)

gui.html(copy_to_clipboard_scripts)

with gui.div(id="main-content", className="container-xxl " + className):
yield current_workspace
with gui.div(
id="main-content",
className="container-xxl "
if not is_builder_sidebar_open
else "mx-2 w-100" + className,
):
yield current_workspace

gui.html(templates.get_template("footer.html").render(**context))
gui.html(templates.get_template("login_scripts.html").render(**context))
gui.html(templates.get_template("footer.html").render(**context))
gui.html(templates.get_template("login_scripts.html").render(**context))


def _render_mobile_search_button(request: Request, search_filters: SearchFilters):
with gui.div(
className="d-flex d-md-none flex-grow-1 justify-content-end",
className="d-flex d-md-none justify-content-end",
):
gui.button(
icons.search,
Expand Down
Loading