Compare commits

...

22 Commits

Author SHA1 Message Date
Lluis Agusti
ee11623735 chore: vercel preview 2025-07-14 16:14:58 +04:00
Lluis Agusti
0bb160e930 chore: generate 2025-07-14 15:38:33 +04:00
Lluis Agusti
81a09738dc chore: CAPTCHA 2025-07-14 15:23:39 +04:00
Lluis Agusti
6feedafd7d Merge 'dev' into 'feat/agent-notifications' 2025-07-14 15:05:36 +04:00
Lluis Agusti
547da633c4 Merge 'dev' into 'feat/agent-notifications' 2025-07-14 14:41:01 +04:00
Lluis Agusti
6d13dfc688 chore: empty state 2025-07-11 19:56:12 +04:00
Lluis Agusti
d0d498fa66 chore: undo 2025-07-11 19:44:27 +04:00
Lluis Agusti
c843dee317 Merge 'dev' into 'feat/agent-notifications' 2025-07-11 19:36:01 +04:00
Lluis Agusti
db969c1bf8 chore: rename 2025-07-11 19:35:53 +04:00
Lluis Agusti
690fac91e4 chore: lint 2025-07-11 18:58:13 +04:00
Lluis Agusti
5368fdc998 chore: tests 2025-07-11 18:31:44 +04:00
Lluis Agusti
b9d293f181 chore: updates 2025-07-11 18:15:30 +04:00
Lluis Agusti
acbcef77b2 Merge 'dev' into 'feat/agent-notifications' 2025-07-11 17:40:50 +04:00
Lluis Agusti
e902848e04 chore: fix 2025-07-10 23:06:07 +04:00
Lluis Agusti
cd917ec919 Merge 'dev' into 'feat/agent-notifications' 2025-07-10 22:48:24 +04:00
Lluis Agusti
8ae37491e4 Merge 'dev' into 'feat/agent-notifications' 2025-07-10 18:41:12 +04:00
Lluis Agusti
f45e5e0d59 chore: prettier 2025-07-09 14:57:33 +04:00
Lluis Agusti
1231236d87 chore: lock 2025-07-09 14:46:43 +04:00
Lluis Agusti
4db0792ade Merge 'dev' into 'feat/agent-notifications' 2025-07-09 14:45:46 +04:00
Lluis Agusti
81cb6fb1e6 chore: fixes... 2025-07-08 19:45:09 +04:00
Lluis Agusti
c16598eed6 Merge 'dev' into 'feat/agent-notifications' 2025-07-08 19:32:04 +04:00
Lluis Agusti
7706740308 chore: agent notifications 2025-07-07 13:36:43 +04:00
41 changed files with 2686 additions and 540 deletions

View File

@@ -75,6 +75,7 @@
"moment": "2.30.1",
"next": "15.3.5",
"next-themes": "0.4.6",
"nuqs": "2.4.3",
"party-js": "2.2.0",
"react": "18.3.1",
"react-day-picker": "9.8.0",

View File

