feat(platform/library): Library v2 > Agent Runs page (#9051)

- Resolves #8780
- Part of #8774

### Changes 🏗️

- Add new UI components
- Add `/agents/[id]` page, with sub-components:
  - `AgentRunsSelectorList`
    - `AgentRunSummaryCard`
      - `AgentRunStatusChip`
  - `AgentRunDetailsView`
  - `AgentRunDraftView`
  - `AgentScheduleDetailsView`

Backend improvements:
- Improve output of execution-related API endpoints: return
`GraphExecution` instead of `NodeExecutionResult[]`
- Reduce log spam from Prisma in tests

General frontend improvements:
- Hide nav link names on smaller screens to prevent navbar overflow
- Clean up styling and fix sizing of `agptui/Button`

Technical frontend improvements:
- Fix tailwind config size increments
- Rename `font-poppin` -> `font-poppins`
- Clean up component implementations and usages
   - Yeet all occurrences of `variant="default"`
- Remove `default` button variant as duplicate of `outline`; make
`outline` the default
- Fix minor typing issues

DX:
- Add front end type-check step to `pre-commit` config
- Fix logging setup in conftest.py

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - `/agents/[id]` (new)
    - Go to page -> list of runs loads
    - Create new run -> runs; all I/O is visible
    - Click "Run again" -> runs again with same input
  - `/monitoring` (existing)
    - Go to page -> everything loads
    - Selecting agents and agent runs works

---------

Co-authored-by: Nicholas Tindle <nicktindle@outlook.com>
Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
Co-authored-by: Swifty <craigswift13@gmail.com>
Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
This commit is contained in:
Reinier van der Leer
2025-02-19 23:07:03 +01:00
committed by GitHub
parent f722c70c50
commit 296eee0b4f
51 changed files with 1330 additions and 236 deletions

View File

@@ -170,6 +170,16 @@ repos:
files: ^classic/benchmark/(agbenchmark|tests)/((?!reports).)*[/.]
args: [--config=classic/benchmark/.flake8]
- repo: local
hooks:
- id: prettier
name: Format (Prettier) - AutoGPT Platform - Frontend
alias: format-platform-frontend
entry: bash -c 'cd autogpt_platform/frontend && npx prettier --write $(echo "$@" | sed "s|autogpt_platform/frontend/||g")' --
files: ^autogpt_platform/frontend/
types: [file]
language: system
- repo: local
# To have watertight type checking, we check *all* the files in an affected
# project. To trigger on poetry.lock we also reset the file `types` filter.
@@ -221,6 +231,16 @@ repos:
language: system
pass_filenames: false
- repo: local
hooks:
- id: tsc
name: Typecheck - AutoGPT Platform - Frontend
entry: bash -c 'cd autogpt_platform/frontend && npm run type-check'
files: ^autogpt_platform/frontend/
types: [file]
language: system
pass_filenames: false
- repo: local
hooks:
- id: pytest

View File

@@ -13,7 +13,6 @@ from typing_extensions import ParamSpec
from .config import SETTINGS
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
P = ParamSpec("P")
T = TypeVar("T")

View File

@@ -23,12 +23,15 @@ from backend.util import type
from .block import BlockInput, BlockType, get_block, get_blocks
from .db import BaseDbModel, transaction
from .execution import ExecutionStatus
from .execution import ExecutionResult, ExecutionStatus
from .includes import AGENT_GRAPH_INCLUDE, AGENT_NODE_INCLUDE
from .integrations import Webhook
logger = logging.getLogger(__name__)
_INPUT_BLOCK_ID = AgentInputBlock().id
_OUTPUT_BLOCK_ID = AgentOutputBlock().id
class Link(BaseDbModel):
source_id: str
@@ -105,7 +108,7 @@ class NodeModel(Node):
Webhook.model_rebuild()
class GraphExecution(BaseDbModel):
class GraphExecutionMeta(BaseDbModel):
execution_id: str
started_at: datetime
ended_at: datetime
@@ -114,33 +117,83 @@ class GraphExecution(BaseDbModel):
status: ExecutionStatus
graph_id: str
graph_version: int
preset_id: Optional[str]
@staticmethod
def from_db(execution: AgentGraphExecution):
def from_db(_graph_exec: AgentGraphExecution):
now = datetime.now(timezone.utc)
start_time = execution.startedAt or execution.createdAt
end_time = execution.updatedAt or now
start_time = _graph_exec.startedAt or _graph_exec.createdAt
end_time = _graph_exec.updatedAt or now
duration = (end_time - start_time).total_seconds()
total_run_time = duration
try:
stats = type.convert(execution.stats or {}, dict[str, Any])
stats = type.convert(_graph_exec.stats or {}, dict[str, Any])
except ValueError:
stats = {}
duration = stats.get("walltime", duration)
total_run_time = stats.get("nodes_walltime", total_run_time)
return GraphExecution(
id=execution.id,
execution_id=execution.id,
return GraphExecutionMeta(
id=_graph_exec.id,
execution_id=_graph_exec.id,
started_at=start_time,
ended_at=end_time,
duration=duration,
total_run_time=total_run_time,
status=ExecutionStatus(execution.executionStatus),
graph_id=execution.agentGraphId,
graph_version=execution.agentGraphVersion,
status=ExecutionStatus(_graph_exec.executionStatus),
graph_id=_graph_exec.agentGraphId,
graph_version=_graph_exec.agentGraphVersion,
preset_id=_graph_exec.agentPresetId,
)
class GraphExecution(GraphExecutionMeta):
inputs: dict[str, Any]
outputs: dict[str, list[Any]]
node_executions: list[ExecutionResult]
@staticmethod
def from_db(_graph_exec: AgentGraphExecution):
if _graph_exec.AgentNodeExecutions is None:
raise ValueError("Node executions must be included in query")
graph_exec = GraphExecutionMeta.from_db(_graph_exec)
node_executions = [
ExecutionResult.from_db(ne) for ne in _graph_exec.AgentNodeExecutions
]
inputs = {
**{
# inputs from Agent Input Blocks
exec.input_data["name"]: exec.input_data["value"]
for exec in node_executions
if exec.block_id == _INPUT_BLOCK_ID
},
**{
# input from webhook-triggered block
"payload": exec.input_data["payload"]
for exec in node_executions
if (block := get_block(exec.block_id))
and block.block_type in [BlockType.WEBHOOK, BlockType.WEBHOOK_MANUAL]
},
}
outputs: dict[str, list] = defaultdict(list)
for exec in node_executions:
if exec.block_id == _OUTPUT_BLOCK_ID:
outputs[exec.input_data["name"]].append(exec.input_data["value"])
return GraphExecution(
**{
field_name: getattr(graph_exec, field_name)
for field_name in graph_exec.model_fields
},
inputs=inputs,
outputs=outputs,
node_executions=node_executions,
)
@@ -514,17 +567,45 @@ async def get_graphs(
return graph_models
async def get_executions(user_id: str) -> list[GraphExecution]:
async def get_graphs_executions(user_id: str) -> list[GraphExecutionMeta]:
executions = await AgentGraphExecution.prisma().find_many(
where={"userId": user_id},
order={"createdAt": "desc"},
)
return [GraphExecution.from_db(execution) for execution in executions]
return [GraphExecutionMeta.from_db(execution) for execution in executions]
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},
order={"createdAt": "desc"},
)
return [GraphExecutionMeta.from_db(execution) for execution in executions]
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}
)
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, "userId": user_id},
include={
"AgentNodeExecutions": {
"include": {"AgentNode": True, "Input": True, "Output": True},
"order_by": [
{"queuedTime": "asc"},
{ # Fallback: Incomplete execs has no queuedTime.
"addedTime": "asc"
},
],
},
},
)
return GraphExecution.from_db(execution) if execution else None
@@ -629,7 +710,7 @@ async def create_graph(graph: Graph, user_id: str) -> GraphModel:
await __create_graph(tx, graph, user_id)
if created_graph := await get_graph(
graph.id, graph.version, graph.is_template, user_id=user_id
graph.id, graph.version, template=graph.is_template, user_id=user_id
):
return created_graph

