Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b7892c3
Add option to hide 'updated at' timestamp in saved workflow preview
avi-007 Jun 19, 2025
7c5043c
Removed underline and added reordering of sub text logic
avi-007 Jun 24, 2025
2152f17
Merge branch 'master' into explore-page-fix
avi-007 Jun 24, 2025
3c89f57
added logic to show run count to a user in workspace
avi-007 Jun 24, 2025
00b81b9
fixed run count shown for all users
avi-007 Jun 24, 2025
7a2c92d
fixed the styling for workspace for individual workflow pages
avi-007 Jun 24, 2025
44db178
addressed coderabbit review comments
avi-007 Jun 25, 2025
c187ffc
Merge branch 'master' into explore-page-fix
avi-007 Jun 25, 2025
639c9ac
addressed coderabbit review comments
avi-007 Jun 25, 2025
06dd3f4
removed dot chars between the sub text, updated styling for run count…
avi-007 Jun 25, 2025
e0edc9a
fixed inconsistent spacing between subtext elements
avi-007 Jun 25, 2025
adc751c
added consistent spacing in footer elements
avi-007 Jun 27, 2025
d57a376
formatting fixed
avi-007 Jun 27, 2025
27071db
Merge branch 'refs/heads/master' into explore-page-fix
avi-007 Jun 27, 2025
6cfa801
removed underline logic for hyperlink
avi-007 Jun 27, 2025
6119aad
Merge branch 'master' into explore-page-fix
avi-007 Jun 30, 2025
39ee2f7
refactor: simplify ownership check in is_user_workspace_owner functio…
avi-007 Jun 30, 2025
70fc307
fix: add missing newline at end of file in workspace.py
avi-007 Jun 30, 2025
bbdb589
refactor: enhance layout and styling of footer elements and run count…
avi-007 Jul 2, 2025
884a28f
Merge branch 'master' into explore-page-fix
avi-007 Jul 2, 2025
a89322a
refactor: update footer layout and styling for improved responsivenes…
avi-007 Jul 3, 2025
2aed697
Merge branch 'master' into explore-page-fix
avi-007 Jul 3, 2025
d823679
fix: add missing newline at end of file in saved_workflow.py
avi-007 Jul 3, 2025
0f3263d
feat: inline column selector in bulk runner
devxpy Jul 13, 2025
92f03b6
Merge branch 'master' into explore-page-fix
avi-007 Jul 14, 2025
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
1 change: 1 addition & 0 deletions utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Utils package
18 changes: 18 additions & 0 deletions utils/workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from app_users.models import AppUser


def is_user_workspace_owner(user: AppUser | None, workspace_filter: str | None) -> bool:
"""Check if the current user owns the filtered workspace."""
if not (user and workspace_filter and not user.is_anonymous):
return False

user_workspace_ids = {w.id for w in user.cached_workspaces}
user_workspace_handles = {w.handle.name for w in user.cached_workspaces if w.handle}

try:
# Check if workspace filter is numeric (workspace ID)
workspace_id = int(workspace_filter)
return workspace_id in user_workspace_ids
except ValueError:
# Workspace filter is a handle name
return workspace_filter in user_workspace_handles
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

Enhance error handling and add input validation.

The current implementation only handles ValueError for int conversion. Consider adding more comprehensive error handling and input validation.

    try:
        # Check if workspace filter is numeric (workspace ID)
        workspace_id = int(workspace_filter)
+       if workspace_id <= 0:
+           return False
        return workspace_id in user_workspace_ids
-   except ValueError:
+   except (ValueError, TypeError):
        # Workspace filter is a handle name
+       if not isinstance(workspace_filter, str) or not workspace_filter.strip():
+           return False
        return workspace_filter in user_workspace_handles