@@ -155,6 +155,9 @@ importers:
next-themes:
specifier: 0.4.6
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nuqs:
specifier: 2.4.3
version: 2.4.3(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
party-js:
specifier: 2.2.0
version: 2.2.0
@@ -5329,6 +5332,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
@@ -5459,6 +5465,24 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
nuqs@2.4.3:
resolution: {integrity: sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==}
peerDependencies:
'@remix-run/react': '>=2'
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: ^6 || ^7
react-router-dom: ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
optional: true
next:
optional: true
react-router:
optional: true
react-router-dom:
optional: true
oas-kit-common@1.0.8:
resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==}
@@ -12998,6 +13022,8 @@ snapshots:
minipass@7.1.2: {}
mitt@3.0.1: {}
module-details-from-path@1.0.4: {}
moment@2.30.1: {}
@@ -13160,6 +13186,13 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.4.3(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
mitt: 3.0.1
react: 18.3.1
optionalDependencies:
next: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
oas-kit-common@1.0.8:
dependencies:
fast-safe-stringify: 2.1.1

View File

@@ -1,5 +1,6 @@
"use client";
import { useParams, useRouter } from "next/navigation";
import { useQueryState } from "nuqs";
import React, {
useCallback,
useEffect,
@@ -45,6 +46,7 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: LibraryAgentID } = useParams();
const [executionId, setExecutionId] = useQueryState("executionId");
const { toast } = useToast();
const router = useRouter();
const api = useBackendAPI();
@@ -202,6 +204,13 @@ export default function AgentRunsPage(): React.ReactElement {
selectPreset,
]);
useEffect(() => {
if (executionId) {
selectRun(executionId as GraphExecutionID);
setExecutionId(null);
}
}, [executionId, selectRun, setExecutionId]);
// Initial load
useEffect(() => {
refreshPageData();
@@ -468,7 +477,7 @@ export default function AgentRunsPage(): React.ReactElement {
}
return (
<div className="container justify-stretch p-0 lg:flex">
<div className="container justify-stretch p-0 pt-16 lg:flex">
{/* Sidebar w/ list of runs */}
{/* TODO: render this below header in sm and md layouts */}
<AgentRunsSelectorList

View File

@@ -23,6 +23,7 @@ export async function login(
return await Sentry.withServerActionInstrumentation("login", {}, async () => {
const supabase = await getServerSupabase();
const api = new BackendAPI();
const isVercelPreview = process.env.VERCEL_ENV === "preview";
if (!supabase) {
redirect("/error");
@@ -30,7 +31,7 @@ export async function login(
// Verify Turnstile token if provided
const success = await verifyTurnstileToken(turnstileToken, "login");
if (!success) {
if (!success && !isVercelPreview) {
return "CAPTCHA verification failed. Please try again.";
}

View File

@@ -21,6 +21,7 @@ export default function LoginPage() {
isLoading,
isLoggedIn,
isCloudEnv,
shouldNotRenderCaptcha,
isUserLoading,
isGoogleLoading,
showNotAllowedModal,
@@ -85,16 +86,18 @@ export default function LoginPage() {
/>
{/* Turnstile CAPTCHA Component */}
<Turnstile
key={captchaKey}
siteKey={turnstile.siteKey}
onVerify={turnstile.handleVerify}
onExpire={turnstile.handleExpire}
onError={turnstile.handleError}
setWidgetId={turnstile.setWidgetId}
action="login"
shouldRender={turnstile.shouldRender}
/>
{shouldNotRenderCaptcha ? null : (
<Turnstile
key={captchaKey}
siteKey={turnstile.siteKey}
onVerify={turnstile.handleVerify}
onExpire={turnstile.handleExpire}
onError={turnstile.handleError}
setWidgetId={turnstile.setWidgetId}
action="login"
shouldRender={turnstile.shouldRender}
/>
)}
<Button
variant="primary"

View File

@@ -28,6 +28,8 @@ export function useLoginPage() {
resetOnError: true,
});
const shouldNotRenderCaptcha = isVercelPreview || turnstile.verified;
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
@@ -125,6 +127,7 @@ export function useLoginPage() {
isLoading,
isCloudEnv,
isUserLoading,
shouldNotRenderCaptcha,
isGoogleLoading,
showNotAllowedModal,
isSupabaseAvailable: !!supabase,

View File

@@ -18,6 +18,7 @@ export async function signup(
{},
async () => {
const supabase = await getServerSupabase();
const isVercelPreview = process.env.VERCEL_ENV === "preview";
if (!supabase) {
redirect("/error");
@@ -25,7 +26,7 @@ export async function signup(
// Verify Turnstile token if provided
const success = await verifyTurnstileToken(turnstileToken, "signup");
if (!success) {
if (!success && !isVercelPreview) {
return "CAPTCHA verification failed. Please try again.";
}

View File

@@ -32,6 +32,7 @@ export default function SignupPage() {
isLoading,
isCloudEnv,
isUserLoading,
shouldNotRenderCaptcha,
isGoogleLoading,
showNotAllowedModal,
isSupabaseAvailable,
@@ -163,16 +164,18 @@ export default function SignupPage() {
/>
{/* Turnstile CAPTCHA Component */}
<Turnstile
key={captchaKey}
siteKey={turnstile.siteKey}
onVerify={turnstile.handleVerify}
onExpire={turnstile.handleExpire}
onError={turnstile.handleError}
setWidgetId={turnstile.setWidgetId}
action="signup"
shouldRender={turnstile.shouldRender}
/>
{shouldNotRenderCaptcha ? null : (
<Turnstile
key={captchaKey}
siteKey={turnstile.siteKey}
onVerify={turnstile.handleVerify}
onExpire={turnstile.handleExpire}
onError={turnstile.handleError}
setWidgetId={turnstile.setWidgetId}
action="signup"
shouldRender={turnstile.shouldRender}
/>
)}
<Button
variant="primary"

View File

@@ -30,6 +30,8 @@ export function useSignupPage() {
resetOnError: true,
});
const shouldNotRenderCaptcha = isVercelPreview || turnstile.verified;
const resetCaptcha = useCallback(() => {
setCaptchaKey((k) => k + 1);
turnstile.reset();
@@ -132,6 +134,7 @@ export function useSignupPage() {
isCloudEnv,
isUserLoading,
isGoogleLoading,
shouldNotRenderCaptcha,
showNotAllowedModal,
isSupabaseAvailable: !!supabase,
handleSubmit: form.handleSubmit(handleSignup),

View File

@@ -39,6 +39,8 @@ import type { Graph } from "../../models/graph";
import type { GraphExecutionMeta } from "../../models/graphExecutionMeta";
import type { GraphMeta } from "../../models/graphMeta";
import type { GraphModel } from "../../models/graphModel";
import type { HTTPValidationError } from "../../models/hTTPValidationError";
@@ -47,8 +49,6 @@ import type { PostV1ExecuteGraphAgentParams } from "../../models/postV1ExecuteGr
import type { PostV1StopGraphExecution200 } from "../../models/postV1StopGraphExecution200";
import type { PostV1StopGraphExecutionsParams } from "../../models/postV1StopGraphExecutionsParams";
import type { SetGraphActiveVersion } from "../../models/setGraphActiveVersion";
import { customMutator } from "../../../mutators/custom-mutator";
@@ -59,7 +59,7 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
* @summary List user graphs
*/
export type getV1ListUserGraphsResponse200 = {
data: GraphModel[];
data: GraphMeta[];
status: 200;
};
@@ -1610,130 +1610,6 @@ export const usePostV1StopGraphExecution = <
return useMutation(mutationOptions, queryClient);
};
/**
* @summary Stop graph executions
*/
export type postV1StopGraphExecutionsResponse200 = {
data: GraphExecutionMeta[];
status: 200;
};
export type postV1StopGraphExecutionsResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type postV1StopGraphExecutionsResponseComposite =
| postV1StopGraphExecutionsResponse200
| postV1StopGraphExecutionsResponse422;
export type postV1StopGraphExecutionsResponse =
postV1StopGraphExecutionsResponseComposite & {
headers: Headers;
};
export const getPostV1StopGraphExecutionsUrl = (
params: PostV1StopGraphExecutionsParams,
) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/executions?${stringifiedParams}`
: `/api/executions`;
};
export const postV1StopGraphExecutions = async (
params: PostV1StopGraphExecutionsParams,
options?: RequestInit,
): Promise<postV1StopGraphExecutionsResponse> => {
return customMutator<postV1StopGraphExecutionsResponse>(
getPostV1StopGraphExecutionsUrl(params),
{
...options,
method: "POST",
},
);
};
export const getPostV1StopGraphExecutionsMutationOptions = <
TError = HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof postV1StopGraphExecutions>>,
TError,
{ params: PostV1StopGraphExecutionsParams },
TContext
>;
request?: SecondParameter<typeof customMutator>;
}): UseMutationOptions<
Awaited<ReturnType<typeof postV1StopGraphExecutions>>,
TError,
{ params: PostV1StopGraphExecutionsParams },
TContext
> => {
const mutationKey = ["postV1StopGraphExecutions"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof postV1StopGraphExecutions>>,
{ params: PostV1StopGraphExecutionsParams }
> = (props) => {
const { params } = props ?? {};
return postV1StopGraphExecutions(params, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type PostV1StopGraphExecutionsMutationResult = NonNullable<
Awaited<ReturnType<typeof postV1StopGraphExecutions>>
>;
export type PostV1StopGraphExecutionsMutationError = HTTPValidationError;
/**
* @summary Stop graph executions
*/
export const usePostV1StopGraphExecutions = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof postV1StopGraphExecutions>>,
TError,
{ params: PostV1StopGraphExecutionsParams },
TContext
>;
request?: SecondParameter<typeof customMutator>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof postV1StopGraphExecutions>>,
TError,
{ params: PostV1StopGraphExecutionsParams },
TContext
> => {
const mutationOptions = getPostV1StopGraphExecutionsMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};
/**
* @summary Get all executions
*/

View File

@@ -37,14 +37,10 @@ import type { HTTPValidationError } from "../../models/hTTPValidationError";
import type { LibraryAgent } from "../../models/libraryAgent";
import type { LibraryAgentPreset } from "../../models/libraryAgentPreset";
import type { LibraryAgentResponse } from "../../models/libraryAgentResponse";
import type { LibraryAgentUpdateRequest } from "../../models/libraryAgentUpdateRequest";
import type { TriggeredPresetSetupParams } from "../../models/triggeredPresetSetupParams";
import { customMutator } from "../../../mutators/custom-mutator";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
@@ -1588,117 +1584,3 @@ export const usePostV2ForkLibraryAgent = <
return useMutation(mutationOptions, queryClient);
};
/**
* Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.
Returns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.
* @summary Setup Trigger
*/
export type postV2SetupTriggerResponse200 = {
data: LibraryAgentPreset;
status: 200;
};
export type postV2SetupTriggerResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type postV2SetupTriggerResponseComposite =
| postV2SetupTriggerResponse200
| postV2SetupTriggerResponse422;
export type postV2SetupTriggerResponse = postV2SetupTriggerResponseComposite & {
headers: Headers;
};
export const getPostV2SetupTriggerUrl = (libraryAgentId: string) => {
return `/api/library/agents/${libraryAgentId}/setup-trigger`;
};
export const postV2SetupTrigger = async (
libraryAgentId: string,
triggeredPresetSetupParams: TriggeredPresetSetupParams,
options?: RequestInit,
): Promise<postV2SetupTriggerResponse> => {
return customMutator<postV2SetupTriggerResponse>(
getPostV2SetupTriggerUrl(libraryAgentId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(triggeredPresetSetupParams),
},
);
};
export const getPostV2SetupTriggerMutationOptions = <
TError = HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ libraryAgentId: string; data: TriggeredPresetSetupParams },
TContext
>;
request?: SecondParameter<typeof customMutator>;
}): UseMutationOptions<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ libraryAgentId: string; data: TriggeredPresetSetupParams },
TContext
> => {
const mutationKey = ["postV2SetupTrigger"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
{ libraryAgentId: string; data: TriggeredPresetSetupParams }
> = (props) => {
const { libraryAgentId, data } = props ?? {};
return postV2SetupTrigger(libraryAgentId, data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type PostV2SetupTriggerMutationResult = NonNullable<
Awaited<ReturnType<typeof postV2SetupTrigger>>
>;
export type PostV2SetupTriggerMutationBody = TriggeredPresetSetupParams;
export type PostV2SetupTriggerMutationError = HTTPValidationError;
/**
* @summary Setup Trigger
*/
export const usePostV2SetupTrigger = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ libraryAgentId: string; data: TriggeredPresetSetupParams },
TContext
>;
request?: SecondParameter<typeof customMutator>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ libraryAgentId: string; data: TriggeredPresetSetupParams },
TContext
> => {
const mutationOptions = getPostV2SetupTriggerMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};

View File

@@ -37,6 +37,8 @@ import type { PostV2CreateANewPresetBody } from "../../models/postV2CreateANewPr
import type { PostV2ExecuteAPreset200 } from "../../models/postV2ExecuteAPreset200";
import type { TriggeredPresetSetupRequest } from "../../models/triggeredPresetSetupRequest";
import { customMutator } from "../../../mutators/custom-mutator";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
@@ -779,6 +781,116 @@ export const useDeleteV2DeleteAPreset = <
return useMutation(mutationOptions, queryClient);
};
/**
* Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.
Returns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.
* @summary Setup Trigger
*/
export type postV2SetupTriggerResponse200 = {
data: LibraryAgentPreset;
status: 200;
};
export type postV2SetupTriggerResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type postV2SetupTriggerResponseComposite =
| postV2SetupTriggerResponse200
| postV2SetupTriggerResponse422;
export type postV2SetupTriggerResponse = postV2SetupTriggerResponseComposite & {
headers: Headers;
};
export const getPostV2SetupTriggerUrl = () => {
return `/api/library/presets/setup-trigger`;
};
export const postV2SetupTrigger = async (
triggeredPresetSetupRequest: TriggeredPresetSetupRequest,
options?: RequestInit,
): Promise<postV2SetupTriggerResponse> => {
return customMutator<postV2SetupTriggerResponse>(getPostV2SetupTriggerUrl(), {
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(triggeredPresetSetupRequest),
});
};
export const getPostV2SetupTriggerMutationOptions = <
TError = HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ data: TriggeredPresetSetupRequest },
TContext
>;
request?: SecondParameter<typeof customMutator>;
}): UseMutationOptions<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ data: TriggeredPresetSetupRequest },
TContext
> => {
const mutationKey = ["postV2SetupTrigger"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
{ data: TriggeredPresetSetupRequest }
> = (props) => {
const { data } = props ?? {};
return postV2SetupTrigger(data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type PostV2SetupTriggerMutationResult = NonNullable<
Awaited<ReturnType<typeof postV2SetupTrigger>>
>;
export type PostV2SetupTriggerMutationBody = TriggeredPresetSetupRequest;
export type PostV2SetupTriggerMutationError = HTTPValidationError;
/**
* @summary Setup Trigger
*/
export const usePostV2SetupTrigger = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ data: TriggeredPresetSetupRequest },
TContext
>;
request?: SecondParameter<typeof customMutator>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof postV2SetupTrigger>>,
TError,
{ data: TriggeredPresetSetupRequest },
TContext
> => {
const mutationOptions = getPostV2SetupTriggerMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};
/**
* Execute a preset with the given graph and node input for the current user.
* @summary Execute a preset

View File

@@ -24,4 +24,5 @@ export interface BaseGraphOutput {
forked_from_version?: BaseGraphOutputForkedFromVersion;
readonly input_schema: BaseGraphOutputInputSchema;
readonly output_schema: BaseGraphOutputOutputSchema;
readonly has_external_trigger: boolean;
}

View File

@@ -6,12 +6,12 @@
* OpenAPI spec version: 0.1
*/
import type { CredentialsMetaInputTitle } from "./credentialsMetaInputTitle";
import type { ProviderName } from "./providerName";
import type { CredentialsMetaInputType } from "./credentialsMetaInputType";
export interface CredentialsMetaInput {
id: string;
title?: CredentialsMetaInputTitle;
provider: ProviderName;
/** Provider name for integrations. Can be any string value, including custom provider names. */
provider: string;
type: CredentialsMetaInputType;
}

View File

@@ -0,0 +1,29 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { GraphMetaForkedFromId } from "./graphMetaForkedFromId";
import type { GraphMetaForkedFromVersion } from "./graphMetaForkedFromVersion";
import type { BaseGraphOutput } from "./baseGraphOutput";
import type { GraphMetaInputSchema } from "./graphMetaInputSchema";
import type { GraphMetaOutputSchema } from "./graphMetaOutputSchema";
import type { GraphMetaCredentialsInputSchema } from "./graphMetaCredentialsInputSchema";
export interface GraphMeta {
id?: string;
version?: number;
is_active?: boolean;
name: string;
description: string;
forked_from_id?: GraphMetaForkedFromId;
forked_from_version?: GraphMetaForkedFromVersion;
sub_graphs?: BaseGraphOutput[];
user_id: string;
readonly input_schema: GraphMetaInputSchema;
readonly output_schema: GraphMetaOutputSchema;
readonly has_external_trigger: boolean;
readonly credentials_input_schema: GraphMetaCredentialsInputSchema;
}

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type GraphMetaCredentialsInputSchema = { [key: string]: unknown };

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type GraphMetaForkedFromId = string | null;

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type GraphMetaForkedFromVersion = number | null;

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type GraphMetaInputSchema = { [key: string]: unknown };

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type GraphMetaOutputSchema = { [key: string]: unknown };

View File

@@ -28,6 +28,6 @@ export interface GraphModel {
user_id: string;
readonly input_schema: GraphModelInputSchema;
readonly output_schema: GraphModelOutputSchema;
readonly has_external_trigger: boolean;
readonly credentials_input_schema: GraphModelCredentialsInputSchema;
readonly has_webhook_trigger: boolean;
}

View File

@@ -5,12 +5,12 @@
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { ProviderName } from "./providerName";
import type { LibraryAgentTriggerInfoConfigSchema } from "./libraryAgentTriggerInfoConfigSchema";
import type { LibraryAgentTriggerInfoCredentialsInputName } from "./libraryAgentTriggerInfoCredentialsInputName";
export interface LibraryAgentTriggerInfo {
provider: ProviderName;
/** Provider name for integrations. Can be any string value, including custom provider names. */
provider: string;
/** Input schema for the trigger block */
config_schema: LibraryAgentTriggerInfoConfigSchema;
credentials_input_name: LibraryAgentTriggerInfoCredentialsInputName;

View File

@@ -0,0 +1,18 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { TriggeredPresetSetupRequestTriggerConfig } from "./triggeredPresetSetupRequestTriggerConfig";
import type { TriggeredPresetSetupRequestAgentCredentials } from "./triggeredPresetSetupRequestAgentCredentials";
export interface TriggeredPresetSetupRequest {
name: string;
description?: string;
graph_id: string;
graph_version: number;
trigger_config: TriggeredPresetSetupRequestTriggerConfig;
agent_credentials?: TriggeredPresetSetupRequestAgentCredentials;
}

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { CredentialsMetaInput } from "./credentialsMetaInput";
export type TriggeredPresetSetupRequestAgentCredentials = {
[key: string]: CredentialsMetaInput;
};

View File

@@ -0,0 +1,11 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type TriggeredPresetSetupRequestTriggerConfig = {
[key: string]: unknown;
};

View File

@@ -5,13 +5,13 @@
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { ProviderName } from "./providerName";
import type { WebhookConfig } from "./webhookConfig";
export interface Webhook {
id?: string;
user_id: string;
provider: ProviderName;
/** Provider name for integrations. Can be any string value, including custom provider names. */
provider: string;
credentials_id: string;
webhook_type: string;
resource: string;

View File

@@ -18,8 +18,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to initiate an OAuth flow for"
"type": "string",
"title": "The provider to initiate an OAuth flow for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
},
{
@@ -64,8 +65,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "The target provider for this OAuth exchange"
"type": "string",
"title": "The target provider for this OAuth exchange",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
}
],
@@ -133,8 +135,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to list credentials for"
"type": "string",
"title": "The provider to list credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
}
],
@@ -173,8 +176,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to create credentials for"
"type": "string",
"title": "The provider to create credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
}
],
@@ -253,8 +257,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to retrieve credentials for"
"type": "string",
"title": "The provider to retrieve credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
},
{
@@ -315,8 +320,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to delete credentials for"
"type": "string",
"title": "The provider to delete credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
},
{
@@ -380,8 +386,9 @@
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProviderName",
"title": "Provider where the webhook was registered"
"type": "string",
"title": "Provider where the webhook was registered",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
}
},
{
@@ -436,6 +443,86 @@
}
}
},
"/api/integrations/providers": {
"get": {
"tags": ["v1", "integrations"],
"summary": "List Providers",
"description": "Get a list of all available provider names.\n\nReturns both statically defined providers (from ProviderName enum)\nand dynamically registered providers (from SDK decorators).\n\nNote: The complete list of provider names is also available as a constant\nin the generated TypeScript client via PROVIDER_NAMES.",
"operationId": "getV1ListProviders",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": { "type": "string" },
"type": "array",
"title": "Response Getv1Listproviders"
}
}
}
}
}
}
},
"/api/integrations/providers/names": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Names",
"description": "Get all provider names in a structured format.\n\nThis endpoint is specifically designed to expose the provider names\nin the OpenAPI schema so that code generators like Orval can create\nappropriate TypeScript constants.",
"operationId": "getV1GetProviderNames",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderNamesResponse"
}
}
}
}
}
}
},
"/api/integrations/providers/constants": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Constants",
"description": "Get provider names as constants.\n\nThis endpoint returns a model with provider names as constants,\nspecifically designed for OpenAPI code generation tools to create\nTypeScript constants.",
"operationId": "getV1GetProviderConstants",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ProviderConstants" }
}
}
}
}
}
},
"/api/integrations/providers/enum-example": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Enum Example",
"description": "Example endpoint that uses the CompleteProviderNames enum.\n\nThis endpoint exists to ensure that the CompleteProviderNames enum is included\nin the OpenAPI schema, which will cause Orval to generate it as a\nTypeScript enum/constant.",
"operationId": "getV1GetProviderEnumExample",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderEnumResponse"
}
}
}
}
}
}
},
"/api/analytics/log_raw_metric": {
"post": {
"tags": ["v1", "analytics"],
@@ -1018,7 +1105,7 @@
"content": {
"application/json": {
"schema": {
"items": { "$ref": "#/components/schemas/GraphModel" },
"items": { "$ref": "#/components/schemas/GraphMeta" },
"type": "array",
"title": "Response Getv1List User Graphs"
}
@@ -1415,49 +1502,6 @@
}
},
"/api/executions": {
"post": {
"tags": ["v1", "graphs"],
"summary": "Stop graph executions",
"operationId": "postV1Stop graph executions",
"parameters": [
{
"name": "graph_id",
"in": "query",
"required": true,
"schema": { "type": "string", "title": "Graph Id" }
},
{
"name": "graph_exec_id",
"in": "query",
"required": true,
"schema": { "type": "string", "title": "Graph Exec Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GraphExecutionMeta"
},
"title": "Response Postv1Stop Graph Executions"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
},
"get": {
"tags": ["v1", "graphs"],
"summary": "Get all executions",
@@ -1468,10 +1512,10 @@
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GraphExecutionMeta"
},
"type": "array",
"title": "Response Getv1Get All Executions"
}
}
@@ -3014,6 +3058,42 @@
}
}
},
"/api/library/presets/setup-trigger": {
"post": {
"tags": ["v2", "presets"],
"summary": "Setup Trigger",
"description": "Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.\nReturns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.",
"operationId": "postV2SetupTrigger",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TriggeredPresetSetupRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgentPreset" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/library/presets/{preset_id}/execute": {
"post": {
"tags": ["v2", "presets", "presets"],
@@ -3401,55 +3481,6 @@
}
}
},
"/api/library/agents/{library_agent_id}/setup-trigger": {
"post": {
"tags": ["v2", "library", "private"],
"summary": "Setup Trigger",
"description": "Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.\nReturns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.",
"operationId": "postV2SetupTrigger",
"parameters": [
{
"name": "library_agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"description": "ID of the library agent",
"title": "Library Agent Id"
},
"description": "ID of the library agent"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TriggeredPresetSetupParams"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgentPreset" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/otto/ask": {
"post": {
"tags": ["v2", "otto"],
@@ -3837,10 +3868,21 @@
"type": "object",
"title": "Output Schema",
"readOnly": true
},
"has_external_trigger": {
"type": "boolean",
"title": "Has External Trigger",
"readOnly": true
}
},
"type": "object",
"required": ["name", "description", "input_schema", "output_schema"],
"required": [
"name",
"description",
"input_schema",
"output_schema",
"has_external_trigger"
],
"title": "BaseGraph"
},
"Body_postV1Callback": {
@@ -4101,7 +4143,11 @@
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Title"
},
"provider": { "$ref": "#/components/schemas/ProviderName" },
"provider": {
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"type": {
"type": "string",
"enum": ["api_key", "oauth2", "user_password", "host_scoped"],
@@ -4405,6 +4451,68 @@
],
"title": "GraphExecutionWithNodes"
},
"GraphMeta": {
"properties": {
"id": { "type": "string", "title": "Id" },
"version": { "type": "integer", "title": "Version", "default": 1 },
"is_active": {
"type": "boolean",
"title": "Is Active",
"default": true
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"forked_from_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Forked From Id"
},
"forked_from_version": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Forked From Version"
},
"sub_graphs": {
"items": { "$ref": "#/components/schemas/BaseGraph-Output" },
"type": "array",
"title": "Sub Graphs",
"default": []
},
"user_id": { "type": "string", "title": "User Id" },
"input_schema": {
"additionalProperties": true,
"type": "object",
"title": "Input Schema",
"readOnly": true
},
"output_schema": {
"additionalProperties": true,
"type": "object",
"title": "Output Schema",
"readOnly": true
},
"has_external_trigger": {
"type": "boolean",
"title": "Has External Trigger",
"readOnly": true
},
"credentials_input_schema": {
"additionalProperties": true,
"type": "object",
"title": "Credentials Input Schema",
"readOnly": true
}
},
"type": "object",
"required": [
"name",
"description",
"user_id",
"input_schema",
"output_schema",
"has_external_trigger",
"credentials_input_schema"
],
"title": "GraphMeta"
},
"GraphModel": {
"properties": {
"id": { "type": "string", "title": "Id" },
@@ -4455,16 +4563,16 @@
"title": "Output Schema",
"readOnly": true
},
"has_external_trigger": {
"type": "boolean",
"title": "Has External Trigger",
"readOnly": true
},
"credentials_input_schema": {
"additionalProperties": true,
"type": "object",
"title": "Credentials Input Schema",
"readOnly": true
},
"has_webhook_trigger": {
"type": "boolean",
"title": "Has Webhook Trigger",
"readOnly": true
}
},
"type": "object",
@@ -4474,8 +4582,8 @@
"user_id",
"input_schema",
"output_schema",
"credentials_input_schema",
"has_webhook_trigger"
"has_external_trigger",
"credentials_input_schema"
],
"title": "GraphModel"
},
@@ -4820,7 +4928,11 @@
},
"LibraryAgentTriggerInfo": {
"properties": {
"provider": { "$ref": "#/components/schemas/ProviderName" },
"provider": {
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"config_schema": {
"additionalProperties": true,
"type": "object",
@@ -5623,51 +5735,44 @@
"required": ["name", "username", "description", "links"],
"title": "ProfileDetails"
},
"ProviderName": {
"type": "string",
"enum": [
"aiml_api",
"anthropic",
"apollo",
"compass",
"discord",
"d_id",
"e2b",
"exa",
"fal",
"generic_webhook",
"github",
"google",
"google_maps",
"groq",
"http",
"hubspot",
"ideogram",
"jina",
"linear",
"llama_api",
"medium",
"mem0",
"notion",
"nvidia",
"ollama",
"openai",
"openweathermap",
"open_router",
"pinecone",
"reddit",
"replicate",
"revid",
"screenshotone",
"slant3d",
"smartlead",
"smtp",
"twitter",
"todoist",
"unreal_speech",
"zerobounce"
],
"title": "ProviderName"
"ProviderConstants": {
"properties": {
"PROVIDER_NAMES": {
"additionalProperties": { "type": "string" },
"type": "object",
"title": "Provider Names",
"description": "All available provider names as a constant mapping"
}
},
"type": "object",
"title": "ProviderConstants",
"description": "Model that exposes all provider names as a constant in the OpenAPI schema.\nThis is designed to be converted by Orval into a TypeScript constant."
},
"ProviderEnumResponse": {
"properties": {
"provider": {
"type": "string",
"title": "Provider",
"description": "A provider name from the complete list of providers"
}
},
"type": "object",
"required": ["provider"],
"title": "ProviderEnumResponse",
"description": "Response containing a provider from the enum."
},
"ProviderNamesResponse": {
"properties": {
"providers": {
"items": { "type": "string" },
"type": "array",
"title": "Providers",
"description": "List of all available provider names"
}
},
"type": "object",
"title": "ProviderNamesResponse",
"description": "Response containing list of all provider names."
},
"RefundRequest": {
"properties": {
@@ -6153,7 +6258,7 @@
"required": ["transactions", "next_transaction_time"],
"title": "TransactionHistory"
},
"TriggeredPresetSetupParams": {
"TriggeredPresetSetupRequest": {
"properties": {
"name": { "type": "string", "title": "Name" },
"description": {
@@ -6161,6 +6266,8 @@
"title": "Description",
"default": ""
},
"graph_id": { "type": "string", "title": "Graph Id" },
"graph_version": { "type": "integer", "title": "Graph Version" },
"trigger_config": {
"additionalProperties": true,
"type": "object",
@@ -6175,8 +6282,8 @@
}
},
"type": "object",
"required": ["name", "trigger_config"],
"title": "TriggeredPresetSetupParams"
"required": ["name", "graph_id", "graph_version", "trigger_config"],
"title": "TriggeredPresetSetupRequest"
},
"TurnstileVerifyRequest": {
"properties": {
@@ -6441,7 +6548,11 @@
"properties": {
"id": { "type": "string", "title": "Id" },
"user_id": { "type": "string", "title": "User Id" },
"provider": { "$ref": "#/components/schemas/ProviderName" },
"provider": {
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"credentials_id": { "type": "string", "title": "Credentials Id" },
"webhook_type": { "type": "string", "title": "Webhook Type" },
"resource": { "type": "string", "title": "Resource" },

View File

@@ -1,31 +1,35 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { TooltipProvider } from "@/components/ui/tooltip";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import OnboardingProvider from "@/components/onboarding/onboarding-provider";
import { QueryClientProvider } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps,
} from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
export function Providers({ children, ...props }: ThemeProviderProps) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
<NextThemesProvider {...props}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<OnboardingProvider>
<TooltipProvider>{children}</TooltipProvider>
</OnboardingProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
<NuqsAdapter>
<NextThemesProvider {...props}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<OnboardingProvider>
<TooltipProvider>{children}</TooltipProvider>
</OnboardingProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
</NuqsAdapter>
</QueryClientProvider>
);
}

View File

@@ -1,8 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import { Plus } from "lucide-react";
import React, { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import {
GraphExecutionID,
GraphExecutionMeta,
@@ -12,14 +11,15 @@ import {
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
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";
import { Button } from "../atoms/Button/Button";
interface AgentRunsSelectorListProps {
agent: LibraryAgent;
@@ -72,17 +72,11 @@ export default function AgentRunsSelectorList({
<aside className={cn("flex flex-col gap-4", className)}>
{allowDraftNewRun && (
<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"
: "")
}
className={"mb-4 hidden lg:flex"}
onClick={onSelectDraftNewRun}
leftIcon={<Plus className="h-6 w-6" />}
>
<Plus className="h-6 w-6" />
<span>New {agent.has_external_trigger ? "trigger" : "run"}</span>
New {agent.has_external_trigger ? "trigger" : "run"}
</Button>
)}
@@ -112,7 +106,7 @@ export default function AgentRunsSelectorList({
{/* New Run button - only in small layouts */}
{allowDraftNewRun && (
<Button
size="card"
size="large"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
@@ -120,9 +114,9 @@ export default function AgentRunsSelectorList({
: "")
}
onClick={onSelectDraftNewRun}
leftIcon={<Plus className="h-6 w-6" />}
>
<Plus className="h-6 w-6" />
<span>New {agent.has_external_trigger ? "trigger" : "run"}</span>
New {agent.has_external_trigger ? "trigger" : "run"}
</Button>
)}

View File

@@ -1,6 +1,7 @@
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
import Wallet from "../../agptui/Wallet";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { AgentActivityDropdown } from "./components/AgentActivityDropdown/AgentActivityDropdown";
import { LoginButton } from "./components/LoginButton";
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
import { NavbarLink } from "./components/NavbarLink";
@@ -33,6 +34,7 @@ export async function Navbar() {
<div className="flex flex-1 items-center justify-end gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
<AgentActivityDropdown />
{profile && <Wallet />}
<AccountMenu
userName={profile?.username}

View File

@@ -0,0 +1,57 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Bell } from "@phosphor-icons/react";
import { useState } from "react";
import { ActivityDropdown } from "./components/ActivityDropdown";
import { formatNotificationCount } from "./helpers";
import { useAgentActivityDropdown } from "./useAgentActivityDropdown";
export function AgentActivityDropdown() {
const [isOpen, setIsOpen] = useState(false);
const { activeExecutions, recentCompletions, recentFailures } =
useAgentActivityDropdown();
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<button
className={`group relative h-[2.5rem] w-[2.5rem] rounded-full p-2 transition-colors hover:bg-white ${isOpen ? "bg-white" : ""}`}
title="Agent Activity"
>
<Bell size={22} className="text-black" />
{activeExecutions.length > 0 && (
<>
{/* Running Agents Rotating Badge */}
<div className="absolute right-[1px] top-[0.5px] flex h-5 w-5 items-center justify-center rounded-full bg-purple-600 text-[10px] font-medium text-white">
{formatNotificationCount(activeExecutions.length)}
<div className="absolute -inset-0.5 animate-spin rounded-full border-[3px] border-transparent border-r-purple-200 border-t-purple-200" />
</div>
{/* Running Agent Hover Hint */}
<div className="absolute bottom-[-2.5rem] left-1/2 z-50 hidden -translate-x-1/2 transform whitespace-nowrap rounded-small bg-white px-4 py-2 shadow-md group-hover:block">
<Text variant="body-medium">
{activeExecutions.length} running agent
{activeExecutions.length > 1 ? "s" : ""}
</Text>
</div>
</>
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="center" sideOffset={8}>
<ActivityDropdown
activeExecutions={activeExecutions}
recentCompletions={recentCompletions}
recentFailures={recentFailures}
/>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,89 @@
"use client";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { Text } from "@/components/atoms/Text/Text";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Bell } from "@phosphor-icons/react";
import { AgentExecutionWithInfo, EXECUTION_DISPLAY_LIMIT } from "../helpers";
import { ActivityItem } from "./ActivityItem";
interface Props {
activeExecutions: AgentExecutionWithInfo[];
recentCompletions: AgentExecutionWithInfo[];
recentFailures: AgentExecutionWithInfo[];
}
export function ActivityDropdown({
activeExecutions,
recentCompletions,
recentFailures,
}: Props) {
// Combine and sort all executions - running/queued at top, then by most recent
function getSortedExecutions() {
const allExecutions = [
...activeExecutions.map((e) => ({ ...e, type: "running" as const })),
...recentCompletions.map((e) => ({ ...e, type: "completed" as const })),
...recentFailures.map((e) => ({ ...e, type: "failed" as const })),
];
return allExecutions
.sort((a, b) => {
// Running/queued always at top
const aIsActive =
a.status === AgentExecutionStatus.RUNNING ||
a.status === AgentExecutionStatus.QUEUED;
const bIsActive =
b.status === AgentExecutionStatus.RUNNING ||
b.status === AgentExecutionStatus.QUEUED;
if (aIsActive && !bIsActive) return -1;
if (!aIsActive && bIsActive) return 1;
// Within same category, sort by most recent
const aTime = aIsActive ? a.started_at : a.ended_at;
const bTime = bIsActive ? b.started_at : b.ended_at;
if (!aTime || !bTime) return 0;
return new Date(bTime).getTime() - new Date(aTime).getTime();
})
.slice(0, EXECUTION_DISPLAY_LIMIT);
}
const sortedExecutions = getSortedExecutions();
return (
<div>
{/* Header */}
<div className="sticky top-0 z-10 px-4 pb-1 pt-4">
<Text variant="large-semibold" className="!text-black">
Agent Activity
</Text>
</div>
{/* Content */}
<ScrollArea className="min-h-[10rem]">
{sortedExecutions.length > 0 ? (
<div className="p-2">
{sortedExecutions.map((execution) => (
<ActivityItem key={execution.id} execution={execution} />
))}
</div>
) : (
<div className="flex h-full flex-col items-center justify-center gap-5 pb-8 pt-6">
<div className="mx-auto inline-flex flex-col items-center justify-center rounded-full bg-lightGrey p-6">
<Bell className="h-6 w-6 text-zinc-300" />
</div>
<div className="flex flex-col items-center justify-center">
<Text variant="body-medium" className="!text-black">
Nothing to show yet
</Text>
<Text variant="body" className="!text-zinc-500">
Start an agent to get updates
</Text>
</div>
</div>
)}
</ScrollArea>
</div>
);
}

View File

@@ -0,0 +1,110 @@
"use client";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { Text } from "@/components/atoms/Text/Text";
import {
CheckCircle,
CircleNotchIcon,
Clock,
WarningOctagonIcon,
StopCircle,
CircleDashed,
} from "@phosphor-icons/react";
import { useRouter } from "next/navigation";
import type { AgentExecutionWithInfo } from "../helpers";
import { formatTimeAgo, getExecutionDuration } from "../helpers";
interface Props {
execution: AgentExecutionWithInfo;
}
export function ActivityItem({ execution }: Props) {
const router = useRouter();
function getStatusIcon() {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return <Clock size={18} className="text-purple-500" />;
case AgentExecutionStatus.RUNNING:
return (
<CircleNotchIcon
size={18}
className="animate-spin text-purple-500"
weight="bold"
/>
);
case AgentExecutionStatus.COMPLETED:
return (
<CheckCircle size={18} weight="fill" className="text-purple-500" />
);
case AgentExecutionStatus.FAILED:
return <WarningOctagonIcon size={18} className="text-purple-500" />;
case AgentExecutionStatus.TERMINATED:
return (
<StopCircle size={18} className="text-purple-500" weight="fill" />
);
case AgentExecutionStatus.INCOMPLETE:
return <CircleDashed size={18} className="text-purple-500" />;
default:
return null;
}
}
function getTimeDisplay() {
const isActiveStatus =
execution.status === AgentExecutionStatus.RUNNING ||
execution.status === AgentExecutionStatus.QUEUED;
if (isActiveStatus) {
const timeAgo = formatTimeAgo(execution.started_at.toString());
const statusText =
execution.status === AgentExecutionStatus.QUEUED ? "queued" : "running";
return `Started ${timeAgo}, ${getExecutionDuration(execution)} ${statusText}`;
}
if (execution.ended_at) {
const timeAgo = formatTimeAgo(execution.ended_at.toString());
switch (execution.status) {
case AgentExecutionStatus.COMPLETED:
return `Completed ${timeAgo}`;
case AgentExecutionStatus.FAILED:
return `Failed ${timeAgo}`;
case AgentExecutionStatus.TERMINATED:
return `Stopped ${timeAgo}`;
case AgentExecutionStatus.INCOMPLETE:
return `Incomplete ${timeAgo}`;
default:
return `Ended ${timeAgo}`;
}
}
return "Unknown";
}
return (
<div
className="cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
onClick={() => {
const agentId = execution.library_agent_id || execution.graph_id;
router.push(`/library/agents/${agentId}?executionId=${execution.id}`);
}}
role="button"
>
{/* Icon + Agent Name */}
<div className="flex items-center space-x-3">
{getStatusIcon()}
<Text variant="body-medium" className="truncate text-gray-900">
{execution.agent_name}
</Text>
</div>
{/* Agent Message - Indented */}
<div className="ml-7 pt-1">
{/* Time - Indented */}
<Text variant="small" className="!text-zinc-500">
{getTimeDisplay()}
</Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,373 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { GraphExecutionMeta as GeneratedGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
// Time constants
const MILLISECONDS_PER_SECOND = 1000;
const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60;
const HOURS_PER_DAY = 24;
const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
const MILLISECONDS_PER_HOUR = MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE;
const MILLISECONDS_PER_DAY = HOURS_PER_DAY * MILLISECONDS_PER_HOUR;
// Display constants
export const EXECUTION_DISPLAY_LIMIT = 6;
const SHORT_DURATION_THRESHOLD_SECONDS = 5;
export function formatTimeAgo(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / MILLISECONDS_PER_MINUTE);
if (diffMins < 1) return "just now";
if (diffMins < SECONDS_PER_MINUTE) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / MINUTES_PER_HOUR);
if (diffHours < HOURS_PER_DAY) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / HOURS_PER_DAY);
return `${diffDays}d ago`;
}
export function getStatusDisplayText(
execution: GeneratedGraphExecutionMeta,
): string {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return "Queued";
case AgentExecutionStatus.RUNNING:
return "Running";
case AgentExecutionStatus.COMPLETED:
return "Completed";
case AgentExecutionStatus.FAILED:
return "Failed";
case AgentExecutionStatus.TERMINATED:
return "Stopped";
case AgentExecutionStatus.INCOMPLETE:
return "Incomplete";
default:
return execution.status;
}
}
export function getStatusColorClass(
execution: GeneratedGraphExecutionMeta,
): string {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return "text-yellow-600";
case AgentExecutionStatus.RUNNING:
return "text-blue-600";
case AgentExecutionStatus.COMPLETED:
return "text-green-600";
case AgentExecutionStatus.FAILED:
case AgentExecutionStatus.TERMINATED:
return "text-red-600";
case AgentExecutionStatus.INCOMPLETE:
return "text-gray-600";
default:
return "text-gray-600";
}
}
export function truncateGraphId(graphId: string, length: number = 8): string {
return `${graphId.slice(0, length)}...`;
}
export function getExecutionDuration(
execution: GeneratedGraphExecutionMeta,
): string {
if (!execution.started_at) return "Unknown";
const start = new Date(execution.started_at);
const end = execution.ended_at ? new Date(execution.ended_at) : new Date();
// Check if dates are valid
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return "Unknown";
}
const durationMs = end.getTime() - start.getTime();
// Handle negative durations (shouldn't happen but just in case)
if (durationMs < 0) return "Unknown";
const durationSec = Math.floor(durationMs / MILLISECONDS_PER_SECOND);
// For short durations (< 5 seconds), show "a few seconds"
if (durationSec < SHORT_DURATION_THRESHOLD_SECONDS) {
return "a few seconds";
}
if (durationSec < SECONDS_PER_MINUTE) return `${durationSec}s`;
const durationMin = Math.floor(durationSec / SECONDS_PER_MINUTE);
if (durationMin < MINUTES_PER_HOUR)
return `${durationMin}m ${durationSec % SECONDS_PER_MINUTE}s`;
const durationHr = Math.floor(durationMin / MINUTES_PER_HOUR);
return `${durationHr}h ${durationMin % MINUTES_PER_HOUR}m`;
}
export function shouldShowNotificationBadge(totalCount: number): boolean {
return totalCount > 0;
}
export function formatNotificationCount(count: number): string {
if (count > 99) return "+99";
return count.toString();
}
export interface AgentExecutionWithInfo extends GeneratedGraphExecutionMeta {
agent_name: string;
agent_description: string;
library_agent_id?: string;
}
export interface NotificationState {
activeExecutions: AgentExecutionWithInfo[];
recentCompletions: AgentExecutionWithInfo[];
recentFailures: AgentExecutionWithInfo[];
totalCount: number;
}
export function createAgentInfoMap(
agents: MyAgent[],
): Map<
string,
{ name: string; description: string; library_agent_id?: string }
> {
const agentMap = new Map<
string,
{ name: string; description: string; library_agent_id?: string }
>();
agents.forEach((agent) => {
agentMap.set(agent.agent_id, {
name: agent.agent_name,
description: agent.description,
library_agent_id: undefined, // MyAgent doesn't have library_agent_id
});
});
return agentMap;
}
export function convertLegacyExecutionToGenerated(
execution: GraphExecution,
): GeneratedGraphExecutionMeta {
return {
id: execution.id,
user_id: execution.user_id,
graph_id: execution.graph_id,
graph_version: execution.graph_version,
preset_id: execution.preset_id,
status: execution.status as AgentExecutionStatus,
started_at: execution.started_at.toISOString(),
ended_at: execution.ended_at.toISOString(),
stats: execution.stats || {
cost: 0,
duration: 0,
duration_cpu_only: 0,
node_exec_time: 0,
node_exec_time_cpu_only: 0,
node_exec_count: 0,
},
};
}
export function enrichExecutionWithAgentInfo(
execution: GeneratedGraphExecutionMeta,
agentInfoMap: Map<
string,
{ name: string; description: string; library_agent_id?: string }
>,
): AgentExecutionWithInfo {
const agentInfo = agentInfoMap.get(execution.graph_id);
return {
...execution,
agent_name: agentInfo?.name || `Graph ${execution.graph_id.slice(0, 8)}...`,
agent_description: agentInfo?.description ?? "",
library_agent_id: agentInfo?.library_agent_id,
};
}
export function isActiveExecution(
execution: GeneratedGraphExecutionMeta,
): boolean {
const status = execution.status;
return (
status === AgentExecutionStatus.RUNNING ||
status === AgentExecutionStatus.QUEUED
);
}
export function isRecentCompletion(
execution: GeneratedGraphExecutionMeta,
thirtyMinutesAgo: Date,
): boolean {
const status = execution.status;
return (
status === AgentExecutionStatus.COMPLETED &&
!!execution.ended_at &&
new Date(execution.ended_at) > thirtyMinutesAgo
);
}
export function isRecentFailure(
execution: GeneratedGraphExecutionMeta,
thirtyMinutesAgo: Date,
): boolean {
const status = execution.status;
return (
(status === AgentExecutionStatus.FAILED ||
status === AgentExecutionStatus.TERMINATED) &&
!!execution.ended_at &&
new Date(execution.ended_at) > thirtyMinutesAgo
);
}
export function isRecentNotification(
execution: AgentExecutionWithInfo,
thirtyMinutesAgo: Date,
): boolean {
return execution.ended_at
? new Date(execution.ended_at) > thirtyMinutesAgo
: false;
}
export function categorizeExecutions(
executions: GeneratedGraphExecutionMeta[],
agentInfoMap: Map<
string,
{ name: string; description: string; library_agent_id?: string }
>,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - MILLISECONDS_PER_DAY);
const enrichedExecutions = executions.map((execution) =>
enrichExecutionWithAgentInfo(execution, agentInfoMap),
);
const activeExecutions = enrichedExecutions
.filter(isActiveExecution)
.slice(0, EXECUTION_DISPLAY_LIMIT);
const recentCompletions = enrichedExecutions
.filter((execution) => isRecentCompletion(execution, twentyFourHoursAgo))
.slice(0, EXECUTION_DISPLAY_LIMIT);
const recentFailures = enrichedExecutions
.filter((execution) => isRecentFailure(execution, twentyFourHoursAgo))
.slice(0, EXECUTION_DISPLAY_LIMIT);
return {
activeExecutions,
recentCompletions,
recentFailures,
totalCount:
activeExecutions.length +
recentCompletions.length +
recentFailures.length,
};
}
export function removeExecutionFromAllCategories(
state: NotificationState,
executionId: string,
): NotificationState {
return {
activeExecutions: state.activeExecutions.filter(
(e) => e.id !== executionId,
),
recentCompletions: state.recentCompletions.filter(
(e) => e.id !== executionId,
),
recentFailures: state.recentFailures.filter((e) => e.id !== executionId),
totalCount: state.totalCount, // Will be recalculated later
};
}
export function addExecutionToCategory(
state: NotificationState,
execution: AgentExecutionWithInfo,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - MILLISECONDS_PER_DAY);
const newState = { ...state };
if (isActiveExecution(execution)) {
newState.activeExecutions = [execution, ...newState.activeExecutions].slice(
0,
EXECUTION_DISPLAY_LIMIT,
);
} else if (isRecentCompletion(execution, twentyFourHoursAgo)) {
newState.recentCompletions = [
execution,
...newState.recentCompletions,
].slice(0, EXECUTION_DISPLAY_LIMIT);
} else if (isRecentFailure(execution, twentyFourHoursAgo)) {
newState.recentFailures = [execution, ...newState.recentFailures].slice(
0,
EXECUTION_DISPLAY_LIMIT,
);
}
return newState;
}
export function cleanupOldNotifications(
state: NotificationState,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
return {
...state,
recentCompletions: state.recentCompletions.filter((e) =>
isRecentNotification(e, twentyFourHoursAgo),
),
recentFailures: state.recentFailures.filter((e) =>
isRecentNotification(e, twentyFourHoursAgo),
),
};
}
export function calculateTotalCount(
state: NotificationState,
): NotificationState {
return {
...state,
totalCount:
state.activeExecutions.length +
state.recentCompletions.length +
state.recentFailures.length,
};
}
export function handleExecutionUpdate(
currentState: NotificationState,
execution: GraphExecution,
agentInfoMap: Map<
string,
{ name: string; description: string; library_agent_id?: string }
>,
): NotificationState {
// Convert and enrich the execution
const convertedExecution = convertLegacyExecutionToGenerated(execution);
const enrichedExecution = enrichExecutionWithAgentInfo(
convertedExecution,
agentInfoMap,
);
// Remove from all categories first
let newState = removeExecutionFromAllCategories(currentState, execution.id);
// Add to appropriate category
newState = addExecutionToCategory(newState, enrichedExecution);
// Clean up old notifications
newState = cleanupOldNotifications(newState);
// Recalculate total count
newState = calculateTotalCount(newState);
return newState;
}

View File

@@ -0,0 +1,164 @@
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import BackendAPI from "@/lib/autogpt-server-api/client";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
import { useCallback, useEffect, useState } from "react";
import {
NotificationState,
categorizeExecutions,
createAgentInfoMap,
handleExecutionUpdate,
} from "./helpers";
type AgentInfoMap = Map<
string,
{ name: string; description: string; library_agent_id?: string }
>;
export function useAgentActivityDropdown() {
const [api] = useState(() => new BackendAPI());
const [notifications, setNotifications] = useState<NotificationState>({
activeExecutions: [],
recentCompletions: [],
recentFailures: [],
totalCount: 0,
});
const [isConnected, setIsConnected] = useState(false);
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
// Get library agents using generated hook
const {
data: myAgentsResponse,
isLoading: isAgentsLoading,
error: agentsError,
} = useGetV2GetMyAgents({
query: {
enabled: true,
},
});
// Get library agents data to map graph_id to library_agent_id
const {
data: libraryAgentsResponse,
isLoading: isLibraryAgentsLoading,
error: libraryAgentsError,
} = useGetV2ListLibraryAgents(
{},
{
query: {
enabled: true,
},
},
);
// Get all executions using generated hook
const {
data: executionsResponse,
isLoading: isExecutionsLoading,
error: executionsError,
} = useGetV1GetAllExecutions({
query: {
enabled: true,
},
});
// Update agent info map when both agent data sources change
useEffect(() => {
if (
myAgentsResponse?.data?.agents &&
libraryAgentsResponse?.data &&
"agents" in libraryAgentsResponse.data
) {
const agentMap = createAgentInfoMap(myAgentsResponse.data.agents);
// Add library agent ID mapping
libraryAgentsResponse.data.agents.forEach(
(libraryAgent: LibraryAgent) => {
const existingInfo = agentMap.get(libraryAgent.graph_id);
if (existingInfo) {
agentMap.set(libraryAgent.graph_id, {
...existingInfo,
library_agent_id: libraryAgent.id,
});
}
},
);
setAgentInfoMap(agentMap);
}
}, [myAgentsResponse, libraryAgentsResponse]);
// Handle real-time execution updates
const handleExecutionEvent = useCallback(
(execution: GraphExecution) => {
setNotifications((currentState) =>
handleExecutionUpdate(currentState, execution, agentInfoMap),
);
},
[agentInfoMap],
);
// Process initial execution state when data loads
useEffect(() => {
if (
executionsResponse?.data &&
!isExecutionsLoading &&
agentInfoMap.size > 0
) {
const newNotifications = categorizeExecutions(
executionsResponse.data,
agentInfoMap,
);
setNotifications(newNotifications);
}
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
// Initialize WebSocket connection for real-time updates
useEffect(() => {
const connectHandler = api.onWebSocketConnect(() => {
setIsConnected(true);
// Subscribe to graph executions for all user agents
if (myAgentsResponse?.data?.agents) {
myAgentsResponse.data.agents.forEach((agent) => {
api
.subscribeToGraphExecutions(agent.agent_id as any)
.catch((error) => {
console.error(
`[AgentNotifications] Failed to subscribe to graph ${agent.agent_id}:`,
error,
);
});
});
}
});
const disconnectHandler = api.onWebSocketDisconnect(() => {
setIsConnected(false);
});
const messageHandler = api.onWebSocketMessage(
"graph_execution_event",
handleExecutionEvent,
);
api.connectWebSocket();
return () => {
connectHandler();
disconnectHandler();
messageHandler();
api.disconnectWebSocket();
};
}, [api, handleExecutionEvent, myAgentsResponse]);
return {
...notifications,
isConnected,
isLoading: isAgentsLoading || isExecutionsLoading || isLibraryAgentsLoading,
error: agentsError || executionsError || libraryAgentsError,
};
}

View File

@@ -0,0 +1,28 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
export function useNavbar() {
const { isLoggedIn, isUserLoading } = useSupabase();
console.log("isLoggedIn", isLoggedIn);
const {
data: profileResponse,
isLoading: isProfileLoading,
error: profileError,
} = useGetV2GetUserProfile({
query: {
enabled: isLoggedIn === true,
},
});
const profile = profileResponse?.data || null;
const isLoading = isUserLoading || (isLoggedIn && isProfileLoading);
return {
isLoggedIn,
profile,
isLoading,
profileError,
};
}

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -23,7 +23,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
"z-50 w-72 rounded-medium border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className,
)}
{...props}
@@ -34,8 +34,8 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
PopoverContent,
PopoverPortal,
PopoverTrigger,
};

View File

@@ -0,0 +1,261 @@
import { test } from "./fixtures";
import { LibraryPage } from "./pages/library.page";
test.describe("Agent Notifications", () => {
let libraryPage: LibraryPage;
test.beforeEach(async ({ page, loginPage, testUser }) => {
libraryPage = new LibraryPage(page);
// Start each test with login using worker auth
await page.goto("/login");
await loginPage.login(testUser.email, testUser.password);
await test.expect(page).toHaveURL("/marketplace");
});
test("notification badge appears when agent is running", async ({ page }) => {
// Navigate to library
await libraryPage.navigateToLibrary();
await test.expect(page).toHaveURL(new RegExp("/library"));
// Click on first available agent
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
// Verify we're on agent page
await test.expect(libraryPage.isLoaded()).resolves.toBeTruthy();
// Initially, no notification badge should be visible
await test
.expect(libraryPage.agentNotifications.isNotificationBadgeVisible())
.resolves.toBeFalsy();
// Run the agent
await libraryPage.runAgent();
// Wait for run to start and notification badge to appear
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Check that notification badge appears
await test
.expect(libraryPage.agentNotifications.isNotificationBadgeVisible())
.resolves.toBeTruthy();
// Check that notification count is at least 1
const notificationCount =
await libraryPage.agentNotifications.getNotificationCount();
test.expect(parseInt(notificationCount)).toBeGreaterThan(0);
});
test("notification dropdown shows running agent with correct status", async ({
page: _page,
}) => {
// Navigate to library and run an agent
await libraryPage.navigateToLibrary();
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
const agentName = await libraryPage.getAgentName();
// Run the agent
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Click on notification button to open dropdown
await libraryPage.agentNotifications.clickNotificationButton();
// Verify dropdown is visible
await test
.expect(libraryPage.agentNotifications.isNotificationDropdownVisible())
.resolves.toBeTruthy();
// Check that running agent appears in dropdown
await test
.expect(
libraryPage.agentNotifications.hasNotificationWithStatus("running"),
)
.resolves.toBeTruthy();
// Check that the agent name appears in notifications
const notification =
await libraryPage.agentNotifications.getNotificationByAgentName(
agentName,
);
test.expect(notification).not.toBeNull();
test.expect(notification?.status).toBe("running");
});
test("notification dropdown shows completed agent after run finishes", async ({
page: _page,
}, testInfo) => {
// Increase timeout for this test since we need to wait for completion
await test.setTimeout(testInfo.timeout * 3);
// Navigate to library and run an agent
await libraryPage.navigateToLibrary();
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
const agentName = await libraryPage.getAgentName();
// Run the agent
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Wait for agent to complete (with longer timeout)
await libraryPage.waitForRunToComplete(60000);
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Click on notification button to open dropdown
await libraryPage.agentNotifications.clickNotificationButton();
// Verify dropdown is visible
await test
.expect(libraryPage.agentNotifications.isNotificationDropdownVisible())
.resolves.toBeTruthy();
// Check that completed agent appears in dropdown
const notification =
await libraryPage.agentNotifications.getNotificationByAgentName(
agentName,
);
test.expect(notification).not.toBeNull();
test.expect(notification?.status).toMatch(/completed|failed|terminated/);
});
test("notification dropdown shows correct time information", async ({
page: _page,
}) => {
// Navigate to library and run an agent
await libraryPage.navigateToLibrary();
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
const agentName = await libraryPage.getAgentName();
// Run the agent
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Click on notification button to open dropdown
await libraryPage.agentNotifications.clickNotificationButton();
// Get notification for this agent
const notification =
await libraryPage.agentNotifications.getNotificationByAgentName(
agentName,
);
test.expect(notification).not.toBeNull();
// Check that time information is present and contains expected text
test.expect(notification?.time).toContain("Started");
test.expect(notification?.time).toMatch(/Started.*ago.*seconds/);
});
test("notification dropdown shows multiple agents when multiple are running", async ({
page: _page,
}) => {
// Navigate to library
await libraryPage.navigateToLibrary();
// Run first agent
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
const firstAgentName = await libraryPage.getAgentName();
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Go back to library and run another agent (if available)
await libraryPage.navigateToLibrary();
const agentCards = await libraryPage.agentCards.count();
if (agentCards > 1) {
await libraryPage.agentCards.nth(1).click();
await libraryPage.waitForAgentPageLoad();
const secondAgentName = await libraryPage.getAgentName();
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Click on notification button to open dropdown
await libraryPage.agentNotifications.clickNotificationButton();
// Verify both agents appear in dropdown
const firstNotification =
await libraryPage.agentNotifications.getNotificationByAgentName(
firstAgentName,
);
const secondNotification =
await libraryPage.agentNotifications.getNotificationByAgentName(
secondAgentName,
);
test.expect(firstNotification).not.toBeNull();
test.expect(secondNotification).not.toBeNull();
// Check that notification count reflects multiple running agents
const notificationCount =
await libraryPage.agentNotifications.getNotificationCount();
test.expect(parseInt(notificationCount)).toBeGreaterThanOrEqual(2);
} else {
// Skip this part if only one agent is available
console.log("Only one agent available, skipping multiple agent test");
}
});
test("notification dropdown closes when clicking outside", async ({
page,
}) => {
// Navigate to library and run an agent
await libraryPage.navigateToLibrary();
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
// Run the agent
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Click on notification button to open dropdown
await libraryPage.agentNotifications.clickNotificationButton();
// Verify dropdown is visible
await test
.expect(libraryPage.agentNotifications.isNotificationDropdownVisible())
.resolves.toBeTruthy();
// Click outside the dropdown
await page.click("body");
// Verify dropdown is no longer visible
await test
.expect(libraryPage.agentNotifications.isNotificationDropdownVisible())
.resolves.toBeFalsy();
});
test("notification badge count updates correctly", async ({
page: _page,
}) => {
// Navigate to library and run an agent
await libraryPage.navigateToLibrary();
await libraryPage.clickFirstAgent();
await libraryPage.waitForAgentPageLoad();
// Initially, no notification badge should be visible
await test
.expect(libraryPage.agentNotifications.isNotificationBadgeVisible())
.resolves.toBeFalsy();
// Run the agent
await libraryPage.runAgent();
await libraryPage.agentNotifications.waitForNotificationUpdate();
// Check that notification count is 1
const notificationCount =
await libraryPage.agentNotifications.getNotificationCount();
test.expect(notificationCount).toBe("1");
// Check that badge is visible and animating
await test
.expect(libraryPage.agentNotifications.isNotificationBadgeVisible())
.resolves.toBeTruthy();
});
});

View File

@@ -0,0 +1,95 @@
import { Locator, Page } from "@playwright/test";
export class AgentNotificationsPage {
constructor(private page: Page) {}
get notificationButton(): Locator {
return this.page.locator('button[title="Agent Activity"]');
}
get notificationBadge(): Locator {
return this.page
.locator('button[title="Agent Activity"] .animate-spin')
.first();
}
get notificationDropdown(): Locator {
return this.page.locator('[role="dialog"]:has-text("Agent Activity")');
}
get notificationItems(): Locator {
return this.notificationDropdown.locator('[role="button"]');
}
async clickNotificationButton(): Promise<void> {
await this.notificationButton.click();
}
async isNotificationBadgeVisible(): Promise<boolean> {
return await this.notificationBadge.isVisible();
}
async isNotificationDropdownVisible(): Promise<boolean> {
return await this.notificationDropdown.isVisible();
}
async getNotificationCount(): Promise<string> {
const badge = this.page.locator(
'button[title="Agent Activity"] .bg-purple-600',
);
return (await badge.textContent()) || "0";
}
async getNotificationItems(): Promise<
{ name: string; status: string; time: string }[]
> {
const items = await this.notificationItems.all();
const results = [];
for (const item of items) {
const name = (await item.locator(".truncate").textContent()) || "";
const time =
(await item.locator(".\\!text-zinc-500").textContent()) || "";
// Determine status from icon classes and text content
let status = "unknown";
if (await item.locator(".animate-spin").isVisible()) {
status = "running";
} else if (await item.locator("svg").first().isVisible()) {
// For non-animated icons, check the text content to determine status
const timeText = time.toLowerCase();
if (timeText.includes("completed")) {
status = "completed";
} else if (timeText.includes("failed")) {
status = "failed";
} else if (timeText.includes("stopped")) {
status = "terminated";
} else if (timeText.includes("incomplete")) {
status = "incomplete";
} else if (timeText.includes("queued")) {
status = "queued";
}
}
results.push({ name, status, time });
}
return results;
}
async waitForNotificationUpdate(_timeout = 10000): Promise<void> {
await this.page.waitForTimeout(1000); // Wait for potential updates
}
async hasNotificationWithStatus(status: string): Promise<boolean> {
const items = await this.getNotificationItems();
return items.some((item) => item.status === status);
}
async getNotificationByAgentName(
agentName: string,
): Promise<{ name: string; status: string; time: string } | null> {
const items = await this.getNotificationItems();
return items.find((item) => item.name.includes(agentName)) || null;
}
}

View File

@@ -0,0 +1,107 @@
import { Locator, Page } from "@playwright/test";
import { AgentNotificationsPage } from "./agent-notifications.page";
export class LibraryPage {
public agentNotifications: AgentNotificationsPage;
constructor(private page: Page) {
this.agentNotifications = new AgentNotificationsPage(page);
}
get libraryTab(): Locator {
return this.page.locator('a[href="/library"]');
}
get agentCards(): Locator {
return this.page.locator(".agpt-div").filter({ hasText: /^test-agent-/ });
}
get runButton(): Locator {
return this.page.locator('button:has-text("Run")');
}
get newRunButton(): Locator {
return this.page.locator('button:has-text("New run")');
}
get runDialogRunButton(): Locator {
return this.page.locator('button:has-text("Run"):last-child');
}
get agentTitle(): Locator {
return this.page.locator("h1").first();
}
async navigateToLibrary(): Promise<void> {
await this.libraryTab.click();
await this.page.waitForURL(/.*\/library/);
}
async clickFirstAgent(): Promise<void> {
const firstAgent = this.agentCards.first();
await firstAgent.click();
}
async navigateToAgentByName(agentName: string): Promise<void> {
const agentCard = this.agentCards.filter({ hasText: agentName }).first();
await agentCard.click();
}
async clickRunButton(): Promise<void> {
await this.runButton.click();
}
async clickNewRunButton(): Promise<void> {
await this.newRunButton.click();
}
async runAgent(inputs: Record<string, string> = {}): Promise<void> {
await this.clickRunButton();
// Fill in any required inputs
for (const [key, value] of Object.entries(inputs)) {
const input = this.page.locator(
`input[placeholder*="${key}"], textarea[placeholder*="${key}"]`,
);
if (await input.isVisible()) {
await input.fill(value);
}
}
// Click the run button in the dialog
await this.runDialogRunButton.click();
}
async waitForAgentPageLoad(): Promise<void> {
await this.page.waitForURL(/.*\/library\/agents\/[^/]+/);
await this.page.waitForLoadState("networkidle");
}
async getAgentName(): Promise<string> {
return (await this.agentTitle.textContent()) || "";
}
async isLoaded(): Promise<boolean> {
return await this.page.locator("h1").isVisible();
}
async waitForRunToComplete(timeout = 30000): Promise<void> {
// Wait for completion badge or status change
await this.page.waitForSelector(
".bg-green-500, .bg-red-500, .bg-purple-500",
{ timeout },
);
}
async getRunStatus(): Promise<string> {
// Check for different status indicators
if (await this.page.locator(".animate-spin").isVisible()) {
return "running";
} else if (await this.page.locator(".bg-green-500").isVisible()) {
return "completed";
} else if (await this.page.locator(".bg-red-500").isVisible()) {
return "failed";
}
return "unknown";
}
}