diff --git a/apps/web/src/components/v2/actions-renderer.tsx b/apps/web/src/components/v2/actions-renderer.tsx index 922f620b..a87fc9f5 100644 --- a/apps/web/src/components/v2/actions-renderer.tsx +++ b/apps/web/src/components/v2/actions-renderer.tsx @@ -31,7 +31,8 @@ import { HumanResponse } from "@langchain/langgraph/prebuilt"; import { LoadingActionsCardContent } from "./thread-view-loading"; import { Interrupt } from "../thread/messages/interrupt"; import { AlertCircle } from "lucide-react"; -import { Alert, AlertTitle, AlertDescription } from "../ui/alert"; +import { ErrorState } from "./types"; +import { CollapsibleAlert } from "./collapsible-alert"; interface AcceptedPlanEventData { planTitle: string; @@ -118,7 +119,7 @@ export function ActionsRenderer({ ); const joinedRunId = useRef(undefined); const [streamLoading, setStreamLoading] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); + const [errorState, setErrorState] = useState(null); const stream = useStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL, @@ -135,13 +136,24 @@ export function ActionsRenderer({ useEffect(() => { if (stream.error) { - const errorMessage = + const rawErrorMessage = typeof stream.error === "object" && "message" in stream.error ? (stream.error.message as string) : "An unknown error occurred in the manager"; - setErrorMessage(errorMessage); + + if (rawErrorMessage.includes("overloaded_error")) { + setErrorState({ + message: + "An Anthropic overloaded error occurred. This error occurs when Anthropic APIs experience high traffic across all users.", + details: rawErrorMessage, + }); + } else { + setErrorState({ + message: rawErrorMessage, + }); + } } else { - setErrorMessage(""); + setErrorState(null); } }, [stream.error]); @@ -319,12 +331,12 @@ export function ActionsRenderer({ thread={stream as UseStream>} /> ) : null} - {errorMessage ? ( - - - An error occurred: - {errorMessage} - + {errorState ? ( + } + /> ) : null} ); diff --git a/apps/web/src/components/v2/collapsible-alert.tsx b/apps/web/src/components/v2/collapsible-alert.tsx new file mode 100644 index 00000000..652e7af2 --- /dev/null +++ b/apps/web/src/components/v2/collapsible-alert.tsx @@ -0,0 +1,111 @@ +import * as React from "react"; +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; +import { ErrorState } from "./types"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground border-border", + destructive: + "border-destructive/50 bg-destructive/10 text-destructive dark:border-red-400 dark:bg-red-950/50 dark:text-red-300 [&>svg]:text-destructive dark:[&>svg]:text-red-400", + warning: + "border-yellow-500/50 bg-yellow-50 text-yellow-900 dark:border-yellow-500 dark:bg-yellow-950 dark:text-yellow-100 [&>svg]:text-yellow-600 dark:[&>svg]:text-yellow-400", + info: "border-blue-500/50 bg-blue-50 text-blue-900 dark:border-blue-500 dark:bg-blue-950 dark:text-blue-100 [&>svg]:text-blue-600 dark:[&>svg]:text-blue-400", + success: + "border-green-500/50 bg-green-50 text-green-900 dark:border-green-500 dark:bg-green-950 dark:text-green-100 [&>svg]:text-green-600 dark:[&>svg]:text-green-400", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +interface CollapsibleAlertProps + extends React.ComponentProps<"div">, + VariantProps { + errorState: ErrorState; + icon?: React.ReactNode; + title?: string; +} + +function CollapsibleAlert({ + className, + variant, + errorState, + icon, + title = "An error occurred:", + ...props +}: CollapsibleAlertProps) { + const [isExpanded, setIsExpanded] = useState(false); + const hasDetails = Boolean(errorState.details); + + return ( +
+ {icon} +
+
+ {title} +
+
+ {errorState.message} +
+ + {hasDetails && ( + <> + + {isExpanded && ( + +
+ {errorState.details} +
+
+ )} +
+ + setIsExpanded(!isExpanded)} + className="flex items-center gap-1 text-xs text-current/70 transition-colors duration-200 hover:text-current" + initial={{ scale: 1 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {isExpanded ? ( + <> + + Hide details + + ) : ( + <> + + Show details + + )} + + + )} +
+
+ ); +} + +export { CollapsibleAlert }; diff --git a/apps/web/src/components/v2/manager-chat.tsx b/apps/web/src/components/v2/manager-chat.tsx index 45950b1e..84895ae5 100644 --- a/apps/web/src/components/v2/manager-chat.tsx +++ b/apps/web/src/components/v2/manager-chat.tsx @@ -19,7 +19,8 @@ import { Button } from "../ui/button"; import { cn } from "@/lib/utils"; import { isAIMessageSDK } from "@/lib/langchain-messages"; import { BasicMarkdownText } from "../thread/markdown-text"; -import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; +import { ErrorState } from "./types"; +import { CollapsibleAlert } from "./collapsible-alert"; function MessageCopyButton({ content }: { content: string }) { const [copied, setCopied] = useState(false); @@ -75,7 +76,7 @@ interface ManagerChatProps { handleSendMessage: () => void; isLoading: boolean; cancelRun: () => void; - errorMessage?: string; + errorState?: ErrorState | null; } function extractResponseFromMessage(message: Message): string { @@ -98,7 +99,7 @@ export function ManagerChat({ handleSendMessage, isLoading, cancelRun, - errorMessage, + errorState, }: ManagerChatProps) { return (
@@ -147,12 +148,12 @@ export function ManagerChat({
); })} - {errorMessage ? ( - - - An error occurred: - {errorMessage} - + {errorState ? ( + } + /> ) : null} } diff --git a/apps/web/src/components/v2/thread-view.tsx b/apps/web/src/components/v2/thread-view.tsx index 46ced88b..9594cb4d 100644 --- a/apps/web/src/components/v2/thread-view.tsx +++ b/apps/web/src/components/v2/thread-view.tsx @@ -30,6 +30,7 @@ import { } from "../../utils/scroll-utils"; import { ManagerChat } from "./manager-chat"; import { CancelStreamButton } from "./cancel-stream-button"; +import { ErrorState } from "./types"; interface ThreadViewProps { stream: ReturnType>; @@ -52,7 +53,7 @@ export function ThreadView({ useState(); const [programmerSession, setProgrammerSession] = useState(); - const [errorMessage, setErrorMessage] = useState(""); + const [errorState, setErrorState] = useState(null); useEffect(() => { if ( @@ -72,13 +73,24 @@ export function ThreadView({ useEffect(() => { if (stream.error) { - const errorMessage = + const rawErrorMessage = typeof stream.error === "object" && "message" in stream.error ? (stream.error.message as string) : "An unknown error occurred in the manager"; - setErrorMessage(errorMessage); + + if (rawErrorMessage.includes("overloaded_error")) { + setErrorState({ + message: + "An Anthropic overloaded error occurred. This error occurs when Anthropic APIs experience high traffic across all users.", + details: rawErrorMessage, + }); + } else { + setErrorState({ + message: rawErrorMessage, + }); + } } else { - setErrorMessage(""); + setErrorState(null); } }, [stream.error]); @@ -183,7 +195,7 @@ export function ThreadView({ handleSendMessage={handleSendMessage} isLoading={stream.isLoading} cancelRun={cancelRun} - errorMessage={errorMessage} + errorState={errorState} /> {/* Right Side - Actions & Plan */}
diff --git a/apps/web/src/components/v2/types.ts b/apps/web/src/components/v2/types.ts index 16fd122f..f9382721 100644 --- a/apps/web/src/components/v2/types.ts +++ b/apps/web/src/components/v2/types.ts @@ -1,6 +1,11 @@ import { ThreadUIStatus } from "@/lib/schemas/thread-status"; import { TaskPlan } from "@open-swe/shared/open-swe/types"; +export interface ErrorState { + message: string; + details?: string; +} + export interface ThreadMetadata { id: string; title: string;