mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 17:23:15 +00:00
feat: Enhance Error Handling with Collapsible Details for Overloaded Errors (#451)
* Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * cr --------- Co-authored-by: open-swe-dev[bot] <open-swe-dev@users.noreply.github.com> Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
parent
e45a4a0cf8
commit
d7cf3e0fb5
5 changed files with 166 additions and 25 deletions
|
|
@ -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<State extends PlannerGraphState | GraphState>({
|
|||
);
|
||||
const joinedRunId = useRef<string | undefined>(undefined);
|
||||
const [streamLoading, setStreamLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [errorState, setErrorState] = useState<ErrorState | null>(null);
|
||||
|
||||
const stream = useStream<State>({
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_URL,
|
||||
|
|
@ -135,13 +136,24 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
|||
|
||||
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<State extends PlannerGraphState | GraphState>({
|
|||
thread={stream as UseStream<Record<string, unknown>>}
|
||||
/>
|
||||
) : null}
|
||||
{errorMessage ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>An error occurred:</AlertTitle>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
{errorState ? (
|
||||
<CollapsibleAlert
|
||||
variant="destructive"
|
||||
errorState={errorState}
|
||||
icon={<AlertCircle className="size-4" />}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
111
apps/web/src/components/v2/collapsible-alert.tsx
Normal file
111
apps/web/src/components/v2/collapsible-alert.tsx
Normal file
|
|
@ -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<typeof alertVariants> {
|
||||
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 (
|
||||
<div
|
||||
data-slot="alert"
|
||||
data-variant={variant}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
<div className="col-start-2 space-y-1">
|
||||
<div className="line-clamp-1 min-h-4 font-medium tracking-tight">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-muted-foreground data-[variant=destructive]:text-destructive/90 text-sm data-[variant=info]:text-blue-800 data-[variant=success]:text-green-800 data-[variant=warning]:text-yellow-800 dark:data-[variant=destructive]:text-red-200 dark:data-[variant=info]:text-blue-200 dark:data-[variant=success]:text-green-200 dark:data-[variant=warning]:text-yellow-200 [&_p]:leading-relaxed">
|
||||
{errorState.message}
|
||||
</div>
|
||||
|
||||
{hasDetails && (
|
||||
<>
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
key="details"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-2 rounded border border-current/20 bg-current/5 p-2 font-mono text-xs">
|
||||
{errorState.details}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.button
|
||||
onClick={() => 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 ? (
|
||||
<>
|
||||
<ChevronUp className="size-3" />
|
||||
Hide details
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="size-3" />
|
||||
Show details
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CollapsibleAlert };
|
||||
|
|
@ -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 (
|
||||
<div className="border-border bg-muted/30 flex h-full w-1/3 flex-col border-r dark:bg-gray-950">
|
||||
|
|
@ -147,12 +148,12 @@ export function ManagerChat({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{errorMessage ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>An error occurred:</AlertTitle>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
{errorState ? (
|
||||
<CollapsibleAlert
|
||||
variant="destructive"
|
||||
errorState={errorState}
|
||||
icon={<AlertCircle className="size-4" />}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof useStream<ManagerGraphState>>;
|
||||
|
|
@ -52,7 +53,7 @@ export function ThreadView({
|
|||
useState<ManagerGraphState["plannerSession"]>();
|
||||
const [programmerSession, setProgrammerSession] =
|
||||
useState<ManagerGraphState["programmerSession"]>();
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [errorState, setErrorState] = useState<ErrorState | null>(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 */}
|
||||
<div className="flex h-full flex-1 flex-col">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue