mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-22 05:28:02 -05:00
Merge branch 'dev' into zamilmajdy/improve-sdm-add-anthropic
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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 ########################
|
||||
########################################################
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add isDeleted column to AgentGraphExecution
|
||||
ALTER TABLE "AgentGraphExecution"
|
||||
ADD COLUMN "isDeleted"
|
||||
BOOLEAN
|
||||
NOT NULL
|
||||
DEFAULT false;
|
||||
@@ -289,6 +289,8 @@ model AgentGraphExecution {
|
||||
updatedAt DateTime? @updatedAt
|
||||
startedAt DateTime?
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
|
||||
executionStatus AgentExecutionStatus @default(COMPLETED)
|
||||
|
||||
agentGraphId String
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user