📝 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
try:
# Check if workspace filter is numeric (workspace ID)
workspace_id = int(workspace_filter)
return workspace_id in user_workspace_ids
except ValueError:
# Workspace filter is a handle name
return workspace_filter in user_workspace_handles
try:
# Check if workspace filter is numeric (workspace ID)
workspace_id = int(workspace_filter)
if workspace_id <= 0:
return False
return workspace_id in user_workspace_ids
except (ValueError, TypeError):
# Workspace filter is a handle name
if not isinstance(workspace_filter, str) or not workspace_filter.strip():
return False
return workspace_filter in user_workspace_handles
🤖 Prompt for AI Agents
In utils/workspace.py around lines 12 to 18, improve the error handling by
adding input validation before attempting to convert workspace_filter to int,
and catch additional potential exceptions beyond ValueError. Validate that
workspace_filter is a string or appropriate type, and handle cases where it
might be None or an unexpected type, returning False or raising a clear error as
needed. Expand the except block to handle other exceptions that could arise
during conversion or membership checks to make the function more robust.

61 changes: 46 additions & 15 deletions widgets/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@
from daras_ai_v2.all_pages import all_home_pages_by_category
from daras_ai_v2.base import BasePage
from daras_ai_v2.grid_layout_widget import grid_layout
from utils.workspace import is_user_workspace_owner
from widgets.workflow_search import (
SearchFilters,
render_search_filters,
render_search_results,
)

META_TITLE = "Explore AI workflows"
META_DESCRIPTION = "Find, fork and run your fields favorite AI recipes on Gooey.AI"
META_DESCRIPTION = "Find, fork and run your field's favorite AI recipes on Gooey.AI"

TITLE = "Explore"
DESCRIPTION = "DISCOVER YOUR FIELDS FAVORITE AI WORKFLOWS"
DESCRIPTION = "DISCOVER YOUR FIELD'S FAVORITE AI WORKFLOWS"


def render(user: AppUser | None, search_filters: SearchFilters | None):
Expand All @@ -44,9 +45,19 @@ def render(user: AppUser | None, search_filters: SearchFilters | None):
if category != "Featured":
section_heading(category)
if category == "Images" or category == "Featured":
grid_layout(3, pages, _render_as_featured, separator=False)
grid_layout(
3,
pages,
lambda page_cls: _render_as_featured(page_cls, user, search_filters),
separator=False,
)
else:
grid_layout(2, pages, _render_non_featured, separator=False)
grid_layout(
2,
pages,
lambda page_cls: _render_non_featured(page_cls, user, search_filters),
separator=False,
)


def heading(
Expand Down Expand Up @@ -87,36 +98,56 @@ def render_image(page: BasePage):
)


def _render_as_featured(page_cls: typing.Type[BasePage]):
def _render_as_featured(
page_cls: typing.Type[BasePage],
user: AppUser | None = None,
search_filters: SearchFilters | None = None,
):
page = page_cls()
render_image(page)
# total_runs = page.get_total_runs()
# render_description(page, state, total_runs)
render_description(page)
render_description(page, user, search_filters)


def _render_non_featured(page_cls):
def _render_non_featured(
page_cls: typing.Type[BasePage],
user: AppUser | None = None,
search_filters: SearchFilters | None = None,
):
page = page_cls()
col1, col2 = gui.columns([1, 2])
with col1:
render_image(page)
with col2:
# total_runs = page.get_total_runs()
# render_description(page, state, total_runs)
render_description(page)
render_description(page, user, search_filters)


def render_description(page: BasePage):
def render_description(
page: BasePage,
user: AppUser | None = None,
search_filters: SearchFilters | None = None,
):
with gui.link(to=page.app_url()):
gui.markdown(f"#### {page.get_recipe_title()}")

root_pr = page.get_root_pr()
with gui.div(className="mb-3"):
gui.write(root_pr.notes, line_clamp=4)
if root_pr.run_count >= 50:

# Check if user is viewing their own workspace
show_all_counts = False
if search_filters and search_filters.workspace:
show_all_counts = is_user_workspace_owner(user, search_filters.workspace)

