mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
fix: Better component for rendering thread errors (#528)
* fix: Better component for rendering thread errors * cr
This commit is contained in:
parent
5c7a7dcb49
commit
3d944cc81e
6 changed files with 159 additions and 3 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { ThreadView } from "@/components/v2/thread-view";
|
||||
import { ThreadViewLoading } from "@/components/v2/thread-view-loading";
|
||||
import { ThreadErrorCard } from "@/components/v2/thread-error-card";
|
||||
import { useThreadMetadata } from "@/hooks/useThreadMetadata";
|
||||
import { useThreadsSWR } from "@/hooks/useThreadsSWR";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
|
@ -53,7 +54,7 @@ export default function ThreadPage({
|
|||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { metadata: currentDisplayThread } = useThreadMetadata(
|
||||
const { metadata: currentDisplayThread, statusError } = useThreadMetadata(
|
||||
dummyThread as any,
|
||||
);
|
||||
|
||||
|
|
@ -61,6 +62,15 @@ export default function ThreadPage({
|
|||
router.push("/chat");
|
||||
};
|
||||
|
||||
if (statusError && "message" in statusError && "type" in statusError) {
|
||||
return (
|
||||
<ThreadErrorCard
|
||||
error={statusError}
|
||||
onGoBack={handleBackToHome}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!thread || threadsLoading) {
|
||||
return <ThreadViewLoading onBackToHome={handleBackToHome} />;
|
||||
}
|
||||
|
|
|
|||
101
apps/web/src/components/v2/thread-error-card.tsx
Normal file
101
apps/web/src/components/v2/thread-error-card.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertCircle, ArrowLeft, Lock, Search } from "lucide-react";
|
||||
|
||||
export interface ThreadStatusError {
|
||||
message: string;
|
||||
type: "not_found" | "unauthorized";
|
||||
}
|
||||
|
||||
interface ThreadErrorCardProps {
|
||||
error: ThreadStatusError;
|
||||
onGoBack: () => void;
|
||||
}
|
||||
|
||||
export function ThreadErrorCard({ error, onGoBack }: ThreadErrorCardProps) {
|
||||
const getErrorIcon = () => {
|
||||
switch (error.type) {
|
||||
case "not_found":
|
||||
return (
|
||||
<Search className="h-4 w-4 text-amber-500 dark:text-amber-400" />
|
||||
);
|
||||
case "unauthorized":
|
||||
return <Lock className="h-4 w-4 text-red-500 dark:text-red-400" />;
|
||||
default:
|
||||
return (
|
||||
<AlertCircle className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getErrorTypeLabel = () => {
|
||||
switch (error.type) {
|
||||
case "not_found":
|
||||
return "Not Found";
|
||||
case "unauthorized":
|
||||
return "Unauthorized";
|
||||
default:
|
||||
return "Error";
|
||||
}
|
||||
};
|
||||
|
||||
const getErrorTypeColor = () => {
|
||||
switch (error.type) {
|
||||
case "not_found":
|
||||
return "border-amber-200 text-amber-600 dark:border-amber-800 dark:text-amber-400";
|
||||
case "unauthorized":
|
||||
return "border-red-200 text-red-600 dark:border-red-800 dark:text-red-400";
|
||||
default:
|
||||
return "border-red-200 text-red-600 dark:border-red-800 dark:text-red-400";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="bg-card w-full max-w-md rounded-lg border p-6 shadow-lg">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{getErrorIcon()}
|
||||
<h4 className="text-sm font-semibold">Thread Error</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground text-xs font-medium">
|
||||
Type
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(getErrorTypeColor())}
|
||||
>
|
||||
{getErrorTypeLabel()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<span className="text-muted-foreground text-xs font-medium">
|
||||
Message
|
||||
</span>
|
||||
<p className="text-sm font-medium">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
onClick={onGoBack}
|
||||
variant="outline"
|
||||
className="flex w-full items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { useThreadStatus } from "./useThreadStatus";
|
|||
import { useMemo } from "react";
|
||||
import { getThreadTitle, computeThreadTitle } from "@/lib/thread";
|
||||
import { calculateLastActivity } from "@/lib/thread-utils";
|
||||
import { ThreadStatusError } from "@/lib/schemas/thread-status";
|
||||
|
||||
/**
|
||||
* Hook that combines thread metadata with real-time status
|
||||
|
|
@ -12,7 +13,7 @@ import { calculateLastActivity } from "@/lib/thread-utils";
|
|||
export function useThreadMetadata(thread: Thread<ManagerGraphState>): {
|
||||
metadata: ThreadMetadata;
|
||||
isStatusLoading: boolean;
|
||||
statusError: Error | null;
|
||||
statusError: Error | ThreadStatusError | null;
|
||||
} {
|
||||
const {
|
||||
status,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function useThreadStatus(
|
|||
status: data?.status || "idle",
|
||||
taskPlan: data?.taskPlan,
|
||||
isLoading,
|
||||
error,
|
||||
error: data?.error ?? error,
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ export function mapLangGraphToUIStatus(status: ThreadStatus): ThreadUIStatus {
|
|||
}
|
||||
}
|
||||
|
||||
export interface ThreadStatusError {
|
||||
message: string;
|
||||
type: "not_found" | "unauthorized";
|
||||
}
|
||||
|
||||
export interface ThreadStatusData {
|
||||
graph:
|
||||
| typeof MANAGER_GRAPH_ID
|
||||
|
|
@ -42,4 +47,5 @@ export interface ThreadStatusData {
|
|||
threadId: string;
|
||||
status: ThreadUIStatus;
|
||||
taskPlan?: TaskPlan; // Task plan data when available from programmer sessions
|
||||
error?: ThreadStatusError;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,42 @@ import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
|||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
import { SessionCache, SessionCacheData } from "@/hooks/useThreadsStatus";
|
||||
|
||||
function getErrorFields(error: unknown): {
|
||||
message: string;
|
||||
type: "not_found" | "unauthorized";
|
||||
} {
|
||||
if (
|
||||
!error ||
|
||||
typeof error !== "object" ||
|
||||
!("message" in error) ||
|
||||
!("status" in error)
|
||||
) {
|
||||
return {
|
||||
message: "Unknown error",
|
||||
type: "unauthorized",
|
||||
};
|
||||
}
|
||||
|
||||
if (error.status === 404) {
|
||||
return {
|
||||
message: "Thread not found",
|
||||
type: "not_found",
|
||||
};
|
||||
}
|
||||
|
||||
if (error.status === 401) {
|
||||
return {
|
||||
message: "Unauthorized",
|
||||
type: "unauthorized",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Unknown error",
|
||||
type: "unauthorized",
|
||||
};
|
||||
}
|
||||
|
||||
interface StatusResult {
|
||||
graph: "manager" | "planner" | "programmer";
|
||||
runId: string;
|
||||
|
|
@ -134,6 +170,7 @@ export async function fetchThreadStatus(
|
|||
sessionCache,
|
||||
);
|
||||
} catch (error) {
|
||||
const errorFields = getErrorFields(error);
|
||||
console.error(`Error fetching thread status for ${threadId}:`, error);
|
||||
|
||||
const graph = lastPollingState?.graph || "manager";
|
||||
|
|
@ -145,6 +182,7 @@ export async function fetchThreadStatus(
|
|||
runId,
|
||||
threadId: errorThreadId,
|
||||
status: "error",
|
||||
error: errorFields,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue