diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py
index 5315e28dc..72411c0c2 100644
--- a/daras_ai_v2/base.py
+++ b/daras_ai_v2/base.py
@@ -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
@@ -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,
@@ -415,6 +416,54 @@ def render(self):
with header_placeholder:
self._render_header()
+ def render_sidebar(self):
+ if not self.current_workspace.bot_builder:
+ 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"
",
+ 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()
+
def _render_header(self):
from widgets.workflow_image import CIRCLE_IMAGE_WORKFLOWS
@@ -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:
@@ -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(
@@ -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,
+ # ),
+ # ),
+ # )
+
def _render_version_history(self):
versions = self.current_pr.versions.all()
first_version = versions[0]
diff --git a/daras_ai_v2/gooey_builder.py b/daras_ai_v2/gooey_builder.py
index 7bd55a71e..e185d7a89 100644
--- a/daras_ai_v2/gooey_builder.py
+++ b/daras_ai_v2/gooey_builder.py
@@ -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"""
+
+
+ """
+ )
+ 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
diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py
index 2de3b1884..3d0e9ceac 100644
--- a/daras_ai_v2/settings.py
+++ b/daras_ai_v2/settings.py
@@ -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)
diff --git a/routers/root.py b/routers/root.py
index c1240225b..b0a1437c2 100644
--- a/routers/root.py
+++ b/routers/root.py
@@ -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
@@ -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(
@@ -720,81 +721,126 @@ def get_og_url_path(request) -> str:
@contextmanager
def page_wrapper(
request: Request,
+ page: None = None,
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()
- 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"
",
+ 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,
diff --git a/usage_costs/migrations/0038_alter_modelpricing_model_name.py b/usage_costs/migrations/0038_alter_modelpricing_model_name.py
new file mode 100644
index 000000000..345008909
--- /dev/null
+++ b/usage_costs/migrations/0038_alter_modelpricing_model_name.py
@@ -0,0 +1,298 @@
+# Generated by Django 5.1.3 on 2025-11-27 17:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("usage_costs", "0037_alter_modelpricing_model_name"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="modelpricing",
+ name="model_name",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ (
+ "apertus_70b_instruct",
+ "Apertus 70B Instruct • SwissAI via PublicAI",
+ ),
+ ("sea_lion_v4_gemma_3_27b_it", "SEA-LION v4 • aisingapore"),
+ ("gpt_5", "GPT-5 • openai"),
+ ("gpt_5_mini", "GPT-5 Mini • openai"),
+ ("gpt_5_nano", "GPT-5 Nano • openai"),
+ ("gpt_5_chat", "GPT-5 Chat • openai"),
+ ("gpt_4_1", "GPT-4.1 • openai"),
+ ("gpt_4_1_mini", "GPT-4.1 Mini • openai"),
+ ("gpt_4_1_nano", "GPT-4.1 Nano • openai"),
+ ("gpt_4_5", "GPT-4.5 • openai [Redirects to GPT-4o • openai]"),
+ ("o4_mini", "o4-mini • openai"),
+ ("o3", "o3 • openai"),
+ ("o3_mini", "o3-mini • openai"),
+ ("o1", "o1 • openai [Redirects to o3 • openai]"),
+ ("o1_preview", "o1-preview • openai [Redirects to o3 • openai]"),
+ ("o1_mini", "o1-mini • openai [Redirects to o3-mini • openai]"),
+ ("gpt_4_o", "GPT-4o • openai"),
+ ("gpt_4_o_mini", "GPT-4o-mini • openai"),
+ ("gpt_4_o_audio", "GPT-4o Audio • openai"),
+ ("gpt_realtime", "GPT-Realtime • openai"),
+ ("gpt_4_o_mini_audio", "GPT-4o-mini Audio • openai"),
+ (
+ "chatgpt_4_o",
+ "ChatGPT-4o • openai 🧪 [Redirects to GPT-4o • openai]",
+ ),
+ (
+ "gpt_4_turbo_vision",
+ "GPT-4 Turbo with Vision • openai [Redirects to GPT-4o • openai]",
+ ),
+ (
+ "gpt_4_vision",
+ "GPT-4 Vision • openai [Redirects to GPT-4o • openai]",
+ ),
+ (
+ "gpt_4_turbo",
+ "GPT-4 Turbo • openai [Redirects to GPT-4o • openai]",
+ ),
+ ("gpt_4", "GPT-4 • openai [Redirects to GPT-4o • openai]"),
+ (
+ "gpt_4_32k",
+ "GPT-4 32K • openai 🔻 [Redirects to GPT-4o • openai]",
+ ),
+ (
+ "gpt_3_5_turbo",
+ "ChatGPT • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "gpt_3_5_turbo_16k",
+ "ChatGPT 16k • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "gpt_3_5_turbo_instruct",
+ "GPT-3.5 Instruct • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ ("deepseek_r1", "DeepSeek R1"),
+ ("llama4_maverick_17b_128e", "Llama 4 Maverick Instruct • Meta AI"),
+ ("llama4_scout_17b_16e", "Llama 4 Scout Instruct • Meta AI"),
+ (
+ "llama3_3_70b",
+ "Llama 3.3 70B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_2_90b_vision",
+ "Llama 3.2 90B + Vision • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_2_11b_vision",
+ "Llama 3.2 11B + Vision • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_2_3b",
+ "Llama 3.2 3B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_2_1b",
+ "Llama 3.2 1B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_1_405b",
+ "Llama 3.1 405B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_1_70b",
+ "Llama 3.1 70B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_1_8b",
+ "Llama 3.1 8B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_70b",
+ "Llama 3 70B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ (
+ "llama3_8b",
+ "Llama 3 8B • Meta AI [Redirects to Llama 4 Maverick Instruct • Meta AI]",
+ ),
+ ("pixtral_large", "Pixtral Large 24/11 • mistral"),
+ ("mistral_large", "Mistral Large 24/11 • mistral"),
+ ("mistral_small_24b_instruct", "Mistral Small 25/01 • mistral"),
+ (
+ "mixtral_8x7b_instruct_0_1",
+ "Mixtral 8x7b Instruct v0.1 • mistral [Deprecated] [Redirects to Mistral Small 25/01 • mistral]",
+ ),
+ ("gemma_2_9b_it", "Gemma 2 9B • Google"),
+ (
+ "gemma_7b_it",
+ "Gemma 7B • Google [Redirects to Gemma 2 9B • Google]",
+ ),
+ ("gemini_2_5_pro", "Gemini 2.5 Pro (Google)"),
+ ("gemini_2_5_flash", "Gemini 2.5 Flash • Google"),
+ ("gemini_2_5_flash_lite", "Gemini 2.5 Flash Lite • Google"),
+ (
+ "gemini_2_5_pro_preview",
+ "Gemini 2.5 Pro • Google [Redirects to Gemini 2.5 Pro (Google)]",
+ ),
+ (
+ "gemini_2_5_flash_preview",
+ "Gemini 2.5 Flash • Google [Redirects to Gemini 2.5 Flash • Google]",
+ ),
+ ("gemini_2_flash_lite", "Gemini 2 Flash Lite • Google"),
+ ("gemini_2_flash", "Gemini 2 Flash • Google"),
+ (
+ "gemini_1_5_flash",
+ "Gemini 1.5 Flash • Google [Redirects to Gemini 2 Flash • Google]",
+ ),
+ (
+ "gemini_1_5_pro",
+ "Gemini 1.5 Pro • Google [Redirects to Gemini 2.5 Pro (Google)]",
+ ),
+ (
+ "gemini_1_pro_vision",
+ "Gemini 1.0 Pro Vision • Google [Redirects to Gemini 2.5 Pro (Google)]",
+ ),
+ (
+ "gemini_1_pro",
+ "Gemini 1.0 Pro • Google [Redirects to Gemini 2.5 Pro (Google)]",
+ ),
+ (
+ "palm2_chat",
+ "PaLM 2 Chat • Google [Redirects to Gemini 2 Flash • Google]",
+ ),
+ (
+ "palm2_text",
+ "PaLM 2 Text • Google [Redirects to Gemini 2 Flash • Google]",
+ ),
+ ("claude_4_1_opus", "Claude 4.1 Opus • Anthropic"),
+ ("claude_4_sonnet", "Claude 4 Sonnet • Anthropic"),
+ ("claude_4_opus", "Claude 4 Opus • Anthropic"),
+ ("claude_3_7_sonnet", "Claude 3.7 Sonnet • Anthropic"),
+ (
+ "claude_3_5_sonnet",
+ "Claude 3.5 Sonnet • Anthropic [Redirects to Claude 3.7 Sonnet • Anthropic]",
+ ),
+ (
+ "claude_3_opus",
+ "Claude 3 Opus • Anthropic [Redirects to Claude 3.7 Sonnet • Anthropic]",
+ ),
+ (
+ "claude_3_sonnet",
+ "Claude 3 Sonnet • Anthropic [Redirects to Claude 3.7 Sonnet • Anthropic]",
+ ),
+ (
+ "claude_3_haiku",
+ "Claude 3 Haiku • Anthropic [Redirects to Claude 3.7 Sonnet • Anthropic]",
+ ),
+ (
+ "afrollama_v1",
+ "AfroLlama3 v1 • Jacaranda [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "llama3_8b_cpt_sea_lion_v2_1_instruct",
+ "Llama3 8B CPT SEA-LIONv2.1 Instruct • aisingapore",
+ ),
+ (
+ "sarvam_2b",
+ "Sarvam 2B • sarvamai [Redirects to GPT-4o-mini • openai]",
+ ),
+ ("sarvam_m", "Sarvam M • sarvam.ai"),
+ (
+ "llama_3_groq_70b_tool_use",
+ "Llama 3 Groq 70b Tool Use [Deprecated] [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "llama_3_groq_8b_tool_use",
+ "Llama 3 Groq 8b Tool Use [Deprecated] [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "llama2_70b_chat",
+ "Llama 2 70B Chat • Meta AI [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "sea_lion_7b_instruct",
+ "SEA-LION-7B-Instruct • aisingapore [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "llama3_8b_cpt_sea_lion_v2_instruct",
+ "Llama3 8B CPT SEA-LIONv2 Instruct • aisingapore [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "text_davinci_003",
+ "GPT-3.5 Davinci-3 • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "text_davinci_002",
+ "GPT-3.5 Davinci-2 • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "code_davinci_002",
+ "Codex • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "text_curie_001",
+ "Curie • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "text_babbage_001",
+ "Babbage • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ (
+ "text_ada_001",
+ "Ada • openai [Redirects to GPT-4o-mini • openai]",
+ ),
+ ("protogen_2_2", "Protogen V2.2 (darkstorm2150)"),
+ ("epicdream", "epiCDream [Deprecated] (epinikion)"),
+ ("flux_1_dev", "FLUX.1 [dev]"),
+ ("dream_shaper", "DreamShaper (Lykon)"),
+ ("dreamlike_2", "Dreamlike Photoreal 2.0 (dreamlike.art)"),
+ ("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
+ ("sd_1_5", "Stable Diffusion v1.5 (RunwayML)"),
+ ("dall_e", "DALL·E 2 (OpenAI)"),
+ ("dall_e_3", "DALL·E 3 (OpenAI)"),
+ ("gpt_image_1", "GPT Image 1 (OpenAI)"),
+ ("nano_banana", "Nano Banana (Google)"),
+ ("openjourney_2", "Open Journey v2 beta [Deprecated] (PromptHero)"),
+ ("openjourney", "Open Journey [Deprecated] (PromptHero)"),
+ ("analog_diffusion", "Analog Diffusion [Deprecated] (wavymulder)"),
+ ("protogen_5_3", "Protogen v5.3 [Deprecated] (darkstorm2150)"),
+ ("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
+ (
+ "rodent_diffusion_1_5",
+ "Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)",
+ ),
+ ("deepfloyd_if", "DeepFloyd IF [Deprecated] (stability.ai)"),
+ ("flux_pro_kontext", "FLUX.1 Pro Kontext (fal.ai)"),
+ ("dream_shaper", "DreamShaper (Lykon)"),
+ ("dreamlike_2", "Dreamlike Photoreal 2.0 (dreamlike.art)"),
+ ("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
+ ("sd_1_5", "Stable Diffusion v1.5 (RunwayML)"),
+ ("dall_e", "Dall-E (OpenAI)"),
+ ("gpt_image_1", "GPT Image 1 (OpenAI)"),
+ ("nano_banana", "Nano Banana (Google)"),
+ ("instruct_pix2pix", "✨ InstructPix2Pix (Tim Brooks)"),
+ ("openjourney_2", "Open Journey v2 beta [Deprecated] (PromptHero)"),
+ ("openjourney", "Open Journey [Deprecated] (PromptHero)"),
+ ("analog_diffusion", "Analog Diffusion [Deprecated] (wavymulder)"),
+ ("protogen_5_3", "Protogen v5.3 [Deprecated] (darkstorm2150)"),
+ ("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
+ (
+ "rodent_diffusion_1_5",
+ "Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)",
+ ),
+ ("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
+ ("runway_ml", "Stable Diffusion v1.5 (RunwayML)"),
+ ("dall_e", "Dall-E (OpenAI)"),
+ ("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
+ ("wav2lip", "LipSync (wav2lip)"),
+ ("sadtalker", "LipSync (sadtalker)"),
+ ("twilio_voice", "Twilio Voice"),
+ ("twilio_sms", "Twilio SMS"),
+ ],
+ default="",
+ help_text="The name of the model. Only used for Display purposes.",
+ max_length=255,
+ ),
+ ),
+ ]
diff --git a/widgets/sidebar.py b/widgets/sidebar.py
new file mode 100644
index 000000000..9acbe5d41
--- /dev/null
+++ b/widgets/sidebar.py
@@ -0,0 +1,325 @@
+from starlette.requests import Request
+import gooey_gui as gui
+from daras_ai_v2 import settings
+from textwrap import dedent
+from daras_ai_v2 import icons
+
+
+class SidebarRef:
+ def __init__(
+ self,
+ key: str,
+ session: dict,
+ is_open: bool = True,
+ is_mobile_open: bool = False,
+ ):
+ self.key = key
+ self.session = session
+ self.is_open = is_open
+ self.is_mobile_open = is_mobile_open
+
+ def set_open(self, value: bool):
+ self.is_open = self.session[self.key] = value
+
+ def set_mobile_open(self, value: bool):
+ self.is_mobile_open = self.session[self.mobile_key] = value
+ # self.set_open(value)
+
+ @property
+ def mobile_key(self):
+ return self.key + ":mobile"
+
+
+def use_sidebar(key: str, session: dict, default_open: bool = True) -> SidebarRef:
+ """Create or get a sidebar reference with state management."""
+ import time
+
+ # Check if this is a fresh page load by comparing timestamps
+ last_load_time = session.get(f"{key}:last_load_time", 0)
+ current_time = time.time()
+
+ # If more than 1 second has passed since last load, consider it a fresh page load
+ if current_time - last_load_time > 0.5:
+ # Fresh page load - clear mobile state
+ mobile_key = key + ":mobile"
+ if mobile_key in session:
+ del session[mobile_key]
+
+ # Update the last load time
+ session[f"{key}:last_load_time"] = current_time
+
+ # set the default open state in session here
+ session[key] = bool(session.get(key, default_open))
+ ref = SidebarRef(
+ key=key,
+ session=session,
+ is_open=bool(session.get(key, default_open)),
+ is_mobile_open=bool(session.get(key + ":mobile", False)),
+ )
+
+ return ref
+
+
+def sidebar_list_item(
+ icon, title, is_sidebar_open, url=None, hover_icon=None, current_url=None
+):
+ is_selected = current_url and url and current_url.startswith(url)
+ with (
+ gui.styled(
+ """
+ & a {
+ font-size: 1rem;
+ text-decoration: none;
+ }
+ & .sidebar-list-item {
+ border-radius: 8px;
+ height: 36px;
+ width: min-content;
+ padding: 6px 10px;
+ }
+ & .sidebar-list-item-hover-icon {
+ display: none;
+ }
+ & .sidebar-list-item:hover {
+ background-color: #f0f0f0;
+ .sidebar-list-item-hover-icon {
+ display: block;
+ }
+ }
+ & .sidebar-list-item.selected {
+ background-color: #ddd;
+ }
+ & .sidebar-list-item-title {
+ font-size: 0.875rem;
+ }
+ """
+ ),
+ gui.div(),
+ ):
+ link_classes = "d-block sidebar-list-item ms-2"
+ if is_sidebar_open:
+ link_classes += " d-flex align-items-baseline justify-content-between w-100"
+ if is_selected:
+ link_classes += " selected"
+ with gui.tag(
+ "a",
+ href=url,
+ className=link_classes,
+ ):
+ with gui.div(className="d-flex align-items-baseline"):
+ icon_classes = "d-block sidebar-list-item-icon"
+ if is_sidebar_open:
+ icon_classes += " me-2"
+
+ if icon:
+ gui.html(
+ icon,
+ className=icon_classes,
+ )
+ if is_sidebar_open:
+ gui.html(title, className="sidebar-list-item-title d-block")
+
+ if hover_icon:
+ with gui.div(className="sidebar-list-item-hover-icon"):
+ gui.html(hover_icon, className="text-secondary")
+
+
+def sidebar_item_list(is_sidebar_open, current_url=None):
+ for i, (url, label, icon) in enumerate(settings.SIDEBAR_LINKS):
+ if icon:
+ with gui.div():
+ sidebar_list_item(
+ icon, label, is_sidebar_open, url, icons.arrow_up_right, current_url
+ )
+ else:
+ with gui.div(
+ className="d-inline-block me-2 small",
+ ):
+ gui.html(" ")
+ gui.html(label)
+
+
+def render_default_sidebar(sidebar_ref: SidebarRef, request=None):
+ is_sidebar_open = sidebar_ref.is_open
+ current_url = request.url.path if request else None
+
+ with gui.div(
+ className=f"d-flex flex-column flex-grow-1 {'pe-3' if is_sidebar_open else ''} my-3 text-nowrap",
+ ):
+ with gui.div(className="mb-4"):
+ sidebar_list_item(
+ "",
+ "Saved",
+ is_sidebar_open,
+ "/account/saved/",
+ current_url=current_url,
+ )
+ sidebar_list_item(
+ icons.search,
+ "Explore",
+ is_sidebar_open,
+ "/explore/",
+ current_url=current_url,
+ )
+
+ if is_sidebar_open:
+ sidebar_item_list(is_sidebar_open, current_url)
+
+
+def sidebar_mobile_header(request: Request):
+ from routers.root import anonymous_login_container
+
+ with gui.div(
+ className="d-flex align-items-center justify-content-between d-md-none me-2 w-100 py-2",
+ style={"height": "54px"},
+ ):
+ sidebar_ref = use_sidebar("main-sidebar", request.session)
+ gui.tag(
+ "img",
+ src=settings.GOOEY_LOGO_RECT,
+ width="120px",
+ height="auto",
+ className=" logo-face",
+ )
+ with gui.div(className="d-flex align-items-center gap-2"):
+ anonymous_login_container(
+ request,
+ context={
+ "request": request,
+ "block_incognito": True,
+ },
+ )
+ open_mobile_sidebar = gui.button(
+ label=icons.sidebar_flip,
+ className="m-0",
+ unsafe_allow_html=True,
+ type="tertiary",
+ style={"padding": "6px 10px"},
+ )
+ if open_mobile_sidebar:
+ sidebar_ref.set_mobile_open(True)
+ raise gui.RerunException()
+
+
+# Sidebar width variables
+sidebar_open_width = "340px"
+sidebar_closed_width = "0px"
+sidebar_mobile_width = "100vw"
+
+
+def sidebar_layout(sidebar_ref: SidebarRef):
+ is_mobile_open = sidebar_ref.is_mobile_open
+ sidebar_funtion_classes = (
+ "gooey-sidebar-open"
+ if sidebar_ref.is_open or sidebar_ref.is_mobile_open
+ else "gooey-sidebar-closed"
+ )
+
+ side_bar_styles = dedent(
+ f"""
+ html {{
+ /* override margin-left from app.css */
+ margin-left: 0 !important;
+ }}
+ & .gooey-btn {{
+ padding: 6px 10px !important;
+ }}
+ & .gooey-btn:hover {{
+ background-color: #f0f0f0 !important;
+ }}
+
+ & .gooey-sidebar {{
+ background-color: #f9f9f9;
+ position: sticky;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ z-index: 999;
+ border-right: 1px solid #e0e0e0;
+ height: 100dvh;
+ }}
+
+ & .gooey-sidebar-open {{
+ min-width: {sidebar_open_width};
+ width: {sidebar_open_width};
+ max-width: {sidebar_open_width};
+ }}
+ & .gooey-sidebar-closed {{
+ min-width: {sidebar_closed_width};
+ width: {sidebar_closed_width};
+ max-width: {sidebar_closed_width};
+ }}
+
+ & .gooey-sidebar-closed:hover {{
+ cursor: e-resize;
+ }}
+
+ @media (max-width: 990px) {{
+ & .gooey-sidebar-open {{
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ min-width: {sidebar_mobile_width};
+ width: {sidebar_mobile_width};
+ max-width: {sidebar_mobile_width};
+ z-index: 2000;
+ border-left: 1px solid #e0e0e0;
+ border-right: none;
+ height: calc(100dvh - 4px); /* 4px for the progress bar */
+ margin-top: auto;
+ }}
+ & .gooey-sidebar-closed {{
+ position: sticky;
+ right: 0;
+ left: auto;
+ min-width: 0px;
+ width: 0px;
+ max-width: 0px;
+ overflow: visible;
+ }}
+ }}
+ """
+ )
+ if not is_mobile_open:
+ side_bar_styles += dedent(
+ """
+ @media (max-width: 767px) {
+ & .gooey-sidebar-open {
+ display: none !important;
+ position: fixed;
+ max-width: 0px !important;
+ }
+ }
+ """
+ )
+
+ with (
+ gui.styled(side_bar_styles),
+ gui.div(
+ className="d-flex w-100 h-100 position-relative sidebar-click-container",
+ style={"height": "100dvh"},
+ onClick=dedent(
+ """
+ if (event.target.id === "sidebar-click-container") {
+ document.getElementById("sidebar-hidden-btn").click();
+ }
+ """
+ if not sidebar_ref.is_open
+ else ""
+ ),
+ ),
+ ):
+ open_sidebar_btn = gui.button(
+ label="",
+ className="d-none",
+ id="sidebar-hidden-btn",
+ )
+ if open_sidebar_btn:
+ sidebar_ref.set_open(True)
+ raise gui.RerunException()
+
+ sidebar_content_placeholder = gui.div(
+ className=f"d-flex flex-column flex-grow-1 gooey-sidebar {sidebar_funtion_classes}",
+ )
+ pane_content_placeholder = gui.div(className="d-flex flex-grow-1 mw-100")
+ return sidebar_content_placeholder, pane_content_placeholder
diff --git a/workspaces/admin.py b/workspaces/admin.py
index c05660d2c..3d4f46240 100644
--- a/workspaces/admin.py
+++ b/workspaces/admin.py
@@ -89,6 +89,8 @@ class WorkspaceAdmin(SafeDeleteAdmin):
"handle",
"created_by",
"is_personal",
+ "bot_builder",
+ "restricted_function_scope",
("is_paying", "stripe_customer_id"),
("total_payments", "total_charged", "total_usage_cost"),
("balance", "subscription"),
diff --git a/workspaces/migrations/0013_workspace_bot_builder_and_more.py b/workspaces/migrations/0013_workspace_bot_builder_and_more.py
new file mode 100644
index 000000000..0ef6ea114
--- /dev/null
+++ b/workspaces/migrations/0013_workspace_bot_builder_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.3 on 2025-11-27 17:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("workspaces", "0012_remove_workspace_workspace_search_vector_idx"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="workspace",
+ name="bot_builder",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="workspace",
+ name="restricted_function_scope",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/workspaces/models.py b/workspaces/models.py
index 5678211de..ac2a5b63b 100644
--- a/workspaces/models.py
+++ b/workspaces/models.py
@@ -155,6 +155,10 @@ class Workspace(SafeDeleteModel):
banner_url = CustomURLField(null=True, blank=True)
description = models.TextField(blank=True, default="")
+ # features
+ bot_builder = models.BooleanField(default=False)
+ restricted_function_scope = models.BooleanField(default=False)
+
# billing
balance = models.IntegerField("bal", default=0)
is_paying = models.BooleanField("paid", default=False)
diff --git a/workspaces/widgets.py b/workspaces/widgets.py
index a6d485e8b..4441f36d4 100644
--- a/workspaces/widgets.py
+++ b/workspaces/widgets.py
@@ -39,6 +39,7 @@ def global_workspace_selector(user: AppUser, session: dict):
except KeyError:
pass
set_current_workspace(session, int(switch_workspace_id))
+ raise gui.RerunException()
try:
current = [