Merge branch 'dev' into swiftyos/secrt-1646-review-chat-logic-and-route-models-in-chatpy-and-modelspy

This commit is contained in:
Swifty
2025-10-24 09:37:19 +02:00
committed by GitHub
25 changed files with 381 additions and 501 deletions

View File

@@ -4,13 +4,13 @@ import mimetypes
from pathlib import Path
from typing import Any
import aiohttp
import discord
from pydantic import SecretStr
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials, SchemaField
from backend.util.file import store_media_file
from backend.util.request import Requests
from backend.util.type import MediaFileType
from ._auth import (
@@ -114,10 +114,9 @@ class ReadDiscordMessagesBlock(Block):
if message.attachments:
attachment = message.attachments[0] # Process the first attachment
if attachment.filename.endswith((".txt", ".py")):
async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as response:
file_content = response.text()
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
response = await Requests().get(attachment.url)
file_content = response.text()
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
await client.close()
@@ -699,16 +698,15 @@ class SendDiscordFileBlock(Block):
elif file.startswith(("http://", "https://")):
# URL - download the file
async with aiohttp.ClientSession() as session:
async with session.get(file) as response:
file_bytes = await response.read()
response = await Requests().get(file)
file_bytes = response.content
# Try to get filename from URL if not provided
if not filename:
from urllib.parse import urlparse
# Try to get filename from URL if not provided
if not filename:
from urllib.parse import urlparse
path = urlparse(file).path
detected_filename = Path(path).name or "download"
path = urlparse(file).path
detected_filename = Path(path).name or "download"
else:
# Local file path - read from stored media file
# This would be a path from a previous block's output

View File

@@ -1,7 +1,5 @@
import asyncio
import logging
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
from typing import Any
@@ -10,6 +8,7 @@ import pydantic
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import Requests
class RSSEntry(pydantic.BaseModel):
@@ -103,35 +102,29 @@ class ReadRSSFeedBlock(Block):
)
@staticmethod
def parse_feed(url: str) -> dict[str, Any]:
async def parse_feed(url: str) -> dict[str, Any]:
# Security fix: Add protection against memory exhaustion attacks
MAX_FEED_SIZE = 10 * 1024 * 1024 # 10MB limit for RSS feeds
# Validate URL
parsed_url = urllib.parse.urlparse(url)
if parsed_url.scheme not in ("http", "https"):
raise ValueError(f"Invalid URL scheme: {parsed_url.scheme}")
# Download with size limit
# Download feed content with size limit
try:
with urllib.request.urlopen(url, timeout=30) as response:
# Check content length if available
content_length = response.headers.get("Content-Length")
if content_length and int(content_length) > MAX_FEED_SIZE:
raise ValueError(
f"Feed too large: {content_length} bytes exceeds {MAX_FEED_SIZE} limit"
)
response = await Requests(raise_for_status=True).get(url)
# Read with size limit
content = response.read(MAX_FEED_SIZE + 1)
if len(content) > MAX_FEED_SIZE:
raise ValueError(
f"Feed too large: exceeds {MAX_FEED_SIZE} byte limit"
)
# Check content length if available
content_length = response.headers.get("Content-Length")
if content_length and int(content_length) > MAX_FEED_SIZE:
raise ValueError(
f"Feed too large: {content_length} bytes exceeds {MAX_FEED_SIZE} limit"
)
# Parse with feedparser using the validated content
# feedparser has built-in protection against XML attacks
return feedparser.parse(content) # type: ignore
# Get content with size limit
content = response.content
if len(content) > MAX_FEED_SIZE:
raise ValueError(f"Feed too large: exceeds {MAX_FEED_SIZE} byte limit")
# Parse with feedparser using the validated content
# feedparser has built-in protection against XML attacks
return feedparser.parse(content) # type: ignore
except Exception as e:
# Log error and return empty feed
logging.warning(f"Failed to parse RSS feed from {url}: {e}")
@@ -145,7 +138,7 @@ class ReadRSSFeedBlock(Block):
while keep_going:
keep_going = input_data.run_continuously
feed = self.parse_feed(input_data.rss_url)
feed = await self.parse_feed(input_data.rss_url)
all_entries = []
for entry in feed["entries"]:

View File

