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 = [