diff --git a/autogpt_platform/backend/backend/data/onboarding.py b/autogpt_platform/backend/backend/data/onboarding.py index a2e022044a..1415c7694e 100644 --- a/autogpt_platform/backend/backend/data/onboarding.py +++ b/autogpt_platform/backend/backend/data/onboarding.py @@ -432,6 +432,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]: slug=agent.slug, agent_name=agent.agent_name, agent_video=agent.agent_video or "", + agent_output_demo=agent.agent_output_demo or "", agent_image=agent.agent_image, creator=agent.creator_username, creator_avatar=agent.creator_avatar, diff --git a/autogpt_platform/backend/backend/server/v2/store/db.py b/autogpt_platform/backend/backend/server/v2/store/db.py index fe782e8eea..33554a9c2a 100644 --- a/autogpt_platform/backend/backend/server/v2/store/db.py +++ b/autogpt_platform/backend/backend/server/v2/store/db.py @@ -327,6 +327,7 @@ async def get_store_agent_details( slug=agent.slug, agent_name=agent.agent_name, agent_video=agent.agent_video or "", + agent_output_demo=agent.agent_output_demo or "", agent_image=agent.agent_image, creator=agent.creator_username or "", creator_avatar=agent.creator_avatar or "", @@ -397,6 +398,7 @@ async def get_store_agent_by_version_id( slug=agent.slug, agent_name=agent.agent_name, agent_video=agent.agent_video or "", + agent_output_demo=agent.agent_output_demo or "", agent_image=agent.agent_image, creator=agent.creator_username or "", creator_avatar=agent.creator_avatar or "", @@ -683,6 +685,7 @@ async def create_store_submission( slug: str, name: str, video_url: str | None = None, + agent_output_demo_url: str | None = None, image_urls: list[str] = [], description: str = "", instructions: str | None = None, @@ -777,6 +780,7 @@ async def create_store_submission( agentGraphVersion=agent_version, name=name, videoUrl=video_url, + agentOutputDemoUrl=agent_output_demo_url, imageUrls=image_urls, description=description, instructions=instructions, @@ -849,6 +853,7 @@ async def edit_store_submission( store_listing_version_id: str, name: str, video_url: str | None = None, + agent_output_demo_url: str | None = None, image_urls: list[str] = [], description: str = "", sub_heading: str = "", @@ -930,6 +935,7 @@ async def edit_store_submission( store_listing_id=current_version.storeListingId, name=name, video_url=video_url, + agent_output_demo_url=agent_output_demo_url, image_urls=image_urls, description=description, sub_heading=sub_heading, @@ -947,6 +953,7 @@ async def edit_store_submission( data=prisma.types.StoreListingVersionUpdateInput( name=name, videoUrl=video_url, + agentOutputDemoUrl=agent_output_demo_url, imageUrls=image_urls, description=description, categories=categories, @@ -1008,6 +1015,7 @@ async def create_store_version( store_listing_id: str, name: str, video_url: str | None = None, + agent_output_demo_url: str | None = None, image_urls: list[str] = [], description: str = "", instructions: str | None = None, @@ -1077,6 +1085,7 @@ async def create_store_version( agentGraphVersion=agent_version, name=name, videoUrl=video_url, + agentOutputDemoUrl=agent_output_demo_url, imageUrls=image_urls, description=description, instructions=instructions, diff --git a/autogpt_platform/backend/backend/server/v2/store/model.py b/autogpt_platform/backend/backend/server/v2/store/model.py index ce2aabaa28..745c969ae6 100644 --- a/autogpt_platform/backend/backend/server/v2/store/model.py +++ b/autogpt_platform/backend/backend/server/v2/store/model.py @@ -44,6 +44,7 @@ class StoreAgentDetails(pydantic.BaseModel): slug: str agent_name: str agent_video: str + agent_output_demo: str agent_image: list[str] creator: str creator_avatar: str @@ -121,6 +122,7 @@ class StoreSubmission(pydantic.BaseModel): # Additional fields for editing video_url: str | None = None + agent_output_demo_url: str | None = None categories: list[str] = [] @@ -157,6 +159,7 @@ class StoreSubmissionRequest(pydantic.BaseModel): name: str sub_heading: str video_url: str | None = None + agent_output_demo_url: str | None = None image_urls: list[str] = [] description: str = "" instructions: str | None = None @@ -169,6 +172,7 @@ class StoreSubmissionEditRequest(pydantic.BaseModel): name: str sub_heading: str video_url: str | None = None + agent_output_demo_url: str | None = None image_urls: list[str] = [] description: str = "" instructions: str | None = None diff --git a/autogpt_platform/backend/backend/server/v2/store/model_test.py b/autogpt_platform/backend/backend/server/v2/store/model_test.py index ec90fe6854..c387dfdecb 100644 --- a/autogpt_platform/backend/backend/server/v2/store/model_test.py +++ b/autogpt_platform/backend/backend/server/v2/store/model_test.py @@ -62,6 +62,7 @@ def test_store_agent_details(): slug="test-agent", agent_name="Test Agent", agent_video="video.mp4", + agent_output_demo="demo.mp4", agent_image=["image1.jpg", "image2.jpg"], creator="creator1", creator_avatar="avatar.jpg", diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 5f357b2df0..b0c1df6e22 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -438,6 +438,7 @@ async def create_submission( slug=submission_request.slug, name=submission_request.name, video_url=submission_request.video_url, + agent_output_demo_url=submission_request.agent_output_demo_url, image_urls=submission_request.image_urls, description=submission_request.description, instructions=submission_request.instructions, @@ -481,6 +482,7 @@ async def edit_submission( store_listing_version_id=store_listing_version_id, name=submission_request.name, video_url=submission_request.video_url, + agent_output_demo_url=submission_request.agent_output_demo_url, image_urls=submission_request.image_urls, description=submission_request.description, instructions=submission_request.instructions, diff --git a/autogpt_platform/backend/backend/server/v2/store/routes_test.py b/autogpt_platform/backend/backend/server/v2/store/routes_test.py index 8dd83149b0..03322ee988 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes_test.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes_test.py @@ -378,6 +378,7 @@ def test_get_agent_details( slug="test-agent", agent_name="Test Agent", agent_video="video.mp4", + agent_output_demo="demo.mp4", agent_image=["image1.jpg", "image2.jpg"], creator="creator1", creator_avatar="avatar1.jpg", diff --git a/autogpt_platform/backend/migrations/20251204012214_add_marketplace_agent_output_video_column/migration.sql b/autogpt_platform/backend/migrations/20251204012214_add_marketplace_agent_output_video_column/migration.sql new file mode 100644 index 0000000000..bdecc9678b --- /dev/null +++ b/autogpt_platform/backend/migrations/20251204012214_add_marketplace_agent_output_video_column/migration.sql @@ -0,0 +1,64 @@ +-- AlterTable +ALTER TABLE "StoreListingVersion" ADD COLUMN "agentOutputDemoUrl" TEXT; + +-- Drop and recreate the StoreAgent view with agentOutputDemoUrl field +DROP VIEW IF EXISTS "StoreAgent"; + +CREATE OR REPLACE VIEW "StoreAgent" AS +WITH latest_versions AS ( + SELECT + "storeListingId", + MAX(version) AS max_version + FROM "StoreListingVersion" + WHERE "submissionStatus" = 'APPROVED' + GROUP BY "storeListingId" +), +agent_versions AS ( + SELECT + "storeListingId", + array_agg(DISTINCT version::text ORDER BY version::text) AS versions + FROM "StoreListingVersion" + WHERE "submissionStatus" = 'APPROVED' + GROUP BY "storeListingId" +) +SELECT + sl.id AS listing_id, + slv.id AS "storeListingVersionId", + slv."createdAt" AS updated_at, + sl.slug, + COALESCE(slv.name, '') AS agent_name, + slv."videoUrl" AS agent_video, + slv."agentOutputDemoUrl" AS agent_output_demo, + COALESCE(slv."imageUrls", ARRAY[]::text[]) AS agent_image, + slv."isFeatured" AS featured, + p.username AS creator_username, -- Allow NULL for malformed sub-agents + p."avatarUrl" AS creator_avatar, -- Allow NULL for malformed sub-agents + slv."subHeading" AS sub_heading, + slv.description, + slv.categories, + slv.search, + COALESCE(ar.run_count, 0::bigint) AS runs, + COALESCE(rs.avg_rating, 0.0)::double precision AS rating, + COALESCE(av.versions, ARRAY[slv.version::text]) AS versions, + slv."isAvailable" AS is_available, + COALESCE(sl."useForOnboarding", false) AS "useForOnboarding" +FROM "StoreListing" sl +JOIN latest_versions lv + ON sl.id = lv."storeListingId" +JOIN "StoreListingVersion" slv + ON slv."storeListingId" = lv."storeListingId" + AND slv.version = lv.max_version + AND slv."submissionStatus" = 'APPROVED' +JOIN "AgentGraph" a + ON slv."agentGraphId" = a.id + AND slv."agentGraphVersion" = a.version +LEFT JOIN "Profile" p + ON sl."owningUserId" = p."userId" +LEFT JOIN "mv_review_stats" rs + ON sl.id = rs."storeListingId" +LEFT JOIN "mv_agent_run_counts" ar + ON a.id = ar."agentGraphId" +LEFT JOIN agent_versions av + ON sl.id = av."storeListingId" +WHERE sl."isDeleted" = false + AND sl."hasApprovedVersion" = true; diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index 0c29c83673..c54b014471 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -701,10 +701,11 @@ view StoreAgent { storeListingVersionId String updated_at DateTime - slug String - agent_name String - agent_video String? - agent_image String[] + slug String + agent_name String + agent_video String? + agent_output_demo String? + agent_image String[] featured Boolean @default(false) creator_username String? @@ -833,13 +834,14 @@ model StoreListingVersion { AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version]) // Content fields - name String - subHeading String - videoUrl String? - imageUrls String[] - description String - instructions String? - categories String[] + name String + subHeading String + videoUrl String? + agentOutputDemoUrl String? + imageUrls String[] + description String + instructions String? + categories String[] isFeatured Boolean @default(false) diff --git a/autogpt_platform/backend/snapshots/agt_details b/autogpt_platform/backend/snapshots/agt_details index 0718ccab5d..649b5ed644 100644 --- a/autogpt_platform/backend/snapshots/agt_details +++ b/autogpt_platform/backend/snapshots/agt_details @@ -3,6 +3,7 @@ "slug": "test-agent", "agent_name": "Test Agent", "agent_video": "video.mp4", + "agent_output_demo": "demo.mp4", "agent_image": [ "image1.jpg", "image2.jpg" diff --git a/autogpt_platform/backend/snapshots/sub_success b/autogpt_platform/backend/snapshots/sub_success index a3816c4384..13e2ec570d 100644 --- a/autogpt_platform/backend/snapshots/sub_success +++ b/autogpt_platform/backend/snapshots/sub_success @@ -23,6 +23,7 @@ "reviewed_at": null, "changes_summary": null, "video_url": "test.mp4", + "agent_output_demo_url": null, "categories": [ "test-category" ] diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/MainAgentPage.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/MainAgentPage.tsx index ce22b73f56..5eb3984cbc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/MainAgentPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/MainAgentPage.tsx @@ -97,11 +97,31 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => { /> { + const orderedImages: string[] = []; + + // 1. YouTube/Overview video (if it exists) + if (agent.agent_video) { + orderedImages.push(agent.agent_video); + } + + // 2. First image (hero) + if (agent.agent_image.length > 0) { + orderedImages.push(agent.agent_image[0]); + } + + // 3. Agent Output Demo (if it exists) + if ((agent as any).agent_output_demo) { + orderedImages.push((agent as any).agent_output_demo); + } + + // 4. Additional images + if (agent.agent_image.length > 1) { + orderedImages.push(...agent.agent_image.slice(1)); + } + + return orderedImages; + })()} /> diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 42be3b0151..fe2f139edb 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -8685,6 +8685,10 @@ "slug": { "type": "string", "title": "Slug" }, "agent_name": { "type": "string", "title": "Agent Name" }, "agent_video": { "type": "string", "title": "Agent Video" }, + "agent_output_demo": { + "type": "string", + "title": "Agent Output Demo" + }, "agent_image": { "items": { "type": "string" }, "type": "array", @@ -8735,6 +8739,7 @@ "slug", "agent_name", "agent_video", + "agent_output_demo", "agent_image", "creator", "creator_avatar", @@ -8902,6 +8907,10 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Video Url" }, + "agent_output_demo_url": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Agent Output Demo Url" + }, "categories": { "items": { "type": "string" }, "type": "array", @@ -8933,6 +8942,10 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Video Url" }, + "agent_output_demo_url": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Agent Output Demo Url" + }, "image_urls": { "items": { "type": "string" }, "type": "array", @@ -8978,6 +8991,10 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Video Url" }, + "agent_output_demo_url": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Agent Output Demo Url" + }, "image_urls": { "items": { "type": "string" }, "type": "array", diff --git a/autogpt_platform/frontend/src/components/contextual/EditAgentModal/components/useEditAgentForm.ts b/autogpt_platform/frontend/src/components/contextual/EditAgentModal/components/useEditAgentForm.ts index 9b9a20d257..8de7c43659 100644 --- a/autogpt_platform/frontend/src/components/contextual/EditAgentModal/components/useEditAgentForm.ts +++ b/autogpt_platform/frontend/src/components/contextual/EditAgentModal/components/useEditAgentForm.ts @@ -6,6 +6,7 @@ import { import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission"; import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest"; import { useToast } from "@/components/molecules/Toast/use-toast"; +import { validateYouTubeUrl } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; import React from "react"; @@ -35,22 +36,7 @@ export const useEditAgentForm = ({ .max(200, "Subheader must be less than 200 characters"), youtubeLink: z .string() - .optional() - .refine((val) => { - if (!val) return true; - try { - const url = new URL(val); - const allowedHosts = [ - "youtube.com", - "www.youtube.com", - "youtu.be", - "www.youtu.be", - ]; - return allowedHosts.includes(url.hostname); - } catch { - return false; - } - }, "Please enter a valid YouTube URL"), + .refine(validateYouTubeUrl, "Please enter a valid YouTube URL"), category: z.string().min(1, "Category is required"), description: z .string() @@ -60,6 +46,9 @@ export const useEditAgentForm = ({ .string() .min(1, "Changes summary is required") .max(200, "Changes summary must be less than 200 characters"), + agentOutputDemo: z + .string() + .refine(validateYouTubeUrl, "Please enter a valid YouTube URL"), }); type EditAgentFormData = z.infer; @@ -91,6 +80,7 @@ export const useEditAgentForm = ({ category: submission.categories?.[0] || "", description: submission.description, changes_summary: submission.changes_summary || "", + agentOutputDemo: submission.agent_output_demo_url || "", }, }); @@ -134,6 +124,7 @@ export const useEditAgentForm = ({ description: data.description, image_urls: images, video_url: data.youtubeLink || "", + agent_output_demo_url: data.agentOutputDemo || "", categories: filteredCategories, changes_summary: data.changes_summary, }, diff --git a/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/AgentInfoStep.tsx b/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/AgentInfoStep.tsx index 23f15cd717..18c00d35d7 100644 --- a/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/AgentInfoStep.tsx +++ b/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/AgentInfoStep.tsx @@ -163,6 +163,21 @@ export function AgentInfoStep({ )} /> + ( + + )} + /> + { - if (!val) return true; - try { - const url = new URL(val); - const allowedHosts = [ - "youtube.com", - "www.youtube.com", - "youtu.be", - "www.youtu.be", - ]; - return allowedHosts.includes(url.hostname); - } catch { - return false; - } - }, "Please enter a valid YouTube URL"), + .refine(validateYouTubeUrl, "Please enter a valid YouTube URL"), category: z.string().min(1, "Category is required"), description: z .string() @@ -48,6 +34,9 @@ export const publishAgentSchema = z.object({ (val) => !val || val.length <= 2000, "Instructions must be less than 2000 characters", ), + agentOutputDemo: z + .string() + .refine(validateYouTubeUrl, "Please enter a valid YouTube URL"), }); export type PublishAgentFormData = z.infer; @@ -64,4 +53,5 @@ export interface PublishAgentInfoInitialData { additionalImages?: string[]; recommendedScheduleCron?: string; instructions?: string; + agentOutputDemo?: string; } diff --git a/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/useAgentInfoStep.ts b/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/useAgentInfoStep.ts index 9e691dad87..6bec8dd355 100644 --- a/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/useAgentInfoStep.ts +++ b/autogpt_platform/frontend/src/components/contextual/PublishAgentModal/components/AgentInfoStep/useAgentInfoStep.ts @@ -46,6 +46,7 @@ export function useAgentInfoStep({ description: "", recommendedScheduleCron: "", instructions: "", + agentOutputDemo: "", }, }); @@ -68,6 +69,7 @@ export function useAgentInfoStep({ description: initialData.description, recommendedScheduleCron: initialData.recommendedScheduleCron || "", instructions: initialData.instructions || "", + agentOutputDemo: initialData.agentOutputDemo || "", }); } }, [initialData, form]); @@ -99,12 +101,13 @@ export function useAgentInfoStep({ instructions: data.instructions || null, image_urls: images, video_url: data.youtubeLink || "", + agent_output_demo_url: data.agentOutputDemo || "", agent_id: selectedAgentId || "", agent_version: selectedAgentVersion || 0, slug: data.slug.replace(/\s+/g, "-"), categories: filteredCategories, recommended_schedule_cron: data.recommendedScheduleCron || null, - }); + } as any); await queryClient.invalidateQueries({ queryKey: getGetV2ListMySubmissionsQueryKey(), diff --git a/autogpt_platform/frontend/src/lib/utils.ts b/autogpt_platform/frontend/src/lib/utils.ts index b7be324f3f..71f38aeb33 100644 --- a/autogpt_platform/frontend/src/lib/utils.ts +++ b/autogpt_platform/frontend/src/lib/utils.ts @@ -428,3 +428,20 @@ export function isEmpty(value: any): boolean { export function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } + +/** Validate YouTube URL */ +export function validateYouTubeUrl(val: string): boolean { + if (!val) return true; + try { + const url = new URL(val); + const allowedHosts = [ + "youtube.com", + "www.youtube.com", + "youtu.be", + "www.youtu.be", + ]; + return allowedHosts.includes(url.hostname); + } catch { + return false; + } +}