Merge branch 'dev' into zamilmajdy/improve-sdm-add-anthropic

This commit is contained in:
Zamil Majdy
2025-03-06 13:20:06 +07:00
committed by GitHub
42 changed files with 627 additions and 538 deletions

View File

@@ -172,7 +172,9 @@ class GithubCreateCheckRunBlock(Block):
data.output = output_data
check_runs_url = f"{repo_url}/check-runs"
response = api.post(check_runs_url)
response = api.post(
check_runs_url, data=data.model_dump_json(exclude_none=True)
)
result = response.json()
return {
@@ -323,7 +325,9 @@ class GithubUpdateCheckRunBlock(Block):
data.output = output_data
check_run_url = f"{repo_url}/check-runs/{check_run_id}"
response = api.patch(check_run_url)
response = api.patch(
check_run_url, data=data.model_dump_json(exclude_none=True)
)
result = response.json()
return {

View File

@@ -144,7 +144,7 @@ class GithubCreateStatusBlock(Block):
data.description = description
status_url = f"{repo_url}/statuses/{sha}"
response = api.post(status_url, json=data)
response = api.post(status_url, data=data.model_dump_json(exclude_none=True))
result = response.json()
return {

View File

@@ -142,6 +142,16 @@ class IdeogramModelBlock(Block):
title="Color Palette Preset",
advanced=True,
)
custom_color_palette: Optional[list[str]] = SchemaField(
description=(
"Only available for model version V_2 or V_2_TURBO. Provide one or more color hex codes "
"(e.g., ['#000030', '#1C0C47', '#9900FF', '#4285F4', '#FFFFFF']) to define a custom color "
"palette. Only used if 'color_palette_name' is 'NONE'."
),
default=None,
title="Custom Color Palette",
advanced=True,
)
class Output(BlockSchema):
result: str = SchemaField(description="Generated image URL")
@@ -164,6 +174,13 @@ class IdeogramModelBlock(Block):
"style_type": StyleType.AUTO,
"negative_prompt": None,
"color_palette_name": ColorPalettePreset.NONE,
"custom_color_palette": [
"#000030",
"#1C0C47",
"#9900FF",
"#4285F4",
"#FFFFFF",
],
"credentials": TEST_CREDENTIALS_INPUT,
},
test_output=[
@@ -173,7 +190,7 @@ class IdeogramModelBlock(Block):
),
],
test_mock={
"run_model": lambda api_key, model_name, prompt, seed, aspect_ratio, magic_prompt_option, style_type, negative_prompt, color_palette_name: "https://ideogram.ai/api/images/test-generated-image-url.png",
"run_model": lambda api_key, model_name, prompt, seed, aspect_ratio, magic_prompt_option, style_type, negative_prompt, color_palette_name, custom_colors: "https://ideogram.ai/api/images/test-generated-image-url.png",
"upscale_image": lambda api_key, image_url: "https://ideogram.ai/api/images/test-upscaled-image-url.png",
},
test_credentials=TEST_CREDENTIALS,
@@ -195,6 +212,7 @@ class IdeogramModelBlock(Block):
style_type=input_data.style_type.value,
negative_prompt=input_data.negative_prompt,
color_palette_name=input_data.color_palette_name.value,
custom_colors=input_data.custom_color_palette,
)
# Step 2: Upscale the image if requested
@@ -217,6 +235,7 @@ class IdeogramModelBlock(Block):
style_type: str,
negative_prompt: Optional[str],
color_palette_name: str,
custom_colors: Optional[list[str]],
):
url = "https://api.ideogram.ai/generate"
headers = {
@@ -241,7 +260,11 @@ class IdeogramModelBlock(Block):
data["image_request"]["negative_prompt"] = negative_prompt
if color_palette_name != "NONE":
data["image_request"]["color_palette"] = {"name": color_palette_name}
data["color_palette"] = {"name": color_palette_name}
elif custom_colors:
data["color_palette"] = {
"members": [{"color_hex": color} for color in custom_colors]
}
try:
response = requests.post(url, json=data, headers=headers)
@@ -267,9 +290,7 @@ class IdeogramModelBlock(Block):
response = requests.post(
url,
headers=headers,
data={
"image_request": "{}", # Empty JSON object
},
data={"image_request": "{}"},
files=files,
)

View File

@@ -1,11 +1,10 @@
from collections import defaultdict
from datetime import datetime, timezone
from multiprocessing import Manager
from typing import Any, AsyncGenerator, Generator, Generic, Optional, Type, TypeVar
from typing import Any, AsyncGenerator, Generator, Generic, Type, TypeVar
from prisma import Json
from prisma.enums import AgentExecutionStatus
from prisma.errors import PrismaError
from prisma.models import (
AgentGraphExecution,
AgentNodeExecution,
@@ -342,28 +341,21 @@ async def update_execution_status(
return ExecutionResult.from_db(res)
async def get_execution(
execution_id: str, user_id: str
) -> Optional[AgentNodeExecution]:
"""
Get an execution by ID. Returns None if not found.
Args:
execution_id: The ID of the execution to retrieve
Returns:
The execution if found, None otherwise
"""
try:
execution = await AgentNodeExecution.prisma().find_unique(
where={
"id": execution_id,
"userId": user_id,
}
async def delete_execution(
graph_exec_id: str, user_id: str, soft_delete: bool = True
) -> None:
if soft_delete:
deleted_count = await AgentGraphExecution.prisma().update_many(
where={"id": graph_exec_id, "userId": user_id}, data={"isDeleted": True}
)
else:
deleted_count = await AgentGraphExecution.prisma().delete_many(
where={"id": graph_exec_id, "userId": user_id}
)
if deleted_count < 1:
raise DatabaseError(
f"Could not delete graph execution #{graph_exec_id}: not found"
)
return execution
except PrismaError:
return None
async def get_execution_results(graph_exec_id: str) -> list[ExecutionResult]:
@@ -385,15 +377,12 @@ async def get_executions_in_timerange(
try:
executions = await AgentGraphExecution.prisma().find_many(
where={
"AND": [
{
"startedAt": {
"gte": datetime.fromisoformat(start_time),
"lte": datetime.fromisoformat(end_time),
}
},
{"userId": user_id},
]
"startedAt": {
"gte": datetime.fromisoformat(start_time),
"lte": datetime.fromisoformat(end_time),
},
"userId": user_id,
"isDeleted": False,
},
include=GRAPH_EXECUTION_INCLUDE,
)

View File

@@ -597,9 +597,10 @@ async def get_graphs(
return graph_models
# TODO: move execution stuff to .execution
async def get_graphs_executions(user_id: str) -> list[GraphExecutionMeta]:
executions = await AgentGraphExecution.prisma().find_many(
where={"userId": user_id},
where={"isDeleted": False, "userId": user_id},
order={"createdAt": "desc"},
)
return [GraphExecutionMeta.from_db(execution) for execution in executions]
@@ -607,7 +608,7 @@ async def get_graphs_executions(user_id: str) -> list[GraphExecutionMeta]:
async def get_graph_executions(graph_id: str, user_id: str) -> list[GraphExecutionMeta]:
executions = await AgentGraphExecution.prisma().find_many(
where={"agentGraphId": graph_id, "userId": user_id},
where={"agentGraphId": graph_id, "isDeleted": False, "userId": user_id},
order={"createdAt": "desc"},
)
return [GraphExecutionMeta.from_db(execution) for execution in executions]
@@ -617,14 +618,14 @@ async def get_execution_meta(
user_id: str, execution_id: str
) -> GraphExecutionMeta | None:
execution = await AgentGraphExecution.prisma().find_first(
where={"id": execution_id, "userId": user_id}
where={"id": execution_id, "isDeleted": False, "userId": user_id}
)
return GraphExecutionMeta.from_db(execution) if execution else None
async def get_execution(user_id: str, execution_id: str) -> GraphExecution | None:
execution = await AgentGraphExecution.prisma().find_first(
where={"id": execution_id, "userId": user_id},
where={"id": execution_id, "isDeleted": False, "userId": user_id},
include={
"AgentNodeExecutions": {
"include": {"AgentNode": True, "Input": True, "Output": True},

View File

@@ -10,12 +10,14 @@ from autogpt_libs.auth.middleware import auth_middleware
from autogpt_libs.feature_flag.client import feature_flag
from autogpt_libs.utils.cache import thread_cached
from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response
from starlette.status import HTTP_204_NO_CONTENT
from typing_extensions import Optional, TypedDict
import backend.data.block
import backend.server.integrations.router
import backend.server.routers.analytics
import backend.server.v2.library.db as library_db
from backend.data import execution as execution_db
from backend.data import graph as graph_db
from backend.data.api_key import (
APIKeyError,
@@ -393,7 +395,8 @@ async def get_graph_all_versions(
path="/graphs", tags=["graphs"], dependencies=[Depends(auth_middleware)]
)
async def create_new_graph(
create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
create_graph: CreateGraph,
user_id: Annotated[str, Depends(get_user_id)],
) -> graph_db.GraphModel:
graph = graph_db.make_graph_model(create_graph.graph, user_id)
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
@@ -401,10 +404,9 @@ async def create_new_graph(
graph = await graph_db.create_graph(graph, user_id=user_id)
# Create a library agent for the new graph
await library_db.create_library_agent(
graph.id,
graph.version,
user_id,
library_agent = await library_db.create_library_agent(graph, user_id)
_ = asyncio.create_task(
library_db.add_generated_agent_image(graph, library_agent.id)
)
graph = await on_graph_activate(
@@ -621,11 +623,26 @@ async def get_graph_execution(
result = await graph_db.get_execution(execution_id=graph_exec_id, user_id=user_id)
if not result:
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
raise HTTPException(
status_code=404, detail=f"Graph execution #{graph_exec_id} not found."
)
return result
@v1_router.delete(
path="/executions/{graph_exec_id}",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
status_code=HTTP_204_NO_CONTENT,
)
async def delete_graph_execution(
graph_exec_id: str,
user_id: Annotated[str, Depends(get_user_id)],
) -> None:
await execution_db.delete_execution(graph_exec_id=graph_exec_id, user_id=user_id)
########################################################
##################### Schedules ########################
########################################################

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
from typing import Optional
@@ -7,14 +8,17 @@ import prisma.fields
import prisma.models
import prisma.types
import backend.data.graph
import backend.data.includes
import backend.server.model
import backend.server.v2.library.model as library_model
import backend.server.v2.store.exceptions as store_exceptions
import backend.server.v2.store.image_gen as store_image_gen
import backend.server.v2.store.media as store_media
from backend.util.settings import Config
logger = logging.getLogger(__name__)
config = Config()
async def list_library_agents(
@@ -168,17 +172,53 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
raise store_exceptions.DatabaseError("Failed to fetch library agent") from e
async def add_generated_agent_image(
graph: backend.data.graph.GraphModel,
library_agent_id: str,
) -> Optional[prisma.models.LibraryAgent]:
"""
Generates an image for the specified LibraryAgent and updates its record.
"""
user_id = graph.user_id
graph_id = graph.id
# Use .jpeg here since we are generating JPEG images
filename = f"agent_{graph_id}.jpeg"
try:
if not (image_url := await store_media.check_media_exists(user_id, filename)):
# Generate agent image as JPEG
if config.use_agent_image_generation_v2:
image = await asyncio.to_thread(
store_image_gen.generate_agent_image_v2, graph=graph
)
else:
image = await store_image_gen.generate_agent_image(agent=graph)
# Create UploadFile with the correct filename and content_type
image_file = fastapi.UploadFile(file=image, filename=filename)
image_url = await store_media.upload_media(
user_id=user_id, file=image_file, use_file_name=True
)
except Exception as e:
logger.warning(f"Error generating and uploading agent image: {e}")
return None
return await prisma.models.LibraryAgent.prisma().update(
where={"id": library_agent_id},
data={"imageUrl": image_url},
)
async def create_library_agent(
agent_id: str,
agent_version: int,
graph: backend.data.graph.GraphModel,
user_id: str,
) -> prisma.models.LibraryAgent:
"""
Adds an agent to the user's library (LibraryAgent table).
Args:
agent_id: The ID of the agent to add.
agent_version: The version of the agent to add.
agent: The agent/Graph to add to the library.
user_id: The user to whom the agent will be added.
Returns:
@@ -189,52 +229,19 @@ async def create_library_agent(
DatabaseError: If there's an error during creation or if image generation fails.
"""
logger.info(
f"Creating library agent for graph #{agent_id} v{agent_version}; "
f"Creating library agent for graph #{graph.id} v{graph.version}; "
f"user #{user_id}"
)
# Fetch agent graph
try:
agent = await prisma.models.AgentGraph.prisma().find_unique(
where={"graphVersionId": {"id": agent_id, "version": agent_version}}
)
except prisma.errors.PrismaError as e:
logger.exception("Database error fetching agent")
raise store_exceptions.DatabaseError("Failed to fetch agent") from e
if not agent:
raise store_exceptions.AgentNotFoundError(
f"Agent #{agent_id} v{agent_version} not found"
)
# Use .jpeg here since we are generating JPEG images
filename = f"agent_{agent_id}.jpeg"
try:
if not (image_url := await store_media.check_media_exists(user_id, filename)):
# Generate agent image as JPEG
image = await store_image_gen.generate_agent_image(agent=agent)
# Create UploadFile with the correct filename and content_type
image_file = fastapi.UploadFile(file=image, filename=filename)
image_url = await store_media.upload_media(
user_id=user_id, file=image_file, use_file_name=True
)
except Exception as e:
logger.warning(f"Error generating and uploading agent image: {e}")
image_url = None
try:
return await prisma.models.LibraryAgent.prisma().create(
data={
"imageUrl": image_url,
"isCreatedByUser": (user_id == agent.userId),
"isCreatedByUser": (user_id == graph.user_id),
"useGraphIsActiveVersion": True,
"User": {"connect": {"id": user_id}},
# "Creator": {"connect": {"id": agent.userId}},
"Agent": {
"connect": {
"graphVersionId": {"id": agent_id, "version": agent_version}
"graphVersionId": {"id": graph.id, "version": graph.version}
}
},
}

View File

@@ -4,14 +4,26 @@ from enum import Enum
import replicate
import replicate.exceptions
import requests
from prisma.models import AgentGraph
from replicate.helpers import FileOutput
from backend.blocks.ideogram import (
AspectRatio,
ColorPalettePreset,
IdeogramModelBlock,
IdeogramModelName,
MagicPromptOption,
StyleType,
UpscaleOption,
)
from backend.data.graph import Graph
from backend.data.model import CredentialsMetaInput, ProviderName
from backend.integrations.credentials_store import ideogram_credentials
from backend.util.request import requests
from backend.util.settings import Settings
logger = logging.getLogger(__name__)
settings = Settings()
class ImageSize(str, Enum):
@@ -22,6 +34,63 @@ class ImageStyle(str, Enum):
DIGITAL_ART = "digital art"
def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO:
"""
Generate an image for an agent using Ideogram model.
Returns:
str: The URL of the generated image
"""
if not ideogram_credentials.api_key:
raise ValueError("Missing Ideogram API key")
name = graph.name
description = f"{name} ({graph.description})" if graph.description else name
prompt = (
f"Create a visually striking retro-futuristic vector pop art illustration prominently featuring "
f'"{name}" in bold typography. The image clearly and literally depicts a {description}, '
f"along with recognizable objects directly associated with the primary function of a {name}. "
f"Ensure the imagery is concrete, intuitive, and immediately understandable, clearly conveying the "
f"purpose of a {name}. Maintain vibrant, limited-palette colors, sharp vector lines, geometric "
f"shapes, flat illustration techniques, and solid colors without gradients or shading. Preserve a "
f"retro-futuristic aesthetic influenced by mid-century futurism and 1960s psychedelia, "
f"prioritizing clear visual storytelling and thematic clarity above all else."
)
custom_colors = [
"#000030",
"#1C0C47",
"#9900FF",
"#4285F4",
"#FFFFFF",
]
# Run the Ideogram model block with the specified parameters
url = IdeogramModelBlock().run_once(
IdeogramModelBlock.Input(
credentials=CredentialsMetaInput(
id=ideogram_credentials.id,
provider=ProviderName.IDEOGRAM,
title=ideogram_credentials.title,
type=ideogram_credentials.type,
),
prompt=prompt,
ideogram_model_name=IdeogramModelName.V2,
aspect_ratio=AspectRatio.ASPECT_4_3,
magic_prompt_option=MagicPromptOption.OFF,
style_type=StyleType.AUTO,
upscale=UpscaleOption.NO_UPSCALE,
color_palette_name=ColorPalettePreset.NONE,
custom_color_palette=custom_colors,
seed=None,
negative_prompt=None,
),
"result",
credentials=ideogram_credentials,
)
return io.BytesIO(requests.get(url).content)
async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO:
"""
Generate an image for an agent using Flux model via Replicate API.
@@ -33,8 +102,6 @@ async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO:
io.BytesIO: The generated image as bytes
"""
try:
settings = Settings()
if not settings.secrets.replicate_api_key:
raise ValueError("Missing Replicate API key in settings")
@@ -71,14 +138,12 @@ async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO:
# If it's a URL string, fetch the image bytes
result_url = output[0]
response = requests.get(result_url)
response.raise_for_status()
image_bytes = response.content
elif isinstance(output, FileOutput):
image_bytes = output.read()
elif isinstance(output, str):
# Output is a URL
response = requests.get(output)
response.raise_for_status()
image_bytes = response.content
else:
raise RuntimeError("Unexpected output format from the model.")

View File

@@ -206,6 +206,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
description="The email address to use for sending emails",
)
use_agent_image_generation_v2: bool = Field(
default=True,
description="Whether to use the new agent image generation service",
)
@field_validator("platform_base_url", "frontend_base_url")
@classmethod
def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str:

View File

@@ -0,0 +1,6 @@
-- Add isDeleted column to AgentGraphExecution
ALTER TABLE "AgentGraphExecution"
ADD COLUMN "isDeleted"
BOOLEAN
NOT NULL
DEFAULT false;

View File

@@ -289,6 +289,8 @@ model AgentGraphExecution {
updatedAt DateTime? @updatedAt
startedAt DateTime?
isDeleted Boolean @default(false)
executionStatus AgentExecutionStatus @default(COMPLETED)
agentGraphId String

View File

@@ -9,6 +9,7 @@ const nextConfig = {
"upload.wikimedia.org",
"storage.googleapis.com",
"ideogram.ai", // for generated images
"picsum.photos", // for placeholder images
"dummyimage.com", // for placeholder images
"placekitten.com", // for placeholder images

View File

@@ -30,7 +30,7 @@ export default function Error({
again later or contact support if the issue persists.
</p>
<div className="mt-6 flex flex-row justify-center gap-4">
<Button onClick={reset} variant="secondary">
<Button onClick={reset} variant="outline">
Retry
</Button>
<Button>

View File

@@ -4,26 +4,26 @@
@layer base {
:root {
--background: 220 14.29% 95.88%;
--foreground: 240 3.7% 15.88%;
--background: 0 0% 99.6%; /* #FEFEFE */
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 3.7% 15.88%;
--primary: 240 3.7% 15.88%;
--primary-foreground: 0 0% 98.04%;
--secondary: 240 5.88% 90%;
--secondary-foreground: 240 3.7% 15.88%;
--muted: 240 5.88% 90%;
--muted-foreground: 240 5.2% 33.92%;
--accent: 220 13.04% 90.98%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.24% 60.2%;
--destructive-foreground: 0 0% 100%;
--border: 240 5.88% 90%;
--input: 240 4.88% 83.92%;
--ring: 216.92 22.29% 65.69%;
--radius: 1rem;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 262 83% 58%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 85%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;

View File

@@ -5,33 +5,37 @@ import { useParams, useRouter } from "next/navigation";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
GraphMeta,
LibraryAgent,
LibraryAgentID,
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import DeleteConfirmDialog from "@/components/agptui/delete-confirm-dialog";
import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
import AgentRunDetailsView from "@/components/agents/agent-run-details-view";
import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list";
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
import AgentDeleteConfirmDialog from "@/components/agents/agent-delete-confirm-dialog";
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: LibraryAgentID } = useParams();
const router = useRouter();
const api = useBackendAPI();
// ============================ STATE =============================
const [graph, setGraph] = useState<GraphMeta | null>(null);
const [agent, setAgent] = useState<LibraryAgent | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedView, selectView] = useState<{
type: "run" | "schedule";
id?: string;
}>({ type: "run" });
const [selectedView, selectView] = useState<
| { type: "run"; id?: GraphExecutionID }
| { type: "schedule"; id: ScheduleID }
>({ type: "run" });
const [selectedRun, setSelectedRun] = useState<
GraphExecution | GraphExecutionMeta | null
>(null);
@@ -41,12 +45,14 @@ export default function AgentRunsPage(): React.ReactElement {
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] =
useState<boolean>(false);
const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] =
useState<GraphExecutionMeta | null>(null);
const openRunDraftView = useCallback(() => {
selectView({ type: "run" });
}, []);
const selectRun = useCallback((id: string) => {
const selectRun = useCallback((id: GraphExecutionID) => {
selectView({ type: "run", id });
}, []);
@@ -114,20 +120,40 @@ export default function AgentRunsPage(): React.ReactElement {
fetchSchedules();
}, [fetchSchedules]);
const removeSchedule = useCallback(
async (scheduleId: string) => {
const removedSchedule = await api.deleteSchedule(scheduleId);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},
[schedules, api],
);
/* TODO: use websockets instead of polling - https://github.com/Significant-Gravitas/AutoGPT/issues/8782 */
useEffect(() => {
const intervalId = setInterval(() => fetchAgents(), 5000);
return () => clearInterval(intervalId);
}, [fetchAgents]);
// =========================== ACTIONS ============================
const deleteRun = useCallback(
async (run: GraphExecutionMeta) => {
if (run.status == "RUNNING" || run.status == "QUEUED") {
await api.stopGraphExecution(run.graph_id, run.execution_id);
}
await api.deleteGraphExecution(run.execution_id);
setConfirmingDeleteAgentRun(null);
if (selectedView.type == "run" && selectedView.id == run.execution_id) {
openRunDraftView();
}
setAgentRuns(
agentRuns.filter((r) => r.execution_id !== run.execution_id),
);
},
[agentRuns, api, selectedView, openRunDraftView],
);
const deleteSchedule = useCallback(
async (scheduleID: ScheduleID) => {
const removedSchedule = await api.deleteSchedule(scheduleID);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},
[schedules, api],
);
const agentActions: ButtonAction[] = useMemo(
() => [
{
@@ -160,7 +186,9 @@ export default function AgentRunsPage(): React.ReactElement {
selectedView={selectedView}
onSelectRun={selectRun}
onSelectSchedule={selectSchedule}
onDraftNewRun={openRunDraftView}
onSelectDraftNewRun={openRunDraftView}
onDeleteRun={setConfirmingDeleteAgentRun}
onDeleteSchedule={(id) => deleteSchedule(id)}
/>
<div className="flex-1">
@@ -180,6 +208,7 @@ export default function AgentRunsPage(): React.ReactElement {
graph={graph}
run={selectedRun}
agentActions={agentActions}
deleteRun={() => setConfirmingDeleteAgentRun(selectedRun)}
/>
)
) : selectedView.type == "run" ? (
@@ -199,7 +228,8 @@ export default function AgentRunsPage(): React.ReactElement {
)
) : null) || <p>Loading...</p>}
<AgentDeleteConfirmDialog
<DeleteConfirmDialog
entityType="agent"
open={agentDeleteDialogOpen}
onOpenChange={setAgentDeleteDialogOpen}
onDoDelete={() =>
@@ -209,6 +239,15 @@ export default function AgentRunsPage(): React.ReactElement {
.then(() => router.push("/library"))
}
/>
<DeleteConfirmDialog
entityType="agent run"
open={!!confirmingDeleteAgentRun}
onOpenChange={(open) => !open && setConfirmingDeleteAgentRun(null)}
onDoDelete={() =>
confirmingDeleteAgentRun && deleteRun(confirmingDeleteAgentRun)
}
/>
</div>
</div>
);

View File

@@ -5,6 +5,7 @@ import {
GraphExecutionMeta,
Schedule,
LibraryAgent,
ScheduleID,
} from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card";
@@ -35,7 +36,7 @@ const Monitor = () => {
}, [api]);
const removeSchedule = useCallback(
async (scheduleId: string) => {
async (scheduleId: ScheduleID) => {
const removedSchedule = await api.deleteSchedule(scheduleId);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},

View File

@@ -837,7 +837,11 @@ export const CustomNode = React.memo(
data={data.executionResults!.at(-1)?.data || {}}
/>
<div className="flex justify-end">
<Button variant="ghost" onClick={handleOutputClick}>
<Button
variant="ghost"
onClick={handleOutputClick}
className="border border-gray-300"
>
View More
</Button>
</div>

View File

@@ -26,7 +26,12 @@ import {
import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import { BlockUIType, formatEdgeID, GraphID } from "@/lib/autogpt-server-api";
import {
BlockUIType,
formatEdgeID,
GraphExecutionID,
GraphID,
} from "@/lib/autogpt-server-api";
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
@@ -86,7 +91,9 @@ const FlowEditor: React.FC<{
const [visualizeBeads, setVisualizeBeads] = useState<
"no" | "static" | "animate"
>("animate");
const [flowExecutionID, setFlowExecutionID] = useState<string | undefined>();
const [flowExecutionID, setFlowExecutionID] = useState<
GraphExecutionID | undefined
>();
const {
agentName,
setAgentName,
@@ -164,7 +171,9 @@ const FlowEditor: React.FC<{
if (params.get("open_scheduling") === "true") {
setOpenCron(true);
}
setFlowExecutionID(params.get("flowExecutionID") || undefined);
setFlowExecutionID(
(params.get("flowExecutionID") as GraphExecutionID) || undefined,
);
}, [params]);
useEffect(() => {

View File

@@ -1,5 +1,6 @@
import React from "react";
import { Clock, LogOut } from "lucide-react";
import React, { useState } from "react";
import { Button } from "./ui/button";
import { Clock, LogOut, ChevronLeft } from "lucide-react";
import { IconPlay, IconSquare } from "@/components/ui/icons";
import {
Tooltip,
@@ -7,7 +8,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { FaSpinner } from "react-icons/fa";
import { Button } from "@/components/ui/button";
interface PrimaryActionBarProps {
onClickAgentOutputs: () => void;
@@ -41,7 +41,12 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
<div className={`flex gap-1 md:gap-4`}>
<Tooltip key="ViewOutputs" delayDuration={500}>
<TooltipTrigger asChild>
<Button onClick={onClickAgentOutputs} variant="outline">
<Button
className="flex items-center gap-2"
onClick={onClickAgentOutputs}
size="primary"
variant="outline"
>
<LogOut className="hidden h-5 w-5 md:flex" />
<span className="text-sm font-medium md:text-lg">
Agent Outputs{" "}
@@ -55,7 +60,9 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
<Tooltip key="RunAgent" delayDuration={500}>
<TooltipTrigger asChild>
<Button
className="flex items-center gap-2"
onClick={runButtonOnClick}
size="primary"
style={{
background: isRunning ? "#DF4444" : "#7544DF",
opacity: isDisabled ? 0.5 : 1,
@@ -75,7 +82,9 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
<Tooltip key="ScheduleAgent" delayDuration={500}>
<TooltipTrigger asChild>
<Button
className="flex items-center gap-2"
onClick={onClickScheduleButton}
size="primary"
disabled={isScheduling}
variant="outline"
data-id="primary-action-schedule-agent"

View File

@@ -57,14 +57,23 @@ const TallyPopupSimple = () => {
return (
<div className="fixed bottom-1 right-6 z-50 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
{show_tutorial && <Button onClick={resetTutorial}>Tutorial</Button>}
{show_tutorial && (
<Button
variant="default"
onClick={resetTutorial}
className="mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left font-inter text-lg font-medium leading-6"
>
Tutorial
</Button>
)}
<Button
size="icon"
className="h-14 w-14 rounded-full bg-[rgba(65,65,64,1)]"
variant="default"
data-tally-open="3yx2L0"
data-tally-emoji-text="👋"
data-tally-emoji-animation="wave"
>
<QuestionMarkCircledIcon />
<QuestionMarkCircledIcon className="h-14 w-14" />
<span className="sr-only">Reach Out</span>
</Button>
</div>

View File

@@ -23,10 +23,12 @@ export default function AgentRunDetailsView({
graph,
run,
agentActions,
deleteRun,
}: {
graph: GraphMeta;
run: GraphExecution | GraphExecutionMeta;
agentActions: ButtonAction[];
deleteRun: () => void;
}): React.ReactNode {
const api = useBackendAPI();
@@ -86,6 +88,11 @@ export default function AgentRunDetailsView({
[api, graph, agentRunInputs],
);
const stopRun = useCallback(
() => api.stopGraphExecution(graph.id, run.execution_id),
[api, graph.id, run.execution_id],
);
const agentRunOutputs:
| Record<
string,
@@ -109,9 +116,23 @@ export default function AgentRunDetailsView({
);
}, [graph, run, runStatus]);
const runActions: { label: string; callback: () => void }[] = useMemo(
() => [{ label: "Run again", callback: () => runAgain() }],
[runAgain],
const runActions: ButtonAction[] = useMemo(
() => [
...(["running", "queued"].includes(runStatus)
? ([
{
label: "Stop run",
variant: "secondary",
callback: stopRun,
},
] satisfies ButtonAction[])
: []),
...(["success", "failed", "stopped"].includes(runStatus)
? [{ label: "Run again", callback: runAgain }]
: []),
{ label: "Delete run", variant: "secondary", callback: deleteRun },
],
[runStatus, runAgain, stopRun, deleteRun],
);
return (
@@ -190,7 +211,11 @@ export default function AgentRunDetailsView({
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Run actions</h3>
{runActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}

View File

@@ -2,7 +2,7 @@
import React, { useCallback, useMemo, useState } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { GraphExecutionID, GraphMeta } from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -15,7 +15,7 @@ export default function AgentRunDraftView({
agentActions,
}: {
graph: GraphMeta;
onRun: (runID: string) => void;
onRun: (runID: GraphExecutionID) => void;
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();

View File

@@ -18,24 +18,24 @@ import AgentRunStatusChip, {
} from "@/components/agents/agent-run-status-chip";
export type AgentRunSummaryProps = {
agentID: string;
agentRunID: string;
status: AgentRunStatus;
title: string;
timestamp: number | Date;
selected?: boolean;
onClick?: () => void;
// onRename: () => void;
onDelete: () => void;
className?: string;
};
export default function AgentRunSummaryCard({
agentID,
agentRunID,
status,
title,
timestamp,
selected = false,
onClick,
// onRename,
onDelete,
className,
}: AgentRunSummaryProps): React.ReactElement {
return (
@@ -55,32 +55,24 @@ export default function AgentRunSummaryCard({
{title}
</h3>
{/* <DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-5 w-5 p-0">
<MoreVertical className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
// TODO: implement
>
Pin into a template
{/* {onPinAsPreset && (
<DropdownMenuItem onClick={onPinAsPreset}>
Pin as a preset
</DropdownMenuItem>
)} */}
<DropdownMenuItem
// TODO: implement
>
Rename
</DropdownMenuItem>
{/* <DropdownMenuItem onClick={onRename}>Rename</DropdownMenuItem> */}
<DropdownMenuItem
// TODO: implement
>
Delete
</DropdownMenuItem>
<DropdownMenuItem onClick={onDelete}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu> */}
</DropdownMenu>
</div>
<p

View File

@@ -4,9 +4,11 @@ import { Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import {
GraphExecutionID,
GraphExecutionMeta,
LibraryAgent,
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -21,9 +23,11 @@ interface AgentRunsSelectorListProps {
agentRuns: GraphExecutionMeta[];
schedules: Schedule[];
selectedView: { type: "run" | "schedule"; id?: string };
onSelectRun: (id: string) => void;
onSelectRun: (id: GraphExecutionID) => void;
onSelectSchedule: (schedule: Schedule) => void;
onDraftNewRun: () => void;
onSelectDraftNewRun: () => void;
onDeleteRun: (id: GraphExecutionMeta) => void;
onDeleteSchedule: (id: ScheduleID) => void;
className?: string;
}
@@ -34,7 +38,9 @@ export default function AgentRunsSelectorList({
selectedView,
onSelectRun,
onSelectSchedule,
onDraftNewRun,
onSelectDraftNewRun,
onDeleteRun,
onDeleteSchedule,
className,
}: AgentRunsSelectorListProps): React.ReactElement {
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
@@ -51,7 +57,7 @@ export default function AgentRunsSelectorList({
? "agpt-card-selected text-accent"
: "")
}
onClick={onDraftNewRun}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
@@ -91,7 +97,7 @@ export default function AgentRunsSelectorList({
? "agpt-card-selected text-accent"
: "")
}
onClick={onDraftNewRun}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
@@ -102,13 +108,12 @@ export default function AgentRunsSelectorList({
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"
key={i}
agentID={run.graph_id}
agentRunID={run.execution_id}
status={agentRunStatusMap[run.status]}
title={agent.name}
timestamp={run.started_at}
selected={selectedView.id === run.execution_id}
onClick={() => onSelectRun(run.execution_id)}
onDelete={() => onDeleteRun(run)}
/>
))
: schedules
@@ -117,13 +122,12 @@ export default function AgentRunsSelectorList({
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"
key={i}
agentID={schedule.graph_id}
agentRunID={schedule.id}
status="scheduled"
title={schedule.name}
timestamp={schedule.next_run_time}
selected={selectedView.id === schedule.id}
onClick={() => onSelectSchedule(schedule)}
onDelete={() => onDeleteSchedule(schedule.id)}
/>
))}
</div>

View File

@@ -1,7 +1,11 @@
"use client";
import React, { useCallback, useMemo } from "react";
import { GraphMeta, Schedule } from "@/lib/autogpt-server-api";
import {
GraphExecutionID,
GraphMeta,
Schedule,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import type { ButtonAction } from "@/components/agptui/types";
@@ -18,7 +22,7 @@ export default function AgentScheduleDetailsView({
}: {
graph: GraphMeta;
schedule: Schedule;
onForcedRun: (runID: string) => void;
onForcedRun: (runID: GraphExecutionID) => void;
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();

View File

@@ -2,7 +2,6 @@
import * as React from "react";
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
import { Button } from "@/components/ui/button";
interface BecomeACreatorProps {
title?: string;
description?: string;
@@ -47,7 +46,16 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
</p>
<PublishAgentPopout
trigger={<Button onClick={handleButtonClick}>{buttonText}</Button>}
trigger={
<button
onClick={handleButtonClick}
className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5"
>
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
{buttonText}
</span>
</button>
}
/>
</div>
</div>

View File

@@ -1,3 +1,5 @@
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
@@ -5,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300 font-neue leading-9 tracking-tight",
"inline-flex items-center whitespace-nowrap overflow-hidden font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300 font-neue leading-9 tracking-tight",
{
variants: {
variant: {
@@ -55,14 +57,39 @@ export interface ButtonProps
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, asChild = false, onClick, ...props }, ref) => {
const [isLoading, setIsLoading] = React.useState(false);
const Comp = asChild ? Slot : "button";
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
if (!onClick) return;
try {
setIsLoading(true);
const result: any = onClick(e);
if (result instanceof Promise) {
await result;
}
} finally {
setIsLoading(false);
}
};
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
className={cn("relative", buttonVariants({ variant, size, className }))}
ref={ref}
onClick={handleClick}
disabled={props.disabled}
{...props}
/>
>
{props.children}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/60">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
)}
</Comp>
);
},
);

View File

@@ -27,33 +27,35 @@ export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
data-testid="featured-store-card"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={backgroundColor}
className={`flex h-full flex-col ${backgroundColor}`}
>
<CardHeader>
<CardTitle>{agent.agent_name}</CardTitle>
<CardDescription>{agent.description}</CardDescription>
<CardTitle className="line-clamp-2 text-base sm:text-xl">
{agent.agent_name}
</CardTitle>
<CardDescription className="text-sm">
By {agent.creator}
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative h-[397px] w-full overflow-hidden rounded-xl">
<CardContent className="flex-1 p-4">
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-xl">
<Image
src={agent.agent_image || "/AUTOgpt_Logo_dark.png"}
alt={`${agent.agent_name} preview`}
fill
sizes="100%"
className={`object-cover transition-opacity duration-200 ${
isHovered ? "opacity-0" : "opacity-100"
}`}
/>
<div
className={`transition-opacity duration-200 ${isHovered ? "opacity-0" : "opacity-100"}`}
>
<Image
src={agent.agent_image || "/AUTOgpt_Logo_dark.png"}
alt={`${agent.agent_name} preview`}
fill
sizes="100%"
className="rounded-xl object-cover"
/>
</div>
<div
className={`absolute inset-0 overflow-y-auto transition-opacity duration-200 ${
className={`absolute inset-0 overflow-y-auto p-4 transition-opacity duration-200 ${
isHovered ? "opacity-100" : "opacity-0"
} rounded-xl dark:bg-neutral-700`}
}`}
>
<p className="text-base text-neutral-800 dark:text-neutral-200">
<CardDescription className="line-clamp-[6] text-xs sm:line-clamp-[8] sm:text-sm">
{agent.description}
</p>
</CardDescription>
</div>
</div>
</CardContent>
@@ -63,13 +65,7 @@ export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
</div>
<div className="flex items-center gap-1.5">
<p>{agent.rating.toFixed(1) ?? "0.0"}</p>
<div
className="inline-flex items-center justify-start gap-px"
role="img"
aria-label={`Rating: ${agent.rating.toFixed(1)} out of 5 stars`}
>
{StarRatingIcons(agent.rating)}
</div>
{StarRatingIcons(agent.rating)}
</div>
</CardFooter>
</Card>

View File

@@ -5,12 +5,12 @@ import { useState } from "react";
import Image from "next/image";
import { Button } from "./Button";
import { IconPersonFill } from "@/components/ui/icons";
import { CreatorDetails, ProfileDetails } from "@/lib/autogpt-server-api/types";
import { Separator } from "@/components/ui/separator";
import useSupabase from "@/hooks/useSupabase";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Button } from "@/components/ui/button";
export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -245,14 +245,21 @@ export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
<div className="flex h-[50px] items-center justify-end gap-3 py-8">
<Button
type="button"
variant="secondary"
className="font-circular h-[50px] rounded-[35px] bg-neutral-200 px-6 py-3 text-base font-medium text-neutral-800 transition-colors hover:bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
onClick={() => {
setProfileData(profile);
}}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting} onClick={submitForm}>
<Button
type="submit"
disabled={isSubmitting}
className="font-circular h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
onClick={submitForm}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>

View File

@@ -46,45 +46,41 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
};
return (
<div className="flex w-full flex-col items-center justify-center">
<div className="w-[99vw]">
<h2 className="mx-auto mb-8 max-w-[1360px] px-4 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
Featured agents
</h2>
<section className="mx-auto w-full max-w-7xl px-4 pb-16">
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
Featured agents
</h2>
<div className="w-[99vw] pb-[60px]">
<Carousel
className="mx-auto pb-10"
opts={{
align: "center",
containScroll: "trimSnaps",
}}
>
<CarouselContent className="ml-[calc(50vw-690px)]">
{featuredAgents.map((agent, index) => (
<CarouselItem
key={index}
className="max-w-[460px] flex-[0_0_auto]"
>
<Link
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
>
<FeaturedAgentCard
agent={agent}
backgroundColor={getBackgroundColor(index)}
/>
</Link>
</CarouselItem>
))}
</CarouselContent>
<div className="relative mx-auto w-full max-w-[1360px] pl-4">
<CarouselIndicator />
<CarouselPrevious afterClick={handlePrevSlide} />
<CarouselNext afterClick={handleNextSlide} />
</div>
</Carousel>
<Carousel
opts={{
align: "center",
containScroll: "trimSnaps",
}}
>
<CarouselContent>
{featuredAgents.map((agent, index) => (
<CarouselItem
key={index}
className="h-[480px] md:basis-1/2 lg:basis-1/3"
>
<Link
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
className="block h-full"
>
<FeaturedAgentCard
agent={agent}
backgroundColor={getBackgroundColor(index)}
/>
</Link>
</CarouselItem>
))}
</CarouselContent>
<div className="relative mt-4">
<CarouselIndicator />
<CarouselPrevious afterClick={handlePrevSlide} />
<CarouselNext afterClick={handleNextSlide} />
</div>
</div>
</div>
</Carousel>
</section>
);
};

View File

@@ -8,25 +8,41 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
export default function AgentDeleteConfirmDialog({
export default function DeleteConfirmDialog({
entityType,
entityName,
open,
onOpenChange,
onDoDelete,
isIrreversible = true,
className,
}: {
entityType: string;
entityName?: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onDoDelete: () => void;
isIrreversible?: boolean;
className?: string;
}): React.ReactNode {
const displayType = entityType
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={className}>
<DialogHeader>
<DialogTitle>Delete Agent</DialogTitle>
<DialogTitle>
Delete {displayType} {entityName && `"${entityName}"`}
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this agent? <br />
This action cannot be undone.
Are you sure you want to delete this {entityType}?
{isIrreversible && (
<b>
<br /> This action cannot be undone.
</b>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>

View File

@@ -19,7 +19,7 @@ export default function AuthButton({
}: Props) {
return (
<Button
className="w-full"
className="mt-2 w-full self-stretch rounded-md bg-slate-900 px-4 py-2"
type={type}
disabled={isLoading || disabled}
onClick={onClick}

View File

@@ -155,6 +155,7 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
data-id="blocks-control-popover-trigger"
data-testid="blocks-control-blocks-button"
name="Blocks"
className="dark:hover:bg-slate-800"
>
<IconToyBrick />
</Button>

View File

@@ -61,6 +61,7 @@ export const ControlPanel = ({
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
>
{control.icon}
<span className="sr-only">{control.label}</span>

View File

@@ -93,7 +93,7 @@ export const SaveControl = ({
data-testid="blocks-control-save-button"
name="Save"
>
<IconSave />
<IconSave className="dark:text-gray-300" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
@@ -153,6 +153,7 @@ export const SaveControl = ({
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button
className="w-full dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleSave}
data-id="save-control-save-agent"
data-testid="save-control-save-agent-button"

View File

@@ -1,4 +1,4 @@
import { LibraryAgent, Schedule } from "@/lib/autogpt-server-api";
import { LibraryAgent, Schedule, ScheduleID } from "@/lib/autogpt-server-api";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
@@ -36,7 +36,7 @@ import { Label } from "../ui/label";
interface SchedulesTableProps {
schedules: Schedule[];
agents: LibraryAgent[];
onRemoveSchedule: (scheduleId: string, enabled: boolean) => void;
onRemoveSchedule: (scheduleId: ScheduleID, enabled: boolean) => void;
sortColumn: keyof Schedule;
sortDirection: "asc" | "desc";
onSort: (column: keyof Schedule) => void;
@@ -73,7 +73,7 @@ export const SchedulesTable = ({
return String(bValue).localeCompare(String(aValue));
});
const handleToggleSchedule = (scheduleId: string, enabled: boolean) => {
const handleToggleSchedule = (scheduleId: ScheduleID, enabled: boolean) => {
onRemoveSchedule(scheduleId, enabled);
if (!enabled) {
toast({

View File

@@ -382,7 +382,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
{/* Form Actions */}
<div className="flex justify-end gap-4">
<Button
variant="secondary"
variant="outline"
type="button"
onClick={onCancel}
disabled={form.formState.isSubmitting}

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@/components/ui/button";
import { Button } from "./button";
import { userEvent, within, expect } from "@storybook/test";
const meta = {
@@ -23,8 +23,7 @@ const meta = {
},
size: {
control: "select",
options: ["default", "sm", "lg", "icon"],
description: "Button size variants. The 'primary' size is deprecated.",
options: ["default", "sm", "lg", "primary", "icon"],
},
disabled: {
control: "boolean",
@@ -46,32 +45,6 @@ export const Default: Story = {
args: {
children: "Button",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Button/i });
// Test default styling
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("rounded-full"),
);
// Test SVG styling is present
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:size-4"),
);
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:shrink-0"),
);
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:pointer-events-none"),
);
},
};
export const Interactive: Story = {
@@ -84,33 +57,14 @@ export const Interactive: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Interactive Button/i });
// Test interaction
await userEvent.click(button);
await expect(button).toHaveFocus();
// Test styling matches the updated component
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("rounded-full"),
);
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("gap-2"),
);
// Test other key button styles
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("inline-flex items-center justify-center"),
);
},
};
export const Variants: Story = {
render: (args) => (
<div className="flex flex-wrap gap-4">
<div className="flex flex-wrap gap-2">
<Button {...args} variant="default">
Default
</Button>
@@ -135,8 +89,6 @@ export const Variants: Story = {
const canvas = within(canvasElement);
const buttons = canvas.getAllByRole("button");
await expect(buttons).toHaveLength(6);
// Test hover states
for (const button of buttons) {
await userEvent.hover(button);
await expect(button).toHaveAttribute(
@@ -144,81 +96,47 @@ export const Variants: Story = {
expect.stringContaining("hover:"),
);
}
// Test rounded-full styling on appropriate variants
const roundedVariants = [
"default",
"destructive",
"outline",
"secondary",
"ghost",
];
for (let i = 0; i < 5; i++) {
await expect(buttons[i]).toHaveAttribute(
"class",
expect.stringContaining("rounded-full"),
);
}
// Link variant should not have rounded-full
await expect(buttons[5]).not.toHaveAttribute(
"class",
expect.stringContaining("rounded-full"),
);
},
};
export const Sizes: Story = {
render: (args) => (
<div className="flex flex-wrap items-center gap-4">
<Button {...args} size="icon">
🚀
</Button>
<div className="flex flex-wrap items-center gap-2">
<Button {...args} size="sm">
Small
</Button>
<Button {...args}>Default</Button>
<Button {...args} size="default">
Default
</Button>
<Button {...args} size="lg">
Large
</Button>
<div className="flex flex-col items-start gap-2 rounded border p-4">
<p className="mb-2 text-xs text-muted-foreground">Deprecated Size:</p>
<Button {...args} size="primary">
Primary (deprecated)
</Button>
</div>
<Button {...args} size="primary">
Primary
</Button>
<Button {...args} size="icon">
🚀
</Button>
</div>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = canvas.getAllByRole("button");
await expect(buttons).toHaveLength(5);
// Test icon size
const iconButton = canvas.getByRole("button", { name: /🚀/i });
await expect(iconButton).toHaveAttribute(
"class",
expect.stringContaining("h-9 w-9"),
);
// Test specific size classes
const smallButton = canvas.getByRole("button", { name: /Small/i });
await expect(smallButton).toHaveAttribute(
"class",
expect.stringContaining("h-8"),
);
const defaultButton = canvas.getByRole("button", { name: /Default/i });
await expect(defaultButton).toHaveAttribute(
"class",
expect.stringContaining("h-9"),
);
const largeButton = canvas.getByRole("button", { name: /Large/i });
await expect(largeButton).toHaveAttribute(
"class",
expect.stringContaining("h-10"),
);
const sizes = ["sm", "default", "lg", "primary", "icon"];
const sizeClasses = [
"h-8 rounded-md px-3 text-xs",
"h-9 px-4 py-2",
"h-10 rounded-md px-8",
"md:h-14 md:w-44 rounded-2xl h-10 w-28",
"h-9 w-9",
];
buttons.forEach(async (button, index) => {
await expect(button).toHaveAttribute(
"class",
expect.stringContaining(sizeClasses[index]),
);
});
},
};
@@ -231,101 +149,39 @@ export const Disabled: Story = {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Disabled Button/i });
await expect(button).toBeDisabled();
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("disabled:pointer-events-none"),
);
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("disabled:opacity-50"),
);
await expect(button).toHaveStyle("pointer-events: none");
await expect(button).not.toHaveFocus();
},
};
export const WithIcon: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex gap-4">
<Button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
Icon Left
</Button>
<Button>
Icon Right
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
</Button>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
Icon with automatic gap spacing:
</p>
<Button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
Button with Icon
</Button>
</div>
</div>
),
args: {
children: (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
Button with Icon
</>
),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = canvas.getAllByRole("button");
const icons = canvasElement.querySelectorAll("svg");
// Test that SVGs are present
await expect(icons.length).toBeGreaterThan(0);
// Test for gap-2 class for spacing
await expect(buttons[0]).toHaveAttribute(
"class",
expect.stringContaining("gap-2"),
);
// Test SVG styling from buttonVariants
await expect(buttons[0]).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:size-4"),
);
await expect(buttons[0]).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:shrink-0"),
);
await expect(buttons[0]).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:pointer-events-none"),
);
const button = canvas.getByRole("button", { name: /Button with Icon/i });
const icon = button.querySelector("svg");
await expect(icon).toBeInTheDocument();
await expect(button).toHaveTextContent("Button with Icon");
},
};
@@ -337,8 +193,10 @@ export const LoadingState: Story = {
render: (args) => (
<Button {...args}>
<svg
className="animate-spin"
className="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -357,56 +215,5 @@ export const LoadingState: Story = {
await expect(button).toBeDisabled();
const spinner = button.querySelector("svg");
await expect(spinner).toHaveClass("animate-spin");
// Test SVG styling from buttonVariants
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("[&_svg]:size-4"),
);
},
};
export const RoundedStyles: Story = {
render: () => (
<div className="flex flex-col gap-6">
<div>
<p className="mb-2 text-sm text-muted-foreground">
Default variants have rounded-full style:
</p>
<div className="flex gap-4">
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
</div>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
Link variant maintains its original style:
</p>
<div className="flex gap-4">
<Button variant="link">Link</Button>
</div>
</div>
</div>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = canvas.getAllByRole("button");
// Test rounded-full on first 5 buttons
for (let i = 0; i < 5; i++) {
await expect(buttons[i]).toHaveAttribute(
"class",
expect.stringContaining("rounded-full"),
);
}
// Test that link variant doesn't have rounded-full
await expect(buttons[5]).not.toHaveAttribute(
"class",
expect.stringContaining("rounded-full"),
);
},
};

View File

@@ -5,28 +5,28 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300",
{
variants: {
variant: {
default:
"bg-primary/90 text-primary-foreground shadow hover:bg-primary rounded-full",
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
destructive:
"bg-destructive/90 text-destructive-foreground shadow-sm hover:bg-destructive rounded-full",
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground rounded-full",
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
secondary:
"bg-secondary/90 text-secondary-foreground shadow-sm hover:bg-secondary rounded-full",
ghost: "hover:bg-accent hover:text-accent-foreground rounded-full",
link: "text-primary underline-offset-4 hover:underline",
"bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
ghost:
"hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
/** @deprecated Use default size with custom classes instead */
primary: "md:h-14 md:w-44 rounded-2xl h-10 w-28",
icon: "h-9 w-9",
},
},
defaultVariants: {

View File

@@ -6,6 +6,7 @@ import BackendAPI, {
BlockUIType,
formatEdgeID,
Graph,
GraphExecutionID,
GraphID,
NodeExecutionResult,
} from "@/lib/autogpt-server-api";
@@ -29,7 +30,7 @@ const ajv = new Ajv({ strict: false, allErrors: true });
export default function useAgentGraph(
flowID?: GraphID,
flowVersion?: number,
flowExecutionID?: string,
flowExecutionID?: GraphExecutionID,
passDataToBeads?: boolean,
) {
const { toast } = useToast();
@@ -65,7 +66,7 @@ export default function useAgentGraph(
| {
request: "run" | "stop";
state: "running" | "stopping" | "error";
activeExecutionID?: string;
activeExecutionID?: GraphExecutionID;
}
>({
request: "none",

View File

@@ -17,6 +17,7 @@ import {
Graph,
GraphCreatable,
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
GraphID,
GraphMeta,
@@ -35,6 +36,7 @@ import {
RefundRequest,
Schedule,
ScheduleCreatable,
ScheduleID,
StoreAgentDetails,
StoreAgentsResponse,
StoreReview,
@@ -211,7 +213,7 @@ export default class BackendAPI {
id: GraphID,
version: number,
inputData: { [key: string]: any } = {},
): Promise<{ graph_exec_id: string }> {
): Promise<{ graph_exec_id: GraphExecutionID }> {
return this._request("POST", `/graphs/${id}/execute/${version}`, inputData);
}
@@ -225,7 +227,7 @@ export default class BackendAPI {
async getGraphExecutionInfo(
graphID: GraphID,
runID: string,
runID: GraphExecutionID,
): Promise<GraphExecution> {
const result = await this._get(`/graphs/${graphID}/executions/${runID}`);
result.node_executions = result.node_executions.map(
@@ -236,7 +238,7 @@ export default class BackendAPI {
async stopGraphExecution(
graphID: GraphID,
runID: string,
runID: GraphExecutionID,
): Promise<GraphExecution> {
const result = await this._request(
"POST",
@@ -248,6 +250,10 @@ export default class BackendAPI {
return result;
}
async deleteGraphExecution(runID: GraphExecutionID): Promise<void> {
await this._request("DELETE", `/executions/${runID}`);
}
oAuthLogin(
provider: string,
scopes?: string[],
@@ -558,13 +564,9 @@ export default class BackendAPI {
});
}
///////////////////////////////////////////
/////////// INTERNAL FUNCTIONS ////////////
//////////////////////////////??///////////
private _get(path: string, query?: Record<string, any>) {
return this._request("GET", path, query);
}
//////////////////////////////////
/////////// SCHEDULES ////////////
//////////////////////////////////
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
return this._request("POST", `/schedules`, schedule).then(
@@ -572,7 +574,7 @@ export default class BackendAPI {
);
}
async deleteSchedule(scheduleId: string): Promise<{ id: string }> {
async deleteSchedule(scheduleId: ScheduleID): Promise<{ id: string }> {
return this._request("DELETE", `/schedules/${scheduleId}`);
}
@@ -582,6 +584,14 @@ export default class BackendAPI {
);
}
///////////////////////////////////////////
/////////// INTERNAL FUNCTIONS ////////////
//////////////////////////////??///////////
private _get(path: string, query?: Record<string, any>) {
return this._request("GET", path, query);
}
private async _uploadFile(path: string, file: File): Promise<string> {
// Get session with retry logic
let token = "no-token-found";

View File

@@ -218,7 +218,7 @@ export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
/* Mirror of backend/data/graph.py:GraphExecutionMeta */
export type GraphExecutionMeta = {
execution_id: string;
execution_id: GraphExecutionID;
started_at: number;
ended_at: number;
cost?: number;
@@ -230,6 +230,8 @@ export type GraphExecutionMeta = {
preset_id?: string;
};
export type GraphExecutionID = Brand<string, "GraphExecutionID">;
/* Mirror of backend/data/graph.py:GraphExecution */
export type GraphExecution = GraphExecutionMeta & {
inputs: Record<string, any>;
@@ -287,7 +289,7 @@ export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string };
export type NodeExecutionResult = {
graph_id: GraphID;
graph_version: number;
graph_exec_id: string;
graph_exec_id: GraphExecutionID;
node_exec_id: string;
node_id: string;
block_id: string;
@@ -624,7 +626,7 @@ export type ProfileDetails = {
};
export type Schedule = {
id: string;
id: ScheduleID;
name: string;
cron: string;
user_id: string;
@@ -634,6 +636,8 @@ export type Schedule = {
next_run_time: Date;
};
export type ScheduleID = Brand<string, "ScheduleID">;
export type ScheduleCreatable = {
cron: string;
graph_id: GraphID;
@@ -642,7 +646,7 @@ export type ScheduleCreatable = {
};
export type MyAgent = {
agent_id: string;
agent_id: GraphID;
agent_version: number;
agent_name: string;
last_edited: string;
@@ -706,7 +710,7 @@ export interface CreditTransaction {
balance: number;
description: string;
usage_graph_id: GraphID;
usage_execution_id: string;
usage_execution_id: GraphExecutionID;
usage_node_count: number;
usage_starting_time: Date;
}