mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 07:08:09 -05:00
feat(platform): Add recommended run schedule for agent execution (#10827)
## Summary <img width="1000" alt="Screenshot 2025-09-02 at 9 46 49 PM" src="https://github.com/user-attachments/assets/d78100c7-7974-4d37-a788-757764d8b6b7" /> <img width="1000" alt="Screenshot 2025-09-02 at 9 20 24 PM" src="https://github.com/user-attachments/assets/cd092963-8e26-4198-b65a-4416b2307a50" /> <img width="1000" alt="Screenshot 2025-09-02 at 9 22 30 PM" src="https://github.com/user-attachments/assets/e16b3bdb-c48c-4dec-9281-b2a35b3e21d0" /> <img width="1000" alt="Screenshot 2025-09-02 at 9 20 38 PM" src="https://github.com/user-attachments/assets/11d74a39-f4b4-4fce-8d30-0e6a925f3a9b" /> • Added recommended schedule cron expression as an optional input throughout the platform • Implemented complete data flow from builder → store submission → agent library → run page • Fixed UI layout issues including button text overflow and ensured proper component reusability ## Changes ### Backend - Added `recommended_schedule_cron` field to `AgentGraph` schema and database migration - Updated API models (`LibraryAgent`, `MyAgent`, `StoreSubmissionRequest`) to include the new field - Enhanced store submission approval flow to persist recommended schedule to database ### Frontend - Added recommended schedule input to builder page (SaveControl component) with overflow-safe styling - Updated store submission modal (PublishAgentModal) with schedule configuration - Enhanced agent run page with schedule tip display and pre-filled schedule dialog - Refactored `CronSchedulerDialog` with discriminated union types for better reusability - Fixed layout issues including button text truncation and popover width constraints - Implemented robust cron expression parsing with 100% reversibility between UI and cron format 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,7 @@ class BaseGraph(BaseDbModel):
|
||||
is_active: bool = True
|
||||
name: str
|
||||
description: str
|
||||
recommended_schedule_cron: str | None = None
|
||||
nodes: list[Node] = []
|
||||
links: list[Link] = []
|
||||
forked_from_id: str | None = None
|
||||
@@ -696,6 +697,7 @@ class GraphModel(Graph):
|
||||
is_active=graph.isActive,
|
||||
name=graph.name or "",
|
||||
description=graph.description or "",
|
||||
recommended_schedule_cron=graph.recommendedScheduleCron,
|
||||
nodes=[NodeModel.from_db(node, for_export) for node in graph.Nodes or []],
|
||||
links=list(
|
||||
{
|
||||
@@ -1083,6 +1085,7 @@ async def __create_graph(tx, graph: Graph, user_id: str):
|
||||
version=graph.version,
|
||||
name=graph.name,
|
||||
description=graph.description,
|
||||
recommendedScheduleCron=graph.recommended_schedule_cron,
|
||||
isActive=graph.is_active,
|
||||
userId=user_id,
|
||||
forkedFromId=graph.forked_from_id,
|
||||
|
||||
@@ -64,6 +64,9 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
# Indicates if this agent is the latest version
|
||||
is_latest_version: bool
|
||||
|
||||
# Recommended schedule cron (from marketplace agents)
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
@staticmethod
|
||||
def from_db(
|
||||
agent: prisma.models.LibraryAgent,
|
||||
@@ -130,6 +133,7 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
new_output=new_output,
|
||||
can_access_graph=can_access_graph,
|
||||
is_latest_version=is_latest_version,
|
||||
recommended_schedule_cron=agent.AgentGraph.recommendedScheduleCron,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ async def test_get_library_agents_success(
|
||||
credentials_input_schema={"type": "object", "properties": {}},
|
||||
has_external_trigger=False,
|
||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||
recommended_schedule_cron=None,
|
||||
new_output=False,
|
||||
can_access_graph=True,
|
||||
is_latest_version=True,
|
||||
@@ -69,6 +70,7 @@ async def test_get_library_agents_success(
|
||||
credentials_input_schema={"type": "object", "properties": {}},
|
||||
has_external_trigger=False,
|
||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||
recommended_schedule_cron=None,
|
||||
new_output=False,
|
||||
can_access_graph=False,
|
||||
is_latest_version=True,
|
||||
|
||||
@@ -183,6 +183,13 @@ async def get_store_agent_details(
|
||||
store_listing.hasApprovedVersion if store_listing else False
|
||||
)
|
||||
|
||||
if store_listing and store_listing.ActiveVersion:
|
||||
recommended_schedule_cron = (
|
||||
store_listing.ActiveVersion.recommendedScheduleCron
|
||||
)
|
||||
else:
|
||||
recommended_schedule_cron = None
|
||||
|
||||
logger.debug(f"Found agent details for {username}/{agent_name}")
|
||||
return backend.server.v2.store.model.StoreAgentDetails(
|
||||
store_listing_version_id=agent.storeListingVersionId,
|
||||
@@ -201,6 +208,7 @@ async def get_store_agent_details(
|
||||
last_updated=agent.updated_at,
|
||||
active_version_id=active_version_id,
|
||||
has_approved_version=has_approved_version,
|
||||
recommended_schedule_cron=recommended_schedule_cron,
|
||||
)
|
||||
except backend.server.v2.store.exceptions.AgentNotFoundError:
|
||||
raise
|
||||
@@ -562,6 +570,7 @@ async def create_store_submission(
|
||||
sub_heading: str = "",
|
||||
categories: list[str] = [],
|
||||
changes_summary: str | None = "Initial Submission",
|
||||
recommended_schedule_cron: str | None = None,
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Create the first (and only) store listing and thus submission as a normal user
|
||||
@@ -655,6 +664,7 @@ async def create_store_submission(
|
||||
submissionStatus=prisma.enums.SubmissionStatus.PENDING,
|
||||
submittedAt=datetime.now(tz=timezone.utc),
|
||||
changesSummary=changes_summary,
|
||||
recommendedScheduleCron=recommended_schedule_cron,
|
||||
)
|
||||
]
|
||||
},
|
||||
@@ -710,6 +720,7 @@ async def edit_store_submission(
|
||||
sub_heading: str = "",
|
||||
categories: list[str] = [],
|
||||
changes_summary: str | None = "Update submission",
|
||||
recommended_schedule_cron: str | None = None,
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Edit an existing store listing submission.
|
||||
@@ -789,6 +800,7 @@ async def edit_store_submission(
|
||||
sub_heading=sub_heading,
|
||||
categories=categories,
|
||||
changes_summary=changes_summary,
|
||||
recommended_schedule_cron=recommended_schedule_cron,
|
||||
)
|
||||
|
||||
# For PENDING submissions, we can update the existing version
|
||||
@@ -804,6 +816,7 @@ async def edit_store_submission(
|
||||
categories=categories,
|
||||
subHeading=sub_heading,
|
||||
changesSummary=changes_summary,
|
||||
recommendedScheduleCron=recommended_schedule_cron,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -866,6 +879,7 @@ async def create_store_version(
|
||||
sub_heading: str = "",
|
||||
categories: list[str] = [],
|
||||
changes_summary: str | None = "Initial submission",
|
||||
recommended_schedule_cron: str | None = None,
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Create a new version for an existing store listing
|
||||
@@ -935,6 +949,7 @@ async def create_store_version(
|
||||
submissionStatus=prisma.enums.SubmissionStatus.PENDING,
|
||||
submittedAt=datetime.now(),
|
||||
changesSummary=changes_summary,
|
||||
recommendedScheduleCron=recommended_schedule_cron,
|
||||
storeListingId=store_listing_id,
|
||||
)
|
||||
)
|
||||
@@ -1150,6 +1165,7 @@ async def get_my_agents(
|
||||
last_edited=graph.updatedAt or graph.createdAt,
|
||||
description=graph.description or "",
|
||||
agent_image=library_agent.imageUrl,
|
||||
recommended_schedule_cron=graph.recommendedScheduleCron,
|
||||
)
|
||||
for library_agent in library_agents
|
||||
if (graph := library_agent.AgentGraph)
|
||||
@@ -1351,6 +1367,21 @@ async def review_store_submission(
|
||||
]
|
||||
)
|
||||
|
||||
# Update the AgentGraph with store listing data
|
||||
await prisma.models.AgentGraph.prisma().update(
|
||||
where={
|
||||
"graphVersionId": {
|
||||
"id": store_listing_version.agentGraphId,
|
||||
"version": store_listing_version.agentGraphVersion,
|
||||
}
|
||||
},
|
||||
data={
|
||||
"name": store_listing_version.name,
|
||||
"description": store_listing_version.description,
|
||||
"recommendedScheduleCron": store_listing_version.recommendedScheduleCron,
|
||||
},
|
||||
)
|
||||
|
||||
await prisma.models.StoreListing.prisma(tx).update(
|
||||
where={"id": store_listing_version.StoreListing.id},
|
||||
data={
|
||||
|
||||
@@ -90,6 +90,8 @@ async def test_get_store_agent_details(mocker):
|
||||
mock_store_listing = mocker.MagicMock()
|
||||
mock_store_listing.activeVersionId = "active-version-id"
|
||||
mock_store_listing.hasApprovedVersion = True
|
||||
mock_store_listing.ActiveVersion = mocker.MagicMock()
|
||||
mock_store_listing.ActiveVersion.recommendedScheduleCron = None
|
||||
|
||||
# Mock StoreAgent prisma call
|
||||
mock_store_agent = mocker.patch("prisma.models.StoreAgent.prisma")
|
||||
|
||||
@@ -14,6 +14,7 @@ class MyAgent(pydantic.BaseModel):
|
||||
agent_image: str | None = None
|
||||
description: str
|
||||
last_edited: datetime.datetime
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
|
||||
class MyAgentsResponse(pydantic.BaseModel):
|
||||
@@ -53,6 +54,7 @@ class StoreAgentDetails(pydantic.BaseModel):
|
||||
rating: float
|
||||
versions: list[str]
|
||||
last_updated: datetime.datetime
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
active_version_id: str | None = None
|
||||
has_approved_version: bool = False
|
||||
@@ -157,6 +159,7 @@ class StoreSubmissionRequest(pydantic.BaseModel):
|
||||
description: str = ""
|
||||
categories: list[str] = []
|
||||
changes_summary: str | None = None
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
|
||||
class StoreSubmissionEditRequest(pydantic.BaseModel):
|
||||
@@ -167,6 +170,7 @@ class StoreSubmissionEditRequest(pydantic.BaseModel):
|
||||
description: str = ""
|
||||
categories: list[str] = []
|
||||
changes_summary: str | None = None
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
|
||||
class ProfileDetails(pydantic.BaseModel):
|
||||
|
||||
@@ -535,6 +535,7 @@ async def create_submission(
|
||||
sub_heading=submission_request.sub_heading,
|
||||
categories=submission_request.categories,
|
||||
changes_summary=submission_request.changes_summary or "Initial Submission",
|
||||
recommended_schedule_cron=submission_request.recommended_schedule_cron,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store submission")
|
||||
@@ -580,6 +581,7 @@ async def edit_submission(
|
||||
sub_heading=submission_request.sub_heading,
|
||||
categories=submission_request.categories,
|
||||
changes_summary=submission_request.changes_summary,
|
||||
recommended_schedule_cron=submission_request.recommended_schedule_cron,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "StoreListingVersion" ADD COLUMN "recommendedScheduleCron" TEXT;
|
||||
ALTER TABLE "AgentGraph" ADD COLUMN "recommendedScheduleCron" TEXT;
|
||||
@@ -110,6 +110,7 @@ model AgentGraph {
|
||||
|
||||
name String?
|
||||
description String?
|
||||
recommendedScheduleCron String?
|
||||
|
||||
isActive Boolean @default(true)
|
||||
|
||||
@@ -785,6 +786,8 @@ model StoreListingVersion {
|
||||
reviewComments String? // Comments visible to creator
|
||||
reviewedAt DateTime?
|
||||
|
||||
recommendedScheduleCron String? // cron expression like "0 9 * * *"
|
||||
|
||||
// Reviews for this specific version
|
||||
Reviews StoreListingReview[]
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"1.1.0"
|
||||
],
|
||||
"last_updated": "2023-01-01T00:00:00",
|
||||
"recommended_schedule_cron": null,
|
||||
"active_version_id": null,
|
||||
"has_approved_version": false
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
"required": [],
|
||||
"type": "object"
|
||||
},
|
||||
"recommended_schedule_cron": null,
|
||||
"sub_graphs": [],
|
||||
"trigger_setup_info": null,
|
||||
"user_id": "test-user-id",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"required": [],
|
||||
"type": "object"
|
||||
},
|
||||
"recommended_schedule_cron": null,
|
||||
"sub_graphs": [],
|
||||
"trigger_setup_info": null,
|
||||
"user_id": "test-user-id",
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"trigger_setup_info": null,
|
||||
"new_output": false,
|
||||
"can_access_graph": true,
|
||||
"is_latest_version": true
|
||||
"is_latest_version": true,
|
||||
"recommended_schedule_cron": null
|
||||
},
|
||||
{
|
||||
"id": "test-agent-2",
|
||||
@@ -56,7 +57,8 @@
|
||||
"trigger_setup_info": null,
|
||||
"new_output": false,
|
||||
"can_access_graph": false,
|
||||
"is_latest_version": true
|
||||
"is_latest_version": true,
|
||||
"recommended_schedule_cron": null
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -180,6 +180,7 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
<ScheduleView
|
||||
scheduleName={scheduleName}
|
||||
cronExpression={cronExpression}
|
||||
recommendedScheduleCron={agent.recommended_schedule_cron}
|
||||
onScheduleNameChange={handleSetScheduleName}
|
||||
onCronExpressionChange={handleSetCronExpression}
|
||||
onValidityChange={setIsScheduleFormValid}
|
||||
@@ -190,6 +191,11 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
|
||||
<Text variant="body" className="mb-3 !text-zinc-500">
|
||||
No schedule configured. Create a schedule to run this
|
||||
agent automatically at a specific time.{" "}
|
||||
{agent.recommended_schedule_cron && (
|
||||
<span className="text-blue-600">
|
||||
This agent has a recommended schedule.
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { validateSchedule } from "./helpers";
|
||||
interface Props {
|
||||
scheduleName: string;
|
||||
cronExpression: string;
|
||||
recommendedScheduleCron?: string | null;
|
||||
onScheduleNameChange: (name: string) => void;
|
||||
onCronExpressionChange: (expression: string) => void;
|
||||
onValidityChange?: (valid: boolean) => void;
|
||||
@@ -17,6 +18,7 @@ interface Props {
|
||||
export function ScheduleView({
|
||||
scheduleName,
|
||||
cronExpression: _cronExpression,
|
||||
recommendedScheduleCron,
|
||||
onScheduleNameChange,
|
||||
onCronExpressionChange,
|
||||
onValidityChange,
|
||||
@@ -72,6 +74,15 @@ export function ScheduleView({
|
||||
error={errors.scheduleName}
|
||||
/>
|
||||
|
||||
{recommendedScheduleCron && (
|
||||
<div className="mb-4 rounded-md bg-blue-50 p-3">
|
||||
<Text variant="body" className="text-blue-800">
|
||||
💡 This agent has a recommended schedule that has been pre-filled
|
||||
below. You can modify it as needed.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
id="repeat"
|
||||
label="Repeats"
|
||||
|
||||
@@ -44,7 +44,9 @@ export function useAgentRunModal(
|
||||
const [presetDescription, setPresetDescription] = useState<string>("");
|
||||
const defaultScheduleName = useMemo(() => `Run ${agent.name}`, [agent.name]);
|
||||
const [scheduleName, setScheduleName] = useState(defaultScheduleName);
|
||||
const [cronExpression, setCronExpression] = useState("0 9 * * 1");
|
||||
const [cronExpression, setCronExpression] = useState(
|
||||
agent.recommended_schedule_cron || "0 9 * * 1",
|
||||
);
|
||||
|
||||
// Determine the default run type based on agent capabilities
|
||||
const defaultRunType: RunVariant = agent.has_external_trigger
|
||||
@@ -322,7 +324,9 @@ export function useAgentRunModal(
|
||||
function handleShowSchedule() {
|
||||
// Initialize with sensible defaults when entering schedule view
|
||||
setScheduleName((prev) => prev || defaultScheduleName);
|
||||
setCronExpression((prev) => prev || "0 9 * * 1");
|
||||
setCronExpression(
|
||||
(prev) => prev || agent.recommended_schedule_cron || "0 9 * * 1",
|
||||
);
|
||||
setShowScheduleView(true);
|
||||
}
|
||||
|
||||
@@ -330,7 +334,7 @@ export function useAgentRunModal(
|
||||
setShowScheduleView(false);
|
||||
// Reset schedule fields on exit
|
||||
setScheduleName(defaultScheduleName);
|
||||
setCronExpression("0 9 * * 1");
|
||||
setCronExpression(agent.recommended_schedule_cron || "0 9 * * 1");
|
||||
}
|
||||
|
||||
function handleSetScheduleName(name: string) {
|
||||
|
||||
@@ -558,6 +558,7 @@ export function OldAgentLibraryView() {
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onCreatePreset={onCreatePreset}
|
||||
agentActions={agentActions}
|
||||
recommendedScheduleCron={agent?.recommended_schedule_cron || null}
|
||||
/>
|
||||
) : selectedView.type == "preset" ? (
|
||||
/* Edit & update presets */
|
||||
@@ -567,6 +568,7 @@ export function OldAgentLibraryView() {
|
||||
agentPresets.find((preset) => preset.id == selectedView.id)!
|
||||
}
|
||||
onRun={selectRun}
|
||||
recommendedScheduleCron={agent?.recommended_schedule_cron || null}
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onUpdatePreset={onUpdatePreset}
|
||||
doDeletePreset={setConfirmingDeleteAgentPreset}
|
||||
|
||||
@@ -16,8 +16,9 @@ import ActionButtonGroup from "@/components/agptui/action-button-group";
|
||||
import type { ButtonAction } from "@/components/agptui/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { IconCross, IconPlay, IconSave } from "@/components/ui/icons";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { CalendarClockIcon, Trash2Icon, ClockIcon } from "lucide-react";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import { ScheduleTaskDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
|
||||
import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentInputs/RunAgentInputs";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
@@ -45,9 +46,11 @@ export function AgentRunDraftView({
|
||||
onCreateSchedule,
|
||||
agentActions,
|
||||
className,
|
||||
recommendedScheduleCron,
|
||||
}: {
|
||||
graph: GraphMeta;
|
||||
agentActions?: ButtonAction[];
|
||||
recommendedScheduleCron?: string | null;
|
||||
doRun?: (
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
@@ -380,20 +383,21 @@ export function AgentRunDraftView({
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<IconPlay className="mr-2 size-4" /> Run
|
||||
<CalendarClockIcon className="mr-2 size-4" /> Schedule run
|
||||
</>
|
||||
),
|
||||
variant: "accent",
|
||||
callback: doRun,
|
||||
extraProps: { "data-testid": "agent-run-button" },
|
||||
callback: openScheduleDialog,
|
||||
extraProps: { "data-testid": "agent-schedule-button" },
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<CalendarClockIcon className="mr-2 size-4" /> Schedule
|
||||
<IconPlay className="mr-2 size-4" /> Manual run
|
||||
</>
|
||||
),
|
||||
callback: openScheduleDialog,
|
||||
callback: doRun,
|
||||
extraProps: { "data-testid": "agent-run-button" },
|
||||
},
|
||||
// {
|
||||
// label: (
|
||||
@@ -570,6 +574,19 @@ export function AgentRunDraftView({
|
||||
<CardTitle className="font-poppins text-lg">Input</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{/* Schedule recommendation tip */}
|
||||
{recommendedScheduleCron && !graph.has_external_trigger && (
|
||||
<div className="flex items-center gap-2 rounded-md border border-violet-200 bg-violet-50 p-3">
|
||||
<ClockIcon className="h-4 w-4 text-violet-600" />
|
||||
<p className="text-sm text-violet-800">
|
||||
<strong>Tip:</strong> For best results, run this agent{" "}
|
||||
{humanizeCronExpression(
|
||||
recommendedScheduleCron,
|
||||
).toLowerCase()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(agentPreset || graph.has_external_trigger) && (
|
||||
<>
|
||||
{/* Preset name and description */}
|
||||
@@ -677,11 +694,12 @@ export function AgentRunDraftView({
|
||||
title={`${graph.has_external_trigger ? "Trigger" : agentPreset ? "Preset" : "Run"} actions`}
|
||||
actions={runActions}
|
||||
/>
|
||||
<CronSchedulerDialog
|
||||
<ScheduleTaskDialog
|
||||
open={cronScheduleDialogOpen}
|
||||
setOpen={setCronScheduleDialogOpen}
|
||||
afterCronCreation={doSetupSchedule}
|
||||
onSubmit={doSetupSchedule}
|
||||
defaultScheduleName={graph.name}
|
||||
defaultCronExpression={recommendedScheduleCron || undefined}
|
||||
/>
|
||||
|
||||
{agentActions && agentActions.length > 0 && (
|
||||
|
||||
@@ -4654,6 +4654,10 @@
|
||||
},
|
||||
"name": { "type": "string", "title": "Name" },
|
||||
"description": { "type": "string", "title": "Description" },
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"nodes": {
|
||||
"items": { "$ref": "#/components/schemas/Node" },
|
||||
"type": "array",
|
||||
@@ -4690,6 +4694,10 @@
|
||||
},
|
||||
"name": { "type": "string", "title": "Name" },
|
||||
"description": { "type": "string", "title": "Description" },
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"nodes": {
|
||||
"items": { "$ref": "#/components/schemas/Node" },
|
||||
"type": "array",
|
||||
@@ -5155,6 +5163,10 @@
|
||||
},
|
||||
"name": { "type": "string", "title": "Name" },
|
||||
"description": { "type": "string", "title": "Description" },
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"nodes": {
|
||||
"items": { "$ref": "#/components/schemas/Node" },
|
||||
"type": "array",
|
||||
@@ -5491,6 +5503,10 @@
|
||||
},
|
||||
"name": { "type": "string", "title": "Name" },
|
||||
"description": { "type": "string", "title": "Description" },
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"forked_from_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Forked From Id"
|
||||
@@ -5561,6 +5577,10 @@
|
||||
},
|
||||
"name": { "type": "string", "title": "Name" },
|
||||
"description": { "type": "string", "title": "Description" },
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"nodes": {
|
||||
"items": { "$ref": "#/components/schemas/NodeModel" },
|
||||
"type": "array",
|
||||
@@ -5788,6 +5808,10 @@
|
||||
"is_latest_version": {
|
||||
"type": "boolean",
|
||||
"title": "Is Latest Version"
|
||||
},
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -6095,6 +6119,10 @@
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Last Edited"
|
||||
},
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -7170,6 +7198,10 @@
|
||||
"format": "date-time",
|
||||
"title": "Last Updated"
|
||||
},
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"active_version_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Active Version Id"
|
||||
@@ -7400,6 +7432,10 @@
|
||||
"changes_summary": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Changes Summary"
|
||||
},
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -7437,6 +7473,10 @@
|
||||
"changes_summary": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Changes Summary"
|
||||
},
|
||||
"recommended_schedule_cron": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Recommended Schedule Cron"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -117,6 +117,8 @@ const FlowEditor: React.FC<{
|
||||
setAgentName,
|
||||
agentDescription,
|
||||
setAgentDescription,
|
||||
agentRecommendedScheduleCron,
|
||||
setAgentRecommendedScheduleCron,
|
||||
savedAgent,
|
||||
availableBlocks,
|
||||
availableFlows,
|
||||
@@ -899,6 +901,10 @@ const FlowEditor: React.FC<{
|
||||
onDescriptionChange={setAgentDescription}
|
||||
agentName={agentName}
|
||||
onNameChange={setAgentName}
|
||||
agentRecommendedScheduleCron={agentRecommendedScheduleCron}
|
||||
onRecommendedScheduleCronChange={
|
||||
setAgentRecommendedScheduleCron
|
||||
}
|
||||
pinSavePopover={pinSavePopover}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import { StepHeader } from "../StepHeader";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import { Form, FormField } from "@/components/ui/form";
|
||||
import { CronExpressionDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import { CalendarClockIcon } from "lucide-react";
|
||||
import { Props, useAgentInfoStep } from "./useAgentInfoStep";
|
||||
import { ThumbnailImages } from "./components/ThumbnailImages";
|
||||
|
||||
@@ -32,6 +35,13 @@ export function AgentInfoStep({
|
||||
initialData,
|
||||
});
|
||||
|
||||
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] =
|
||||
React.useState(false);
|
||||
|
||||
const handleScheduleChange = (cronExpression: string) => {
|
||||
form.setValue("recommendedScheduleCron", cronExpression);
|
||||
};
|
||||
|
||||
const categoryOptions = [
|
||||
{ value: "productivity", label: "Productivity" },
|
||||
{ value: "writing", label: "Writing & Content" },
|
||||
@@ -153,6 +163,32 @@ export function AgentInfoStep({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="recommendedScheduleCron"
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Recommended Schedule
|
||||
</label>
|
||||
<p className="text-xs text-gray-600">
|
||||
Suggest when users should run this agent for best results
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setCronScheduleDialogOpen(true)}
|
||||
className="w-full justify-start text-sm"
|
||||
>
|
||||
<CalendarClockIcon className="mr-2 h-4 w-4" />
|
||||
{field.value
|
||||
? humanizeCronExpression(field.value)
|
||||
: "Set schedule"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between gap-4 pt-6">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -175,6 +211,14 @@ export function AgentInfoStep({
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<CronExpressionDialog
|
||||
open={cronScheduleDialogOpen}
|
||||
setOpen={setCronScheduleDialogOpen}
|
||||
onSubmit={handleScheduleChange}
|
||||
defaultCronExpression={form.getValues("recommendedScheduleCron") || ""}
|
||||
title="Recommended Schedule"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export const publishAgentSchema = z.object({
|
||||
.string()
|
||||
.min(1, "Description is required")
|
||||
.max(1000, "Description must be less than 1000 characters"),
|
||||
recommendedScheduleCron: z.string().optional(),
|
||||
});
|
||||
|
||||
export type PublishAgentFormData = z.infer<typeof publishAgentSchema>;
|
||||
@@ -54,4 +55,5 @@ export interface PublishAgentInfoInitialData {
|
||||
category: string;
|
||||
description: string;
|
||||
additionalImages?: string[];
|
||||
recommendedScheduleCron?: string;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function useAgentInfoStep({
|
||||
youtubeLink: "",
|
||||
category: "",
|
||||
description: "",
|
||||
recommendedScheduleCron: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,6 +65,7 @@ export function useAgentInfoStep({
|
||||
youtubeLink: initialData.youtubeLink,
|
||||
category: initialData.category,
|
||||
description: initialData.description,
|
||||
recommendedScheduleCron: initialData.recommendedScheduleCron || "",
|
||||
});
|
||||
}
|
||||
}, [initialData, form]);
|
||||
@@ -98,6 +100,7 @@ export function useAgentInfoStep({
|
||||
agent_version: selectedAgentVersion || 0,
|
||||
slug: data.slug.replace(/\s+/g, "-"),
|
||||
categories: filteredCategories,
|
||||
recommended_schedule_cron: data.recommendedScheduleCron || null,
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { Text } from "../../../../atoms/Text/Text";
|
||||
import { Button } from "../../../../atoms/Button/Button";
|
||||
import { StepHeader } from "../StepHeader";
|
||||
@@ -15,7 +16,12 @@ interface Props {
|
||||
onNext: (
|
||||
agentId: string,
|
||||
agentVersion: number,
|
||||
agentData: { name: string; description: string; imageSrc: string },
|
||||
agentData: {
|
||||
name: string;
|
||||
description: string;
|
||||
imageSrc: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
},
|
||||
) => void;
|
||||
onOpenBuilder: () => void;
|
||||
}
|
||||
@@ -147,11 +153,12 @@ export function AgentSelectStep({
|
||||
aria-pressed={selectedAgentId === agent.id}
|
||||
>
|
||||
<div className="relative h-32 bg-zinc-400 sm:h-40">
|
||||
<img
|
||||
<Image
|
||||
src={agent.imageSrc}
|
||||
alt={agent.name}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
className="object-cover"
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Agent {
|
||||
lastEdited: string;
|
||||
imageSrc: string;
|
||||
description: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
}
|
||||
|
||||
interface UseAgentSelectStepProps {
|
||||
@@ -15,7 +16,12 @@ interface UseAgentSelectStepProps {
|
||||
onNext: (
|
||||
agentId: string,
|
||||
agentVersion: number,
|
||||
agentData: { name: string; description: string; imageSrc: string },
|
||||
agentData: {
|
||||
name: string;
|
||||
description: string;
|
||||
imageSrc: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
},
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -43,6 +49,7 @@ export function useAgentSelectStep({
|
||||
lastEdited: agent.last_edited.toLocaleDateString(),
|
||||
imageSrc: agent.agent_image || "https://picsum.photos/300/200",
|
||||
description: agent.description || "",
|
||||
recommendedScheduleCron: agent.recommended_schedule_cron ?? null,
|
||||
}),
|
||||
)
|
||||
.sort(
|
||||
@@ -71,6 +78,7 @@ export function useAgentSelectStep({
|
||||
name: selectedAgent.name,
|
||||
description: selectedAgent.description,
|
||||
imageSrc: selectedAgent.imageSrc,
|
||||
recommendedScheduleCron: selectedAgent.recommendedScheduleCron,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ export const emptyModalState = {
|
||||
youtubeLink: "",
|
||||
category: "",
|
||||
description: "",
|
||||
recommendedScheduleCron: "",
|
||||
};
|
||||
|
||||
@@ -91,7 +91,12 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
|
||||
function handleNextFromSelect(
|
||||
agentId: string,
|
||||
agentVersion: number,
|
||||
agentData: { name: string; description: string; imageSrc: string },
|
||||
agentData: {
|
||||
name: string;
|
||||
description: string;
|
||||
imageSrc: string;
|
||||
recommendedScheduleCron: string | null;
|
||||
},
|
||||
) {
|
||||
setInitialData({
|
||||
...emptyModalState,
|
||||
@@ -100,6 +105,7 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
|
||||
description: agentData.description,
|
||||
thumbnailSrc: agentData.imageSrc,
|
||||
slug: agentData.name.replace(/ /g, "-"),
|
||||
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
|
||||
});
|
||||
|
||||
updateState({
|
||||
|
||||
@@ -8,22 +8,46 @@ import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/
|
||||
import { getTimezoneDisplayName } from "@/lib/timezone-utils";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
// Base type for cron expression only
|
||||
type CronOnlyCallback = (cronExpression: string) => void;
|
||||
// Type for cron expression with schedule name
|
||||
type CronWithNameCallback = (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
) => void;
|
||||
|
||||
type CronSchedulerDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
afterCronCreation: (cronExpression: string, scheduleName: string) => void;
|
||||
defaultScheduleName?: string;
|
||||
};
|
||||
defaultCronExpression?: string;
|
||||
title?: string;
|
||||
} & (
|
||||
| {
|
||||
// For cases where only cron expression is needed (builder, submission)
|
||||
mode: "cron-only";
|
||||
onSubmit: CronOnlyCallback;
|
||||
}
|
||||
| {
|
||||
// For cases where schedule name is required (agent run)
|
||||
mode: "with-name";
|
||||
onSubmit: CronWithNameCallback;
|
||||
defaultScheduleName?: string;
|
||||
}
|
||||
);
|
||||
|
||||
export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
|
||||
const {
|
||||
open,
|
||||
setOpen,
|
||||
defaultCronExpression = "",
|
||||
title = "Schedule Task",
|
||||
} = props;
|
||||
|
||||
export function CronSchedulerDialog({
|
||||
open,
|
||||
setOpen,
|
||||
afterCronCreation,
|
||||
defaultScheduleName = "",
|
||||
}: CronSchedulerDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const [cronExpression, setCronExpression] = useState<string>("");
|
||||
const [scheduleName, setScheduleName] = useState<string>(defaultScheduleName);
|
||||
const [scheduleName, setScheduleName] = useState<string>(
|
||||
props.mode === "with-name" ? props.defaultScheduleName || "" : "",
|
||||
);
|
||||
|
||||
// Get user's timezone
|
||||
const { data: userTimezone } = useGetV1GetUserTimezone({
|
||||
@@ -36,13 +60,15 @@ export function CronSchedulerDialog({
|
||||
// Reset state when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setScheduleName(defaultScheduleName);
|
||||
setCronExpression("");
|
||||
const defaultName =
|
||||
props.mode === "with-name" ? props.defaultScheduleName || "" : "";
|
||||
setScheduleName(defaultName);
|
||||
setCronExpression(defaultCronExpression);
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, props, defaultCronExpression]);
|
||||
|
||||
const handleDone = () => {
|
||||
if (!scheduleName.trim()) {
|
||||
if (props.mode === "with-name" && !scheduleName.trim()) {
|
||||
toast({
|
||||
title: "Please enter a schedule name",
|
||||
variant: "destructive",
|
||||
@@ -60,28 +86,38 @@ export function CronSchedulerDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
afterCronCreation(cronExpression, scheduleName);
|
||||
if (props.mode === "with-name") {
|
||||
props.onSubmit(cronExpression, scheduleName);
|
||||
} else {
|
||||
props.onSubmit(cronExpression);
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{ isOpen: open, set: setOpen }}
|
||||
title="Schedule Task"
|
||||
title={title}
|
||||
styling={{ maxWidth: "600px" }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex max-w-[448px] flex-col space-y-2">
|
||||
<label className="text-sm font-medium">Schedule Name</label>
|
||||
<Input
|
||||
value={scheduleName}
|
||||
onChange={(e) => setScheduleName(e.target.value)}
|
||||
placeholder="Enter a name for this schedule"
|
||||
/>
|
||||
</div>
|
||||
{props.mode === "with-name" && (
|
||||
<div className="flex max-w-[448px] flex-col space-y-2">
|
||||
<label className="text-sm font-medium">Schedule Name</label>
|
||||
<Input
|
||||
value={scheduleName}
|
||||
onChange={(e) => setScheduleName(e.target.value)}
|
||||
placeholder="Enter a name for this schedule"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CronScheduler onCronExpressionChange={setCronExpression} />
|
||||
<CronScheduler
|
||||
onCronExpressionChange={setCronExpression}
|
||||
initialCronExpression={defaultCronExpression}
|
||||
key={`${open}-${defaultCronExpression}`}
|
||||
/>
|
||||
|
||||
{/* Timezone info */}
|
||||
{userTimezone === "not-set" ? (
|
||||
@@ -114,3 +150,57 @@ export function CronSchedulerDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Convenience components for common use cases
|
||||
export function CronExpressionDialog({
|
||||
open,
|
||||
setOpen,
|
||||
onSubmit,
|
||||
defaultCronExpression,
|
||||
title = "Set Schedule",
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onSubmit: (cronExpression: string) => void;
|
||||
defaultCronExpression?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<CronSchedulerDialog
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
mode="cron-only"
|
||||
onSubmit={onSubmit}
|
||||
defaultCronExpression={defaultCronExpression}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScheduleTaskDialog({
|
||||
open,
|
||||
setOpen,
|
||||
onSubmit,
|
||||
defaultScheduleName,
|
||||
defaultCronExpression,
|
||||
title = "Schedule Task",
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onSubmit: (cronExpression: string, scheduleName: string) => void;
|
||||
defaultScheduleName?: string;
|
||||
defaultCronExpression?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<CronSchedulerDialog
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
mode="with-name"
|
||||
onSubmit={onSubmit}
|
||||
defaultScheduleName={defaultScheduleName}
|
||||
defaultCronExpression={defaultCronExpression}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,10 +38,12 @@ const months = [
|
||||
|
||||
type CronSchedulerProps = {
|
||||
onCronExpressionChange: (cronExpression: string) => void;
|
||||
initialCronExpression?: string;
|
||||
};
|
||||
|
||||
export function CronScheduler({
|
||||
onCronExpressionChange,
|
||||
initialCronExpression,
|
||||
}: CronSchedulerProps): React.ReactElement {
|
||||
const [frequency, setFrequency] = useState<CronFrequency>("daily");
|
||||
const [selectedMinute, setSelectedMinute] = useState<string>("0");
|
||||
@@ -53,7 +55,142 @@ export function CronScheduler({
|
||||
value: number;
|
||||
unit: "minutes" | "hours" | "days";
|
||||
}>({ value: 1, unit: "minutes" });
|
||||
const [showCustomDays, setShowCustomDays] = useState<boolean>(false);
|
||||
|
||||
// Parse initial cron expression and set state
|
||||
useEffect(() => {
|
||||
if (!initialCronExpression) {
|
||||
// Reset to defaults when no initial expression
|
||||
setFrequency("daily");
|
||||
setSelectedWeekDays([]);
|
||||
setSelectedMonthDays([]);
|
||||
setSelectedMonths([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = initialCronExpression.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return;
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
// Reset all state first
|
||||
setSelectedWeekDays([]);
|
||||
setSelectedMonthDays([]);
|
||||
setSelectedMonths([]);
|
||||
|
||||
// Parse patterns in order of specificity
|
||||
if (
|
||||
minute === "*" &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
setFrequency("every minute");
|
||||
} else if (
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/")
|
||||
) {
|
||||
setFrequency("hourly");
|
||||
setSelectedMinute(minute);
|
||||
} else if (
|
||||
minute.startsWith("*/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
setFrequency("custom");
|
||||
const interval = parseInt(minute.substring(2));
|
||||
if (!isNaN(interval)) {
|
||||
setCustomInterval({ value: interval, unit: "minutes" });
|
||||
}
|
||||
} else if (
|
||||
hour.startsWith("*/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
setFrequency("custom");
|
||||
const interval = parseInt(hour.substring(2));
|
||||
if (!isNaN(interval)) {
|
||||
setCustomInterval({ value: interval, unit: "hours" });
|
||||
}
|
||||
} else if (
|
||||
dayOfMonth.startsWith("*/") &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
setFrequency("custom");
|
||||
const interval = parseInt(dayOfMonth.substring(2));
|
||||
if (!isNaN(interval)) {
|
||||
setCustomInterval({ value: interval, unit: "days" });
|
||||
const hourNum = parseInt(hour);
|
||||
const minuteNum = parseInt(minute);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
|
||||
setSelectedTime(
|
||||
`${hourNum.toString().padStart(2, "0")}:${minuteNum.toString().padStart(2, "0")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
||||
setFrequency("daily");
|
||||
const hourNum = parseInt(hour);
|
||||
const minuteNum = parseInt(minute);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
|
||||
setSelectedTime(
|
||||
`${hourNum.toString().padStart(2, "0")}:${minuteNum.toString().padStart(2, "0")}`,
|
||||
);
|
||||
}
|
||||
} else if (dayOfWeek !== "*" && dayOfMonth === "*" && month === "*") {
|
||||
setFrequency("weekly");
|
||||
const hourNum = parseInt(hour);
|
||||
const minuteNum = parseInt(minute);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
|
||||
setSelectedTime(
|
||||
`${hourNum.toString().padStart(2, "0")}:${minuteNum.toString().padStart(2, "0")}`,
|
||||
);
|
||||
}
|
||||
const days = dayOfWeek
|
||||
.split(",")
|
||||
.map((d) => parseInt(d))
|
||||
.filter((d) => !isNaN(d));
|
||||
setSelectedWeekDays(days);
|
||||
} else if (dayOfMonth !== "*" && month === "*" && dayOfWeek === "*") {
|
||||
setFrequency("monthly");
|
||||
const hourNum = parseInt(hour);
|
||||
const minuteNum = parseInt(minute);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
|
||||
setSelectedTime(
|
||||
`${hourNum.toString().padStart(2, "0")}:${minuteNum.toString().padStart(2, "0")}`,
|
||||
);
|
||||
}
|
||||
const days = dayOfMonth
|
||||
.split(",")
|
||||
.map((d) => parseInt(d))
|
||||
.filter((d) => !isNaN(d) && d >= 1 && d <= 31);
|
||||
setSelectedMonthDays(days);
|
||||
} else if (dayOfMonth !== "*" && month !== "*" && dayOfWeek === "*") {
|
||||
setFrequency("yearly");
|
||||
const hourNum = parseInt(hour);
|
||||
const minuteNum = parseInt(minute);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
|
||||
setSelectedTime(
|
||||
`${hourNum.toString().padStart(2, "0")}:${minuteNum.toString().padStart(2, "0")}`,
|
||||
);
|
||||
}
|
||||
const months = month
|
||||
.split(",")
|
||||
.map((m) => parseInt(m))
|
||||
.filter((m) => !isNaN(m) && m >= 1 && m <= 12);
|
||||
setSelectedMonths(months);
|
||||
}
|
||||
}, [initialCronExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
const cronExpr = makeCronExpression({
|
||||
@@ -225,9 +362,8 @@ export function CronScheduler({
|
||||
<Label>Days of Month</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={!showCustomDays ? "default" : "outline"}
|
||||
variant={selectedMonthDays.length === 31 ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(false);
|
||||
setSelectedMonthDays(
|
||||
Array.from({ length: 31 }, (_, i) => i + 1),
|
||||
);
|
||||
@@ -236,9 +372,12 @@ export function CronScheduler({
|
||||
All Days
|
||||
</Button>
|
||||
<Button
|
||||
variant={showCustomDays ? "default" : "outline"}
|
||||
variant={
|
||||
selectedMonthDays.length < 31 && selectedMonthDays.length > 0
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => {
|
||||
setShowCustomDays(true);
|
||||
setSelectedMonthDays([]);
|
||||
}}
|
||||
>
|
||||
@@ -257,7 +396,7 @@ export function CronScheduler({
|
||||
Last Day
|
||||
</Button>
|
||||
</div>
|
||||
{showCustomDays && (
|
||||
{selectedMonthDays.length < 31 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 31 }, (_, i) => (
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -18,15 +18,20 @@ import {
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getGetV2ListMySubmissionsQueryKey } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { CronExpressionDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import { CalendarClockIcon } from "lucide-react";
|
||||
|
||||
interface SaveControlProps {
|
||||
agentMeta: GraphMeta | null;
|
||||
agentName: string;
|
||||
agentDescription: string;
|
||||
agentRecommendedScheduleCron: string;
|
||||
canSave: boolean;
|
||||
onSave: () => Promise<void>;
|
||||
onNameChange: (name: string) => void;
|
||||
onDescriptionChange: (description: string) => void;
|
||||
onRecommendedScheduleCronChange: (cron: string) => void;
|
||||
pinSavePopover: boolean;
|
||||
}
|
||||
|
||||
@@ -50,6 +55,8 @@ export const SaveControl = ({
|
||||
onNameChange,
|
||||
agentDescription,
|
||||
onDescriptionChange,
|
||||
agentRecommendedScheduleCron,
|
||||
onRecommendedScheduleCronChange,
|
||||
pinSavePopover,
|
||||
}: SaveControlProps) => {
|
||||
/**
|
||||
@@ -60,6 +67,11 @@ export const SaveControl = ({
|
||||
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] = useState(false);
|
||||
|
||||
const handleScheduleChange = (cronExpression: string) => {
|
||||
onRecommendedScheduleCronChange(cronExpression);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
@@ -106,50 +118,77 @@ export const SaveControl = ({
|
||||
sideOffset={15}
|
||||
align="start"
|
||||
data-id="save-control-popover-content"
|
||||
className="w-96 max-w-[400px]"
|
||||
>
|
||||
<Card className="border-none shadow-none dark:bg-slate-900">
|
||||
<CardContent className="p-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name" className="dark:text-gray-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Enter your agent name"
|
||||
className="col-span-3"
|
||||
value={agentName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
data-id="save-control-name-input"
|
||||
data-testid="save-control-name-input"
|
||||
maxLength={100}
|
||||
/>
|
||||
<Label htmlFor="description" className="dark:text-gray-300">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Your agent description"
|
||||
className="col-span-3"
|
||||
value={agentDescription}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
data-id="save-control-description-input"
|
||||
data-testid="save-control-description-input"
|
||||
maxLength={500}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="name" className="dark:text-gray-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Enter your agent name"
|
||||
value={agentName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
data-id="save-control-name-input"
|
||||
data-testid="save-control-name-input"
|
||||
maxLength={100}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description" className="dark:text-gray-300">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Your agent description"
|
||||
value={agentDescription}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
data-id="save-control-description-input"
|
||||
data-testid="save-control-description-input"
|
||||
maxLength={500}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="dark:text-gray-300">
|
||||
Recommended Schedule
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCronScheduleDialogOpen(true)}
|
||||
className="mt-1 w-full min-w-0 justify-start text-sm"
|
||||
data-id="save-control-recommended-schedule-button"
|
||||
data-testid="save-control-recommended-schedule-button"
|
||||
>
|
||||
<CalendarClockIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{agentRecommendedScheduleCron
|
||||
? humanizeCronExpression(agentRecommendedScheduleCron)
|
||||
: "Set schedule"}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{agentMeta?.version && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="version" className="dark:text-gray-300">
|
||||
Version
|
||||
</Label>
|
||||
<Input
|
||||
id="version"
|
||||
placeholder="Version"
|
||||
className="col-span-3"
|
||||
value={agentMeta?.version || "-"}
|
||||
disabled
|
||||
data-testid="save-control-version-output"
|
||||
className="mt-1"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -166,6 +205,13 @@ export const SaveControl = ({
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
<CronExpressionDialog
|
||||
open={cronScheduleDialogOpen}
|
||||
setOpen={setCronScheduleDialogOpen}
|
||||
onSubmit={handleScheduleChange}
|
||||
defaultCronExpression={agentRecommendedScheduleCron}
|
||||
title="Recommended Schedule"
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,8 @@ export default function useAgentGraph(
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
const [agentDescription, setAgentDescription] = useState<string>("");
|
||||
const [agentName, setAgentName] = useState<string>("");
|
||||
const [agentRecommendedScheduleCron, setAgentRecommendedScheduleCron] =
|
||||
useState<string>("");
|
||||
const [allBlocks, setAllBlocks] = useState<Block[]>([]);
|
||||
const [availableFlows, setAvailableFlows] = useState<GraphMeta[]>([]);
|
||||
const [updateQueue, setUpdateQueue] = useState<NodeExecutionResult[]>([]);
|
||||
@@ -155,6 +157,7 @@ export default function useAgentGraph(
|
||||
setSavedAgent(graph);
|
||||
setAgentName(graph.name);
|
||||
setAgentDescription(graph.description);
|
||||
setAgentRecommendedScheduleCron(graph.recommended_schedule_cron || "");
|
||||
|
||||
const getGraphName = (node: Node) => {
|
||||
if (node.input_default.agent_name) {
|
||||
@@ -597,6 +600,7 @@ export default function useAgentGraph(
|
||||
return {
|
||||
name: agentName || `New Agent ${new Date().toISOString()}`,
|
||||
description: agentDescription || "",
|
||||
recommended_schedule_cron: agentRecommendedScheduleCron || null,
|
||||
nodes: xyNodes.map(
|
||||
(node): NodeCreatable => ({
|
||||
id: node.id,
|
||||
@@ -615,6 +619,7 @@ export default function useAgentGraph(
|
||||
xyEdges,
|
||||
agentName,
|
||||
agentDescription,
|
||||
agentRecommendedScheduleCron,
|
||||
prepareNodeInputData,
|
||||
getToolFuncName,
|
||||
]);
|
||||
@@ -932,6 +937,8 @@ export default function useAgentGraph(
|
||||
setAgentName,
|
||||
agentDescription,
|
||||
setAgentDescription,
|
||||
agentRecommendedScheduleCron,
|
||||
setAgentRecommendedScheduleCron,
|
||||
savedAgent,
|
||||
availableBlocks,
|
||||
availableFlows,
|
||||
|
||||
@@ -298,6 +298,7 @@ export type GraphMeta = {
|
||||
is_active: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
recommended_schedule_cron: string | null;
|
||||
forked_from_id?: GraphID | null;
|
||||
forked_from_version?: number | null;
|
||||
input_schema: GraphIOSchema;
|
||||
@@ -431,6 +432,7 @@ export type LibraryAgent = {
|
||||
new_output: boolean;
|
||||
can_access_graph: boolean;
|
||||
is_latest_version: boolean;
|
||||
recommended_schedule_cron: string | null;
|
||||
} & (
|
||||
| {
|
||||
has_external_trigger: true;
|
||||
@@ -773,6 +775,7 @@ export type StoreSubmissionRequest = {
|
||||
description: string;
|
||||
categories: string[];
|
||||
changes_summary?: string;
|
||||
recommended_schedule_cron?: string | null;
|
||||
};
|
||||
|
||||
export type ProfileDetails = {
|
||||
@@ -815,6 +818,7 @@ export type MyAgent = {
|
||||
agent_image: string | null;
|
||||
last_edited: string;
|
||||
description: string;
|
||||
recommended_schedule_cron: string | null;
|
||||
};
|
||||
|
||||
export type MyAgentsResponse = {
|
||||
|
||||
Reference in New Issue
Block a user