@@ -1,7 +1,11 @@
from typing import Type
from backend.blocks.ai_music_generator import AIMusicGeneratorBlock
from backend.blocks.ai_shortform_video_block import AIShortformVideoCreatorBlock
from backend.blocks.ai_shortform_video_block import (
AIAdMakerVideoCreatorBlock,
AIScreenshotToVideoAdBlock,
AIShortformVideoCreatorBlock,
)
from backend.blocks.apollo.organization import SearchOrganizationsBlock
from backend.blocks.apollo.people import SearchPeopleBlock
from backend.blocks.apollo.person import GetPersonDetailBlock
@@ -323,7 +327,31 @@ BLOCK_COSTS: dict[Type[Block], list[BlockCost]] = {
],
AIShortformVideoCreatorBlock: [
BlockCost(
cost_amount=50,
cost_amount=307,
cost_filter={
"credentials": {
"id": revid_credentials.id,
"provider": revid_credentials.provider,
"type": revid_credentials.type,
}
},
)
],
AIAdMakerVideoCreatorBlock: [
BlockCost(
cost_amount=714,
cost_filter={
"credentials": {
"id": revid_credentials.id,
"provider": revid_credentials.provider,
"type": revid_credentials.type,
}
},
)
],
AIScreenshotToVideoAdBlock: [
BlockCost(
cost_amount=612,
cost_filter={
"credentials": {
"id": revid_credentials.id,

View File

@@ -3,6 +3,7 @@ import logging
import bleach
from bleach.css_sanitizer import CSSSanitizer
from jinja2 import BaseLoader
from jinja2.exceptions import TemplateError
from jinja2.sandbox import SandboxedEnvironment
from markupsafe import Markup
@@ -101,8 +102,11 @@ class TextFormatter:
def format_string(self, template_str: str, values=None, **kwargs) -> str:
"""Regular template rendering with escaping"""
template = self.env.from_string(template_str)
return template.render(values or {}, **kwargs)
try:
template = self.env.from_string(template_str)
return template.render(values or {}, **kwargs)
except TemplateError as e:
raise ValueError(e) from e
def format_email(
self,

View File

@@ -33,7 +33,7 @@ Sentry.init({
enableLogs: true,
integrations: [
Sentry.captureConsoleIntegration(),
Sentry.captureConsoleIntegration({ levels: ["fatal", "error", "warn"] }),
Sentry.extraErrorDataIntegration(),
],
});

View File

@@ -1,12 +1,12 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Card } from "@/components/atoms/Card/Card";
import { WaitlistErrorContent } from "@/components/auth/WaitlistErrorContent";
import { isWaitlistErrorFromParams } from "@/app/api/auth/utils";
import { isWaitlistError } from "@/app/api/auth/utils";
import { useRouter } from "next/navigation";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { environment } from "@/services/environment";
export default function AuthErrorPage() {
@@ -38,12 +38,9 @@ export default function AuthErrorPage() {
}
// Check if this is a waitlist/not allowed error
const isWaitlistError = isWaitlistErrorFromParams(
errorCode,
errorDescription,
);
const isWaitlistErr = isWaitlistError(errorCode, errorDescription);
if (isWaitlistError) {
if (isWaitlistErr) {
return (
<div className="flex h-screen items-center justify-center">
<Card className="w-full max-w-md p-8">
@@ -56,34 +53,25 @@ export default function AuthErrorPage() {
);
}
// Default error display for other types of errors
// Use ErrorCard for consistent error display
const errorMessage = errorDescription
? `${errorDescription}. If this error persists, please contact support at contact@agpt.co`
: "An authentication error occurred. Please contact support at contact@agpt.co";
return (
<div className="flex h-screen items-center justify-center">
<Card className="w-full max-w-md p-8">
<div className="flex flex-col items-center gap-6">
<Text variant="h3">Authentication Error</Text>
<div className="flex flex-col gap-2 text-center">
{errorType && (
<Text variant="body">
<strong>Error Type:</strong> {errorType}
</Text>
)}
{errorCode && (
<Text variant="body">
<strong>Error Code:</strong> {errorCode}
</Text>
)}
{errorDescription && (
<Text variant="body">
<strong>Description:</strong> {errorDescription}
</Text>
)}
</div>
<Button variant="primary" onClick={() => router.push("/login")}>
Back to Login
</Button>
</div>
</Card>
<div className="flex h-screen items-center justify-center p-4">
<div className="w-full max-w-md">
<ErrorCard
responseError={{
message: errorMessage,
detail: errorCode
? `Error code: ${errorCode}${errorType ? ` (${errorType})` : ""}`
: undefined,
}}
context="authentication"
onRetry={() => router.push("/login")}
/>
</div>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import Shepherd from "shepherd.js";
import "shepherd.js/dist/css/shepherd.css";
import { sendGAEvent } from "@/services/analytics/google-analytics";
import { Key, storage } from "@/services/storage/local-storage";
import { analytics } from "@/services/analytics";
export const startTutorial = (
emptyNodeList: (forceEmpty: boolean) => boolean,
@@ -555,7 +555,7 @@ export const startTutorial = (
"use client";
console.debug("sendTutorialStep");
sendGAEvent("event", "tutorial_step_shown", { value: step.id });
analytics.sendGAEvent("event", "tutorial_step_shown", { value: step.id });
});
}

View File

@@ -42,8 +42,16 @@ function isVideoUrl(url: string): boolean {
if (url.includes("youtube.com/watch") || url.includes("youtu.be/")) {
return true;
}
if (url.includes("vimeo.com/")) {
return true;
try {
const parsed = new URL(url);
if (
parsed.hostname === "vimeo.com" ||
parsed.hostname === "www.vimeo.com"
) {
return true;
}
} catch {
// If URL parsing fails, treat as not a Vimeo URL.
}
return videoExtensions.some((ext) => url.toLowerCase().includes(ext));
}

View File

@@ -17,6 +17,7 @@ import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecu
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { analytics } from "@/services/analytics";
export type RunVariant =
| "manual"
@@ -78,6 +79,10 @@ export function useAgentRunModal(
agent.graph_id,
).queryKey,
});
analytics.sendDatafastEvent("run_agent", {
name: agent.name,
id: agent.graph_id,
});
setIsOpen(false);
}
},
@@ -105,6 +110,11 @@ export function useAgentRunModal(
agent.graph_id,
),
});
analytics.sendDatafastEvent("schedule_agent", {
name: agent.name,
id: agent.graph_id,
cronExpression: cronExpression,
});
setIsOpen(false);
}
},

