Compare commits

..

1 Commits

Author SHA1 Message Date
Bentlybro
8b1f312126 fix(frontend): Handle object values in FileInput component
Fixes #11800

The FileInput component crashed with 'TypeError: e.startsWith is not a function'
when the value was an object (from external API) instead of a string.

Changes:
- Updated getFileLabelFromValue() to handle object format: { name, type, size, data }
- Added type guards for string vs object values
- Graceful fallback for edge cases (null, undefined, empty object)

Test cases verified:
- Object with name: returns filename
- Object with type only: extracts and formats MIME type
- String data URI: parses correctly
- String file path: extracts extension
- Edge cases: returns 'File' fallback
2026-02-03 11:19:10 +00:00
31 changed files with 190 additions and 306 deletions

1
.gitignore vendored
View File

@@ -180,4 +180,3 @@ autogpt_platform/backend/settings.py
.claude/settings.local.json .claude/settings.local.json
CLAUDE.local.md CLAUDE.local.md
/autogpt_platform/backend/logs /autogpt_platform/backend/logs
.next

View File

@@ -38,8 +38,6 @@ class ResponseType(str, Enum):
OPERATION_STARTED = "operation_started" OPERATION_STARTED = "operation_started"
OPERATION_PENDING = "operation_pending" OPERATION_PENDING = "operation_pending"
OPERATION_IN_PROGRESS = "operation_in_progress" OPERATION_IN_PROGRESS = "operation_in_progress"
# Input validation
INPUT_VALIDATION_ERROR = "input_validation_error"
# Base response model # Base response model
@@ -70,10 +68,6 @@ class AgentInfo(BaseModel):
has_external_trigger: bool | None = None has_external_trigger: bool | None = None
new_output: bool | None = None new_output: bool | None = None
graph_id: str | None = None graph_id: str | None = None
inputs: dict[str, Any] | None = Field(
default=None,
description="Input schema for the agent, including field names, types, and defaults",
)
class AgentsFoundResponse(ToolResponseBase): class AgentsFoundResponse(ToolResponseBase):
@@ -200,20 +194,6 @@ class ErrorResponse(ToolResponseBase):
details: dict[str, Any] | None = None details: dict[str, Any] | None = None
class InputValidationErrorResponse(ToolResponseBase):
"""Response when run_agent receives unknown input fields."""
type: ResponseType = ResponseType.INPUT_VALIDATION_ERROR
unrecognized_fields: list[str] = Field(
description="List of input field names that were not recognized"
)
inputs: dict[str, Any] = Field(
description="The agent's valid input schema for reference"
)
graph_id: str | None = None
graph_version: int | None = None
# Agent output models # Agent output models
class ExecutionOutputInfo(BaseModel): class ExecutionOutputInfo(BaseModel):
"""Summary of a single execution's outputs.""" """Summary of a single execution's outputs."""

View File