View File

@@ -155,7 +155,7 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod
async def test_get_graph_run_status(graph_exec_id: str, user_id: str):
execution = await backend.data.graph.get_execution(
execution = await backend.data.graph.get_execution_meta(
user_id=user_id, execution_id=graph_exec_id
)
if not execution:
@@ -163,10 +163,10 @@ class AgentServer(backend.util.service.AppProcess):
return execution.status
@staticmethod
async def test_get_graph_run_node_execution_results(
async def test_get_graph_run_results(
graph_id: str, graph_exec_id: str, user_id: str
):
return await backend.server.routers.v1.get_graph_run_node_execution_results(
return await backend.server.routers.v1.get_graph_execution(
graph_id, graph_exec_id, user_id
)

View File

@@ -16,7 +16,6 @@ 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,
@@ -546,7 +545,7 @@ async def set_graph_active_version(
)
def execute_graph(
graph_id: str,
node_input: Annotated[dict[str, Any], Body(..., embed=True, default_factory=dict)],
node_input: Annotated[dict[str, Any], Body(..., default_factory=dict)],
user_id: Annotated[str, Depends(get_user_id)],
graph_version: Optional[int] = None,
) -> ExecuteGraphResponse:
@@ -567,8 +566,10 @@ def execute_graph(
)
async def stop_graph_run(
graph_exec_id: str, user_id: Annotated[str, Depends(get_user_id)]
) -> Sequence[execution_db.ExecutionResult]:
if not await graph_db.get_execution(user_id=user_id, execution_id=graph_exec_id):
) -> graph_db.GraphExecution:
if not await graph_db.get_execution_meta(
user_id=user_id, execution_id=graph_exec_id
):
raise HTTPException(404, detail=f"Agent execution #{graph_exec_id} not found")
await asyncio.to_thread(
@@ -576,7 +577,13 @@ async def stop_graph_run(
)
# Retrieve & return canceled graph execution in its final state
return await execution_db.get_execution_results(graph_exec_id)
result = await graph_db.get_execution(execution_id=graph_exec_id, user_id=user_id)
if not result:
raise HTTPException(
500,
detail=f"Could not fetch graph execution #{graph_exec_id} after stopping",
)
return result
@v1_router.get(
@@ -584,10 +591,22 @@ async def stop_graph_run(
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_executions(
async def get_graphs_executions(
user_id: Annotated[str, Depends(get_user_id)],
) -> list[graph_db.GraphExecution]:
return await graph_db.get_executions(user_id=user_id)
) -> list[graph_db.GraphExecutionMeta]:
return await graph_db.get_graphs_executions(user_id=user_id)
@v1_router.get(
path="/graphs/{graph_id}/executions",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_graph_executions(
graph_id: str,
user_id: Annotated[str, Depends(get_user_id)],
) -> list[graph_db.GraphExecutionMeta]:
return await graph_db.get_graph_executions(graph_id=graph_id, user_id=user_id)
@v1_router.get(
@@ -595,16 +614,20 @@ async def get_executions(
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_graph_run_node_execution_results(
async def get_graph_execution(
graph_id: str,
graph_exec_id: str,
user_id: Annotated[str, Depends(get_user_id)],
) -> Sequence[execution_db.ExecutionResult]:
) -> graph_db.GraphExecution:
graph = await graph_db.get_graph(graph_id, user_id=user_id)
if not graph:
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
return await execution_db.get_execution_results(graph_exec_id)
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.")
return result
########################################################

View File

@@ -669,7 +669,8 @@ async def review_submission(
reviewer_id=user.user_id,
)
return submission
except Exception:
except Exception as e:
logger.error(f"Could not create store submission review: {e}")
raise fastapi.HTTPException(
status_code=500,
detail="An error occurred while creating the store submission review",

View File

@@ -78,9 +78,10 @@ async def wait_execution(
# Wait for the executions to complete
for i in range(timeout):
if await is_execution_completed():
return await AgentServer().test_get_graph_run_node_execution_results(
graph_exec = await AgentServer().test_get_graph_run_results(
graph_id, graph_exec_id, user_id
)
return graph_exec.node_executions
time.sleep(1)
assert False, "Execution did not complete in time."

View File

@@ -1,23 +1,25 @@
import logging
import os
import pytest
from backend.util.test import SpinTestServer
from backend.util.logging import configure_logging
# NOTE: You can run tests like with the --log-cli-level=INFO to see the logs
# Set up logging
configure_logging()
logger = logging.getLogger(__name__)
# Create console handler with formatting
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
logger.addHandler(ch)
# Reduce Prisma log spam unless PRISMA_DEBUG is set
if not os.getenv("PRISMA_DEBUG"):
prisma_logger = logging.getLogger("prisma")
prisma_logger.setLevel(logging.INFO)
@pytest.fixture(scope="session")
async def server():
from backend.util.test import SpinTestServer
async with SpinTestServer() as server:
yield server

View File

@@ -58,7 +58,7 @@ async def assert_sample_graph_executions(
graph_exec_id: str,
):
logger.info(f"Checking execution results for graph {test_graph.id}")
executions = await agent_server.test_get_graph_run_node_execution_results(
graph_run = await agent_server.test_get_graph_run_results(
test_graph.id,
graph_exec_id,
test_user.id,
@@ -77,7 +77,7 @@ async def assert_sample_graph_executions(
]
# Executing StoreValueBlock
exec = executions[0]
exec = graph_run.node_executions[0]
logger.info(f"Checking first StoreValueBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
@@ -90,7 +90,7 @@ async def assert_sample_graph_executions(
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
# Executing StoreValueBlock
exec = executions[1]
exec = graph_run.node_executions[1]
logger.info(f"Checking second StoreValueBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
@@ -103,7 +103,7 @@ async def assert_sample_graph_executions(
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
# Executing FillTextTemplateBlock
exec = executions[2]
exec = graph_run.node_executions[2]
logger.info(f"Checking FillTextTemplateBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
@@ -118,7 +118,7 @@ async def assert_sample_graph_executions(
assert exec.node_id == test_graph.nodes[2].id
# Executing PrintToConsoleBlock
exec = executions[3]
exec = graph_run.node_executions[3]
logger.info(f"Checking PrintToConsoleBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
@@ -201,14 +201,14 @@ async def test_input_pin_always_waited(server: SpinTestServer):
)
logger.info("Checking execution results")
executions = await server.agent_server.test_get_graph_run_node_execution_results(
graph_exec = await server.agent_server.test_get_graph_run_results(
test_graph.id, graph_exec_id, test_user.id
)
assert len(executions) == 3
assert len(graph_exec.node_executions) == 3
# FindInDictionaryBlock should wait for the input pin to be provided,
# Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
assert executions[2].status == execution.ExecutionStatus.COMPLETED
assert executions[2].output_data == {"output": ["value2"]}
assert graph_exec.node_executions[2].status == execution.ExecutionStatus.COMPLETED
assert graph_exec.node_executions[2].output_data == {"output": ["value2"]}
logger.info("Completed test_input_pin_always_waited")
@@ -284,12 +284,12 @@ async def test_static_input_link_on_graph(server: SpinTestServer):
server.agent_server, test_graph, test_user, {}, 8
)
logger.info("Checking execution results")
executions = await server.agent_server.test_get_graph_run_node_execution_results(
graph_exec = await server.agent_server.test_get_graph_run_results(
test_graph.id, graph_exec_id, test_user.id
)
assert len(executions) == 8
assert len(graph_exec.node_executions) == 8
# The last 3 executions will be a+b=4+5=9
for i, exec_data in enumerate(executions[-3:]):
for i, exec_data in enumerate(graph_exec.node_executions[-3:]):
logger.info(f"Checking execution {i+1} of last 3: {exec_data}")
assert exec_data.status == execution.ExecutionStatus.COMPLETED
assert exec_data.output_data == {"result": [9]}

View File

@@ -0,0 +1,184 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
GraphExecution,
GraphExecutionMeta,
GraphMeta,
Schedule,
} from "@/lib/autogpt-server-api";
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";
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: string } = useParams();
const router = useRouter();
const api = useBackendAPI();
const [agent, setAgent] = useState<GraphMeta | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedView, selectView] = useState<{
type: "run" | "schedule";
id?: string;
}>({ type: "run" });
const [selectedRun, setSelectedRun] = useState<
GraphExecution | GraphExecutionMeta | null
>(null);
const [selectedSchedule, setSelectedSchedule] = useState<Schedule | null>(
null,
);
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const openRunDraftView = useCallback(() => {
selectView({ type: "run" });
}, []);
const selectRun = useCallback((id: string) => {
selectView({ type: "run", id });
}, []);
const selectSchedule = useCallback((schedule: Schedule) => {
selectView({ type: "schedule", id: schedule.id });
setSelectedSchedule(schedule);
}, []);
const fetchAgents = useCallback(() => {
api.getGraph(agentID).then(setAgent);
api.getGraphExecutions(agentID).then((agentRuns) => {
const sortedRuns = agentRuns.toSorted(
(a, b) => b.started_at - a.started_at,
);
setAgentRuns(sortedRuns);
if (!selectedView.id && isFirstLoad && sortedRuns.length > 0) {
// only for first load or first execution
setIsFirstLoad(false);
selectView({ type: "run", id: sortedRuns[0].execution_id });
setSelectedRun(sortedRuns[0]);
}
});
if (selectedView.type == "run" && selectedView.id) {
api.getGraphExecutionInfo(agentID, selectedView.id).then(setSelectedRun);
}
}, [api, agentID, selectedView, isFirstLoad]);
useEffect(() => {
fetchAgents();
}, []);
// load selectedRun based on selectedView
useEffect(() => {
if (selectedView.type != "run" || !selectedView.id) return;
// pull partial data from "cache" while waiting for the rest to load
if (selectedView.id !== selectedRun?.execution_id) {
setSelectedRun(
agentRuns.find((r) => r.execution_id == selectedView.id) ?? null,
);
}
api.getGraphExecutionInfo(agentID, selectedView.id).then(setSelectedRun);
}, [api, selectedView, agentRuns, agentID]);
const fetchSchedules = useCallback(async () => {
// TODO: filter in backend - https://github.com/Significant-Gravitas/AutoGPT/issues/9183
setSchedules(
(await api.listSchedules()).filter((s) => s.graph_id == agentID),
);
}, [api, agentID]);
useEffect(() => {
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, agent]);
const agentActions: { label: string; callback: () => void }[] = useMemo(
() => [
{
label: "Open in builder",
callback: () => agent && router.push(`/build?flowID=${agent.id}`),
},
],
[agent, router],
);
if (!agent) {
/* TODO: implement loading indicators / skeleton page */
return <span>Loading...</span>;
}
return (
<div className="container justify-stretch p-0 lg:flex">
{/* Sidebar w/ list of runs */}
{/* TODO: render this below header in sm and md layouts */}
<AgentRunsSelectorList
className="agpt-div w-full border-b lg:w-auto lg:border-b-0 lg:border-r"
agent={agent}
agentRuns={agentRuns}
schedules={schedules}
selectedView={selectedView}
onSelectRun={selectRun}
onSelectSchedule={selectSchedule}
onDraftNewRun={openRunDraftView}
/>
<div className="flex-1">
{/* Header */}
<div className="agpt-div w-full border-b">
<h1 className="font-poppins text-3xl font-medium">
{
agent.name /* TODO: use dynamic/custom run title - https://github.com/Significant-Gravitas/AutoGPT/issues/9184 */
}
</h1>
</div>
{/* Run / Schedule views */}
{(selectedView.type == "run" ? (
selectedView.id ? (
selectedRun && (
<AgentRunDetailsView
agent={agent}
run={selectedRun}
agentActions={agentActions}
/>
)
) : (
<AgentRunDraftView
agent={agent}
onRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
)
) : selectedView.type == "schedule" ? (
selectedSchedule && (
<AgentScheduleDetailsView
agent={agent}
schedule={selectedSchedule}
onForcedRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
)
) : null) || <p>Loading...</p>}
</div>
</div>
);
}

View File

@@ -2,54 +2,9 @@
@tailwind components;
@tailwind utilities;
@layer base {
.font-neue {
font-family: "PP Neue Montreal TT", sans-serif;
}
}
@layer utilities {
.w-110 {
width: 27.5rem;
}
.h-7\.5 {
height: 1.1875rem;
}
.h-18 {
height: 4.5rem;
}
.h-238 {
height: 14.875rem;
}
.top-158 {
top: 9.875rem;
}
.top-254 {
top: 15.875rem;
}
.top-284 {
top: 17.75rem;
}
.top-360 {
top: 22.5rem;
}
.left-297 {
left: 18.5625rem;
}
.left-34 {
left: 2.125rem;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--background: 0 0% 99.6%; /* #FEFEFE */
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
@@ -61,8 +16,8 @@
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--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%;
@@ -102,9 +57,7 @@
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
@@ -112,6 +65,14 @@
@apply bg-background text-foreground;
}
.font-neue {
font-family: "PP Neue Montreal TT", sans-serif;
}
}
/* *** AutoGPT Design Components *** */
@layer components {
.agpt-border-input {
@apply border border-input focus-visible:border-gray-400 focus-visible:outline-none;
}
@@ -119,4 +80,67 @@
.agpt-shadow-input {
@apply shadow-sm focus-visible:shadow-md;
}
.agpt-rounded-card {
@apply rounded-2xl;
}
.agpt-rounded-box {
@apply rounded-3xl;
}
.agpt-card {
@apply agpt-rounded-card border border-zinc-300 bg-white p-[1px];
}
.agpt-box {
@apply agpt-card agpt-rounded-box;
}
.agpt-div {
@apply border-zinc-200 p-5;
}
}
@layer utilities {
.agpt-card-selected {
@apply border-2 border-accent bg-violet-50/50 p-0;
}
}
@layer utilities {
/* TODO: 1. remove unused utility classes */
/* TODO: 2. fix naming of numbered dimensions so that the number is 4*dimension */
/* TODO: 3. move to tailwind.config.ts spacing config */
.h-7\.5 {
height: 1.1875rem;
}
.h-18 {
height: 4.5rem;
}
.h-238 {
height: 14.875rem;
}
.top-158 {
top: 9.875rem;
}
.top-254 {
top: 15.875rem;
}
.top-284 {
top: 17.75rem;
}
.top-360 {
top: 22.5rem;
}
.left-297 {
left: 18.5625rem;
}
.left-34 {
left: 2.125rem;
}
.text-balance {
text-wrap: balance;
}
}

View File

@@ -2,7 +2,7 @@
import React, { useCallback, useEffect, useState } from "react";
import {
GraphExecution,
GraphExecutionMeta,
Schedule,
LibraryAgent,
} from "@/lib/autogpt-server-api";
@@ -20,10 +20,12 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const Monitor = () => {
const [flows, setFlows] = useState<LibraryAgent[]>([]);
const [executions, setExecutions] = useState<GraphExecution[]>([]);
const [executions, setExecutions] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedFlow, setSelectedFlow] = useState<LibraryAgent | null>(null);
const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
const [selectedRun, setSelectedRun] = useState<GraphExecutionMeta | null>(
null,
);
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const api = useBackendAPI();

View File

@@ -260,7 +260,6 @@ export default function CreditsPage() {
</p>
<Button
type="submit"
variant="default"
className="w-full"
onClick={() => openBillingPortal()}
>

View File

@@ -84,8 +84,6 @@ export default function Page({}: {}) {
<PublishAgentPopout
trigger={
<Button
variant="default"
size="sm"
onClick={onOpenPopout}
className="h-9 rounded-full bg-black px-4 text-sm font-medium text-white hover:bg-neutral-700 dark:hover:bg-neutral-600"
>

View File

@@ -0,0 +1,214 @@
"use client";
import React, { useCallback, useMemo } from "react";
import moment from "moment";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
BlockIOSubType,
GraphExecution,
GraphExecutionMeta,
GraphMeta,
} from "@/lib/autogpt-server-api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
import {
AgentRunStatus,
agentRunStatusMap,
} from "@/components/agents/agent-run-status-chip";
export default function AgentRunDetailsView({
agent,
run,
agentActions,
}: {
agent: GraphMeta;
run: GraphExecution | GraphExecutionMeta;
agentActions: { label: string; callback: () => void }[];
}): React.ReactNode {
const api = useBackendAPI();
const selectedRunStatus: AgentRunStatus = useMemo(
() => agentRunStatusMap[run.status],
[run],
);
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
if (!run) return [];
return [
{
label: "Status",
value:
selectedRunStatus.charAt(0).toUpperCase() +
selectedRunStatus.slice(1),
},
{
label: "Started",
value: `${moment(run.started_at).fromNow()}, ${moment(run.started_at).format("HH:mm")}`,
},
{
label: "Duration",
value: `${moment.duration(run.duration, "seconds").humanize()}`,
},
// { label: "Cost", value: selectedRun.cost }, // TODO: implement cost - https://github.com/Significant-Gravitas/AutoGPT/issues/9181
];
}, [run, selectedRunStatus]);
const agentRunInputs:
| Record<string, { title?: string; /* type: BlockIOSubType; */ value: any }>
| undefined = useMemo(() => {
if (!("inputs" in run)) return undefined;
// TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(run.inputs).map(([k, v]) => [
k,
{
title: agent.input_schema.properties[k].title,
// type: agent.input_schema.properties[k].type, // TODO: implement typed graph inputs
value: v,
},
]),
);
}, [agent, run]);
const runAgain = useCallback(
() =>
agentRunInputs &&
api.executeGraph(
agent.id,
agent.version,
Object.fromEntries(
Object.entries(agentRunInputs).map(([k, v]) => [k, v.value]),
),
),
[api, agent, agentRunInputs],
);
const agentRunOutputs:
| Record<
string,
{ title?: string; /* type: BlockIOSubType; */ values: Array<any> }
>
| null
| undefined = useMemo(() => {
if (!("outputs" in run)) return undefined;
if (!["running", "success", "failed"].includes(selectedRunStatus))
return null;
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(run.outputs).map(([k, v]) => [
k,
{
title: agent.output_schema.properties[k].title,
/* type: agent.output_schema.properties[k].type */
values: v,
},
]),
);
}, [agent, run, selectedRunStatus]);
const runActions: { label: string; callback: () => void }[] = useMemo(
() => [{ label: "Run again", callback: () => runAgain() }],
[runAgain],
);
return (
<div className="agpt-div flex gap-6">
<div className="flex flex-1 flex-col gap-4">
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Info</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-stretch gap-4">
{infoStats.map(({ label, value }) => (
<div key={label} className="flex-1">
<p className="text-sm font-medium text-black">{label}</p>
<p className="text-sm text-neutral-600">{value}</p>
</div>
))}
</div>
</CardContent>
</Card>
{agentRunOutputs !== null && (
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Output</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{agentRunOutputs !== undefined ? (
Object.entries(agentRunOutputs).map(
([key, { title, values }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">
{title || key}
</label>
{values.map((value, i) => (
<pre key={i}>{value}</pre>
))}
{/* TODO: pretty type-dependent rendering */}
</div>
),
)
) : (
<p>Loading...</p>
)}
</CardContent>
</Card>
)}
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{agentRunInputs !== undefined ? (
Object.entries(agentRunInputs).map(([key, { title, value }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{title || key}</label>
<Input
defaultValue={value}
className="rounded-full"
disabled
/>
</div>
))
) : (
<p>Loading...</p>
)}
</CardContent>
</Card>
</div>
{/* Run / Agent Actions */}
<aside className="w-48 xl:w-56">
<div className="flex flex-col gap-8">
<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}>
{action.label}
</Button>
))}
</div>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import React, { useCallback, useMemo, useState } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button, ButtonProps } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
export default function AgentRunDraftView({
agent,
onRun,
agentActions,
}: {
agent: GraphMeta;
onRun: (runID: string) => void;
agentActions: { label: string; callback: () => void }[];
}): React.ReactNode {
const api = useBackendAPI();
const agentInputs = agent.input_schema.properties;
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const doRun = useCallback(
() =>
api
.executeGraph(agent.id, agent.version, inputValues)
.then((newRun) => onRun(newRun.graph_exec_id)),
[api, agent, inputValues, onRun],
);
const runActions: {
label: string;
variant?: ButtonProps["variant"];
callback: () => void;
}[] = useMemo(
() => [{ label: "Run", variant: "accent", callback: () => doRun() }],
[doRun],
);
return (
<div className="agpt-div flex gap-6">
<div className="flex flex-1 flex-col gap-4">
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{Object.entries(agentInputs).map(([key, inputSubSchema]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">
{inputSubSchema.title || key}
</label>
<Input
// TODO: render specific inputs based on input types
defaultValue={
"default" in inputSubSchema ? inputSubSchema.default : ""
}
className="rounded-full"
onChange={(e) =>
setInputValues((obj) => ({ ...obj, [key]: e.target.value }))
}
/>
</div>
))}
</CardContent>
</Card>
</div>
{/* Actions */}
<aside className="w-48 xl:w-56">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Run actions</h3>
{runActions.map((action, i) => (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}
</div>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import React from "react";
import { Badge } from "@/components/ui/badge";
import { GraphExecutionMeta } from "@/lib/autogpt-server-api/types";
export type AgentRunStatus =
| "success"
| "failed"
| "queued"
| "running"
| "stopped"
| "scheduled"
| "draft";
export const agentRunStatusMap: Record<
GraphExecutionMeta["status"],
AgentRunStatus
> = {
COMPLETED: "success",
FAILED: "failed",
QUEUED: "queued",
RUNNING: "running",
TERMINATED: "stopped",
// TODO: implement "draft" - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
};
const statusData: Record<
AgentRunStatus,
{ label: string; variant: keyof typeof statusStyles }
> = {
success: { label: "Success", variant: "success" },
running: { label: "Running", variant: "info" },
failed: { label: "Failed", variant: "destructive" },
queued: { label: "Queued", variant: "warning" },
draft: { label: "Draft", variant: "secondary" },
stopped: { label: "Stopped", variant: "secondary" },
scheduled: { label: "Scheduled", variant: "secondary" },
};
const statusStyles = {
success:
"bg-green-100 text-green-800 hover:bg-green-100 hover:text-green-800",
destructive: "bg-red-100 text-red-800 hover:bg-red-100 hover:text-red-800",
warning:
"bg-yellow-100 text-yellow-800 hover:bg-yellow-100 hover:text-yellow-800",
info: "bg-blue-100 text-blue-800 hover:bg-blue-100 hover:text-blue-800",
secondary:
"bg-slate-100 text-slate-800 hover:bg-slate-100 hover:text-slate-800",
};
export default function AgentRunStatusChip({
status,
}: {
status: AgentRunStatus;
}): React.ReactElement {
return (
<Badge
variant="secondary"
className={`text-xs font-medium ${statusStyles[statusData[status].variant]} rounded-[45px] px-[9px] py-[3px]`}
>
{statusData[status].label}
</Badge>
);
}

View File

@@ -0,0 +1,95 @@
import React from "react";
import moment from "moment";
import { MoreVertical } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import AgentRunStatusChip, {
AgentRunStatus,
} 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;
className?: string;
};
export default function AgentRunSummaryCard({
agentID,
agentRunID,
status,
title,
timestamp,
selected = false,
onClick,
className,
}: AgentRunSummaryProps): React.ReactElement {
return (
<Card
className={cn(
"agpt-rounded-card cursor-pointer border-zinc-300",
selected ? "agpt-card-selected" : "",
className,
)}
onClick={onClick}
>
<CardContent className="relative p-2.5 lg:p-4">
<AgentRunStatusChip status={status} />
<div className="mt-5 flex items-center justify-between">
<h3 className="truncate pr-2 text-base font-medium text-neutral-900">
{title}
</h3>
{/* <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
</DropdownMenuItem>
<DropdownMenuItem
// TODO: implement
>
Rename
</DropdownMenuItem>
<DropdownMenuItem
// TODO: implement
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu> */}
</div>
<p
className="mt-1 text-sm font-normal text-neutral-500"
title={moment(timestamp).toString()}
>
Ran {moment(timestamp).fromNow()}
</p>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import React, { useState } from "react";
import { Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import {
GraphExecutionMeta,
GraphMeta,
Schedule,
} from "@/lib/autogpt-server-api";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/agptui/Button";
import { Badge } from "@/components/ui/badge";
import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip";
import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card";
interface AgentRunsSelectorListProps {
agent: GraphMeta;
agentRuns: GraphExecutionMeta[];
schedules: Schedule[];
selectedView: { type: "run" | "schedule"; id?: string };
onSelectRun: (id: string) => void;
onSelectSchedule: (schedule: Schedule) => void;
onDraftNewRun: () => void;
className?: string;
}
export default function AgentRunsSelectorList({
agent,
agentRuns,
schedules,
selectedView,
onSelectRun,
onSelectSchedule,
onDraftNewRun,
className,
}: AgentRunsSelectorListProps): React.ReactElement {
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
"runs",
);
return (
<aside className={cn("flex flex-col gap-4", className)}>
<Button
size="card"
className={
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
<div className="flex gap-2">
<Badge
variant={activeListTab === "runs" ? "secondary" : "outline"}
className="cursor-pointer gap-2 rounded-full text-base"
onClick={() => setActiveListTab("runs")}
>
<span>Runs</span>
<span className="text-neutral-600">{agentRuns.length}</span>
</Badge>
<Badge
variant={activeListTab === "scheduled" ? "secondary" : "outline"}
className="cursor-pointer gap-2 rounded-full text-base"
onClick={() => setActiveListTab("scheduled")}
>
<span>Scheduled</span>
<span className="text-neutral-600">
{schedules.filter((s) => s.graph_id === agent.id).length}
</span>
</Badge>
</div>
{/* Runs / Schedules list */}
<ScrollArea className="lg:h-[calc(100vh-200px)]">
<div className="flex gap-2 lg:flex-col">
{/* New Run button - only in small layouts */}
<Button
size="card"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{activeListTab === "runs"
? agentRuns.map((run, i) => (
<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)}
/>
))
: schedules
.filter((schedule) => schedule.graph_id === agent.id)
.map((schedule, i) => (
<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)}
/>
))}
</div>
</ScrollArea>
</aside>
);
}

View File

@@ -0,0 +1,141 @@
"use client";
import React, { useCallback, useMemo } from "react";
import { BlockIOSubType, GraphMeta, Schedule } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AgentRunStatus } from "@/components/agents/agent-run-status-chip";
import { Button } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
export default function AgentScheduleDetailsView({
agent,
schedule,
onForcedRun,
agentActions,
}: {
agent: GraphMeta;
schedule: Schedule;
onForcedRun: (runID: string) => void;
agentActions: { label: string; callback: () => void }[];
}): React.ReactNode {
const api = useBackendAPI();
const selectedRunStatus: AgentRunStatus = "scheduled";
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
return [
{
label: "Status",
value:
selectedRunStatus.charAt(0).toUpperCase() +
selectedRunStatus.slice(1),
},
{
label: "Scheduled for",
value: schedule.next_run_time.toLocaleString(),
},
];
}, [schedule, selectedRunStatus]);
const agentRunInputs: Record<
string,
{ title?: string; /* type: BlockIOSubType; */ value: any }
> = useMemo(() => {
// TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(schedule.input_data).map(([k, v]) => [
k,
{
title: agent.input_schema.properties[k].title,
/* TODO: type: agent.input_schema.properties[k].type */
value: v,
},
]),
);
}, [agent, schedule]);
const runNow = useCallback(
() =>
api
.executeGraph(agent.id, agent.version, schedule.input_data)
.then((run) => onForcedRun(run.graph_exec_id)),
[api, agent, schedule, onForcedRun],
);
const runActions: { label: string; callback: () => void }[] = useMemo(
() => [{ label: "Run now", callback: () => runNow() }],
[runNow],
);
return (
<div className="agpt-div flex gap-6">
<div className="flex flex-1 flex-col gap-4">
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Info</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-stretch gap-4">
{infoStats.map(({ label, value }) => (
<div key={label} className="flex-1">
<p className="text-sm font-medium text-black">{label}</p>
<p className="text-sm text-neutral-600">{value}</p>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{agentRunInputs !== undefined ? (
Object.entries(agentRunInputs).map(([key, { title, value }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{title || key}</label>
<Input
defaultValue={value}
className="rounded-full"
disabled
/>
</div>
))
) : (
<p>Loading...</p>
)}
</CardContent>
</Card>
</div>
{/* Run / Agent Actions */}
<aside className="w-48 xl:w-56">
<div className="flex flex-col gap-8">
<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}>
{action.label}
</Button>
))}
</div>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}

View File

@@ -92,7 +92,6 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
{isVideoFile && playingVideoIndex !== index && (
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]">
<Button
variant="default"
size="default"
onClick={() => {
if (videoRef.current) {

View File

@@ -95,7 +95,7 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
{/* Title */}
<div className="font-poppins mb-3 w-full text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10">
<div className="mb-3 w-full font-poppins text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10">
{name}
</div>

View File

@@ -26,13 +26,13 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
<div className="left-0 top-0 h-px w-full bg-gray-200 dark:bg-gray-700" />
{/* Title */}
<h2 className="font-poppins underline-from-font decoration-skip-ink-none mb-[77px] mt-[25px] text-left text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
<h2 className="underline-from-font decoration-skip-ink-none mb-[77px] mt-[25px] text-left font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{title}
</h2>
{/* Content Container */}
<div className="absolute left-1/2 top-1/2 w-full max-w-[900px] -translate-x-1/2 -translate-y-1/2 px-4 pt-16 text-center md:px-6 lg:px-0">
<h2 className="font-poppins underline-from-font decoration-skip-ink-none mb-6 text-center text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
<h2 className="underline-from-font decoration-skip-ink-none mb-6 text-center font-poppins text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
Build AI agents and share
<br />
<span className="text-violet-600 dark:text-violet-400">
@@ -51,7 +51,7 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
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="font-poppins whitespace-nowrap text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
<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>

View File

@@ -65,15 +65,12 @@ export const Interactive: Story = {
export const Variants: Story = {
render: (args) => (
<div className="flex flex-wrap gap-2">
<Button {...args} variant="default">
Default
<Button {...args} variant="outline">
Outline (default)
</Button>
<Button {...args} variant="destructive">
Destructive
</Button>
<Button {...args} variant="outline">
Outline
</Button>
<Button {...args} variant="secondary">
Secondary
</Button>

View File

@@ -5,16 +5,15 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-[80px] text-xl 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 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: {
default:
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
destructive:
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
outline:
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
"border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
secondary:
"bg-neutral-100 text-[#272727] border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600",
ghost:
@@ -22,17 +21,17 @@ const buttonVariants = cva(
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
},
size: {
default:
"h-10 px-4 py-2 text-sm sm:h-12 sm:px-5 sm:py-2.5 sm:text-base md:h-14 md:px-6 md:py-3 md:text-lg lg:h-[4.375rem] lg:px-[1.625rem] lg:py-[0.4375rem] lg:text-xl",
sm: "h-8 px-3 py-1.5 text-xs sm:h-9 sm:px-3.5 sm:py-2 sm:text-sm md:h-10 md:px-4 md:py-2 md:text-base lg:h-[3.125rem] lg:px-[1.25rem] lg:py-[0.3125rem] lg:text-sm",
lg: "h-12 px-5 py-2.5 text-lg sm:h-14 sm:px-6 sm:py-3 sm:text-xl md:h-16 md:px-7 md:py-3.5 md:text-2xl lg:h-[5.625rem] lg:px-[2rem] lg:py-[0.5625rem] lg:text-2xl",
default: "h-10 px-4 py-2 rounded-full text-sm",
sm: "h-8 px-3 py-1.5 rounded-full text-xs",
lg: "h-12 px-5 py-2.5 rounded-full text-lg",
primary:
"h-10 w-28 sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]",
icon: "h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-[4.375rem] lg:w-[4.375rem]",
"h-10 w-28 rounded-full sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]",
icon: "h-10 w-10",
card: "h-12 p-5 agpt-rounded-card justify-center text-lg",
},
},
defaultVariants: {
variant: "default",
variant: "outline",
size: "default",
},
},
@@ -43,13 +42,13 @@ export interface ButtonProps
VariantProps<typeof buttonVariants> {
asChild?: boolean;
variant?:
| "default"
| "destructive"
| "accent"
| "outline"
| "secondary"
| "ghost"
| "link";
size?: "default" | "sm" | "lg" | "primary" | "icon";
size?: "default" | "sm" | "lg" | "primary" | "icon" | "card";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(

View File

@@ -33,7 +33,7 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
</AvatarFallback>
</Avatar>
<div className="flex w-full flex-col items-start justify-start gap-1.5">
<div className="font-poppins w-full text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10">
<div className="w-full font-poppins text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10">
{username}
</div>
<div className="font-geist w-full text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7">

View File

@@ -72,7 +72,6 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
) : (
<Link href="/login">
<Button
variant="default"
size="sm"
className="flex items-center justify-end space-x-2"
>
@@ -119,11 +118,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
href="/login"
className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden"
>
<Button
variant="default"
size="sm"
className="flex items-center space-x-2"
>
<Button size="sm" className="flex items-center space-x-2">
<IconLogIn className="h-5 w-5" />
<span>Log In</span>
</Button>

View File

@@ -53,7 +53,7 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
/>
)}
<div
className={`font-poppins text-[20px] font-medium leading-[28px] ${
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
activeLink === href
? "text-neutral-50 dark:text-neutral-900"
: "text-neutral-900 dark:text-neutral-50"

View File

@@ -256,7 +256,6 @@ export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
</Button>
<Button
type="submit"
variant="default"
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}

View File

@@ -100,14 +100,12 @@ export const PublishAgentAwaitingReview: React.FC<
<div className="flex w-full flex-col items-center justify-center gap-4 border-t border-slate-200 p-6 dark:border-slate-700 sm:flex-row">
<Button
onClick={onDone}
variant="outline"
className="h-12 w-full rounded-[59px] sm:flex-1"
>
Done
</Button>
<Button
onClick={onViewProgress}
variant="default"
className="h-12 w-full rounded-[59px] bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-600 sm:flex-1"
>
View progress

View File

@@ -84,7 +84,6 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
</div>
<Button
onClick={onOpenBuilder}
variant="default"
size="lg"
className="bg-neutral-800 text-white hover:bg-neutral-900"
>
@@ -150,12 +149,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
</div>
<div className="flex justify-between gap-4 border-t border-slate-200 p-4 dark:border-slate-700 sm:p-6">
<Button
onClick={onCancel}
variant="outline"
size="default"
className="w-full sm:flex-1"
>
<Button onClick={onCancel} size="lg" className="w-full sm:flex-1">
Back
</Button>
<Button
@@ -165,8 +159,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
}
}}
disabled={!selectedAgentId || !selectedAgentVersion}
variant="default"
size="default"
size="lg"
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 sm:flex-1"
>
Next

View File

@@ -344,8 +344,6 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
You can use AI to generate a cover image for you
</p>
<Button
variant="default"
size="sm"
className={`bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 ${
images.length >= 5 ? "cursor-not-allowed opacity-50" : ""
}`}
@@ -426,16 +424,14 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
<div className="flex justify-between gap-4 border-t border-slate-200 p-6 dark:border-slate-700">
<Button
onClick={onBack}
variant="outline"
size="default"
size="lg"
className="w-full dark:border-slate-700 dark:text-slate-300 sm:flex-1"
>
Back
</Button>
<Button
onClick={handleSubmit}
variant="default"
size="default"
size="lg"
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 sm:flex-1"
>
Submit for review

View File

@@ -71,7 +71,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
{/* Content Section */}
<div className="w-full px-2 py-4">
{/* Title and Creator */}
<h3 className="font-poppins mb-0.5 text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
<h3 className="mb-0.5 font-poppins text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
{agentName}
</h3>
{!hideAvatar && creatorName && (

View File

@@ -47,7 +47,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
return (
<div className="flex flex-col items-center justify-center py-4 lg:py-8">
<div className="w-full max-w-[1360px]">
<div className="font-poppins decoration-skip-ink-none mb-8 text-left text-[18px] font-[600] leading-7 text-[#282828] underline-offset-[from-font] dark:text-neutral-200">
<div className="decoration-skip-ink-none mb-8 text-left font-poppins text-[18px] font-[600] leading-7 text-[#282828] underline-offset-[from-font] dark:text-neutral-200">
{sectionTitle}
</div>
{!displayedAgents || displayedAgents.length === 0 ? (
@@ -65,7 +65,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
>
<CarouselContent>
{displayedAgents.map((agent, index) => (
<CarouselItem key={index} className="min-w-64 max-w-68">
<CarouselItem key={index} className="min-w-64 max-w-71">
<StoreCard
agentName={agent.agent_name}
agentImage={agent.agent_image}

View File

@@ -33,7 +33,7 @@ export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
return (
<div className="flex w-full flex-col items-center justify-center py-16">
<div className="w-full max-w-[1360px]">
<h2 className="font-poppins mb-8 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{title}
</h2>

View File

@@ -48,7 +48,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
return (
<div className="flex w-full flex-col items-center justify-center">
<div className="w-[99vw]">
<h2 className="font-poppins mx-auto mb-8 max-w-[1360px] px-4 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
<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>

View File

@@ -18,17 +18,17 @@ export const HeroSection: React.FC = () => {
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">
<div className="mb-4 text-center md:mb-8">
<h1 className="text-center">
<span className="font-poppin text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
Explore AI agents built for{" "}
</span>
<span className="font-poppin text-[48px] font-semibold leading-[54px] text-violet-600">
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-violet-600">
you
</span>
<br />
<span className="font-poppin text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
by the{" "}
</span>
<span className="font-poppin text-[48px] font-semibold leading-[54px] text-blue-500">
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-blue-500">
community
</span>
</h1>

View File

@@ -284,7 +284,7 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
}}
>
<PopoverTrigger asChild>
{trigger || <Button variant="default">Publish Agent</Button>}
{trigger || <Button>Publish Agent</Button>}
</PopoverTrigger>
<PopoverAnchor asChild>
<div className="fixed left-0 top-0 hidden h-screen w-screen items-center justify-center"></div>

View File

@@ -1,4 +1,4 @@
import { GraphExecution, LibraryAgent } from "@/lib/autogpt-server-api";
import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api";
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -37,7 +37,7 @@ export const AgentFlowList = ({
className,
}: {
flows: LibraryAgent[];
executions?: GraphExecution[];
executions?: GraphExecutionMeta[];
selectedFlow: LibraryAgent | null;
onSelectFlow: (f: LibraryAgent) => void;
className?: string;
@@ -106,7 +106,7 @@ export const AgentFlowList = ({
{flows
.map((flow) => {
let runCount = 0,
lastRun: GraphExecution | null = null;
lastRun: GraphExecutionMeta | null = null;
if (executions) {
const _flowRuns = executions.filter(
(r) => r.graph_id == flow.agent_id,

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback } from "react";
import {
GraphExecution,
GraphExecutionMeta,
Graph,
safeCopyGraph,
BlockUIType,
@@ -32,7 +32,6 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { useToast } from "@/components/ui/use-toast";
import { CronScheduler } from "@/components/cronScheduler";
import RunnerInputUI from "@/components/runner-ui/RunnerInputUI";
import useAgentGraph from "@/hooks/useAgentGraph";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
@@ -40,7 +39,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const FlowInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
flow: LibraryAgent;
executions: GraphExecution[];
executions: GraphExecutionMeta[];
flowVersion?: number | "all";
refresh: () => void;
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import {
GraphExecution,
GraphExecutionMeta,
LibraryAgent,
NodeExecutionResult,
SpecialBlockID,
@@ -18,7 +18,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const FlowRunInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
flow: LibraryAgent;
execution: GraphExecution;
execution: GraphExecutionMeta;
}
> = ({ flow, execution, ...props }) => {
const [isOutputOpen, setIsOutputOpen] = useState(false);
@@ -26,10 +26,9 @@ export const FlowRunInfo: React.FC<
const api = useBackendAPI();
const fetchBlockResults = useCallback(async () => {
const executionResults = await api.getGraphExecutionInfo(
flow.agent_id,
execution.execution_id,
);
const executionResults = (
await api.getGraphExecutionInfo(flow.agent_id, execution.execution_id)
).node_executions;
// Create a map of the latest COMPLETED execution results of output nodes by node_id
const latestCompletedResults = executionResults

View File

@@ -1,10 +1,10 @@
import React from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { GraphExecution } from "@/lib/autogpt-server-api";
import { GraphExecutionMeta } from "@/lib/autogpt-server-api";
export const FlowRunStatusBadge: React.FC<{
status: GraphExecution["status"];
status: GraphExecutionMeta["status"];
className?: string;
}> = ({ status, className }) => (
<Badge

View File

@@ -1,5 +1,5 @@
import React from "react";
import { GraphExecution, LibraryAgent } from "@/lib/autogpt-server-api";
import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
@@ -15,10 +15,10 @@ import { TextRenderer } from "../ui/render";
export const FlowRunsList: React.FC<{
flows: LibraryAgent[];
executions: GraphExecution[];
executions: GraphExecutionMeta[];
className?: string;
selectedRun?: GraphExecution | null;
onSelectRun: (r: GraphExecution) => void;
selectedRun?: GraphExecutionMeta | null;
onSelectRun: (r: GraphExecutionMeta) => void;
}> = ({ flows, executions, selectedRun, onSelectRun, className }) => (
<Card className={className}>
<CardHeader>

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { GraphExecution, LibraryAgent } from "@/lib/autogpt-server-api";
import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api";
import { CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
@@ -12,7 +12,7 @@ import { FlowRunsTimeline } from "@/components/monitor/FlowRunsTimeline";
export const FlowRunsStatus: React.FC<{
flows: LibraryAgent[];
executions: GraphExecution[];
executions: GraphExecutionMeta[];
title?: string;
className?: string;
}> = ({ flows, executions: executions, title, className }) => {

View File

@@ -1,4 +1,4 @@
import { GraphExecution, LibraryAgent } from "@/lib/autogpt-server-api";
import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api";
import {
ComposedChart,
DefaultLegendContentProps,
@@ -23,7 +23,7 @@ export const FlowRunsTimeline = ({
className,
}: {
flows: LibraryAgent[];
executions: GraphExecution[];
executions: GraphExecutionMeta[];
dataMin: "dataMin" | number;
className?: string;
}) => (
@@ -60,8 +60,10 @@ export const FlowRunsTimeline = ({
<Tooltip
content={({ payload, label }) => {
if (payload && payload.length) {
const data: GraphExecution & { time: number; _duration: number } =
payload[0].payload;
const data: GraphExecutionMeta & {
time: number;
_duration: number;
} = payload[0].payload;
const flow = flows.find((f) => f.agent_id === data.graph_id);
return (
<Card className="p-2 text-xs leading-normal">

View File

@@ -242,7 +242,7 @@ export const SchedulesTable = ({
</TableCell>
<TableCell>{schedule.graph_version}</TableCell>
<TableCell>
{new Date(schedule.next_run_time).toLocaleString()}
{schedule.next_run_time.toLocaleString()}
</TableCell>
<TableCell>
<Badge variant="secondary">

View File

@@ -8,10 +8,7 @@ const Card = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border border-gray-300 bg-white text-neutral-950 shadow",
className,
)}
className={cn("agpt-card text-neutral-950", className)}
{...props}
/>
));

View File

@@ -606,8 +606,11 @@ export default function useAgentGraph(
}
const fetchExecutions = async () => {
const results = await api.getGraphExecutionInfo(flowID, flowExecutionID);
setUpdateQueue((prev) => [...prev, ...results]);
const execution = await api.getGraphExecutionInfo(
flowID,
flowExecutionID,
);
setUpdateQueue((prev) => [...prev, ...execution.node_executions]);
// Track execution until completed
const pendingNodeExecutions: Set<string> = new Set();

View File

@@ -14,6 +14,7 @@ import {
CredentialsDeleteResponse,
CredentialsMetaResponse,
GraphExecution,
GraphExecutionMeta,
Graph,
GraphCreatable,
GraphMeta,
@@ -161,10 +162,6 @@ export default class BackendAPI {
return this._get(`/graphs`);
}
getExecutions(): Promise<GraphExecution[]> {
return this._get(`/executions`);
}
getGraph(
id: string,
version?: number,
@@ -212,22 +209,37 @@ export default class BackendAPI {
return this._request("POST", `/graphs/${id}/execute/${version}`, inputData);
}
getExecutions(): Promise<GraphExecutionMeta[]> {
return this._get(`/executions`);
}
getGraphExecutions(graphID: string): Promise<GraphExecutionMeta[]> {
return this._get(`/graphs/${graphID}/executions`);
}
async getGraphExecutionInfo(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
): Promise<GraphExecution> {
const result = await this._get(`/graphs/${graphID}/executions/${runID}`);
result.node_executions = result.node_executions.map(
parseNodeExecutionResultTimestamps,
);
return result;
}
async stopGraphExecution(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
).map(parseNodeExecutionResultTimestamps);
): Promise<GraphExecution> {
const result = await this._request(
"POST",
`/graphs/${graphID}/executions/${runID}/stop`,
);
result.node_executions = result.node_executions.map(
parseNodeExecutionResultTimestamps,
);
return result;
}
oAuthLogin(
@@ -484,15 +496,19 @@ export default class BackendAPI {
}
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
return this._request("POST", `/schedules`, schedule);
return this._request("POST", `/schedules`, schedule).then(
parseScheduleTimestamp,
);
}
async deleteSchedule(scheduleId: string): Promise<Schedule> {
async deleteSchedule(scheduleId: string): Promise<{ id: string }> {
return this._request("DELETE", `/schedules/${scheduleId}`);
}
async listSchedules(): Promise<Schedule[]> {
return this._get(`/schedules`);
return this._get(`/schedules`).then((schedules) =>
schedules.map(parseScheduleTimestamp),
);
}
private async _uploadFile(path: string, file: File): Promise<string> {
@@ -824,3 +840,10 @@ function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
end_time: result.end_time ? new Date(result.end_time) : undefined,
};
}
function parseScheduleTimestamp(result: any): Schedule {
return {
...result,
next_run_time: new Date(result.next_run_time),
};
}

View File

@@ -41,6 +41,8 @@ export type BlockIOSubSchema =
| BlockIOSimpleTypeSubSchema
| BlockIOCombinedTypeSubSchema;
export type BlockIOSubType = BlockIOSimpleTypeSubSchema["type"];
export type BlockIOSimpleTypeSubSchema =
| BlockIOObjectSubSchema
| BlockIOCredentialsSubSchema
@@ -152,8 +154,7 @@ export const PROVIDER_NAMES = {
export type CredentialsProviderName =
(typeof PROVIDER_NAMES)[keyof typeof PROVIDER_NAMES];
export type BlockIOCredentialsSubSchema = BlockIOSubSchemaMeta & {
type: "object";
export type BlockIOCredentialsSubSchema = BlockIOObjectSubSchema & {
/* Mirror of backend/data/model.py:CredentialsFieldSchemaExtra */
credentials_provider: CredentialsProviderName[];
credentials_scopes?: string[];
@@ -170,21 +171,17 @@ export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
// At the time of writing, combined schemas only occur on the first nested level in a
// block schema. It is typed this way to make the use of these objects less tedious.
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta &
(
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & { type: never } & (
| {
type: "allOf";
allOf: [BlockIOSimpleTypeSubSchema];
secret?: boolean;
}
| {
type: "anyOf";
anyOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
secret?: boolean;
}
| {
type: "oneOf";
oneOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
secret?: boolean;
@@ -219,8 +216,8 @@ export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
id?: string;
};
/* Mirror of backend/data/graph.py:GraphExecution */
export type GraphExecution = {
/* Mirror of backend/data/graph.py:GraphExecutionMeta */
export type GraphExecutionMeta = {
execution_id: string;
started_at: number;
ended_at: number;
@@ -229,6 +226,14 @@ export type GraphExecution = {
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
graph_id: GraphID;
graph_version: number;
preset_id?: string;
};
/* Mirror of backend/data/graph.py:GraphExecution */
export type GraphExecution = GraphExecutionMeta & {
inputs: Record<string, any>;
outputs: Record<string, Array<any>>;
node_executions: NodeExecutionResult[];
};
export type GraphMeta = {
@@ -237,12 +242,27 @@ export type GraphMeta = {
is_active: boolean;
name: string;
description: string;
input_schema: BlockIOObjectSubSchema;
output_schema: BlockIOObjectSubSchema;
input_schema: GraphIOSchema;
output_schema: GraphIOSchema;
};
export type GraphID = Brand<string, "GraphID">;
/* Derived from backend/data/graph.py:Graph._generate_schema() */
export type GraphIOSchema = {
type: "object";
properties: { [key: string]: GraphIOSubSchema };
required: (keyof BlockIORootSchema["properties"])[];
};
export type GraphIOSubSchema = Omit<
BlockIOSubSchemaMeta,
"placeholder" | "depends_on" | "hidden"
> & {
type: never; // bodge to avoid type checking hell; doesn't exist at runtime
default?: string;
secret: boolean;
};
/* Mirror of backend/data/graph.py:Graph */
export type Graph = GraphMeta & {
nodes: Array<Node>;
@@ -256,8 +276,8 @@ export type GraphUpdateable = Omit<
version?: number;
is_active?: boolean;
links: Array<LinkCreatable>;
input_schema?: BlockIOObjectSubSchema;
output_schema?: BlockIOObjectSubSchema;
input_schema?: GraphIOSchema;
output_schema?: GraphIOSchema;
};
export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string };
@@ -553,7 +573,7 @@ export type Schedule = {
graph_id: GraphID;
graph_version: number;
input_data: { [key: string]: any };
next_run_time: string;
next_run_time: Date;
};
export type ScheduleCreatable = {

View File

@@ -18,7 +18,7 @@ const config = {
mono: ["var(--font-geist-mono)"],
// Include the custom font family
neue: ['"PP Neue Montreal TT"', "sans-serif"],
poppin: ["var(--font-poppins)"],
poppins: ["var(--font-poppins)"],
inter: ["var(--font-inter)"],
},
colors: {
@@ -95,26 +95,20 @@ const config = {
28: "7rem",
32: "8rem",
36: "9rem",
39: "9.875rem",
40: "10rem",
44: "11rem",
48: "12rem",
52: "13rem",
56: "14rem",
69: "14.875rem",
60: "15rem",
63: "15.875rem",
64: "16rem",
68: "17.75rem",
68: "17rem",
70: "17.5rem",
71: "17.75rem",
72: "18rem",
77: "18.5625rem",
76: "19rem",
80: "20rem",
89: "22.5rem",
96: "24rem",
110: "27.5rem",
139: "37.1875rem",
167: "41.6875rem",
225: "56.25rem",
},
borderRadius: {
lg: "var(--radius)",