if root_pr.run_count >= 50 or show_all_counts:
run_count = format_number_with_suffix(root_pr.run_count)
gui.caption(
f"{icons.run} {run_count} runs",
unsafe_allow_html=True,
style={"fontSize": "0.9rem"},
)
with gui.div(className="d-flex align-items-center"):
gui.caption(
f"{icons.run} {run_count} runs",
unsafe_allow_html=True,
className="text-muted",
style={"fontSize": "0.9rem"},
)
111 changes: 72 additions & 39 deletions widgets/saved_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def render_saved_workflow_preview(
hide_visibility_pill: bool = False,
hide_version_notes: bool = False,
hide_last_editor: bool = False,
hide_updated_at: bool = False,
show_all_run_counts: bool = False,
):
tb = get_title_breadcrumbs(page_cls, published_run.saved_run, published_run)

Expand Down Expand Up @@ -82,6 +84,8 @@ def render_saved_workflow_preview(
hide_version_notes=hide_version_notes,
hide_visibility_pill=hide_visibility_pill,
hide_last_editor=hide_last_editor,
hide_updated_at=hide_updated_at,
show_all_run_counts=show_all_run_counts,
)

if output_url:
Expand Down Expand Up @@ -129,7 +133,7 @@ def render_title_pills(published_run: PublishedRun, workflow_pill: str | None):
white-space: nowrap;
}
& .author-name {
max-width: 150px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
Expand All @@ -141,20 +145,38 @@ def render_title_pills(published_run: PublishedRun, workflow_pill: str | None):
margin: 0 2px;
text-align: center;
}
& > :not(:empty):not(:first-child):not(.newline-sm):before {
content: "•";
margin: 0 0.5rem;
color: black;
display: inline-block;
vertical-align: middle;
& > div {
margin-right: 0.75rem;
display: flex;
align-items: center;
}
@media (max-width: 768px) {
& .newline-sm {
width: 100%;
height: 0.12rem;
& {
grid-template-areas:
"workspace author"
"notes notes"
"time runs";
gap: 0.25rem 0.75rem;
align-items: start;
white-space: normal;
}
& .newline-sm:before, & .newline-sm + :before {
content: unset !important;
& .workspace-container {
grid-area: workspace;
}
& .author-container {
grid-area: author;
}
& .notes-container {
grid-area: notes;
}
& .time-container {
grid-area: time;
}
& .runs-container {
grid-area: runs;
}
& > div {
margin: 0;
}
}
"""
Expand All @@ -166,6 +188,8 @@ def render_footer_breadcrumbs(
hide_visibility_pill: bool,
hide_version_notes: bool,
hide_last_editor: bool,
hide_updated_at: bool,
show_all_run_counts: bool = False,
):
latest_version = published_run.versions.latest()

Expand All @@ -175,47 +199,56 @@ def render_footer_breadcrumbs(
className="flex-grow-1 d-flex align-items-end flex-wrap flex-lg-nowrap"
),
):
if not hide_version_notes and latest_version and latest_version.change_notes:
gui.caption(
f"{icons.notes} {html.escape(latest_version.change_notes)}",
unsafe_allow_html=True,
line_clamp=1,
lineClampExpand=False,
)
gui.div(className="newline-sm")

if published_run.workspace.is_personal:
show_workspace_author = False
if show_workspace_author:
# don't repeat author for personal workspaces
with gui.div(className="d-flex align-items-center"):
with gui.div(className="d-flex align-items-center workspace-container"):
render_author_from_workspace(
published_run.workspace, image_size="24px", responsive=False
published_run.workspace,
image_size="24px",
responsive=False,
)

if not hide_last_editor and published_run.last_edited_by:
with gui.div(className="d-flex align-items-center text-truncate"):
with gui.div(
className="d-flex align-items-center text-truncate author-container"
):
render_author_from_user(
published_run.last_edited_by, image_size="24px", responsive=False
published_run.last_edited_by,
image_size="24px",
responsive=False,
)
if show_workspace_author:
gui.div(className="newline-sm")

if not hide_version_notes and latest_version and latest_version.change_notes:
with gui.div(
className="text-truncate text-muted d-flex align-items-center notes-container",
style={"maxWidth": "250px"},
):
gui.html(f"{icons.notes} {html.escape(latest_version.change_notes)}")
# gui.div(className="mobile-line-break")

updated_at = published_run.saved_run.updated_at
if updated_at and isinstance(updated_at, datetime.datetime):
gui.write(
f"{icons.time} {get_relative_time(updated_at)}",
unsafe_allow_html=True,
)
if (
updated_at
and isinstance(updated_at, datetime.datetime)
and not hide_updated_at
):
with gui.div(className="d-flex align-items-center time-container"):
gui.write(
f"{icons.time} {get_relative_time(updated_at)}",
unsafe_allow_html=True,
className="text-muted",
)

if published_run.run_count >= 50:
if published_run.run_count >= 50 or show_all_run_counts:
run_count = format_number_with_suffix(published_run.run_count)
gui.write(
f"{icons.run} {run_count} runs",
unsafe_allow_html=True,
className="text-dark text-nowrap",
)
gui.div(className="newline-sm")
with gui.div(className="d-flex align-items-center runs-container"):
gui.write(
f"{icons.run} {run_count} runs",
unsafe_allow_html=True,
className="text-muted text-nowrap",
)

if not hide_visibility_pill:
gui.caption(
Expand Down
51 changes: 39 additions & 12 deletions widgets/workflow_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from daras_ai_v2.grid_layout_widget import grid_layout
from widgets.saved_workflow import render_saved_workflow_preview
from workspaces.models import WorkspaceRole
from utils.workspace import is_user_workspace_owner


class SearchFilters(BaseModel):
Expand Down Expand Up @@ -181,20 +182,46 @@ def _render_selectbox(
def render_search_results(user: AppUser | None, search_filters: SearchFilters):
qs = get_filtered_published_runs(user, search_filters)
qs = qs.select_related("workspace", "created_by", "saved_run")
grid_layout(1, qs, _render_run)

def _render_run(pr: PublishedRun):
workflow = Workflow(pr.workflow)

def _render_run(pr: PublishedRun):
workflow = Workflow(pr.workflow)
hide_last_editor = bool(pr.workspace_id and not getattr(pr, "is_member", False))
render_saved_workflow_preview(
workflow.page_cls,
pr,
workflow_pill=f"{workflow.get_or_create_metadata().emoji} {workflow.short_title}",
hide_visibility_pill=True,
show_workspace_author=True,
hide_last_editor=hide_last_editor,
)
# Extract UI display flags
display_flags = _determine_display_flags(pr, user, search_filters)

render_saved_workflow_preview(
workflow.page_cls,
pr,
workflow_pill=f"{workflow.get_or_create_metadata().emoji} {workflow.short_title}",
hide_visibility_pill=True,
**display_flags,
)

def _determine_display_flags(
pr: PublishedRun, user: AppUser | None, search_filters: SearchFilters
) -> dict:
"""Determine UI display flags for the published run preview."""
show_workspace_author = not bool(search_filters and search_filters.workspace)

is_member = bool(getattr(pr, "is_member", False))
hide_last_editor = bool(pr.workspace_id and not is_member)
hide_updated_at = hide_last_editor

# Only show all run counts if user is a member AND they're filtering by their workspace
show_all_run_counts = False
if is_member and search_filters and search_filters.workspace:
show_all_run_counts = is_user_workspace_owner(
user, search_filters.workspace
)

return {
"show_workspace_author": show_workspace_author,
"hide_last_editor": hide_last_editor,
"hide_updated_at": hide_updated_at,
"show_all_run_counts": show_all_run_counts,
}

grid_layout(1, qs, _render_run)


def get_filtered_published_runs(
Expand Down