@@ -30,7 +30,6 @@ from .models import (
ErrorResponse, ErrorResponse,
ExecutionOptions, ExecutionOptions,
ExecutionStartedResponse, ExecutionStartedResponse,
InputValidationErrorResponse,
SetupInfo, SetupInfo,
SetupRequirementsResponse, SetupRequirementsResponse,
ToolResponseBase, ToolResponseBase,
@@ -274,22 +273,6 @@ class RunAgentTool(BaseTool):
input_properties = graph.input_schema.get("properties", {}) input_properties = graph.input_schema.get("properties", {})
required_fields = set(graph.input_schema.get("required", [])) required_fields = set(graph.input_schema.get("required", []))
provided_inputs = set(params.inputs.keys()) provided_inputs = set(params.inputs.keys())
valid_fields = set(input_properties.keys())
# Check for unknown input fields
unrecognized_fields = provided_inputs - valid_fields
if unrecognized_fields:
return InputValidationErrorResponse(
message=(
f"Unknown input field(s) provided: {', '.join(sorted(unrecognized_fields))}. "
f"Agent was not executed. Please use the correct field names from the schema."
),
session_id=session_id,
unrecognized_fields=sorted(unrecognized_fields),
inputs=graph.input_schema,
graph_id=graph.id,
graph_version=graph.version,
)
# If agent has inputs but none were provided AND use_defaults is not set, # If agent has inputs but none were provided AND use_defaults is not set,
# always show what's available first so user can decide # always show what's available first so user can decide

View File

@@ -402,42 +402,3 @@ async def test_run_agent_schedule_without_name(setup_test_data):
# Should return error about missing schedule_name # Should return error about missing schedule_name
assert result_data.get("type") == "error" assert result_data.get("type") == "error"
assert "schedule_name" in result_data["message"].lower() assert "schedule_name" in result_data["message"].lower()
@pytest.mark.asyncio(loop_scope="session")
async def test_run_agent_rejects_unknown_input_fields(setup_test_data):
"""Test that run_agent returns input_validation_error for unknown input fields."""
user = setup_test_data["user"]
store_submission = setup_test_data["store_submission"]
tool = RunAgentTool()
agent_marketplace_id = f"{user.email.split('@')[0]}/{store_submission.slug}"
session = make_session(user_id=user.id)
# Execute with unknown input field names
response = await tool.execute(
user_id=user.id,
session_id=str(uuid.uuid4()),
tool_call_id=str(uuid.uuid4()),
username_agent_slug=agent_marketplace_id,
inputs={
"unknown_field": "some value",
"another_unknown": "another value",
},
session=session,
)
assert response is not None
assert hasattr(response, "output")
assert isinstance(response.output, str)
result_data = orjson.loads(response.output)
# Should return input_validation_error type with unrecognized fields
assert result_data.get("type") == "input_validation_error"
assert "unrecognized_fields" in result_data
assert set(result_data["unrecognized_fields"]) == {
"another_unknown",
"unknown_field",
}
assert "inputs" in result_data # Contains the valid schema
assert "Agent was not executed" in result_data["message"]

View File

@@ -5,8 +5,6 @@ import uuid
from collections import defaultdict from collections import defaultdict
from typing import Any from typing import Any
from pydantic_core import PydanticUndefined
from backend.api.features.chat.model import ChatSession from backend.api.features.chat.model import ChatSession
from backend.data.block import get_block from backend.data.block import get_block
from backend.data.execution import ExecutionContext from backend.data.execution import ExecutionContext
@@ -77,22 +75,15 @@ class RunBlockTool(BaseTool):
self, self,
user_id: str, user_id: str,
block: Any, block: Any,
input_data: dict[str, Any] | None = None,
) -> tuple[dict[str, CredentialsMetaInput], list[CredentialsMetaInput]]: ) -> tuple[dict[str, CredentialsMetaInput], list[CredentialsMetaInput]]:
""" """
Check if user has required credentials for a block. Check if user has required credentials for a block.
Args:
user_id: User ID
block: Block to check credentials for
input_data: Input data for the block (used to determine provider via discriminator)
Returns: Returns:
tuple[matched_credentials, missing_credentials] tuple[matched_credentials, missing_credentials]
""" """
matched_credentials: dict[str, CredentialsMetaInput] = {} matched_credentials: dict[str, CredentialsMetaInput] = {}
missing_credentials: list[CredentialsMetaInput] = [] missing_credentials: list[CredentialsMetaInput] = []
input_data = input_data or {}
# Get credential field info from block's input schema # Get credential field info from block's input schema
credentials_fields_info = block.input_schema.get_credentials_fields_info() credentials_fields_info = block.input_schema.get_credentials_fields_info()
@@ -105,33 +96,14 @@ class RunBlockTool(BaseTool):
available_creds = await creds_manager.store.get_all_creds(user_id) available_creds = await creds_manager.store.get_all_creds(user_id)
for field_name, field_info in credentials_fields_info.items(): for field_name, field_info in credentials_fields_info.items():
effective_field_info = field_info # field_info.provider is a frozenset of acceptable providers
if field_info.discriminator and field_info.discriminator_mapping: # field_info.supported_types is a frozenset of acceptable types
# Get discriminator from input, falling back to schema default
discriminator_value = input_data.get(field_info.discriminator)
if discriminator_value is None:
field = block.input_schema.model_fields.get(
field_info.discriminator
)
if field and field.default is not PydanticUndefined:
discriminator_value = field.default
if (
discriminator_value
and discriminator_value in field_info.discriminator_mapping
):
effective_field_info = field_info.discriminate(discriminator_value)
logger.debug(
f"Discriminated provider for {field_name}: "
f"{discriminator_value} -> {effective_field_info.provider}"
)
matching_cred = next( matching_cred = next(
( (
cred cred
for cred in available_creds for cred in available_creds
if cred.provider in effective_field_info.provider if cred.provider in field_info.provider
and cred.type in effective_field_info.supported_types and cred.type in field_info.supported_types
), ),
None, None,
) )
@@ -145,8 +117,8 @@ class RunBlockTool(BaseTool):
) )
else: else:
# Create a placeholder for the missing credential # Create a placeholder for the missing credential
provider = next(iter(effective_field_info.provider), "unknown") provider = next(iter(field_info.provider), "unknown")
cred_type = next(iter(effective_field_info.supported_types), "api_key") cred_type = next(iter(field_info.supported_types), "api_key")
missing_credentials.append( missing_credentials.append(
CredentialsMetaInput( CredentialsMetaInput(
id=field_name, id=field_name,
@@ -214,9 +186,10 @@ class RunBlockTool(BaseTool):
logger.info(f"Executing block {block.name} ({block_id}) for user {user_id}") logger.info(f"Executing block {block.name} ({block_id}) for user {user_id}")
# Check credentials
creds_manager = IntegrationCredentialsManager() creds_manager = IntegrationCredentialsManager()
matched_credentials, missing_credentials = await self._check_block_credentials( matched_credentials, missing_credentials = await self._check_block_credentials(
user_id, block, input_data user_id, block
) )
if missing_credentials: if missing_credentials:

View File

@@ -6,8 +6,6 @@ from pydantic import SecretStr
from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.anthropic import AnthropicIntegration from sentry_sdk.integrations.anthropic import AnthropicIntegration
from sentry_sdk.integrations.asyncio import AsyncioIntegration from sentry_sdk.integrations.asyncio import AsyncioIntegration
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.httpx import HttpxIntegration
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration
from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.logging import LoggingIntegration
@@ -39,8 +37,6 @@ def sentry_init():
_experiments={"enable_logs": True}, _experiments={"enable_logs": True},
integrations=[ integrations=[
AsyncioIntegration(), AsyncioIntegration(),
FastApiIntegration(), # Traces FastAPI requests with detailed spans
HttpxIntegration(), # Traces outgoing HTTP calls (OpenAI, external APIs)
LoggingIntegration(sentry_logs_level=logging.INFO), LoggingIntegration(sentry_logs_level=logging.INFO),
AnthropicIntegration( AnthropicIntegration(
include_prompts=False, include_prompts=False,

View File

@@ -1,9 +1,10 @@
"use client"; "use client";
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { getOnboardingStatus, resolveResponse } from "@/app/api/helpers";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { resolveResponse, getOnboardingStatus } from "@/app/api/helpers";
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { getHomepageRoute } from "@/lib/constants";
export default function OnboardingPage() { export default function OnboardingPage() {
const router = useRouter(); const router = useRouter();
@@ -12,10 +13,12 @@ export default function OnboardingPage() {
async function redirectToStep() { async function redirectToStep() {
try { try {
// Check if onboarding is enabled (also gets chat flag for redirect) // Check if onboarding is enabled (also gets chat flag for redirect)
const { shouldShowOnboarding } = await getOnboardingStatus(); const { shouldShowOnboarding, isChatEnabled } =
await getOnboardingStatus();
const homepageRoute = getHomepageRoute(isChatEnabled);
if (!shouldShowOnboarding) { if (!shouldShowOnboarding) {
router.replace("/"); router.replace(homepageRoute);
return; return;
} }
@@ -23,7 +26,7 @@ export default function OnboardingPage() {
// Handle completed onboarding // Handle completed onboarding
if (onboarding.completedSteps.includes("GET_RESULTS")) { if (onboarding.completedSteps.includes("GET_RESULTS")) {
router.replace("/"); router.replace(homepageRoute);
return; return;
} }

View File

@@ -1,8 +1,9 @@
import { getOnboardingStatus } from "@/app/api/helpers";
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { revalidatePath } from "next/cache"; import { getHomepageRoute } from "@/lib/constants";
import BackendAPI from "@/lib/autogpt-server-api";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import { getOnboardingStatus } from "@/app/api/helpers";
// Handle the callback to complete the user session login // Handle the callback to complete the user session login
export async function GET(request: Request) { export async function GET(request: Request) {
@@ -26,12 +27,13 @@ export async function GET(request: Request) {
await api.createUser(); await api.createUser();
// Get onboarding status from backend (includes chat flag evaluated for this user) // Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding } = await getOnboardingStatus(); const { shouldShowOnboarding, isChatEnabled } =
await getOnboardingStatus();
if (shouldShowOnboarding) { if (shouldShowOnboarding) {
next = "/onboarding"; next = "/onboarding";
revalidatePath("/onboarding", "layout"); revalidatePath("/onboarding", "layout");
} else { } else {
next = "/"; next = getHomepageRoute(isChatEnabled);
revalidatePath(next, "layout"); revalidatePath(next, "layout");
} }
} catch (createUserError) { } catch (createUserError) {

View File

@@ -1,13 +1,6 @@
"use client"; import type { ReactNode } from "react";
import { FeatureFlagPage } from "@/services/feature-flags/FeatureFlagPage";
import { Flag } from "@/services/feature-flags/use-get-flag";
import { type ReactNode } from "react";
import { CopilotShell } from "./components/CopilotShell/CopilotShell"; import { CopilotShell } from "./components/CopilotShell/CopilotShell";
export default function CopilotLayout({ children }: { children: ReactNode }) { export default function CopilotLayout({ children }: { children: ReactNode }) {
return ( return <CopilotShell>{children}</CopilotShell>;
<FeatureFlagPage flag={Flag.CHAT} whenDisabled="/library">
<CopilotShell>{children}</CopilotShell>
</FeatureFlagPage>
);
} }

View File

@@ -14,8 +14,14 @@ export default function CopilotPage() {
const isInterruptModalOpen = useCopilotStore((s) => s.isInterruptModalOpen); const isInterruptModalOpen = useCopilotStore((s) => s.isInterruptModalOpen);
const confirmInterrupt = useCopilotStore((s) => s.confirmInterrupt); const confirmInterrupt = useCopilotStore((s) => s.confirmInterrupt);
const cancelInterrupt = useCopilotStore((s) => s.cancelInterrupt); const cancelInterrupt = useCopilotStore((s) => s.cancelInterrupt);
const { greetingName, quickActions, isLoading, hasSession, initialPrompt } = const {
state; greetingName,
quickActions,
isLoading,
hasSession,
initialPrompt,
isReady,
} = state;
const { const {
handleQuickAction, handleQuickAction,
startChatWithPrompt, startChatWithPrompt,
@@ -23,6 +29,8 @@ export default function CopilotPage() {
handleStreamingChange, handleStreamingChange,
} = handlers; } = handlers;
if (!isReady) return null;
if (hasSession) { if (hasSession) {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">

View File

@@ -3,11 +3,18 @@ import {
postV2CreateSession, postV2CreateSession,
} from "@/app/api/__generated__/endpoints/chat/chat"; } from "@/app/api/__generated__/endpoints/chat/chat";
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider"; import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import {
Flag,
type FlagValues,
useGetFlag,
} from "@/services/feature-flags/use-get-flag";
import { SessionKey, sessionStorage } from "@/services/storage/session-storage"; import { SessionKey, sessionStorage } from "@/services/storage/session-storage";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { useCopilotStore } from "./copilot-page-store"; import { useCopilotStore } from "./copilot-page-store";
@@ -26,6 +33,22 @@ export function useCopilotPage() {
const isCreating = useCopilotStore((s) => s.isCreatingSession); const isCreating = useCopilotStore((s) => s.isCreatingSession);
const setIsCreating = useCopilotStore((s) => s.setIsCreatingSession); const setIsCreating = useCopilotStore((s) => s.setIsCreatingSession);
// Complete VISIT_COPILOT onboarding step to grant $5 welcome bonus
useEffect(() => {
if (isLoggedIn) {
completeStep("VISIT_COPILOT");
}
}, [completeStep, isLoggedIn]);
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
const homepageRoute = getHomepageRoute(isChatEnabled);
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
const isFlagReady =
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
const greetingName = getGreetingName(user); const greetingName = getGreetingName(user);
const quickActions = getQuickActions(); const quickActions = getQuickActions();
@@ -35,8 +58,11 @@ export function useCopilotPage() {
: undefined; : undefined;
useEffect(() => { useEffect(() => {
if (isLoggedIn) completeStep("VISIT_COPILOT"); if (!isFlagReady) return;
}, [completeStep, isLoggedIn]); if (isChatEnabled === false) {
router.replace(homepageRoute);
}
}, [homepageRoute, isChatEnabled, isFlagReady, router]);
async function startChatWithPrompt(prompt: string) { async function startChatWithPrompt(prompt: string) {
if (!prompt?.trim()) return; if (!prompt?.trim()) return;
@@ -90,6 +116,7 @@ export function useCopilotPage() {
isLoading: isUserLoading, isLoading: isUserLoading,
hasSession, hasSession,
initialPrompt, initialPrompt,
isReady: isFlagReady && isChatEnabled !== false && isLoggedIn,
}, },
handlers: { handlers: {
handleQuickAction, handleQuickAction,

View File

@@ -1,6 +1,8 @@
"use client"; "use client";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { getHomepageRoute } from "@/lib/constants";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { Suspense } from "react"; import { Suspense } from "react";
import { getErrorDetails } from "./helpers"; import { getErrorDetails } from "./helpers";
@@ -9,6 +11,8 @@ function ErrorPageContent() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const errorMessage = searchParams.get("message"); const errorMessage = searchParams.get("message");
const errorDetails = getErrorDetails(errorMessage); const errorDetails = getErrorDetails(errorMessage);
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
function handleRetry() { function handleRetry() {
// Auth-related errors should redirect to login // Auth-related errors should redirect to login
@@ -26,7 +30,7 @@ function ErrorPageContent() {
}, 2000); }, 2000);
} else { } else {
// For server/network errors, go to home // For server/network errors, go to home
window.location.href = "/"; window.location.href = homepageRoute;
} }
} }

View File

@@ -1,5 +1,6 @@
"use server"; "use server";
import { getHomepageRoute } from "@/lib/constants";
import BackendAPI from "@/lib/autogpt-server-api"; import BackendAPI from "@/lib/autogpt-server-api";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { loginFormSchema } from "@/types/auth"; import { loginFormSchema } from "@/types/auth";
@@ -37,8 +38,10 @@ export async function login(email: string, password: string) {
await api.createUser(); await api.createUser();
// Get onboarding status from backend (includes chat flag evaluated for this user) // Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding } = await getOnboardingStatus(); const { shouldShowOnboarding, isChatEnabled } = await getOnboardingStatus();
const next = shouldShowOnboarding ? "/onboarding" : "/"; const next = shouldShowOnboarding
? "/onboarding"
: getHomepageRoute(isChatEnabled);
return { return {
success: true, success: true,

View File

@@ -1,6 +1,8 @@
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { loginFormSchema, LoginProvider } from "@/types/auth"; import { loginFormSchema, LoginProvider } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
@@ -20,15 +22,17 @@ export function useLoginPage() {
const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud(); const isCloudEnv = environment.isCloud();
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
// Get redirect destination from 'next' query parameter // Get redirect destination from 'next' query parameter
const nextUrl = searchParams.get("next"); const nextUrl = searchParams.get("next");
useEffect(() => { useEffect(() => {
if (isLoggedIn && !isLoggingIn) { if (isLoggedIn && !isLoggingIn) {
router.push(nextUrl || "/"); router.push(nextUrl || homepageRoute);
} }
}, [isLoggedIn, isLoggingIn, nextUrl, router]); }, [homepageRoute, isLoggedIn, isLoggingIn, nextUrl, router]);
const form = useForm<z.infer<typeof loginFormSchema>>({ const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema), resolver: zodResolver(loginFormSchema),
@@ -94,7 +98,7 @@ export function useLoginPage() {
} }
// Prefer URL's next parameter, then use backend-determined route // Prefer URL's next parameter, then use backend-determined route
router.replace(nextUrl || result.next || "/"); router.replace(nextUrl || result.next || homepageRoute);
} catch (error) { } catch (error) {
toast({ toast({
title: title:

View File

@@ -1,5 +1,6 @@
"use server"; "use server";
import { getHomepageRoute } from "@/lib/constants";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { signupFormSchema } from "@/types/auth"; import { signupFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
@@ -58,8 +59,10 @@ export async function signup(
} }
// Get onboarding status from backend (includes chat flag evaluated for this user) // Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding } = await getOnboardingStatus(); const { shouldShowOnboarding, isChatEnabled } = await getOnboardingStatus();
const next = shouldShowOnboarding ? "/onboarding" : "/"; const next = shouldShowOnboarding
? "/onboarding"
: getHomepageRoute(isChatEnabled);
return { success: true, next }; return { success: true, next };
} catch (err) { } catch (err) {

View File

@@ -1,6 +1,8 @@
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { LoginProvider, signupFormSchema } from "@/types/auth"; import { LoginProvider, signupFormSchema } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
@@ -20,15 +22,17 @@ export function useSignupPage() {
const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud(); const isCloudEnv = environment.isCloud();
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
// Get redirect destination from 'next' query parameter // Get redirect destination from 'next' query parameter
const nextUrl = searchParams.get("next"); const nextUrl = searchParams.get("next");
useEffect(() => { useEffect(() => {
if (isLoggedIn && !isSigningUp) { if (isLoggedIn && !isSigningUp) {
router.push(nextUrl || "/"); router.push(nextUrl || homepageRoute);
} }
}, [isLoggedIn, isSigningUp, nextUrl, router]); }, [homepageRoute, isLoggedIn, isSigningUp, nextUrl, router]);
const form = useForm<z.infer<typeof signupFormSchema>>({ const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema), resolver: zodResolver(signupFormSchema),
@@ -129,7 +133,7 @@ export function useSignupPage() {
} }
// Prefer the URL's next parameter, then result.next (for onboarding), then default // Prefer the URL's next parameter, then result.next (for onboarding), then default
const redirectTo = nextUrl || result.next || "/"; const redirectTo = nextUrl || result.next || homepageRoute;
router.replace(redirectTo); router.replace(redirectTo);
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);

View File

@@ -181,5 +181,6 @@ export async function getOnboardingStatus() {
const isCompleted = onboarding.completedSteps.includes("CONGRATS"); const isCompleted = onboarding.completedSteps.includes("CONGRATS");
return { return {
shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted, shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted,
isChatEnabled: status.is_chat_enabled,
}; };
} }

View File

@@ -1,15 +1,27 @@
"use client"; "use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { getHomepageRoute } from "@/lib/constants";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
export default function Page() { export default function Page() {
const isChatEnabled = useGetFlag(Flag.CHAT);
const router = useRouter(); const router = useRouter();
const homepageRoute = getHomepageRoute(isChatEnabled);
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
const isFlagReady =
!isLaunchDarklyConfigured || typeof isChatEnabled === "boolean";
useEffect(() => { useEffect(
router.replace("/copilot"); function redirectToHomepage() {
}, [router]); if (!isFlagReady) return;
router.replace(homepageRoute);
},
[homepageRoute, isFlagReady, router],
);
return <LoadingSpinner size="large" cover />; return null;
} }

View File

@@ -104,7 +104,28 @@ export function FileInput(props: Props) {
return false; return false;
} }
const getFileLabelFromValue = (val: string) => { const getFileLabelFromValue = (val: unknown): string => {
// Handle object format from external API: { name, type, size, data }
if (val && typeof val === "object") {
const obj = val as Record<string, unknown>;
if (typeof obj.name === "string") {
return getFileLabel(obj.name, (obj.type as string) || "");
}
if (typeof obj.type === "string") {
const mimeParts = obj.type.split("/");
if (mimeParts.length > 1) {
return `${mimeParts[1].toUpperCase()} file`;
}
return `${obj.type} file`;
}
return "File";
}
// Handle string values (data URIs or file paths)
if (typeof val !== "string") {
return "File";
}
if (val.startsWith("data:")) { if (val.startsWith("data:")) {
const matches = val.match(/^data:([^;]+);/); const matches = val.match(/^data:([^;]+);/);
if (matches?.[1]) { if (matches?.[1]) {

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { IconLaptop } from "@/components/__legacy__/ui/icons"; import { IconLaptop } from "@/components/__legacy__/ui/icons";
import { getHomepageRoute } from "@/lib/constants";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { ListChecksIcon } from "@phosphor-icons/react/dist/ssr"; import { ListChecksIcon } from "@phosphor-icons/react/dist/ssr";
@@ -23,11 +24,11 @@ interface Props {
export function NavbarLink({ name, href }: Props) { export function NavbarLink({ name, href }: Props) {
const pathname = usePathname(); const pathname = usePathname();
const isChatEnabled = useGetFlag(Flag.CHAT); const isChatEnabled = useGetFlag(Flag.CHAT);
const expectedHomeRoute = isChatEnabled ? "/copilot" : "/library"; const homepageRoute = getHomepageRoute(isChatEnabled);
const isActive = const isActive =
href === expectedHomeRoute href === homepageRoute
? pathname === "/" || pathname.startsWith(expectedHomeRoute) ? pathname === "/" || pathname.startsWith(homepageRoute)
: pathname.includes(href); : pathname.includes(href);
return ( return (

View File

@@ -66,7 +66,7 @@ export default function useAgentGraph(
>(null); >(null);
const [xyNodes, setXYNodes] = useState<CustomNode[]>([]); const [xyNodes, setXYNodes] = useState<CustomNode[]>([]);
const [xyEdges, setXYEdges] = useState<CustomEdge[]>([]); const [xyEdges, setXYEdges] = useState<CustomEdge[]>([]);
const betaBlocks = useGetFlag(Flag.BETA_BLOCKS) as string[]; const betaBlocks = useGetFlag(Flag.BETA_BLOCKS);
// Filter blocks based on beta flags // Filter blocks based on beta flags
const availableBlocks = useMemo(() => { const availableBlocks = useMemo(() => {

View File

@@ -11,3 +11,10 @@ export const API_KEY_HEADER_NAME = "X-API-Key";
// Layout // Layout
export const NAVBAR_HEIGHT_PX = 60; export const NAVBAR_HEIGHT_PX = 60;
// Routes
export function getHomepageRoute(isChatEnabled?: boolean | null): string {
if (isChatEnabled === true) return "/copilot";
if (isChatEnabled === false) return "/library";
return "/";
}

View File

@@ -1,3 +1,4 @@
import { getHomepageRoute } from "@/lib/constants";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { Key, storage } from "@/services/storage/local-storage"; import { Key, storage } from "@/services/storage/local-storage";
import { type CookieOptions } from "@supabase/ssr"; import { type CookieOptions } from "@supabase/ssr";
@@ -70,7 +71,7 @@ export function getRedirectPath(
} }
if (isAdminPage(path) && userRole !== "admin") { if (isAdminPage(path) && userRole !== "admin") {
return "/"; return getHomepageRoute();
} }
return null; return null;

View File

@@ -1,3 +1,4 @@
import { getHomepageRoute } from "@/lib/constants";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { createServerClient } from "@supabase/ssr"; import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
@@ -66,7 +67,7 @@ export async function updateSession(request: NextRequest) {
// 2. Check if user is authenticated but lacks admin role when accessing admin pages // 2. Check if user is authenticated but lacks admin role when accessing admin pages
if (user && userRole !== "admin" && isAdminPage(pathname)) { if (user && userRole !== "admin" && isAdminPage(pathname)) {
url.pathname = "/"; url.pathname = getHomepageRoute();
return NextResponse.redirect(url); return NextResponse.redirect(url);
} }

View File

@@ -23,7 +23,9 @@ import {
WebSocketNotification, WebSocketNotification,
} from "@/lib/autogpt-server-api"; } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { import {
@@ -102,6 +104,8 @@ export default function OnboardingProvider({
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { isLoggedIn } = useSupabase(); const { isLoggedIn } = useSupabase();
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
useOnboardingTimezoneDetection(); useOnboardingTimezoneDetection();
@@ -146,7 +150,7 @@ export default function OnboardingProvider({
if (isOnOnboardingRoute) { if (isOnOnboardingRoute) {
const enabled = await resolveResponse(getV1IsOnboardingEnabled()); const enabled = await resolveResponse(getV1IsOnboardingEnabled());
if (!enabled) { if (!enabled) {
router.push("/"); router.push(homepageRoute);
return; return;
} }
} }
@@ -158,7 +162,7 @@ export default function OnboardingProvider({
isOnOnboardingRoute && isOnOnboardingRoute &&
shouldRedirectFromOnboarding(onboarding.completedSteps, pathname) shouldRedirectFromOnboarding(onboarding.completedSteps, pathname)
) { ) {
router.push("/"); router.push(homepageRoute);
} }
} catch (error) { } catch (error) {
console.error("Failed to initialize onboarding:", error); console.error("Failed to initialize onboarding:", error);
@@ -173,7 +177,7 @@ export default function OnboardingProvider({
} }
initializeOnboarding(); initializeOnboarding();
}, [api, isOnOnboardingRoute, router, isLoggedIn, pathname]); }, [api, homepageRoute, isOnOnboardingRoute, router, isLoggedIn, pathname]);
const handleOnboardingNotification = useCallback( const handleOnboardingNotification = useCallback(
(notification: WebSocketNotification) => { (notification: WebSocketNotification) => {

View File

@@ -83,10 +83,6 @@ function getPostHogCredentials() {
}; };
} }
function getLaunchDarklyClientId() {
return process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
}
function isProductionBuild() { function isProductionBuild() {
return process.env.NODE_ENV === "production"; return process.env.NODE_ENV === "production";
} }
@@ -124,10 +120,7 @@ function isVercelPreview() {
} }
function areFeatureFlagsEnabled() { function areFeatureFlagsEnabled() {
return ( return process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "enabled";
process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true" &&
Boolean(process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID)
);
} }
function isPostHogEnabled() { function isPostHogEnabled() {
@@ -150,7 +143,6 @@ export const environment = {
getSupabaseAnonKey, getSupabaseAnonKey,
getPreviewStealingDev, getPreviewStealingDev,
getPostHogCredentials, getPostHogCredentials,
getLaunchDarklyClientId,
// Assertions // Assertions
isServerSide, isServerSide,
isClientSide, isClientSide,

View File

@@ -1,59 +0,0 @@
"use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useLDClient } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { environment } from "../environment";
import { Flag, useGetFlag } from "./use-get-flag";
interface FeatureFlagRedirectProps {
flag: Flag;
whenDisabled: string;
children: ReactNode;
}
export function FeatureFlagPage({
flag,
whenDisabled,
children,
}: FeatureFlagRedirectProps) {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const flagValue = useGetFlag(flag);
const ldClient = useLDClient();
const ldEnabled = environment.areFeatureFlagsEnabled();
const ldReady = Boolean(ldClient);
const flagEnabled = Boolean(flagValue);
useEffect(() => {
const initialize = async () => {
if (!ldEnabled) {
router.replace(whenDisabled);
setIsLoading(false);
return;
}
// Wait for LaunchDarkly to initialize when enabled to prevent race conditions
if (ldEnabled && !ldReady) return;
try {
await ldClient?.waitForInitialization();
if (!flagEnabled) router.replace(whenDisabled);
} catch (error) {
console.error(error);
router.replace(whenDisabled);
} finally {
setIsLoading(false);
}
};
initialize();
}, [ldReady, flagEnabled]);
return isLoading || !flagEnabled ? (
<LoadingSpinner size="large" cover />
) : (
<>{children}</>
);
}

View File

@@ -1,51 +0,0 @@
"use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useLDClient } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { environment } from "../environment";
import { Flag, useGetFlag } from "./use-get-flag";
interface FeatureFlagRedirectProps {
flag: Flag;
whenEnabled: string;
whenDisabled: string;
}
export function FeatureFlagRedirect({
flag,
whenEnabled,
whenDisabled,
}: FeatureFlagRedirectProps) {
const router = useRouter();
const flagValue = useGetFlag(flag);
const ldEnabled = environment.areFeatureFlagsEnabled();
const ldClient = useLDClient();
const ldReady = Boolean(ldClient);
const flagEnabled = Boolean(flagValue);
useEffect(() => {
const initialize = async () => {
if (!ldEnabled) {
router.replace(whenDisabled);
return;
}
// Wait for LaunchDarkly to initialize when enabled to prevent race conditions
if (ldEnabled && !ldReady) return;
try {
await ldClient?.waitForInitialization();
router.replace(flagEnabled ? whenEnabled : whenDisabled);
} catch (error) {
console.error(error);
router.replace(whenDisabled);
}
};
initialize();
}, [ldReady, flagEnabled]);
return <LoadingSpinner size="large" cover />;
}

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { LDProvider } from "launchdarkly-react-client-sdk"; import { LDProvider } from "launchdarkly-react-client-sdk";
@@ -8,17 +7,17 @@ import type { ReactNode } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import { environment } from "../environment"; import { environment } from "../environment";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const LAUNCHDARKLY_INIT_TIMEOUT_MS = 5000; const LAUNCHDARKLY_INIT_TIMEOUT_MS = 5000;
export function LaunchDarklyProvider({ children }: { children: ReactNode }) { export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
const { user, isUserLoading } = useSupabase(); const { user, isUserLoading } = useSupabase();
const envEnabled = environment.areFeatureFlagsEnabled(); const isCloud = environment.isCloud();
const clientId = environment.getLaunchDarklyClientId(); const isLaunchDarklyConfigured = isCloud && envEnabled && clientId;
const context = useMemo(() => { const context = useMemo(() => {
if (isUserLoading) return; if (isUserLoading || !user) {
if (!user) {
return { return {
kind: "user" as const, kind: "user" as const,
key: "anonymous", key: "anonymous",
@@ -37,17 +36,15 @@ export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
}; };
}, [user, isUserLoading]); }, [user, isUserLoading]);
if (!envEnabled) { if (!isLaunchDarklyConfigured) {
return <>{children}</>; return <>{children}</>;
} }
if (isUserLoading) {
return <LoadingSpinner size="large" cover />;
}
return ( return (
<LDProvider <LDProvider
clientSideID={clientId ?? ""} // Add this key prop. It will be 'anonymous' when logged out,
key={context.key}
clientSideID={clientId}
context={context} context={context}
timeout={LAUNCHDARKLY_INIT_TIMEOUT_MS} timeout={LAUNCHDARKLY_INIT_TIMEOUT_MS}
reactOptions={{ useCamelCaseFlagKeys: false }} reactOptions={{ useCamelCaseFlagKeys: false }}

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { DEFAULT_SEARCH_TERMS } from "@/app/(platform)/marketplace/components/HeroSection/helpers"; import { DEFAULT_SEARCH_TERMS } from "@/app/(platform)/marketplace/components/HeroSection/helpers";
import { environment } from "@/services/environment";
import { useFlags } from "launchdarkly-react-client-sdk"; import { useFlags } from "launchdarkly-react-client-sdk";
export enum Flag { export enum Flag {
@@ -19,9 +18,24 @@ export enum Flag {
CHAT = "chat", CHAT = "chat",
} }
export type FlagValues = {
[Flag.BETA_BLOCKS]: string[];
[Flag.NEW_BLOCK_MENU]: boolean;
[Flag.NEW_AGENT_RUNS]: boolean;
[Flag.GRAPH_SEARCH]: boolean;
[Flag.ENABLE_ENHANCED_OUTPUT_HANDLING]: boolean;
[Flag.NEW_FLOW_EDITOR]: boolean;
[Flag.BUILDER_VIEW_SWITCH]: boolean;
[Flag.SHARE_EXECUTION_RESULTS]: boolean;
[Flag.AGENT_FAVORITING]: boolean;
[Flag.MARKETPLACE_SEARCH_TERMS]: string[];
[Flag.ENABLE_PLATFORM_PAYMENT]: boolean;
[Flag.CHAT]: boolean;
};
const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true"; const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true";
const defaultFlags = { const mockFlags = {
[Flag.BETA_BLOCKS]: [], [Flag.BETA_BLOCKS]: [],
[Flag.NEW_BLOCK_MENU]: false, [Flag.NEW_BLOCK_MENU]: false,
[Flag.NEW_AGENT_RUNS]: false, [Flag.NEW_AGENT_RUNS]: false,
@@ -36,16 +50,17 @@ const defaultFlags = {
[Flag.CHAT]: false, [Flag.CHAT]: false,
}; };
type FlagValues = typeof defaultFlags; export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] {
const currentFlags = useFlags<FlagValues>(); const currentFlags = useFlags<FlagValues>();
const flagValue = currentFlags[flag]; const flagValue = currentFlags[flag];
const areFlagsEnabled = environment.areFeatureFlagsEnabled();
if (!areFlagsEnabled || isPwMockEnabled) { const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
return defaultFlags[flag]; const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
if (!isLaunchDarklyConfigured || isPwMockEnabled) {
return mockFlags[flag];
} }
return flagValue ?? defaultFlags[flag]; return flagValue ?? mockFlags[flag];
} }

View File

@@ -8,7 +8,6 @@
.buildlog/ .buildlog/
.history .history
.svn/ .svn/
.next/
migrate_working_dir/ migrate_working_dir/
# IntelliJ related # IntelliJ related