mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-20 20:48:11 -05:00
Compare commits
18 Commits
make-old-w
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2435067c | ||
|
|
519d4bd67e | ||
|
|
4350ad95d7 | ||
|
|
8769104242 | ||
|
|
22c486583a | ||
|
|
87d4bcd025 | ||
|
|
c2c7cc94cf | ||
|
|
e6eef0379e | ||
|
|
cc2c183a5f | ||
|
|
73fcadf0e8 | ||
|
|
44d17885ed | ||
|
|
21652a5422 | ||
|
|
4a7bc006a8 | ||
|
|
1a8ed4c291 | ||
|
|
c5acb0d4cc | ||
|
|
fc0d0903f2 | ||
|
|
ca53b752d2 | ||
|
|
1d3d60f7ef |
@@ -442,6 +442,8 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
versions=agent.versions,
|
||||
agentGraphVersions=agent.agentGraphVersions,
|
||||
agentGraphId=agent.agentGraphId,
|
||||
last_updated=agent.updated_at,
|
||||
)
|
||||
for agent in recommended_agents
|
||||
|
||||
@@ -538,6 +538,7 @@ async def update_library_agent(
|
||||
library_agent_id: str,
|
||||
user_id: str,
|
||||
auto_update_version: Optional[bool] = None,
|
||||
graph_version: Optional[int] = None,
|
||||
is_favorite: Optional[bool] = None,
|
||||
is_archived: Optional[bool] = None,
|
||||
is_deleted: Optional[Literal[False]] = None,
|
||||
@@ -550,6 +551,7 @@ async def update_library_agent(
|
||||
library_agent_id: The ID of the LibraryAgent to update.
|
||||
user_id: The owner of this LibraryAgent.
|
||||
auto_update_version: Whether the agent should auto-update to active version.
|
||||
graph_version: Specific graph version to update to.
|
||||
is_favorite: Whether this agent is marked as a favorite.
|
||||
is_archived: Whether this agent is archived.
|
||||
settings: User-specific settings for this library agent.
|
||||
@@ -563,8 +565,8 @@ async def update_library_agent(
|
||||
"""
|
||||
logger.debug(
|
||||
f"Updating library agent {library_agent_id} for user {user_id} with "
|
||||
f"auto_update_version={auto_update_version}, is_favorite={is_favorite}, "
|
||||
f"is_archived={is_archived}, settings={settings}"
|
||||
f"auto_update_version={auto_update_version}, graph_version={graph_version}, "
|
||||
f"is_favorite={is_favorite}, is_archived={is_archived}, settings={settings}"
|
||||
)
|
||||
update_fields: prisma.types.LibraryAgentUpdateManyMutationInput = {}
|
||||
if auto_update_version is not None:
|
||||
@@ -581,10 +583,23 @@ async def update_library_agent(
|
||||
update_fields["isDeleted"] = is_deleted
|
||||
if settings is not None:
|
||||
update_fields["settings"] = SafeJson(settings.model_dump())
|
||||
if not update_fields:
|
||||
raise ValueError("No values were passed to update")
|
||||
|
||||
try:
|
||||
# If graph_version is provided, update to that specific version
|
||||
if graph_version is not None:
|
||||
# Get the current agent to find its graph_id
|
||||
agent = await get_library_agent(id=library_agent_id, user_id=user_id)
|
||||
# Update to the specified version using existing function
|
||||
return await update_agent_version_in_library(
|
||||
user_id=user_id,
|
||||
agent_graph_id=agent.graph_id,
|
||||
agent_graph_version=graph_version,
|
||||
)
|
||||
|
||||
# Otherwise, just update the simple fields
|
||||
if not update_fields:
|
||||
raise ValueError("No values were passed to update")
|
||||
|
||||
n_updated = await prisma.models.LibraryAgent.prisma().update_many(
|
||||
where={"id": library_agent_id, "userId": user_id},
|
||||
data=update_fields,
|
||||
|
||||
@@ -385,6 +385,9 @@ class LibraryAgentUpdateRequest(pydantic.BaseModel):
|
||||
auto_update_version: Optional[bool] = pydantic.Field(
|
||||
default=None, description="Auto-update the agent version"
|
||||
)
|
||||
graph_version: Optional[int] = pydantic.Field(
|
||||
default=None, description="Specific graph version to update to"
|
||||
)
|
||||
is_favorite: Optional[bool] = pydantic.Field(
|
||||
default=None, description="Mark the agent as a favorite"
|
||||
)
|
||||
|
||||
@@ -284,6 +284,7 @@ async def update_library_agent(
|
||||
library_agent_id=library_agent_id,
|
||||
user_id=user_id,
|
||||
auto_update_version=payload.auto_update_version,
|
||||
graph_version=payload.graph_version,
|
||||
is_favorite=payload.is_favorite,
|
||||
is_archived=payload.is_archived,
|
||||
settings=payload.settings,
|
||||
|
||||
@@ -42,10 +42,12 @@ async def _get_cached_store_agents(
|
||||
|
||||
# Cache individual agent details for 15 minutes
|
||||
@cached(maxsize=200, ttl_seconds=300, shared_cache=True)
|
||||
async def _get_cached_agent_details(username: str, agent_name: str):
|
||||
async def _get_cached_agent_details(
|
||||
username: str, agent_name: str, include_changelog: bool = False
|
||||
):
|
||||
"""Cached helper to get agent details."""
|
||||
return await backend.server.v2.store.db.get_store_agent_details(
|
||||
username=username, agent_name=agent_name
|
||||
username=username, agent_name=agent_name, include_changelog=include_changelog
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ async def log_search_term(search_query: str):
|
||||
|
||||
|
||||
async def get_store_agent_details(
|
||||
username: str, agent_name: str
|
||||
username: str, agent_name: str, include_changelog: bool = False
|
||||
) -> backend.server.v2.store.model.StoreAgentDetails:
|
||||
"""Get PUBLIC store agent details from the StoreAgent view"""
|
||||
logger.debug(f"Getting store agent details for {username}/{agent_name}")
|
||||
@@ -321,6 +321,27 @@ async def get_store_agent_details(
|
||||
else:
|
||||
recommended_schedule_cron = None
|
||||
|
||||
# Fetch changelog data if requested
|
||||
changelog_data = None
|
||||
if include_changelog and store_listing:
|
||||
changelog_versions = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_many(
|
||||
where={
|
||||
"storeListingId": store_listing.id,
|
||||
"submissionStatus": prisma.enums.SubmissionStatus.APPROVED,
|
||||
},
|
||||
order=[{"version": "desc"}],
|
||||
)
|
||||
)
|
||||
changelog_data = [
|
||||
backend.server.v2.store.model.ChangelogEntry(
|
||||
version=str(version.version),
|
||||
changes_summary=version.changesSummary or "No changes recorded",
|
||||
date=version.createdAt,
|
||||
)
|
||||
for version in changelog_versions
|
||||
]
|
||||
|
||||
logger.debug(f"Found agent details for {username}/{agent_name}")
|
||||
return backend.server.v2.store.model.StoreAgentDetails(
|
||||
store_listing_version_id=agent.storeListingVersionId,
|
||||
@@ -337,10 +358,13 @@ async def get_store_agent_details(
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
versions=agent.versions,
|
||||
agentGraphVersions=agent.agentGraphVersions,
|
||||
agentGraphId=agent.agentGraphId,
|
||||
last_updated=agent.updated_at,
|
||||
active_version_id=active_version_id,
|
||||
has_approved_version=has_approved_version,
|
||||
recommended_schedule_cron=recommended_schedule_cron,
|
||||
changelog=changelog_data,
|
||||
)
|
||||
except backend.server.v2.store.exceptions.AgentNotFoundError:
|
||||
raise
|
||||
@@ -408,6 +432,8 @@ async def get_store_agent_by_version_id(
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
versions=agent.versions,
|
||||
agentGraphVersions=agent.agentGraphVersions,
|
||||
agentGraphId=agent.agentGraphId,
|
||||
last_updated=agent.updated_at,
|
||||
)
|
||||
except backend.server.v2.store.exceptions.AgentNotFoundError:
|
||||
|
||||
@@ -40,6 +40,8 @@ async def test_get_store_agents(mocker):
|
||||
runs=10,
|
||||
rating=4.5,
|
||||
versions=["1.0"],
|
||||
agentGraphVersions=["1"],
|
||||
agentGraphId="test-graph-id",
|
||||
updated_at=datetime.now(),
|
||||
is_available=False,
|
||||
useForOnboarding=False,
|
||||
@@ -83,6 +85,8 @@ async def test_get_store_agent_details(mocker):
|
||||
runs=10,
|
||||
rating=4.5,
|
||||
versions=["1.0"],
|
||||
agentGraphVersions=["1"],
|
||||
agentGraphId="test-graph-id",
|
||||
updated_at=datetime.now(),
|
||||
is_available=False,
|
||||
useForOnboarding=False,
|
||||
@@ -105,6 +109,8 @@ async def test_get_store_agent_details(mocker):
|
||||
runs=15,
|
||||
rating=4.8,
|
||||
versions=["1.0", "2.0"],
|
||||
agentGraphVersions=["1", "2"],
|
||||
agentGraphId="test-graph-id-active",
|
||||
updated_at=datetime.now(),
|
||||
is_available=True,
|
||||
useForOnboarding=False,
|
||||
|
||||
@@ -7,6 +7,12 @@ import pydantic
|
||||
from backend.util.models import Pagination
|
||||
|
||||
|
||||
class ChangelogEntry(pydantic.BaseModel):
|
||||
version: str
|
||||
changes_summary: str
|
||||
date: datetime.datetime
|
||||
|
||||
|
||||
class MyAgent(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
@@ -55,12 +61,17 @@ class StoreAgentDetails(pydantic.BaseModel):
|
||||
runs: int
|
||||
rating: float
|
||||
versions: list[str]
|
||||
agentGraphVersions: list[str]
|
||||
agentGraphId: str
|
||||
last_updated: datetime.datetime
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
active_version_id: str | None = None
|
||||
has_approved_version: bool = False
|
||||
|
||||
# Optional changelog data when include_changelog=True
|
||||
changelog: list[ChangelogEntry] | None = None
|
||||
|
||||
|
||||
class Creator(pydantic.BaseModel):
|
||||
name: str
|
||||
|
||||
@@ -72,6 +72,8 @@ def test_store_agent_details():
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
versions=["1.0", "2.0"],
|
||||
agentGraphVersions=["1", "2"],
|
||||
agentGraphId="test-graph-id",
|
||||
last_updated=datetime.datetime.now(),
|
||||
)
|
||||
assert details.slug == "test-agent"
|
||||
|
||||
@@ -154,7 +154,11 @@ async def get_agents(
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.StoreAgentDetails,
|
||||
)
|
||||
async def get_agent(username: str, agent_name: str):
|
||||
async def get_agent(
|
||||
username: str,
|
||||
agent_name: str,
|
||||
include_changelog: bool = fastapi.Query(default=False),
|
||||
):
|
||||
"""
|
||||
This is only used on the AgentDetails Page.
|
||||
|
||||
@@ -164,7 +168,7 @@ async def get_agent(username: str, agent_name: str):
|
||||
# URL decode the agent name since it comes from the URL path
|
||||
agent_name = urllib.parse.unquote(agent_name).lower()
|
||||
agent = await store_cache._get_cached_agent_details(
|
||||
username=username, agent_name=agent_name
|
||||
username=username, agent_name=agent_name, include_changelog=include_changelog
|
||||
)
|
||||
return agent
|
||||
|
||||
|
||||
@@ -388,6 +388,8 @@ def test_get_agent_details(
|
||||
runs=100,
|
||||
rating=4.5,
|
||||
versions=["1.0.0", "1.1.0"],
|
||||
agentGraphVersions=["1", "2"],
|
||||
agentGraphId="test-graph-id",
|
||||
last_updated=FIXED_NOW,
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agent_details")
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- Fix StoreSubmission view to use agentGraphVersion instead of version for agent_version field
|
||||
-- This ensures that submission.agent_version returns the actual agent graph version, not the store listing version number
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Recreate the view with the corrected agent_version field (using agentGraphVersion instead of version)
|
||||
CREATE OR REPLACE VIEW "StoreSubmission" AS
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
sl."owningUserId" AS user_id,
|
||||
slv."agentGraphId" AS agent_id,
|
||||
slv."agentGraphVersion" AS agent_version,
|
||||
sl.slug,
|
||||
COALESCE(slv.name, '') AS name,
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description,
|
||||
slv.instructions,
|
||||
slv."imageUrls" AS image_urls,
|
||||
slv."submittedAt" AS date_submitted,
|
||||
slv."submissionStatus" AS status,
|
||||
COALESCE(ar.run_count, 0::bigint) AS runs,
|
||||
COALESCE(avg(sr.score::numeric), 0.0)::double precision AS rating,
|
||||
slv.id AS store_listing_version_id,
|
||||
slv."reviewerId" AS reviewer_id,
|
||||
slv."reviewComments" AS review_comments,
|
||||
slv."internalComments" AS internal_comments,
|
||||
slv."reviewedAt" AS reviewed_at,
|
||||
slv."changesSummary" AS changes_summary,
|
||||
slv."videoUrl" AS video_url,
|
||||
slv.categories
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
|
||||
LEFT JOIN (
|
||||
SELECT "AgentGraphExecution"."agentGraphId", count(*) AS run_count
|
||||
FROM "AgentGraphExecution"
|
||||
GROUP BY "AgentGraphExecution"."agentGraphId"
|
||||
) ar ON ar."agentGraphId" = slv."agentGraphId"
|
||||
WHERE sl."isDeleted" = false
|
||||
GROUP BY sl.id, sl."owningUserId", slv.id, slv."agentGraphId", slv."agentGraphVersion", sl.slug, slv.name,
|
||||
slv."subHeading", slv.description, slv.instructions, slv."imageUrls", slv."submittedAt",
|
||||
slv."submissionStatus", slv."reviewerId", slv."reviewComments", slv."internalComments",
|
||||
slv."reviewedAt", slv."changesSummary", slv."videoUrl", slv.categories, ar.run_count;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,81 @@
|
||||
-- Add agentGraphVersions field to StoreAgent view for consistent version comparison
|
||||
-- This keeps the existing versions field unchanged and adds a new field with graph versions
|
||||
-- This makes it safe for version comparison with LibraryAgent.graph_version
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Drop and recreate the StoreAgent view with new agentGraphVersions 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"
|
||||
),
|
||||
agent_graph_versions AS (
|
||||
SELECT
|
||||
"storeListingId",
|
||||
array_agg(DISTINCT "agentGraphVersion"::text ORDER BY "agentGraphVersion"::text) AS graph_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,
|
||||
COALESCE(agv.graph_versions, ARRAY[slv."agentGraphVersion"::text]) AS "agentGraphVersions",
|
||||
slv."agentGraphId",
|
||||
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"
|
||||
LEFT JOIN agent_graph_versions agv
|
||||
ON sl.id = agv."storeListingId"
|
||||
WHERE sl."isDeleted" = false
|
||||
AND sl."hasApprovedVersion" = true;
|
||||
|
||||
COMMIT;
|
||||
@@ -728,11 +728,13 @@ view StoreAgent {
|
||||
description String
|
||||
categories String[]
|
||||
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
|
||||
runs Int
|
||||
rating Float
|
||||
versions String[]
|
||||
is_available Boolean @default(true)
|
||||
useForOnboarding Boolean @default(false)
|
||||
runs Int
|
||||
rating Float
|
||||
versions String[]
|
||||
agentGraphVersions String[]
|
||||
agentGraphId String
|
||||
is_available Boolean @default(true)
|
||||
useForOnboarding Boolean @default(false)
|
||||
|
||||
// Materialized views used (refreshed every 15 minutes via pg_cron):
|
||||
// - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId
|
||||
|
||||
@@ -2,6 +2,7 @@ import { parseAsString, useQueryStates } from "nuqs";
|
||||
import { AgentOutputs } from "./components/AgentOutputs/AgentOutputs";
|
||||
import { RunGraph } from "./components/RunGraph/RunGraph";
|
||||
import { ScheduleGraph } from "./components/ScheduleGraph/ScheduleGraph";
|
||||
import { PublishToMarketplace } from "./components/PublishToMarketplace/PublishToMarketplace";
|
||||
import { memo } from "react";
|
||||
|
||||
export const BuilderActions = memo(() => {
|
||||
@@ -13,6 +14,7 @@ export const BuilderActions = memo(() => {
|
||||
<AgentOutputs flowID={flowID} />
|
||||
<RunGraph flowID={flowID} />
|
||||
<ScheduleGraph flowID={flowID} />
|
||||
<PublishToMarketplace flowID={flowID} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ShareIcon } from "@phosphor-icons/react";
|
||||
import { BuilderActionButton } from "../BuilderActionButton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { usePublishToMarketplace } from "./usePublishToMarketplace";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
|
||||
export const PublishToMarketplace = ({ flowID }: { flowID: string | null }) => {
|
||||
const { handlePublishToMarketplace, publishState, handleStateChange } =
|
||||
usePublishToMarketplace({ flowID });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<BuilderActionButton
|
||||
onClick={handlePublishToMarketplace}
|
||||
disabled={!flowID}
|
||||
>
|
||||
<ShareIcon className="size-6 drop-shadow-sm" />
|
||||
</BuilderActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Publish to Marketplace</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<PublishAgentModal
|
||||
targetState={publishState}
|
||||
onStateChange={handleStateChange}
|
||||
preSelectedAgentId={flowID || undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
|
||||
export type PublishStep = "select" | "info" | "review";
|
||||
|
||||
export type PublishState = {
|
||||
isOpen: boolean;
|
||||
step: PublishStep;
|
||||
submissionData: StoreSubmission | null;
|
||||
};
|
||||
|
||||
const defaultPublishState: PublishState = {
|
||||
isOpen: false,
|
||||
step: "select",
|
||||
submissionData: null,
|
||||
};
|
||||
|
||||
interface UsePublishToMarketplaceProps {
|
||||
flowID: string | null;
|
||||
}
|
||||
|
||||
export function usePublishToMarketplace({
|
||||
flowID,
|
||||
}: UsePublishToMarketplaceProps) {
|
||||
const [publishState, setPublishState] =
|
||||
useState<PublishState>(defaultPublishState);
|
||||
|
||||
const handlePublishToMarketplace = () => {
|
||||
if (!flowID) return;
|
||||
|
||||
// Open the publish modal starting with the select step
|
||||
setPublishState({
|
||||
isOpen: true,
|
||||
step: "select",
|
||||
submissionData: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = useCallback((newState: PublishState) => {
|
||||
setPublishState(newState);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handlePublishToMarketplace,
|
||||
publishState,
|
||||
handleStateChange,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
interface MarketplaceBannersProps {
|
||||
hasUpdate?: boolean;
|
||||
latestVersion?: number;
|
||||
hasUnpublishedChanges?: boolean;
|
||||
currentVersion?: number;
|
||||
isUpdating?: boolean;
|
||||
onUpdate?: () => void;
|
||||
onPublish?: () => void;
|
||||
onViewChanges?: () => void;
|
||||
}
|
||||
|
||||
export function MarketplaceBanners({
|
||||
hasUpdate,
|
||||
latestVersion,
|
||||
hasUnpublishedChanges,
|
||||
isUpdating,
|
||||
onUpdate,
|
||||
onPublish,
|
||||
}: MarketplaceBannersProps) {
|
||||
const renderUpdateBanner = () => {
|
||||
if (hasUpdate && latestVersion) {
|
||||
return (
|
||||
<div className="mb-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-900">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<Text
|
||||
variant="large-medium"
|
||||
className="mb-2 text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Update available
|
||||
</Text>
|
||||
<Text variant="body" className="text-gray-700 dark:text-gray-300">
|
||||
You should update your agent in order to get the latest / best
|
||||
results
|
||||
</Text>
|
||||
</div>
|
||||
{onUpdate && (
|
||||
<div className="flex justify-start">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={onUpdate}
|
||||
disabled={isUpdating}
|
||||
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{isUpdating ? "Updating..." : "Update agent"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderUnpublishedChangesBanner = () => {
|
||||
if (hasUnpublishedChanges) {
|
||||
return (
|
||||
<div className="mb-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-900">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<Text
|
||||
variant="large-medium"
|
||||
className="mb-2 text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Unpublished changes
|
||||
</Text>
|
||||
<Text variant="body" className="text-gray-700 dark:text-gray-300">
|
||||
You've made changes to this agent that aren't
|
||||
published yet. Would you like to publish the latest version?
|
||||
</Text>
|
||||
</div>
|
||||
{onPublish && (
|
||||
<div className="flex justify-start">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={onPublish}
|
||||
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Publish changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderUpdateBanner()}
|
||||
{renderUnpublishedChangesBanner()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
import * as React from "react";
|
||||
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
|
||||
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
|
||||
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
|
||||
import { MarketplaceBanners } from "../../../../../components/MarketplaceBanners/MarketplaceBanners";
|
||||
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
|
||||
import { EmptySchedules } from "./components/other/EmptySchedules";
|
||||
import { EmptyTasks } from "./components/other/EmptyTasks";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
||||
import { EmptyTriggers } from "./components/other/EmptyTriggers";
|
||||
import { SectionWrap } from "./components/other/SectionWrap";
|
||||
@@ -24,7 +28,6 @@ import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
||||
|
||||
export function NewAgentLibraryView() {
|
||||
const {
|
||||
agentId,
|
||||
agent,
|
||||
ready,
|
||||
activeTemplate,
|
||||
@@ -43,6 +46,189 @@ export function NewAgentLibraryView() {
|
||||
onScheduleCreated,
|
||||
} = useNewAgentLibraryView();
|
||||
|
||||
const {
|
||||
hasAgentMarketplaceUpdate,
|
||||
hasMarketplaceUpdate,
|
||||
latestMarketplaceVersion,
|
||||
isUpdating,
|
||||
modalOpen,
|
||||
setModalOpen,
|
||||
handlePublishUpdate,
|
||||
handleUpdateToLatest,
|
||||
} = useMarketplaceUpdate({ agent });
|
||||
|
||||
const [changelogOpen, setChangelogOpen] = React.useState(false);
|
||||
|
||||
function renderMarketplaceUpdateBanner() {
|
||||
return (
|
||||
<MarketplaceBanners
|
||||
hasUpdate={!!hasMarketplaceUpdate}
|
||||
latestVersion={latestMarketplaceVersion}
|
||||
hasUnpublishedChanges={!!hasAgentMarketplaceUpdate}
|
||||
currentVersion={agent?.graph_version}
|
||||
isUpdating={isUpdating}
|
||||
onUpdate={handleUpdateToLatest}
|
||||
onPublish={handlePublishUpdate}
|
||||
onViewChanges={() => setChangelogOpen(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPublishAgentModal() {
|
||||
if (!modalOpen || !agent) return null;
|
||||
|
||||
return (
|
||||
<PublishAgentModal
|
||||
targetState={{
|
||||
isOpen: true,
|
||||
step: "info",
|
||||
submissionData: { isMarketplaceUpdate: true } as any,
|
||||
}}
|
||||
preSelectedAgentId={agent.graph_id}
|
||||
preSelectedAgentVersion={agent.graph_version}
|
||||
onStateChange={(state) => {
|
||||
if (!state.isOpen) {
|
||||
setModalOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAgentLibraryView() {
|
||||
if (!agent) return null;
|
||||
|
||||
return (
|
||||
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
|
||||
<SectionWrap className="mb-3 block">
|
||||
<div
|
||||
className={cn(
|
||||
"border-b border-zinc-100 pb-5",
|
||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
||||
)}
|
||||
>
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="w-full"
|
||||
disabled={isTemplateLoading && activeTab === "templates"}
|
||||
>
|
||||
<PlusIcon size={20} /> New task
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
onRunCreated={onRunInitiated}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
initialInputValues={activeTemplate?.inputs}
|
||||
initialInputCredentials={activeTemplate?.credentials}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarRunsList
|
||||
agent={agent}
|
||||
selectedRunId={activeItem ?? undefined}
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onTabChange={setActiveTab}
|
||||
onCountsChange={handleCountsChange}
|
||||
/>
|
||||
</SectionWrap>
|
||||
|
||||
{activeItem ? (
|
||||
activeTab === "scheduled" ? (
|
||||
<SelectedScheduleView
|
||||
agent={agent}
|
||||
scheduleId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
/>
|
||||
) : activeTab === "templates" ? (
|
||||
<SelectedTemplateView
|
||||
agent={agent}
|
||||
templateId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onRunCreated={(execution) =>
|
||||
handleSelectRun(execution.id, "runs")
|
||||
}
|
||||
onSwitchToRunsTab={() => setActiveTab("runs")}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
/>
|
||||
) : activeTab === "triggers" ? (
|
||||
<SelectedTriggerView
|
||||
agent={agent}
|
||||
triggerId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onSwitchToRunsTab={() => setActiveTab("runs")}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
/>
|
||||
) : (
|
||||
<SelectedRunView
|
||||
agent={agent}
|
||||
runId={activeItem}
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
/>
|
||||
)
|
||||
) : sidebarLoading ? (
|
||||
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
|
||||
) : activeTab === "scheduled" ? (
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
>
|
||||
<EmptySchedules />
|
||||
</SelectedViewLayout>
|
||||
) : activeTab === "templates" ? (
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
>
|
||||
<EmptyTemplates />
|
||||
</SelectedViewLayout>
|
||||
) : activeTab === "triggers" ? (
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
>
|
||||
<EmptyTriggers />
|
||||
</SelectedViewLayout>
|
||||
) : (
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
>
|
||||
<EmptyTasks
|
||||
agent={agent}
|
||||
onRun={onRunInitiated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
/>
|
||||
</SelectedViewLayout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderVersionChangelog() {
|
||||
if (!agent) return null;
|
||||
|
||||
return (
|
||||
<AgentVersionChangelog
|
||||
agent={agent}
|
||||
isOpen={changelogOpen}
|
||||
onClose={() => setChangelogOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCard
|
||||
@@ -60,120 +246,30 @@ export function NewAgentLibraryView() {
|
||||
|
||||
if (!sidebarLoading && !hasAnyItems) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="mx-6 pt-4">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: agent.name, link: `/library/agents/${agentId}` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<>
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
>
|
||||
<EmptyTasks
|
||||
agent={agent}
|
||||
onRun={onRunInitiated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SelectedViewLayout>
|
||||
{renderPublishAgentModal()}
|
||||
{renderVersionChangelog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
|
||||
<SectionWrap className="mb-3 block">
|
||||
<div
|
||||
className={cn(
|
||||
"border-b border-zinc-100 pb-5",
|
||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
||||
)}
|
||||
>
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="w-full"
|
||||
disabled={isTemplateLoading && activeTab === "templates"}
|
||||
>
|
||||
<PlusIcon size={20} /> New task
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
onRunCreated={onRunInitiated}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
initialInputValues={activeTemplate?.inputs}
|
||||
initialInputCredentials={activeTemplate?.credentials}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarRunsList
|
||||
agent={agent}
|
||||
selectedRunId={activeItem ?? undefined}
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onTabChange={setActiveTab}
|
||||
onCountsChange={handleCountsChange}
|
||||
/>
|
||||
</SectionWrap>
|
||||
|
||||
{activeItem ? (
|
||||
activeTab === "scheduled" ? (
|
||||
<SelectedScheduleView
|
||||
agent={agent}
|
||||
scheduleId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
) : activeTab === "templates" ? (
|
||||
<SelectedTemplateView
|
||||
agent={agent}
|
||||
templateId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
|
||||
onSwitchToRunsTab={() => setActiveTab("runs")}
|
||||
/>
|
||||
) : activeTab === "triggers" ? (
|
||||
<SelectedTriggerView
|
||||
agent={agent}
|
||||
triggerId={activeItem}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
onSwitchToRunsTab={() => setActiveTab("runs")}
|
||||
/>
|
||||
) : (
|
||||
<SelectedRunView
|
||||
agent={agent}
|
||||
runId={activeItem}
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
)
|
||||
) : sidebarLoading ? (
|
||||
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
|
||||
) : activeTab === "scheduled" ? (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<EmptySchedules />
|
||||
</SelectedViewLayout>
|
||||
) : activeTab === "templates" ? (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<EmptyTemplates />
|
||||
</SelectedViewLayout>
|
||||
) : activeTab === "triggers" ? (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<EmptyTriggers />
|
||||
</SelectedViewLayout>
|
||||
) : (
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<EmptyTasks
|
||||
agent={agent}
|
||||
onRun={onRunInitiated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
/>
|
||||
</SelectedViewLayout>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{renderAgentLibraryView()}
|
||||
{renderPublishAgentModal()}
|
||||
{renderVersionChangelog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import React from "react";
|
||||
|
||||
interface AgentVersionChangelogProps {
|
||||
agent: LibraryAgent;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface VersionInfo {
|
||||
version: number;
|
||||
isCurrentVersion: boolean;
|
||||
}
|
||||
|
||||
export function AgentVersionChangelog({
|
||||
agent,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: AgentVersionChangelogProps) {
|
||||
// Get marketplace data if agent has marketplace listing
|
||||
const { data: storeAgentData, isLoading } = useGetV2GetSpecificAgent(
|
||||
agent?.marketplace_listing?.creator.slug || "",
|
||||
agent?.marketplace_listing?.slug || "",
|
||||
{},
|
||||
{
|
||||
query: {
|
||||
enabled: !!(
|
||||
agent?.marketplace_listing?.creator.slug &&
|
||||
agent?.marketplace_listing?.slug
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Create version info from available graph versions
|
||||
const storeData = okData<StoreAgentDetails>(storeAgentData);
|
||||
const agentVersions: VersionInfo[] = storeData?.agentGraphVersions
|
||||
? storeData.agentGraphVersions
|
||||
.map((versionStr: string) => parseInt(versionStr, 10))
|
||||
.sort((a: number, b: number) => b - a) // Sort descending (newest first)
|
||||
.map((version: number) => ({
|
||||
version,
|
||||
isCurrentVersion: version === agent.graph_version,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const renderVersionItem = (versionInfo: VersionInfo) => {
|
||||
return (
|
||||
<div
|
||||
key={versionInfo.version}
|
||||
className={`rounded-lg border p-4 ${
|
||||
versionInfo.isCurrentVersion
|
||||
? "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950"
|
||||
: "border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Text variant="body" className="font-semibold">
|
||||
v{versionInfo.version}
|
||||
</Text>
|
||||
{versionInfo.isCurrentVersion && (
|
||||
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-100">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text
|
||||
variant="small"
|
||||
className="mt-1 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
Available marketplace version
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={`Version History - ${agent.name}`}
|
||||
styling={{
|
||||
maxWidth: "45rem",
|
||||
}}
|
||||
controlled={{
|
||||
isOpen: isOpen,
|
||||
set: (isOpen) => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="max-h-[70vh] overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</div>
|
||||
) : agentVersions.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<Text
|
||||
variant="small"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
View changes and updates across different versions of this
|
||||
agent.
|
||||
</Text>
|
||||
{agentVersions.map(renderVersionItem)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
No version history available for this agent.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ interface Props {
|
||||
runId: string;
|
||||
onSelectRun?: (id: string) => void;
|
||||
onClearSelectedRun?: () => void;
|
||||
banner?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SelectedRunView({
|
||||
@@ -39,6 +40,7 @@ export function SelectedRunView({
|
||||
runId,
|
||||
onSelectRun,
|
||||
onClearSelectedRun,
|
||||
banner,
|
||||
}: Props) {
|
||||
const { run, preset, isLoading, responseError, httpError } =
|
||||
useSelectedRunView(agent.graph_id, runId);
|
||||
@@ -78,7 +80,11 @@ export function SelectedRunView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={banner}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={run} />
|
||||
|
||||
|
||||
@@ -20,12 +20,14 @@ interface Props {
|
||||
agent: LibraryAgent;
|
||||
scheduleId: string;
|
||||
onClearSelectedRun?: () => void;
|
||||
banner?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SelectedScheduleView({
|
||||
agent,
|
||||
scheduleId,
|
||||
onClearSelectedRun,
|
||||
banner,
|
||||
}: Props) {
|
||||
const { schedule, isLoading, error } = useSelectedScheduleView(
|
||||
agent.graph_id,
|
||||
@@ -74,7 +76,11 @@ export function SelectedScheduleView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={banner}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-0">
|
||||
<RunDetailHeader
|
||||
|
||||
@@ -24,6 +24,7 @@ interface Props {
|
||||
onClearSelectedRun?: () => void;
|
||||
onRunCreated?: (execution: GraphExecutionMeta) => void;
|
||||
onSwitchToRunsTab?: () => void;
|
||||
banner?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SelectedTemplateView({
|
||||
@@ -32,6 +33,7 @@ export function SelectedTemplateView({
|
||||
onClearSelectedRun,
|
||||
onRunCreated,
|
||||
onSwitchToRunsTab,
|
||||
banner,
|
||||
}: Props) {
|
||||
const {
|
||||
template,
|
||||
@@ -100,7 +102,11 @@ export function SelectedTemplateView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={banner}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={undefined} />
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ interface Props {
|
||||
triggerId: string;
|
||||
onClearSelectedRun?: () => void;
|
||||
onSwitchToRunsTab?: () => void;
|
||||
banner?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SelectedTriggerView({
|
||||
@@ -29,6 +30,7 @@ export function SelectedTriggerView({
|
||||
triggerId,
|
||||
onClearSelectedRun,
|
||||
onSwitchToRunsTab,
|
||||
banner,
|
||||
}: Props) {
|
||||
const {
|
||||
trigger,
|
||||
@@ -93,7 +95,11 @@ export function SelectedTriggerView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
|
||||
<SelectedViewLayout
|
||||
agentName={agent.name}
|
||||
agentId={agent.id}
|
||||
banner={banner}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={undefined} />
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interface Props {
|
||||
agentName: string;
|
||||
agentId: string;
|
||||
children: React.ReactNode;
|
||||
banner?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SelectedViewLayout(props: Props) {
|
||||
@@ -14,10 +15,15 @@ export function SelectedViewLayout(props: Props) {
|
||||
<div
|
||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
|
||||
>
|
||||
{props.banner && <div className="mb-4">{props.banner}</div>}
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: props.agentName, link: `/library/agents/${props.agentId}` },
|
||||
{
|
||||
name: props.agentName,
|
||||
link: `/library/agents/${props.agentId}`,
|
||||
testId: "agent-title"
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
useGetV2GetSpecificAgent,
|
||||
useGetV2ListMySubmissions,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import {
|
||||
usePatchV2UpdateLibraryAgent,
|
||||
getGetV2GetLibraryAgentQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface UseMarketplaceUpdateProps {
|
||||
agent: LibraryAgent | null | undefined;
|
||||
}
|
||||
|
||||
export function useMarketplaceUpdate({ agent }: UseMarketplaceUpdateProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const user = useSupabaseStore((state) => state.user);
|
||||
|
||||
// Get marketplace data if agent has marketplace listing
|
||||
const { data: storeAgentData } = useGetV2GetSpecificAgent(
|
||||
agent?.marketplace_listing?.creator.slug || "",
|
||||
agent?.marketplace_listing?.slug || "",
|
||||
{},
|
||||
{
|
||||
query: {
|
||||
enabled: !!(
|
||||
agent?.marketplace_listing?.creator.slug &&
|
||||
agent?.marketplace_listing?.slug
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Get user's submissions to check for pending submissions
|
||||
const { data: submissionsData } = useGetV2ListMySubmissions(
|
||||
{ page: 1, page_size: 50 }, // Get enough to cover recent submissions
|
||||
{
|
||||
query: {
|
||||
enabled: !!user?.id, // Only fetch if user is authenticated
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const updateToLatestMutation = usePatchV2UpdateLibraryAgent({
|
||||
mutation: {
|
||||
onError: (err) => {
|
||||
toast({
|
||||
title: "Update Failed",
|
||||
description: "Failed to update agent to latest version",
|
||||
variant: "destructive",
|
||||
});
|
||||
console.error("Failed to update agent:", err);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Agent Updated",
|
||||
description: "Agent updated to latest version successfully",
|
||||
});
|
||||
// Invalidate to get the updated agent data from the server
|
||||
if (agent?.id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2GetLibraryAgentQueryKey(agent.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check if marketplace has a newer version than user's current version
|
||||
const marketplaceUpdateInfo = React.useMemo(() => {
|
||||
const storeAgent = okData(storeAgentData) as any;
|
||||
if (!agent || !storeAgent) {
|
||||
return {
|
||||
hasUpdate: false,
|
||||
latestVersion: undefined,
|
||||
isUserCreator: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the latest version from the marketplace
|
||||
// agentGraphVersions array contains graph version numbers as strings, get the highest one
|
||||
const latestMarketplaceVersion =
|
||||
storeAgent.agentGraphVersions?.length > 0
|
||||
? Math.max(
|
||||
...storeAgent.agentGraphVersions.map((v: string) =>
|
||||
parseInt(v, 10),
|
||||
),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// Determine if the user is the creator of this agent
|
||||
// Compare current user ID with the marketplace listing creator ID
|
||||
const isUserCreator =
|
||||
user?.id && agent.marketplace_listing?.creator.id === user.id;
|
||||
|
||||
// Check if there's a pending submission for this specific agent version
|
||||
const submissionsResponse = okData(submissionsData) as any;
|
||||
const hasPendingSubmissionForCurrentVersion =
|
||||
isUserCreator &&
|
||||
submissionsResponse?.submissions?.some(
|
||||
(submission: StoreSubmission) =>
|
||||
submission.agent_id === agent.graph_id &&
|
||||
submission.agent_version === agent.graph_version &&
|
||||
submission.status === "PENDING",
|
||||
);
|
||||
|
||||
// If user is creator and their version is newer than marketplace, show publish update banner
|
||||
// BUT only if there's no pending submission for this version
|
||||
const hasPublishUpdate =
|
||||
isUserCreator &&
|
||||
!hasPendingSubmissionForCurrentVersion &&
|
||||
latestMarketplaceVersion !== undefined &&
|
||||
agent.graph_version > latestMarketplaceVersion;
|
||||
|
||||
// If marketplace version is newer than user's version, show update banner
|
||||
// This applies to both creators and non-creators
|
||||
const hasMarketplaceUpdate =
|
||||
latestMarketplaceVersion !== undefined &&
|
||||
latestMarketplaceVersion > agent.graph_version;
|
||||
|
||||
return {
|
||||
hasUpdate: hasMarketplaceUpdate,
|
||||
latestVersion: latestMarketplaceVersion,
|
||||
isUserCreator,
|
||||
hasPublishUpdate,
|
||||
};
|
||||
}, [agent, storeAgentData, user, submissionsData]);
|
||||
|
||||
const handlePublishUpdate = () => {
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdateToLatest = () => {
|
||||
if (!agent || marketplaceUpdateInfo.latestVersion === undefined) return;
|
||||
// Update to the specific marketplace version using the new graph_version parameter
|
||||
updateToLatestMutation.mutate({
|
||||
libraryAgentId: agent.id,
|
||||
data: {
|
||||
graph_version: marketplaceUpdateInfo.latestVersion,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
hasAgentMarketplaceUpdate: marketplaceUpdateInfo.hasPublishUpdate,
|
||||
hasMarketplaceUpdate: marketplaceUpdateInfo.hasUpdate,
|
||||
latestMarketplaceVersion: marketplaceUpdateInfo.latestVersion,
|
||||
isUpdating: updateToLatestMutation.isPending,
|
||||
modalOpen,
|
||||
setModalOpen,
|
||||
handlePublishUpdate,
|
||||
handleUpdateToLatest,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,20 @@ import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import Link from "next/link";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import type { ChangelogEntry } from "@/app/api/__generated__/models/changelogEntry";
|
||||
import { useAgentInfo } from "./useAgentInfo";
|
||||
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
|
||||
import * as React from "react";
|
||||
import { MarketplaceBanners } from "../../../components/MarketplaceBanners/MarketplaceBanners";
|
||||
import {
|
||||
getLatestMarketplaceVersion,
|
||||
isUserCreator as checkIsUserCreator,
|
||||
calculateUpdateStatus,
|
||||
} from "@/components/contextual/marketplaceHelpers";
|
||||
|
||||
interface AgentInfoProps {
|
||||
user: User | null;
|
||||
@@ -21,6 +34,8 @@ interface AgentInfoProps {
|
||||
version: string;
|
||||
storeListingVersionId: string;
|
||||
isAgentAddedToLibrary: boolean;
|
||||
creatorSlug?: string;
|
||||
agentSlug?: string;
|
||||
}
|
||||
|
||||
export const AgentInfo = ({
|
||||
@@ -37,6 +52,8 @@ export const AgentInfo = ({
|
||||
version,
|
||||
storeListingVersionId,
|
||||
isAgentAddedToLibrary,
|
||||
creatorSlug,
|
||||
agentSlug,
|
||||
}: AgentInfoProps) => {
|
||||
const {
|
||||
handleDownload,
|
||||
@@ -45,6 +62,137 @@ export const AgentInfo = ({
|
||||
isAddingAgentToLibrary,
|
||||
} = useAgentInfo({ storeListingVersionId });
|
||||
|
||||
// Get current user for update detection
|
||||
const currentUser = useSupabaseStore((state) => state.user);
|
||||
|
||||
// State for expanding version list - start with 3, then show 3 more each time
|
||||
const [visibleVersionCount, setVisibleVersionCount] = React.useState(3);
|
||||
|
||||
// Get store agent data for version history
|
||||
const { data: storeAgentData } = useGetV2GetSpecificAgent(
|
||||
creatorSlug || "",
|
||||
agentSlug || "",
|
||||
{ include_changelog: true },
|
||||
{
|
||||
query: {
|
||||
enabled: !!(creatorSlug && agentSlug),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Calculate update information using simple helper functions
|
||||
const storeData = okData<StoreAgentDetails>(storeAgentData);
|
||||
const latestMarketplaceVersion = getLatestMarketplaceVersion(
|
||||
storeData?.agentGraphVersions,
|
||||
);
|
||||
const currentVersion = parseInt(version, 10);
|
||||
const isCreator = checkIsUserCreator(creator, currentUser);
|
||||
const updateStatus = calculateUpdateStatus({
|
||||
latestMarketplaceVersion,
|
||||
currentVersion,
|
||||
isUserCreator: isCreator,
|
||||
isAgentAddedToLibrary,
|
||||
});
|
||||
|
||||
const updateInfo = {
|
||||
...updateStatus,
|
||||
latestVersion: latestMarketplaceVersion,
|
||||
isUserCreator: isCreator,
|
||||
};
|
||||
|
||||
// Process version data for display - use store listing versions (not agentGraphVersions)
|
||||
const allVersions = storeData?.versions
|
||||
? storeData.versions
|
||||
.map((versionStr: string) => parseInt(versionStr, 10))
|
||||
.sort((a: number, b: number) => b - a)
|
||||
.map((versionNum: number) => ({
|
||||
version: versionNum,
|
||||
isCurrentVersion: false, // We'll update this logic if needed
|
||||
}))
|
||||
: [];
|
||||
|
||||
const agentVersions = allVersions.slice(0, visibleVersionCount);
|
||||
const hasMoreVersions = allVersions.length > visibleVersionCount;
|
||||
|
||||
const formatDate = (version: number) => {
|
||||
// Generate sample dates based on version
|
||||
const baseDate = new Date("2025-12-18");
|
||||
baseDate.setDate(baseDate.getDate() - (19 - version) * 7);
|
||||
return baseDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const renderVersionItem = (versionInfo: {
|
||||
version: number;
|
||||
isCurrentVersion: boolean;
|
||||
}) => {
|
||||
// Find real changelog data for this version
|
||||
const storeData = okData<StoreAgentDetails>(storeAgentData);
|
||||
const changelogEntry = storeData?.changelog?.find(
|
||||
(entry: ChangelogEntry) =>
|
||||
entry.version === versionInfo.version.toString(),
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={versionInfo.version} className="mb-6 last:mb-0">
|
||||
{/* Version Header */}
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="body"
|
||||
className="font-semibold text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Version {versionInfo.version}.0
|
||||
</Text>
|
||||
{versionInfo.isCurrentVersion && (
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-100">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Text
|
||||
variant="small"
|
||||
className="text-neutral-500 dark:text-neutral-400"
|
||||
>
|
||||
{changelogEntry
|
||||
? new Date(changelogEntry.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: formatDate(versionInfo.version)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Real Changelog Content */}
|
||||
{changelogEntry && (
|
||||
<div className="space-y-2">
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
{changelogEntry.changes_summary}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMarketplaceBanners = () => {
|
||||
return (
|
||||
<MarketplaceBanners
|
||||
hasUpdate={updateInfo.hasUpdate}
|
||||
latestVersion={updateInfo.latestVersion}
|
||||
hasUnpublishedChanges={updateInfo.hasUnpublishedChanges}
|
||||
currentVersion={parseInt(version, 10)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
@@ -158,17 +306,51 @@ export const AgentInfo = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
|
||||
{/* Update/Unpublished Changes Banners */}
|
||||
{renderMarketplaceBanners()}
|
||||
|
||||
{/* Changelog */}
|
||||
<div className="flex w-full flex-col gap-1.5 sm:gap-2">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Version history
|
||||
Changelog
|
||||
</div>
|
||||
<div className="decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
|
||||
Last updated {lastUpdated}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}
|
||||
</div>
|
||||
|
||||
{/* Version List */}
|
||||
{agentVersions.length > 0 ? (
|
||||
<div className="mt-4">
|
||||
{agentVersions.map(renderVersionItem)}
|
||||
{hasMoreVersions && (
|
||||
<button
|
||||
onClick={() => setVisibleVersionCount((prev) => prev + 3)}
|
||||
className="mt-2 flex items-center gap-1 text-sm font-medium text-neutral-900 hover:text-neutral-700 dark:text-neutral-100 dark:hover:text-neutral-300"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M4 6l4 4 4-4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Read more</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page";
|
||||
import { AgentImages } from "../AgentImages/AgentImage";
|
||||
import { AgentInfo } from "../AgentInfo/AgentInfo";
|
||||
@@ -46,7 +48,8 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
const agentData = okData<StoreAgentDetails>(agent);
|
||||
if (!agentData) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
@@ -67,10 +70,10 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
const breadcrumbs = [
|
||||
{ name: "Marketplace", link: "/marketplace" },
|
||||
{
|
||||
name: agent.creator,
|
||||
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
|
||||
name: agentData.creator ?? "",
|
||||
link: `/marketplace/creator/${encodeURIComponent(agentData.creator ?? "")}`,
|
||||
},
|
||||
{ name: agent.agent_name, link: "#" },
|
||||
{ name: agentData.agent_name ?? "", link: "#" },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -82,18 +85,29 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
user={user}
|
||||
agentId={agent.active_version_id ?? "–"}
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.sub_heading}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.last_updated.toISOString()}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
agentId={agentData.active_version_id ?? "–"}
|
||||
name={agentData.agent_name ?? ""}
|
||||
creator={agentData.creator ?? ""}
|
||||
shortDescription={agentData.sub_heading ?? ""}
|
||||
longDescription={agentData.description ?? ""}
|
||||
rating={agentData.rating ?? 0}
|
||||
runs={agentData.runs ?? 0}
|
||||
categories={agentData.categories ?? []}
|
||||
lastUpdated={
|
||||
agentData.last_updated?.toISOString() ??
|
||||
new Date().toISOString()
|
||||
}
|
||||
version={
|
||||
agentData.versions
|
||||
? Math.max(
|
||||
...agentData.versions.map((v: string) => parseInt(v, 10)),
|
||||
).toString()
|
||||
: "1"
|
||||
}
|
||||
storeListingVersionId={agentData.store_listing_version_id ?? ""}
|
||||
isAgentAddedToLibrary={Boolean(libraryAgent)}
|
||||
creatorSlug={params.creator}
|
||||
agentSlug={params.slug}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
@@ -101,23 +115,23 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
const orderedImages: string[] = [];
|
||||
|
||||
// 1. YouTube/Overview video (if it exists)
|
||||
if (agent.agent_video) {
|
||||
orderedImages.push(agent.agent_video);
|
||||
if (agentData.agent_video) {
|
||||
orderedImages.push(agentData.agent_video);
|
||||
}
|
||||
|
||||
// 2. First image (hero)
|
||||
if (agent.agent_image.length > 0) {
|
||||
orderedImages.push(agent.agent_image[0]);
|
||||
if (agentData.agent_image?.length > 0) {
|
||||
orderedImages.push(agentData.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);
|
||||
if (agentData.agent_output_demo) {
|
||||
orderedImages.push(agentData.agent_output_demo);
|
||||
}
|
||||
|
||||
// 4. Additional images
|
||||
if (agent.agent_image.length > 1) {
|
||||
orderedImages.push(...agent.agent_image.slice(1));
|
||||
if (agentData.agent_image && agentData.agent_image.length > 1) {
|
||||
orderedImages.push(...agentData.agent_image.slice(1));
|
||||
}
|
||||
|
||||
return orderedImages;
|
||||
@@ -129,7 +143,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
sectionTitle={`Other agents by ${agentData.creator ?? ""}`}
|
||||
/>
|
||||
)}
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useGetV2GetAgentByStoreId } from "@/app/api/__generated__/endpoints/lib
|
||||
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
|
||||
export const useMainAgentPage = ({
|
||||
@@ -20,13 +21,7 @@ export const useMainAgentPage = ({
|
||||
data: agent,
|
||||
isLoading: isAgentLoading,
|
||||
isError: isAgentError,
|
||||
} = useGetV2GetSpecificAgent(creator_lower, params.slug, {
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as StoreAgentDetails;
|
||||
},
|
||||
},
|
||||
});
|
||||
} = useGetV2GetSpecificAgent(creator_lower, params.slug);
|
||||
const {
|
||||
data: otherAgents,
|
||||
isLoading: isOtherAgentsLoading,
|
||||
@@ -59,14 +54,18 @@ export const useMainAgentPage = ({
|
||||
data: libraryAgent,
|
||||
isLoading: isLibraryAgentLoading,
|
||||
isError: isLibraryAgentError,
|
||||
} = useGetV2GetAgentByStoreId(agent?.active_version_id ?? "", {
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as LibraryAgent;
|
||||
} = useGetV2GetAgentByStoreId(
|
||||
okData<StoreAgentDetails>(agent)?.active_version_id ?? "",
|
||||
{
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as LibraryAgent;
|
||||
},
|
||||
enabled:
|
||||
!!user && !!okData<StoreAgentDetails>(agent)?.active_version_id,
|
||||
},
|
||||
enabled: !!user && !!agent?.active_version_id,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
isAgentLoading ||
|
||||
|
||||
@@ -2796,6 +2796,16 @@
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Agent Name" }
|
||||
},
|
||||
{
|
||||
"name": "include_changelog",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Include Changelog"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5983,6 +5993,16 @@
|
||||
"required": ["file"],
|
||||
"title": "Body_postV2Upload submission media"
|
||||
},
|
||||
"ChangelogEntry": {
|
||||
"properties": {
|
||||
"version": { "type": "string", "title": "Version" },
|
||||
"changes_summary": { "type": "string", "title": "Changes Summary" },
|
||||
"date": { "type": "string", "format": "date-time", "title": "Date" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["version", "changes_summary", "date"],
|
||||
"title": "ChangelogEntry"
|
||||
},
|
||||
"ChatRequest": {
|
||||
"properties": {
|
||||
"query": { "type": "string", "title": "Query" },
|
||||
@@ -7426,6 +7446,11 @@
|
||||
"title": "Auto Update Version",
|
||||
"description": "Auto-update the agent version"
|
||||
},
|
||||
"graph_version": {
|
||||
"anyOf": [{ "type": "integer" }, { "type": "null" }],
|
||||
"title": "Graph Version",
|
||||
"description": "Specific graph version to update to"
|
||||
},
|
||||
"is_favorite": {
|
||||
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
|
||||
"title": "Is Favorite",
|
||||
@@ -8902,6 +8927,12 @@
|
||||
"type": "array",
|
||||
"title": "Versions"
|
||||
},
|
||||
"agentGraphVersions": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
"title": "Agentgraphversions"
|
||||
},
|
||||
"agentGraphId": { "type": "string", "title": "Agentgraphid" },
|
||||
"last_updated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@@ -8919,6 +8950,16 @@
|
||||
"type": "boolean",
|
||||
"title": "Has Approved Version",
|
||||
"default": false
|
||||
},
|
||||
"changelog": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": { "$ref": "#/components/schemas/ChangelogEntry" },
|
||||
"type": "array"
|
||||
},
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Changelog"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -8937,6 +8978,8 @@
|
||||
"runs",
|
||||
"rating",
|
||||
"versions",
|
||||
"agentGraphVersions",
|
||||
"agentGraphId",
|
||||
"last_updated"
|
||||
],
|
||||
"title": "StoreAgentDetails"
|
||||
|
||||
@@ -18,6 +18,8 @@ export function PublishAgentModal({
|
||||
trigger,
|
||||
targetState,
|
||||
onStateChange,
|
||||
preSelectedAgentId,
|
||||
preSelectedAgentVersion,
|
||||
}: Props) {
|
||||
const {
|
||||
// State
|
||||
@@ -34,7 +36,12 @@ export function PublishAgentModal({
|
||||
handleGoToBuilder,
|
||||
handleSuccessFromInfo,
|
||||
handleBack,
|
||||
} = usePublishAgentModal({ targetState, onStateChange });
|
||||
} = usePublishAgentModal({
|
||||
targetState,
|
||||
onStateChange,
|
||||
preSelectedAgentId,
|
||||
preSelectedAgentVersion,
|
||||
});
|
||||
|
||||
const { user, isUserLoading } = useSupabase();
|
||||
|
||||
@@ -65,6 +72,7 @@ export function PublishAgentModal({
|
||||
selectedAgentId={selectedAgentId}
|
||||
selectedAgentVersion={selectedAgentVersion}
|
||||
initialData={initialData}
|
||||
isMarketplaceUpdate={!!currentState.submissionData}
|
||||
/>
|
||||
);
|
||||
case "review":
|
||||
|
||||
@@ -19,6 +19,7 @@ export function AgentInfoStep({
|
||||
selectedAgentId,
|
||||
selectedAgentVersion,
|
||||
initialData,
|
||||
isMarketplaceUpdate,
|
||||
}: Props) {
|
||||
const {
|
||||
form,
|
||||
@@ -34,6 +35,7 @@ export function AgentInfoStep({
|
||||
selectedAgentId,
|
||||
selectedAgentVersion,
|
||||
initialData,
|
||||
isMarketplaceUpdate,
|
||||
});
|
||||
|
||||
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] =
|
||||
@@ -65,6 +67,41 @@ export function AgentInfoStep({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit} className="flex-grow overflow-y-auto p-6">
|
||||
{/* Changes summary field - only shown for updates */}
|
||||
{isMarketplaceUpdate && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="changesSummary"
|
||||
render={({ field }) => (
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
id={field.name}
|
||||
label="What changed?"
|
||||
type="textarea"
|
||||
placeholder="Describe what's new or improved in this version..."
|
||||
error={form.formState.errors.changesSummary?.message}
|
||||
required
|
||||
{...field}
|
||||
/>
|
||||
<Text variant="small" className="mt-1 text-gray-600">
|
||||
This is required to help users understand what's
|
||||
different in this update.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Optional section label for updates */}
|
||||
{isMarketplaceUpdate && (
|
||||
<div className="mb-4">
|
||||
<Text variant="body" className="font-medium text-gray-700">
|
||||
Optional: Update any of the following details (or leave them
|
||||
as-is)
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
|
||||
@@ -25,6 +25,17 @@ export function useThumbnailImages({
|
||||
const thumbnailsContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Memoize the stringified version to detect actual changes
|
||||
const initialImagesKey = JSON.stringify(initialImages);
|
||||
|
||||
// Update images when initialImages prop changes (by value, not reference)
|
||||
useEffect(() => {
|
||||
if (initialImages.length > 0) {
|
||||
setImages(initialImages);
|
||||
setSelectedImage(initialSelectedImage || initialImages[0]);
|
||||
}
|
||||
}, [initialImagesKey, initialSelectedImage]); // Use stringified key instead of array reference
|
||||
|
||||
// Notify parent when images change
|
||||
useEffect(() => {
|
||||
onImagesChange(images);
|
||||
|
||||
@@ -1,45 +1,113 @@
|
||||
import z from "zod";
|
||||
import { validateYouTubeUrl } from "@/lib/utils";
|
||||
|
||||
export const publishAgentSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(1, "Title is required")
|
||||
.max(100, "Title must be less than 100 characters"),
|
||||
subheader: z
|
||||
.string()
|
||||
.min(1, "Subheader is required")
|
||||
.max(200, "Subheader must be less than 200 characters"),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, "Slug is required")
|
||||
.max(50, "Slug must be less than 50 characters")
|
||||
.regex(
|
||||
/^[a-z0-9-]+$/,
|
||||
"Slug can only contain lowercase letters, numbers, and hyphens",
|
||||
),
|
||||
youtubeLink: z
|
||||
.string()
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
category: z.string().min(1, "Category is required"),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, "Description is required")
|
||||
.max(1000, "Description must be less than 1000 characters"),
|
||||
recommendedScheduleCron: z.string().optional(),
|
||||
instructions: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || val.length <= 2000,
|
||||
"Instructions must be less than 2000 characters",
|
||||
),
|
||||
agentOutputDemo: z
|
||||
.string()
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
});
|
||||
// Create conditional schema that changes based on whether it's a marketplace update
|
||||
export const publishAgentSchemaFactory = (
|
||||
isMarketplaceUpdate: boolean = false,
|
||||
) => {
|
||||
const baseSchema = {
|
||||
changesSummary: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.min(1, "Changes summary is required for updates")
|
||||
.max(500, "Changes summary must be less than 500 characters")
|
||||
: z.string().optional(),
|
||||
title: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || val.length <= 100,
|
||||
"Title must be less than 100 characters",
|
||||
)
|
||||
: z
|
||||
.string()
|
||||
.min(1, "Title is required")
|
||||
.max(100, "Title must be less than 100 characters"),
|
||||
subheader: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || val.length <= 200,
|
||||
"Subheader must be less than 200 characters",
|
||||
)
|
||||
: z
|
||||
.string()
|
||||
.min(1, "Subheader is required")
|
||||
.max(200, "Subheader must be less than 200 characters"),
|
||||
slug: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || (val.length <= 50 && /^[a-z0-9-]+$/.test(val)),
|
||||
"Slug can only contain lowercase letters, numbers, and hyphens",
|
||||
)
|
||||
: z
|
||||
.string()
|
||||
.min(1, "Slug is required")
|
||||
.max(50, "Slug must be less than 50 characters")
|
||||
.regex(
|
||||
/^[a-z0-9-]+$/,
|
||||
"Slug can only contain lowercase letters, numbers, and hyphens",
|
||||
),
|
||||
youtubeLink: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || validateYouTubeUrl(val),
|
||||
"Please enter a valid YouTube URL",
|
||||
)
|
||||
: z
|
||||
.string()
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
category: isMarketplaceUpdate
|
||||
? z.string().optional()
|
||||
: z.string().min(1, "Category is required"),
|
||||
description: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || val.length <= 1000,
|
||||
"Description must be less than 1000 characters",
|
||||
)
|
||||
: z
|
||||
.string()
|
||||
.min(1, "Description is required")
|
||||
.max(1000, "Description must be less than 1000 characters"),
|
||||
recommendedScheduleCron: z.string().optional(),
|
||||
instructions: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || val.length <= 2000,
|
||||
"Instructions must be less than 2000 characters",
|
||||
),
|
||||
agentOutputDemo: isMarketplaceUpdate
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => !val || validateYouTubeUrl(val),
|
||||
"Please enter a valid YouTube URL",
|
||||
)
|
||||
: z
|
||||
.string()
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
};
|
||||
|
||||
export type PublishAgentFormData = z.infer<typeof publishAgentSchema>;
|
||||
return z.object(baseSchema);
|
||||
};
|
||||
|
||||
// Default schema for backwards compatibility
|
||||
export const publishAgentSchema = publishAgentSchemaFactory(false);
|
||||
|
||||
export type PublishAgentFormData = z.infer<
|
||||
ReturnType<typeof publishAgentSchemaFactory>
|
||||
>;
|
||||
|
||||
export interface PublishAgentInfoInitialData {
|
||||
agent_id: string;
|
||||
@@ -54,4 +122,5 @@ export interface PublishAgentInfoInitialData {
|
||||
recommendedScheduleCron?: string;
|
||||
instructions?: string;
|
||||
agentOutputDemo?: string;
|
||||
changesSummary?: string;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as Sentry from "@sentry/nextjs";
|
||||
import {
|
||||
PublishAgentFormData,
|
||||
PublishAgentInfoInitialData,
|
||||
publishAgentSchema,
|
||||
publishAgentSchemaFactory,
|
||||
} from "./helpers";
|
||||
|
||||
export interface Props {
|
||||
@@ -18,6 +18,7 @@ export interface Props {
|
||||
selectedAgentId: string | null;
|
||||
selectedAgentVersion: number | null;
|
||||
initialData?: PublishAgentInfoInitialData;
|
||||
isMarketplaceUpdate?: boolean;
|
||||
}
|
||||
|
||||
export function useAgentInfoStep({
|
||||
@@ -26,6 +27,7 @@ export function useAgentInfoStep({
|
||||
selectedAgentId,
|
||||
selectedAgentVersion,
|
||||
initialData,
|
||||
isMarketplaceUpdate = false,
|
||||
}: Props) {
|
||||
const [agentId, setAgentId] = useState<string | null>(null);
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
@@ -36,8 +38,9 @@ export function useAgentInfoStep({
|
||||
const api = useBackendAPI();
|
||||
|
||||
const form = useForm<PublishAgentFormData>({
|
||||
resolver: zodResolver(publishAgentSchema),
|
||||
resolver: zodResolver(publishAgentSchemaFactory(isMarketplaceUpdate)),
|
||||
defaultValues: {
|
||||
changesSummary: "",
|
||||
title: "",
|
||||
subheader: "",
|
||||
slug: "",
|
||||
@@ -61,6 +64,7 @@ export function useAgentInfoStep({
|
||||
|
||||
// Update form with initial data
|
||||
form.reset({
|
||||
changesSummary: initialData.changesSummary || "",
|
||||
title: initialData.title,
|
||||
subheader: initialData.subheader,
|
||||
slug: initialData.slug.toLocaleLowerCase().trim(),
|
||||
@@ -104,9 +108,10 @@ export function useAgentInfoStep({
|
||||
agent_output_demo_url: data.agentOutputDemo || "",
|
||||
agent_id: selectedAgentId || "",
|
||||
agent_version: selectedAgentVersion || 0,
|
||||
slug: data.slug.replace(/\s+/g, "-"),
|
||||
slug: (data.slug || "").replace(/\s+/g, "-"),
|
||||
categories: filteredCategories,
|
||||
recommended_schedule_cron: data.recommendedScheduleCron || null,
|
||||
changes_summary: data.changesSummary || null,
|
||||
} as any);
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
|
||||
@@ -52,7 +52,7 @@ export function AgentReviewStep({
|
||||
</Text>
|
||||
<Text
|
||||
variant="large"
|
||||
className="line-clamp-1 text-ellipsis text-center !text-neutral-500"
|
||||
className="line-clamp-1 text-ellipsis text-center text-neutral-500"
|
||||
>
|
||||
{subheader}
|
||||
</Text>
|
||||
@@ -80,7 +80,7 @@ export function AgentReviewStep({
|
||||
{description ? (
|
||||
<Text
|
||||
variant="large"
|
||||
className="line-clamp-1 text-ellipsis pt-2 text-center !text-neutral-500"
|
||||
className="line-clamp-1 text-ellipsis pt-2 text-center text-neutral-500"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { useAgentSelectStep } from "./useAgentSelectStep";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
|
||||
interface Props {
|
||||
onSelect: (agentId: string, agentVersion: number) => void;
|
||||
@@ -22,6 +23,7 @@ interface Props {
|
||||
imageSrc: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
},
|
||||
publishedSubmissionData?: StoreSubmission | null,
|
||||
) => void;
|
||||
onOpenBuilder: () => void;
|
||||
}
|
||||
@@ -42,6 +44,8 @@ export function AgentSelectStep({
|
||||
// Handlers
|
||||
handleAgentClick,
|
||||
handleNext,
|
||||
// Utils
|
||||
getPublishedVersion,
|
||||
// Computed
|
||||
isNextDisabled,
|
||||
} = useAgentSelectStep({ onSelect, onNext });
|
||||
@@ -131,26 +135,17 @@ export function AgentSelectStep({
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{agents.map((agent) => (
|
||||
<div
|
||||
<button
|
||||
key={agent.id}
|
||||
data-testid="agent-card"
|
||||
className={`cursor-pointer select-none overflow-hidden rounded-2xl border border-neutral-200 shadow-sm transition-all ${
|
||||
onClick={() =>
|
||||
handleAgentClick(agent.name, agent.id, agent.version)
|
||||
}
|
||||
className={`w-full select-none overflow-hidden rounded-2xl border border-neutral-200 text-left shadow-sm transition-all ${
|
||||
selectedAgentId === agent.id
|
||||
? "border-transparent shadow-none ring-4 ring-violet-600"
|
||||
: "hover:shadow-md"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleAgentClick(agent.name, agent.id, agent.version)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleAgentClick(agent.name, agent.id, agent.version);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-pressed={selectedAgentId === agent.id}
|
||||
>
|
||||
<div className="relative h-32 bg-zinc-400 sm:h-40">
|
||||
<Image
|
||||
@@ -162,12 +157,44 @@ export function AgentSelectStep({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Text variant="large-medium">{agent.name}</Text>
|
||||
<Text variant="small" className="!text-neutral-500">
|
||||
Edited {agent.lastEdited}
|
||||
<Text variant="large-medium" className="line-clamp-2">
|
||||
{agent.name}
|
||||
</Text>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<Text variant="small" className="text-neutral-500">
|
||||
Edited {agent.lastEdited}
|
||||
</Text>
|
||||
{agent.isMarketplaceUpdate &&
|
||||
(() => {
|
||||
const publishedVersion = getPublishedVersion(
|
||||
agent.id,
|
||||
);
|
||||
return (
|
||||
publishedVersion && (
|
||||
<Text
|
||||
variant="small"
|
||||
className="block text-neutral-500"
|
||||
>
|
||||
v{publishedVersion} → v{agent.version}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{agent.isMarketplaceUpdate && (
|
||||
<span className="shrink-0 rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
Update
|
||||
</span>
|
||||
)}
|
||||
{!agent.isMarketplaceUpdate && (
|
||||
<span className="shrink-0 rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import {
|
||||
useGetV2GetMyAgents,
|
||||
useGetV2ListMySubmissions,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { MyAgent } from "@/app/api/__generated__/models/myAgent";
|
||||
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
|
||||
export interface Agent {
|
||||
name: string;
|
||||
@@ -9,6 +15,7 @@ export interface Agent {
|
||||
imageSrc: string;
|
||||
description: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
isMarketplaceUpdate: boolean; // true if this is an update to existing published agent
|
||||
}
|
||||
|
||||
interface UseAgentSelectStepProps {
|
||||
@@ -22,6 +29,7 @@ interface UseAgentSelectStepProps {
|
||||
imageSrc: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
},
|
||||
publishedSubmissionData?: StoreSubmission | null, // For pre-filling updates
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -36,27 +44,88 @@ export function useAgentSelectStep({
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const { data: myAgents, isLoading, error } = useGetV2GetMyAgents();
|
||||
const {
|
||||
data: myAgents,
|
||||
isLoading: agentsLoading,
|
||||
error: agentsError,
|
||||
} = useGetV2GetMyAgents();
|
||||
const {
|
||||
data: mySubmissions,
|
||||
isLoading: submissionsLoading,
|
||||
error: submissionsError,
|
||||
} = useGetV2ListMySubmissions();
|
||||
|
||||
const agents: Agent[] =
|
||||
(myAgents?.status === 200 &&
|
||||
myAgents.data.agents
|
||||
.map(
|
||||
(agent): Agent => ({
|
||||
name: agent.agent_name,
|
||||
id: agent.agent_id,
|
||||
version: agent.agent_version,
|
||||
lastEdited: agent.last_edited.toLocaleDateString(),
|
||||
imageSrc: agent.agent_image || "https://picsum.photos/300/200",
|
||||
description: agent.description || "",
|
||||
recommendedScheduleCron: agent.recommended_schedule_cron ?? null,
|
||||
}),
|
||||
)
|
||||
.sort(
|
||||
(a: Agent, b: Agent) =>
|
||||
new Date(b.lastEdited).getTime() - new Date(a.lastEdited).getTime(),
|
||||
)) ||
|
||||
[];
|
||||
const isLoading = agentsLoading || submissionsLoading;
|
||||
const error = agentsError || submissionsError;
|
||||
|
||||
const agents: Agent[] = React.useMemo(() => {
|
||||
// Properly handle API responses with okData helper
|
||||
const agentsData = (okData(myAgents) as any)?.agents || [];
|
||||
const submissionsData = (okData(mySubmissions) as any)?.submissions || [];
|
||||
|
||||
if (agentsData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return agentsData
|
||||
.map((agent: MyAgent): Agent | null => {
|
||||
// Find the highest published agent_version for this agent from approved submissions
|
||||
const publishedVersion = submissionsData
|
||||
.filter(
|
||||
(s: StoreSubmission) =>
|
||||
s.status === "APPROVED" && s.agent_id === agent.agent_id,
|
||||
)
|
||||
.reduce(
|
||||
(max: number | undefined, s: StoreSubmission) =>
|
||||
max === undefined || s.agent_version > max
|
||||
? s.agent_version
|
||||
: max,
|
||||
undefined,
|
||||
);
|
||||
const isMarketplaceUpdate =
|
||||
publishedVersion !== undefined &&
|
||||
agent.agent_version > publishedVersion;
|
||||
const isNewAgent = publishedVersion === undefined;
|
||||
|
||||
// Only include agents that are either new or have newer versions than published
|
||||
if (!isNewAgent && !isMarketplaceUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: agent.agent_name,
|
||||
id: agent.agent_id,
|
||||
version: agent.agent_version,
|
||||
lastEdited: agent.last_edited.toLocaleDateString(),
|
||||
imageSrc: agent.agent_image || "https://picsum.photos/300/200",
|
||||
description: agent.description || "",
|
||||
recommendedScheduleCron: agent.recommended_schedule_cron ?? null,
|
||||
isMarketplaceUpdate,
|
||||
};
|
||||
})
|
||||
.filter((agent: Agent | null): agent is Agent => agent !== null)
|
||||
.sort(
|
||||
(a: Agent, b: Agent) =>
|
||||
new Date(b.lastEdited).getTime() - new Date(a.lastEdited).getTime(),
|
||||
);
|
||||
}, [myAgents, mySubmissions]);
|
||||
|
||||
// Function to get published submission data for pre-filling updates
|
||||
const getPublishedSubmissionData = (agentId: string) => {
|
||||
const submissionsData = (okData(mySubmissions) as any)?.submissions || [];
|
||||
|
||||
const approvedSubmissions = submissionsData
|
||||
.filter(
|
||||
(submission: StoreSubmission) =>
|
||||
submission.agent_id === agentId && submission.status === "APPROVED",
|
||||
)
|
||||
.sort(
|
||||
(a: StoreSubmission, b: StoreSubmission) =>
|
||||
b.agent_version - a.agent_version,
|
||||
);
|
||||
|
||||
return approvedSubmissions[0] || null;
|
||||
};
|
||||
|
||||
const handleAgentClick = (
|
||||
_: string,
|
||||
@@ -74,16 +143,42 @@ export function useAgentSelectStep({
|
||||
(agent) => agent.id === selectedAgentId,
|
||||
);
|
||||
if (selectedAgent) {
|
||||
onNext(selectedAgentId, selectedAgentVersion, {
|
||||
name: selectedAgent.name,
|
||||
description: selectedAgent.description,
|
||||
imageSrc: selectedAgent.imageSrc,
|
||||
recommendedScheduleCron: selectedAgent.recommendedScheduleCron,
|
||||
});
|
||||
// Get published submission data for pre-filling if this is an update
|
||||
const publishedSubmissionData = selectedAgent.isMarketplaceUpdate
|
||||
? getPublishedSubmissionData(selectedAgentId)
|
||||
: undefined;
|
||||
|
||||
onNext(
|
||||
selectedAgentId,
|
||||
selectedAgentVersion,
|
||||
{
|
||||
name: selectedAgent.name,
|
||||
description: selectedAgent.description,
|
||||
imageSrc: selectedAgent.imageSrc,
|
||||
recommendedScheduleCron: selectedAgent.recommendedScheduleCron,
|
||||
},
|
||||
publishedSubmissionData,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to get published version for an agent
|
||||
const getPublishedVersion = (agentId: string): number | undefined => {
|
||||
const submissionsData = (okData(mySubmissions) as any)?.submissions || [];
|
||||
|
||||
return submissionsData
|
||||
.filter(
|
||||
(s: StoreSubmission) =>
|
||||
s.status === "APPROVED" && s.agent_id === agentId,
|
||||
)
|
||||
.reduce(
|
||||
(max: number | undefined, s: StoreSubmission) =>
|
||||
max === undefined || s.agent_version > max ? s.agent_version : max,
|
||||
undefined,
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
// Data
|
||||
agents,
|
||||
@@ -94,6 +189,9 @@ export function useAgentSelectStep({
|
||||
// Handlers
|
||||
handleAgentClick,
|
||||
handleNext,
|
||||
// Utils
|
||||
getPublishedSubmissionData,
|
||||
getPublishedVersion,
|
||||
// Computed
|
||||
isNextDisabled: !selectedAgentId || !selectedAgentVersion,
|
||||
};
|
||||
|
||||
@@ -8,4 +8,8 @@ export const emptyModalState = {
|
||||
category: "",
|
||||
description: "",
|
||||
recommendedScheduleCron: "",
|
||||
instructions: "",
|
||||
agentOutputDemo: "",
|
||||
changesSummary: "",
|
||||
additionalImages: [],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,12 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { PublishAgentInfoInitialData } from "./components/AgentInfoStep/helpers";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { emptyModalState } from "./helpers";
|
||||
import {
|
||||
useGetV2GetMyAgents,
|
||||
useGetV2ListMySubmissions,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { MyAgent } from "@/app/api/__generated__/models/myAgent";
|
||||
|
||||
const defaultTargetState: PublishState = {
|
||||
isOpen: false,
|
||||
@@ -22,9 +28,16 @@ export interface Props {
|
||||
trigger?: React.ReactNode;
|
||||
targetState?: PublishState;
|
||||
onStateChange?: (state: PublishState) => void;
|
||||
preSelectedAgentId?: string;
|
||||
preSelectedAgentVersion?: number;
|
||||
}
|
||||
|
||||
export function usePublishAgentModal({ targetState, onStateChange }: Props) {
|
||||
export function usePublishAgentModal({
|
||||
targetState,
|
||||
onStateChange,
|
||||
preSelectedAgentId,
|
||||
preSelectedAgentVersion,
|
||||
}: Props) {
|
||||
const [currentState, setCurrentState] = useState<PublishState>(
|
||||
targetState || defaultTargetState,
|
||||
);
|
||||
@@ -42,14 +55,20 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
|
||||
|
||||
const [_, setSelectedAgent] = useState<string | null>(null);
|
||||
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(
|
||||
preSelectedAgentId || null,
|
||||
);
|
||||
|
||||
const [selectedAgentVersion, setSelectedAgentVersion] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
>(preSelectedAgentVersion || null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Fetch agent data for pre-populating form when agent is pre-selected
|
||||
const { data: myAgents } = useGetV2GetMyAgents();
|
||||
const { data: mySubmissions } = useGetV2ListMySubmissions();
|
||||
|
||||
// Sync currentState with targetState when it changes from outside
|
||||
useEffect(() => {
|
||||
if (targetState) {
|
||||
@@ -60,13 +79,90 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
|
||||
// Reset internal state when modal opens
|
||||
useEffect(() => {
|
||||
if (!targetState) return;
|
||||
if (targetState.isOpen && targetState.step === "select") {
|
||||
if (targetState.isOpen) {
|
||||
setSelectedAgent(null);
|
||||
setSelectedAgentId(null);
|
||||
setSelectedAgentVersion(null);
|
||||
setSelectedAgentId(preSelectedAgentId || null);
|
||||
setSelectedAgentVersion(preSelectedAgentVersion || null);
|
||||
setInitialData(emptyModalState);
|
||||
}
|
||||
}, [targetState]);
|
||||
}, [targetState, preSelectedAgentId, preSelectedAgentVersion]);
|
||||
|
||||
// Pre-populate form data when modal opens with info step and pre-selected agent
|
||||
useEffect(() => {
|
||||
if (
|
||||
!targetState?.isOpen ||
|
||||
targetState.step !== "info" ||
|
||||
!preSelectedAgentId ||
|
||||
!preSelectedAgentVersion
|
||||
)
|
||||
return;
|
||||
const agentsData = okData(myAgents) as any;
|
||||
const submissionsData = okData(mySubmissions) as any;
|
||||
|
||||
if (!agentsData || !submissionsData) return;
|
||||
|
||||
// Find the agent data
|
||||
const agent = agentsData.agents?.find(
|
||||
(a: MyAgent) => a.agent_id === preSelectedAgentId,
|
||||
);
|
||||
if (!agent) return;
|
||||
|
||||
// Find published submission data for this agent (for updates)
|
||||
const publishedSubmissionData = submissionsData.submissions
|
||||
?.filter(
|
||||
(s: StoreSubmission) =>
|
||||
s.status === "APPROVED" && s.agent_id === preSelectedAgentId,
|
||||
)
|
||||
.sort(
|
||||
(a: StoreSubmission, b: StoreSubmission) =>
|
||||
b.agent_version - a.agent_version,
|
||||
)[0];
|
||||
|
||||
// Populate initial data (same logic as handleNextFromSelect)
|
||||
const initialFormData: PublishAgentInfoInitialData = publishedSubmissionData
|
||||
? {
|
||||
agent_id: preSelectedAgentId,
|
||||
title: publishedSubmissionData.name,
|
||||
subheader: publishedSubmissionData.sub_heading || "",
|
||||
description: publishedSubmissionData.description,
|
||||
instructions: publishedSubmissionData.instructions || "",
|
||||
youtubeLink: publishedSubmissionData.video_url || "",
|
||||
agentOutputDemo: publishedSubmissionData.agent_output_demo_url || "",
|
||||
additionalImages: [
|
||||
...new Set(publishedSubmissionData.image_urls || []),
|
||||
].filter(Boolean) as string[],
|
||||
category: publishedSubmissionData.categories?.[0] || "",
|
||||
thumbnailSrc: agent.agent_image || "https://picsum.photos/300/200",
|
||||
slug: publishedSubmissionData.slug,
|
||||
recommendedScheduleCron: agent.recommended_schedule_cron || "",
|
||||
changesSummary: "", // Empty for user to fill in what changed
|
||||
}
|
||||
: {
|
||||
...emptyModalState,
|
||||
agent_id: preSelectedAgentId,
|
||||
title: agent.agent_name,
|
||||
description: agent.description || "",
|
||||
thumbnailSrc: agent.agent_image || "https://picsum.photos/300/200",
|
||||
slug: agent.agent_name.replace(/ /g, "-"),
|
||||
recommendedScheduleCron: agent.recommended_schedule_cron || "",
|
||||
};
|
||||
|
||||
setInitialData(initialFormData);
|
||||
|
||||
// Update the state with the submission data if this is an update
|
||||
if (publishedSubmissionData) {
|
||||
setCurrentState((prevState) => ({
|
||||
...prevState,
|
||||
submissionData: publishedSubmissionData,
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
targetState,
|
||||
preSelectedAgentId,
|
||||
preSelectedAgentVersion,
|
||||
myAgents,
|
||||
mySubmissions,
|
||||
]);
|
||||
|
||||
function handleClose() {
|
||||
// Reset all internal state
|
||||
@@ -97,20 +193,43 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
|
||||
imageSrc: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
},
|
||||
publishedSubmissionData?: StoreSubmission | null,
|
||||
) {
|
||||
setInitialData({
|
||||
...emptyModalState,
|
||||
agent_id: agentId,
|
||||
title: agentData.name,
|
||||
description: agentData.description,
|
||||
thumbnailSrc: agentData.imageSrc,
|
||||
slug: agentData.name.replace(/ /g, "-"),
|
||||
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
|
||||
});
|
||||
// Pre-populate with published data if this is an update, otherwise use agent data
|
||||
const initialFormData: PublishAgentInfoInitialData = publishedSubmissionData
|
||||
? {
|
||||
agent_id: agentId,
|
||||
title: publishedSubmissionData.name,
|
||||
subheader: publishedSubmissionData.sub_heading || "",
|
||||
description: publishedSubmissionData.description,
|
||||
instructions: publishedSubmissionData.instructions || "",
|
||||
youtubeLink: publishedSubmissionData.video_url || "",
|
||||
agentOutputDemo: publishedSubmissionData.agent_output_demo_url || "",
|
||||
additionalImages: [
|
||||
...new Set(publishedSubmissionData.image_urls || []),
|
||||
].filter(Boolean) as string[],
|
||||
category: publishedSubmissionData.categories?.[0] || "", // Take first category
|
||||
thumbnailSrc: agentData.imageSrc, // Use current agent image
|
||||
slug: publishedSubmissionData.slug,
|
||||
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
|
||||
changesSummary: "", // Empty for user to fill in what changed
|
||||
}
|
||||
: {
|
||||
...emptyModalState,
|
||||
agent_id: agentId,
|
||||
title: agentData.name,
|
||||
description: agentData.description,
|
||||
thumbnailSrc: agentData.imageSrc,
|
||||
slug: agentData.name.replace(/ /g, "-"),
|
||||
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
|
||||
};
|
||||
|
||||
setInitialData(initialFormData);
|
||||
|
||||
updateState({
|
||||
...currentState,
|
||||
step: "info",
|
||||
submissionData: publishedSubmissionData || null,
|
||||
});
|
||||
|
||||
setSelectedAgentId(agentId);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Marketplace-specific helper functions that can be reused across different marketplace screens
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate the latest marketplace version from agent graph versions
|
||||
*/
|
||||
export function getLatestMarketplaceVersion(
|
||||
agentGraphVersions?: string[],
|
||||
): number | undefined {
|
||||
if (!agentGraphVersions?.length) return undefined;
|
||||
|
||||
return Math.max(...agentGraphVersions.map((v: string) => parseInt(v, 10)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is the creator of the agent
|
||||
*/
|
||||
export function isUserCreator(
|
||||
creator: string,
|
||||
currentUser: { email?: string } | null,
|
||||
): boolean {
|
||||
if (!currentUser?.email) return false;
|
||||
|
||||
const userHandle = currentUser.email.split("@")[0]?.toLowerCase() || "";
|
||||
return creator.toLowerCase().includes(userHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate update status for an agent
|
||||
*/
|
||||
export function calculateUpdateStatus({
|
||||
latestMarketplaceVersion,
|
||||
currentVersion,
|
||||
isUserCreator,
|
||||
isAgentAddedToLibrary,
|
||||
}: {
|
||||
latestMarketplaceVersion?: number;
|
||||
currentVersion: number;
|
||||
isUserCreator: boolean;
|
||||
isAgentAddedToLibrary: boolean;
|
||||
}) {
|
||||
if (!latestMarketplaceVersion) {
|
||||
return { hasUpdate: false, hasUnpublishedChanges: false };
|
||||
}
|
||||
|
||||
const hasUnpublishedChanges =
|
||||
isUserCreator &&
|
||||
isAgentAddedToLibrary &&
|
||||
currentVersion > latestMarketplaceVersion;
|
||||
|
||||
const hasUpdate =
|
||||
isAgentAddedToLibrary &&
|
||||
!isUserCreator &&
|
||||
latestMarketplaceVersion > currentVersion;
|
||||
|
||||
return { hasUpdate, hasUnpublishedChanges };
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import * as React from "react";
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
link: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +20,7 @@ export function Breadcrumbs({ items }: Props) {
|
||||
<Link
|
||||
href={item.link}
|
||||
className="text-[0.75rem] font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline"
|
||||
data-testid={item.testId}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
|
||||
@@ -48,7 +48,7 @@ const mockFlags = {
|
||||
[Flag.AGENT_FAVORITING]: false,
|
||||
[Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS,
|
||||
[Flag.ENABLE_PLATFORM_PAYMENT]: false,
|
||||
[Flag.CHAT]: true,
|
||||
[Flag.CHAT]: false,
|
||||
};
|
||||
|
||||
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {
|
||||
|
||||
Reference in New Issue
Block a user