View File

@@ -37,6 +37,7 @@ import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
import { AgentRunStatus, agentRunStatusMap } from "./agent-run-status-chip";
import useCredits from "@/hooks/useCredits";
import { AgentRunOutputView } from "./agent-run-output-view";
import { analytics } from "@/services/analytics";
export function AgentRunDetailsView({
agent,
@@ -131,7 +132,13 @@ export function AgentRunDetailsView({
run.inputs!,
run.credential_inputs!,
)
.then(({ id }) => onRun(id))
.then(({ id }) => {
analytics.sendDatafastEvent("run_agent", {
name: graph.name,
id: graph.id,
});
onRun(id);
})
.catch(toastOnFail("execute agent preset"));
}
@@ -142,7 +149,13 @@ export function AgentRunDetailsView({
run.inputs!,
run.credential_inputs!,
)
.then(({ id }) => onRun(id))
.then(({ id }) => {
analytics.sendDatafastEvent("run_agent", {
name: graph.name,
id: graph.id,
});
onRun(id);
})
.catch(toastOnFail("execute agent"));
}, [api, graph, run, onRun, toastOnFail]);

View File

@@ -43,6 +43,7 @@ import {
import { AgentStatus, AgentStatusChip } from "./agent-status-chip";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { analytics } from "@/services/analytics";
export function AgentRunDraftView({
graph,
@@ -197,6 +198,12 @@ export function AgentRunDraftView({
}
// Mark run agent onboarding step as completed
completeOnboardingStep("MARKETPLACE_RUN_AGENT");
analytics.sendDatafastEvent("run_agent", {
name: graph.name,
id: graph.id,
});
if (runCount > 0) {
completeOnboardingStep("RE_RUN_AGENT");
}
@@ -373,6 +380,12 @@ export function AgentRunDraftView({
})
.catch(toastOnFail("set up agent run schedule"));
analytics.sendDatafastEvent("schedule_agent", {
name: graph.name,
id: graph.id,
cronExpression: cronExpression,
});
if (schedule && onCreateSchedule) onCreateSchedule(schedule);
},
[api, graph, inputValues, inputCredentials, onCreateSchedule, toastOnFail],

View File

@@ -6,10 +6,10 @@ import Link from "next/link";
import { User } from "@supabase/supabase-js";
import { cn } from "@/lib/utils";
import { useAgentInfo } from "./useAgentInfo";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
interface AgentInfoProps {
user: User | null;
agentId: string;
name: string;
creator: string;
shortDescription: string;
@@ -20,11 +20,12 @@ interface AgentInfoProps {
lastUpdated: string;
version: string;
storeListingVersionId: string;
libraryAgent: LibraryAgent | undefined;
isAgentAddedToLibrary: boolean;
}
export const AgentInfo = ({
user,
agentId,
name,
creator,
shortDescription,
@@ -35,7 +36,7 @@ export const AgentInfo = ({
lastUpdated,
version,
storeListingVersionId,
libraryAgent,
isAgentAddedToLibrary,
}: AgentInfoProps) => {
const {
handleDownload,
@@ -82,11 +83,15 @@ export const AgentInfo = ({
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
)}
data-testid={"agent-add-library-button"}
onClick={handleLibraryAction}
disabled={isAddingAgentToLibrary}
onClick={() =>
handleLibraryAction({
isAddingAgentFirstTime: !isAgentAddedToLibrary,
})
}
>
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
{libraryAgent ? "See runs" : "Add to library"}
{isAgentAddedToLibrary ? "See runs" : "Add to library"}
</span>
</button>
)}
@@ -96,7 +101,7 @@ export const AgentInfo = ({
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
)}
data-testid={"agent-download-button"}
onClick={handleDownload}
onClick={() => handleDownload(agentId, name)}
disabled={isDownloadingAgent}
>
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">

View File

@@ -1,10 +1,11 @@
import { usePostV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useRouter } from "next/navigation";
import * as Sentry from "@sentry/nextjs";
import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/store/store";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { analytics } from "@/services/analytics";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
interface UseAgentInfoProps {
storeListingVersionId: string;
@@ -16,29 +17,9 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
const { completeStep } = useOnboarding();
const {
mutate: addMarketplaceAgentToLibrary,
mutateAsync: addMarketplaceAgentToLibrary,
isPending: isAddingAgentToLibrary,
} = usePostV2AddMarketplaceAgent({
mutation: {
onSuccess: ({ data }) => {
completeStep("MARKETPLACE_ADD_AGENT");
router.push(`/library/agents/${(data as LibraryAgent).id}`);
toast({
title: "Agent Added",
description: "Redirecting to your library...",
duration: 2000,
});
},
onError: (error) => {
Sentry.captureException(error);
toast({
title: "Error",
description: "Failed to add agent to library. Please try again.",
variant: "destructive",
});
},
},
});
} = usePostV2AddMarketplaceAgent();
const { refetch: downloadAgent, isFetching: isDownloadingAgent } =
useGetV2DownloadAgentFile(storeListingVersionId, {
@@ -50,13 +31,46 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
},
});
const handleLibraryAction = async () => {
addMarketplaceAgentToLibrary({
data: { store_listing_version_id: storeListingVersionId },
});
const handleLibraryAction = async ({
isAddingAgentFirstTime,
}: {
isAddingAgentFirstTime: boolean;
}) => {
try {
const { data: response } = await addMarketplaceAgentToLibrary({
data: { store_listing_version_id: storeListingVersionId },
});
const data = response as LibraryAgent;
if (isAddingAgentFirstTime) {
completeStep("MARKETPLACE_ADD_AGENT");
analytics.sendDatafastEvent("add_to_library", {
name: data.name,
id: data.id,
});
}
router.push(`/library/agents/${data.id}`);
toast({
title: "Agent Added",
description: "Redirecting to your library...",
duration: 2000,
});
} catch (error) {
Sentry.captureException(error);
toast({
title: "Error",
description: "Failed to add agent to library. Please try again.",
variant: "destructive",
});
}
};
const handleDownload = async () => {
const handleDownload = async (agentId: string, agentName: string) => {
try {
const { data: file } = await downloadAgent();
@@ -74,6 +88,11 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
window.URL.revokeObjectURL(url);
analytics.sendDatafastEvent("download_agent", {
name: agentName,
id: agentId,
});
toast({
title: "Download Complete",
description: "Your agent has been successfully downloaded.",

View File

@@ -82,6 +82,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
<div className="w-full md:w-auto md:shrink-0">
<AgentInfo
user={user}
agentId={agent.active_version_id ?? ""}
name={agent.agent_name}
creator={agent.creator}
shortDescription={agent.sub_heading}
@@ -92,7 +93,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
lastUpdated={agent.last_updated.toISOString()}
version={agent.versions[agent.versions.length - 1]}
storeListingVersionId={agent.store_listing_version_id}
libraryAgent={libraryAgent}
isAgentAddedToLibrary={Boolean(libraryAgent)}
/>
</div>
<AgentImages

View File

@@ -59,6 +59,7 @@ export function useSignupPage() {
resetCaptcha();
return;
}
try {
const response = await fetch("/api/auth/provider", {
method: "POST",
@@ -71,7 +72,6 @@ export function useSignupPage() {
setIsGoogleLoading(false);
resetCaptcha();
// Check for waitlist error
if (error === "not_allowed") {
setShowNotAllowedModal(true);
return;
@@ -149,6 +149,7 @@ export function useSignupPage() {
setShowNotAllowedModal(true);
return;
}
toast({
title: result?.error || "Signup failed",
variant: "destructive",

View File

@@ -33,7 +33,7 @@ export async function POST(request: Request) {
if (error) {
// Check for waitlist/allowlist error
if (isWaitlistError(error)) {
if (isWaitlistError(error?.code, error?.message)) {
logWaitlistError("OAuth Provider", error.message);
return NextResponse.json({ error: "not_allowed" }, { status: 403 });
}

View File

@@ -30,6 +30,7 @@ export async function POST(request: Request) {
turnstileToken ?? "",
"signup",
);
if (!captchaOk) {
return NextResponse.json(
{ error: "CAPTCHA verification failed. Please try again." },
@@ -48,8 +49,7 @@ export async function POST(request: Request) {
const { data, error } = await supabase.auth.signUp(parsed.data);
if (error) {
// Check for waitlist/allowlist error
if (isWaitlistError(error)) {
if (isWaitlistError(error?.code, error?.message)) {
logWaitlistError("Signup", error.message);
return NextResponse.json({ error: "not_allowed" }, { status: 403 });
}

View File

@@ -1,49 +1,45 @@
/**
* Checks if a Supabase auth error is related to the waitlist/allowlist
* Checks if an error is related to the waitlist/allowlist
*
* Can be used with either:
* - Error objects from Supabase auth operations: `isWaitlistError(error?.code, error?.message)`
* - URL parameters from OAuth callbacks: `isWaitlistError(errorCode, errorDescription)`
*
* The PostgreSQL trigger raises P0001 with message format:
* "The email address "email" is not allowed to register. Please contact support for assistance."
*
* @param error - The error object from Supabase auth operations
* @returns true if this is a waitlist/allowlist error
*/
export function isWaitlistError(error: any): boolean {
if (!error?.message) return false;
if (error?.code === "P0001") return true;
return (
error.message.includes("P0001") || // PostgreSQL custom error code
error.message.includes("not allowed to register") || // Trigger message
error.message.toLowerCase().includes("allowed_users") // Table reference
);
}
/**
* Checks if OAuth callback URL parameters indicate a waitlist error
*
* This is for the auth-code-error page which receives errors via URL hash params
* from Supabase OAuth redirects
*
* @param errorCode - The error_code parameter from the URL
* @param errorDescription - The error_description parameter from the URL
* @param code - Error code (e.g., "P0001", "unexpected_failure") or null
* @param message - Error message/description or null
* @returns true if this appears to be a waitlist/allowlist error
*/
export function isWaitlistErrorFromParams(
errorCode?: string | null,
errorDescription?: string | null,
export function isWaitlistError(
code?: string | null,
message?: string | null,
): boolean {
if (!errorDescription) return false;
// Check for explicit PostgreSQL trigger error code
if (code === "P0001") return true;
if (errorCode === "P0001") return true;
if (!message) return false;
const description = errorDescription.toLowerCase();
const lowerMessage = message.toLowerCase();
// Check for the generic database error that occurs during waitlist check
// This happens when Supabase wraps the PostgreSQL trigger error
if (
code === "unexpected_failure" &&
message === "Database error saving new user"
) {
return true;
}
// Check for various waitlist-related patterns in the message
return (
description.includes("p0001") || // PostgreSQL error code might be in description
description.includes("not allowed") ||
description.includes("waitlist") ||
description.includes("allowlist") ||
description.includes("allowed_users")
lowerMessage.includes("p0001") || // PostgreSQL error code in message
lowerMessage.includes("not allowed") || // Common waitlist message
lowerMessage.includes("waitlist") || // Explicit waitlist mention
lowerMessage.includes("allowlist") || // Explicit allowlist mention
lowerMessage.includes("allowed_users") || // Database table reference
lowerMessage.includes("not allowed to register") // Full trigger message
);
}

View File

@@ -6,14 +6,12 @@ import "./globals.css";
import { Providers } from "@/app/providers";
import TallyPopupSimple from "@/components/molecules/TallyPoup/TallyPopup";
import { GoogleAnalytics } from "@/services/analytics/google-analytics";
import { Toaster } from "@/components/molecules/Toast/toaster";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/next";
import Script from "next/script";
import { environment } from "@/services/environment";
import { headers } from "next/headers";
import { SetupAnalytics } from "@/services/analytics";
export const metadata: Metadata = {
title: "AutoGPT Platform",
@@ -27,7 +25,6 @@ export default async function RootLayout({
}>) {
const headersList = await headers();
const host = headersList.get("host") || "";
const withAnalytics = environment.areAnalyticsEnabled(host);
return (
<html
@@ -36,17 +33,12 @@ export default async function RootLayout({
suppressHydrationWarning
>
<head>
<GoogleAnalytics
gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN"} // This is the measurement Id for the Google Analytics dev project
<SetupAnalytics
host={host}
ga={{
gaId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
}}
/>
{withAnalytics ? (
<Script
strategy="afterInteractive"
data-website-id="dfid_g5wtBIiHUwSkWKcGz80lu"
data-domain="agpt.co"
src="https://datafa.st/js/script.js"
/>
) : null}
</head>
<body>
<Providers

View File

@@ -1,248 +0,0 @@
"use client";
import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
import { Separator } from "@/components/__legacy__/ui/separator";
import BackendAPI, { LibraryAgent } from "@/lib/autogpt-server-api";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { User } from "@supabase/supabase-js";
import { cn } from "@/lib/utils";
import { FC, useCallback, useMemo, useState } from "react";
interface AgentInfoProps {
user: User | null;
name: string;
creator: string;
shortDescription: string;
longDescription: string;
rating: number;
runs: number;
categories: string[];
lastUpdated: string;
version: string;
storeListingVersionId: string;
libraryAgent: LibraryAgent | null;
}
export const AgentInfo: FC<AgentInfoProps> = ({
user,
name,
creator,
shortDescription,
longDescription,
rating,
runs,
categories,
lastUpdated,
version,
storeListingVersionId,
libraryAgent,
}) => {
const router = useRouter();
const api = useMemo(() => new BackendAPI(), []);
const { toast } = useToast();
const { completeStep } = useOnboarding();
const [adding, setAdding] = useState(false);
const [downloading, setDownloading] = useState(false);
const libraryAction = useCallback(async () => {
setAdding(true);
if (libraryAgent) {
toast({
description: "Redirecting to your library...",
duration: 2000,
});
// Redirect to the library agent page
router.push(`/library/agents/${libraryAgent.id}`);
return;
}
try {
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
storeListingVersionId,
);
completeStep("MARKETPLACE_ADD_AGENT");
router.push(`/library/agents/${newLibraryAgent.id}`);
toast({
title: "Agent Added",
description: "Redirecting to your library...",
duration: 2000,
});
} catch (error) {
console.error("Failed to add agent to library:", error);
toast({
title: "Error",
description: "Failed to add agent to library. Please try again.",
variant: "destructive",
});
}
}, [toast, api, storeListingVersionId, completeStep, router]);
const handleDownload = useCallback(async () => {
const downloadAgent = async (): Promise<void> => {
setDownloading(true);
try {
const file = await api.downloadStoreAgent(storeListingVersionId);
// Similar to Marketplace v1
const jsonData = JSON.stringify(file, null, 2);
// Create a Blob from the file content
const blob = new Blob([jsonData], { type: "application/json" });
// Create a temporary URL for the Blob
const url = window.URL.createObjectURL(blob);
// Create a temporary anchor element
const a = document.createElement("a");
a.href = url;
a.download = `agent_${storeListingVersionId}.json`; // Set the filename
// Append the anchor to the body, click it, and remove it
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Revoke the temporary URL
window.URL.revokeObjectURL(url);
toast({
title: "Download Complete",
description: "Your agent has been successfully downloaded.",
});
} catch (error) {
console.error(`Error downloading agent:`, error);
toast({
title: "Error",
description: "Failed to download agent. Please try again.",
variant: "destructive",
});
}
};
await downloadAgent();
setDownloading(false);
}, [setDownloading, api, storeListingVersionId, toast]);
return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
{/* Title */}
<div
data-testid="agent-title"
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>
{/* Creator */}
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
<div className="text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
by
</div>
<Link
data-testid={"agent-creator"}
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
className="text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
>
{creator}
</Link>
</div>
{/* Short Description */}
<div className="mb-4 line-clamp-2 w-full text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-6 lg:text-xl lg:leading-7">
{shortDescription}
</div>
{/* Buttons */}
<div className="mb-4 flex w-full gap-3 lg:mb-[60px]">
{user && (
<button
className={cn(
"inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3",
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
)}
data-testid={"agent-add-library-button"}
onClick={libraryAction}
disabled={adding}
>
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
{libraryAgent ? "See runs" : "Add to library"}
</span>
</button>
)}
<button
className={cn(
"inline-flex min-w-24 items-center justify-center rounded-full bg-zinc-200 px-4 py-3",
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
)}
data-testid={"agent-download-button"}
onClick={handleDownload}
disabled={downloading}
>
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">
Download agent
</div>
</button>
</div>
{/* Rating and Runs */}
<div className="mb-4 flex w-full items-center justify-between lg:mb-[44px]">
<div className="flex items-center gap-1.5 sm:gap-2">
<span className="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
{rating.toFixed(1)}
</span>
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
</div>
<div className="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
{runs.toLocaleString()} runs
</div>
</div>
{/* Separator */}
<Separator className="mb-4 lg:mb-[44px]" />
{/* Description Section */}
<div className="mb-4 w-full lg:mb-[36px]">
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
Description
</div>
<div
data-testid={"agent-description"}
className="whitespace-pre-line text-base font-normal leading-6 text-neutral-600 dark:text-neutral-400"
>
{longDescription}
</div>
</div>
{/* Categories */}
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-[36px]">
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
Categories
</div>
<div className="flex flex-wrap gap-1.5 sm:gap-2">
{categories.map((category, index) => (
<div
key={index}
className="decoration-skip-ink-none whitespace-nowrap rounded-full border border-neutral-600 bg-white px-2 py-0.5 text-base font-normal leading-6 text-neutral-800 underline-offset-[from-font] dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-[16px] sm:py-[10px]"
>
{category}
</div>
))}
</div>
</div>
{/* Version History */}
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
Version history
</div>
<div className="decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
Last updated {lastUpdated}
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Version {version}
</div>
</div>
</div>
);
};

View File

@@ -43,7 +43,15 @@ export function WaitlistErrorContent({
exact same email address you used when signing up.
</Text>
<Text variant="small" className="text-center text-muted-foreground">
If you&apos;re not sure which email you used or need help,{" "}
If you&apos;re not sure which email you used or need help, contact us
at{" "}
<a
href="mailto:contact@agpt.co"
className="underline hover:text-foreground"
>
contact@agpt.co
</a>{" "}
or{" "}
<a
href="https://discord.gg/autogpt"
target="_blank"

View File

@@ -40,12 +40,11 @@ export function buildRequestUrl(
method: string,
payload?: Record<string, any>,
): string {
let url = baseUrl + path;
const url = baseUrl + path;
const payloadAsQuery = ["GET", "DELETE"].includes(method);
if (payloadAsQuery && payload) {
const queryParams = new URLSearchParams(payload);
url += `?${queryParams.toString()}`;
return buildUrlWithQuery(url, payload);
}
return url;
@@ -61,12 +60,23 @@ export function buildServerUrl(path: string): string {
export function buildUrlWithQuery(
url: string,
payload?: Record<string, any>,
query?: Record<string, any>,
): string {
if (!payload) return url;
if (!query) return url;
const queryParams = new URLSearchParams(payload);
return `${url}?${queryParams.toString()}`;
// Filter out undefined values to prevent them from being included as "undefined" strings
const filteredQuery = Object.entries(query).reduce(
(acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
},
{} as Record<string, any>,
);
const queryParams = new URLSearchParams(filteredQuery);
return queryParams.size > 0 ? `${url}?${queryParams.toString()}` : url;
}
export async function handleFetchError(response: Response): Promise<ApiError> {

View File

@@ -1,68 +0,0 @@
/**
* Modified copy of ga.tsx from @next/third-parties/google, with modified gtag.js source URL.
* Original source file: https://github.com/vercel/next.js/blob/b304b45e3a6e3e79338568d76e28805e77c03ec9/packages/third-parties/src/google/ga.tsx
*/
"use client";
import type { GAParams } from "@/types/google";
import Script from "next/script";
import { useEffect } from "react";
let currDataLayerName: string | undefined = undefined;
export function GoogleAnalytics(props: GAParams) {
const { gaId, debugMode, dataLayerName = "dataLayer", nonce } = props;
if (currDataLayerName === undefined) {
currDataLayerName = dataLayerName;
}
useEffect(() => {
// Feature usage signal (same as original implementation)
performance.mark("mark_feature_usage", {
detail: {
feature: "custom-ga",
},
});
}, []);
return (
<>
<Script
id="_custom-ga-init"
// Using "afterInteractive" to avoid blocking the initial page rendering
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window['${dataLayerName}'] = window['${dataLayerName}'] || [];
function gtag(){window['${dataLayerName}'].push(arguments);}
gtag('js', new Date());
gtag('config', '${gaId}' ${debugMode ? ",{ 'debug_mode': true }" : ""});
`,
}}
nonce={nonce}
/>
<Script
id="_custom-ga"
strategy="afterInteractive"
src="/gtag.js"
nonce={nonce}
/>
</>
);
}
export function sendGAEvent(...args: any[]) {
if (currDataLayerName === undefined) {
console.warn(`Custom GA: GA has not been initialized`);
return;
}
const dataLayer = (window as any)[currDataLayerName];
if (dataLayer) {
dataLayer.push(...args);
} else {
console.warn(`Custom GA: dataLayer ${currDataLayerName} does not exist`);
}
}

View File

@@ -0,0 +1,114 @@
/**
* Modified copy of ga.tsx from @next/third-parties/google, with modified gtag.js source URL.
* Original source file: https://github.com/vercel/next.js/blob/b304b45e3a6e3e79338568d76e28805e77c03ec9/packages/third-parties/src/google/ga.tsx
*/
"use client";
import type { GAParams } from "@/types/google";
import Script from "next/script";
import { useEffect } from "react";
import { environment } from "../environment";
declare global {
interface Window {
datafast: (name: string, metadata: Record<string, unknown>) => void;
[key: string]: unknown[] | ((...args: unknown[]) => void) | unknown;
}
}
let currDataLayerName: string | undefined = undefined;
type SetupProps = {
ga: GAParams;
host: string;
};
export function SetupAnalytics(props: SetupProps) {
const { ga, host } = props;
const { gaId, debugMode, dataLayerName = "dataLayer", nonce } = ga;
const isProductionDomain = host.includes("platform.agpt.co");
// Datafa.st journey analytics only on production
const dataFastEnabled = isProductionDomain;
// We collect analytics too for open source developers running the platform locally
const googleAnalyticsEnabled = environment.isLocal() || isProductionDomain;
if (currDataLayerName === undefined) {
currDataLayerName = dataLayerName;
}
useEffect(() => {
if (!googleAnalyticsEnabled) return;
// Google Analytics: feature usage signal (same as original implementation)
performance.mark("mark_feature_usage", {
detail: {
feature: "custom-ga",
},
});
}, [googleAnalyticsEnabled]);
return (
<>
{/* Google Analytics */}
{googleAnalyticsEnabled ? (
<>
<Script
id="_custom-ga-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window['${dataLayerName}'] = window['${dataLayerName}'] || [];
function gtag(){window['${dataLayerName}'].push(arguments);}
gtag('js', new Date());
gtag('config', '${gaId}' ${debugMode ? ",{ 'debug_mode': true }" : ""});
`,
}}
nonce={nonce}
/>
{/* Google Tag Manager */}
<Script
id="_custom-ga"
strategy="afterInteractive"
src="/gtag.js"
nonce={nonce}
/>
</>
) : null}
{/* Datafa.st */}
{dataFastEnabled ? (
<Script
strategy="afterInteractive"
data-website-id="dfid_g5wtBIiHUwSkWKcGz80lu"
data-domain="agpt.co"
src="https://datafa.st/js/script.js"
/>
) : null}
</>
);
}
export const analytics = {
sendGAEvent,
sendDatafastEvent,
};
function sendGAEvent(...args: unknown[]) {
if (typeof window === "undefined") return;
if (currDataLayerName === undefined) return;
const dataLayer = window[currDataLayerName];
if (!dataLayer) return;
if (Array.isArray(dataLayer)) {
dataLayer.push(...args);
} else {
console.warn(`Custom GA: dataLayer ${currDataLayerName} does not exist`);
}
}
function sendDatafastEvent(name: string, metadata: Record<string, unknown>) {
if (typeof window === "undefined" || !window.datafast) return;
window.datafast(name, metadata);
}

View File

@@ -92,10 +92,6 @@ function areFeatureFlagsEnabled() {
return process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "enabled";
}
function areAnalyticsEnabled(host: string) {
return host.includes("platform.agpt.co");
}
export const environment = {
// Generic
getEnvironmentStr,
@@ -115,6 +111,5 @@ export const environment = {
isCloud,
isLocal,
isCAPTCHAEnabled,
areAnalyticsEnabled,
areFeatureFlagsEnabled,
};