mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
feat: Add loading state to threads page (#234)
* feat: Add loading state to threads page * format * cr
This commit is contained in:
parent
0546a44d99
commit
b752b2326e
5 changed files with 170 additions and 11 deletions
5
apps/web/src/app/(v2)/chat/[thread_id]/loading.tsx
Normal file
5
apps/web/src/app/(v2)/chat/[thread_id]/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ThreadViewLoading } from "@/components/v2/thread-view-loading";
|
||||
|
||||
export default function Loading() {
|
||||
return <ThreadViewLoading />;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { ThreadView } from "@/components/v2/thread-view";
|
||||
import { ThreadViewLoading } from "@/components/v2/thread-view-loading";
|
||||
import { ThreadDisplayInfo, threadToDisplayInfo } from "@/components/v2/types";
|
||||
import { useThreads } from "@/hooks/useThreads";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
|
@ -28,13 +29,16 @@ export default function ThreadPage({
|
|||
reconnectOnMount: true,
|
||||
});
|
||||
|
||||
const { threads } = useThreads<GraphState>();
|
||||
|
||||
const { threads, threadsLoading } = useThreads<GraphState>();
|
||||
// Find the thread by ID
|
||||
const thread = threads?.find((t) => t.thread_id === thread_id);
|
||||
// If thread not found, show 404
|
||||
if (!thread) {
|
||||
return <>Loading...</>;
|
||||
|
||||
const handleBackToHome = () => {
|
||||
router.push("/chat");
|
||||
};
|
||||
|
||||
if (!thread || threadsLoading || !threadsLoading) {
|
||||
return <ThreadViewLoading onBackToHome={handleBackToHome} />;
|
||||
}
|
||||
|
||||
// Convert all threads to display format
|
||||
|
|
@ -46,10 +50,6 @@ export default function ThreadPage({
|
|||
router.push(`/chat/${selectedThread.id}`);
|
||||
};
|
||||
|
||||
const handleBackToHome = () => {
|
||||
router.push("/chat");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-black">
|
||||
<ThreadView
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default function ChatPage() {
|
|||
|
||||
return (
|
||||
<div className="h-screen overflow-y-hidden bg-black">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Suspense>
|
||||
<Toaster />
|
||||
<GitHubAppProvider>
|
||||
<DefaultView threads={displayThreads} />
|
||||
|
|
|
|||
150
apps/web/src/components/v2/thread-view-loading.tsx
Normal file
150
apps/web/src/components/v2/thread-view-loading.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { ArrowLeft, Loader2 } from "lucide-react";
|
||||
|
||||
interface ThreadViewLoadingProps {
|
||||
onBackToHome?: () => void;
|
||||
}
|
||||
|
||||
const isEven = (num: number) => num % 2 === 0;
|
||||
|
||||
export function LoadingActionsSection() {
|
||||
return (
|
||||
<Card className="border-gray-800 bg-gray-950">
|
||||
<CardHeader className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<Loader2 className="h-3 w-3 animate-spin text-gray-500" />
|
||||
<span className="text-xs text-gray-500">Loading actions...</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 p-3 pt-0">
|
||||
{Array.from({ length: 8 }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="space-y-2 rounded-lg border border-gray-800 bg-gray-900 p-3"
|
||||
>
|
||||
{isEven(i) ? (
|
||||
<div className="space-y-1 rounded bg-black p-2">
|
||||
<div className="h-2 w-32 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-2 w-48 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-3 w-40 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-16 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-4 w-4 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-2 rounded-lg border-2 border-dashed border-gray-700 p-3">
|
||||
<div className="h-12 animate-pulse rounded bg-gray-800"></div>
|
||||
<div className="h-7 w-16 animate-pulse rounded bg-gray-800"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-3">
|
||||
<div className="h-8 flex-1 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-8 flex-1 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingChatSection() {
|
||||
return (
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm">Loading conversation...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex gap-3"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="h-6 w-6 animate-pulse rounded-full bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-16 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-3 w-12 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="h-3 w-full animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-3 w-3/4 animate-pulse rounded bg-gray-700"></div>
|
||||
{i === 1 && (
|
||||
<div className="h-3 w-1/2 animate-pulse rounded bg-gray-700"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThreadViewLoading({ onBackToHome }: ThreadViewLoadingProps) {
|
||||
return (
|
||||
<div className="flex h-screen flex-1 flex-col bg-black">
|
||||
<div className="absolute top-0 right-0 left-0 z-10 border-b border-gray-900 bg-black px-4 py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-gray-600 hover:bg-gray-900 hover:text-gray-400"
|
||||
onClick={onBackToHome}
|
||||
>
|
||||
<ArrowLeft className="h-3 w-3" />
|
||||
</Button>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-gray-700"></div>
|
||||
<div className="h-3 w-48 animate-pulse rounded bg-gray-700"></div>
|
||||
<span className="text-xs text-gray-600">•</span>
|
||||
<div className="h-3 w-24 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="h-7 w-28 animate-pulse rounded bg-gray-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full pt-12">
|
||||
<div className="flex h-full w-1/3 flex-col border-r border-gray-900 bg-gray-950">
|
||||
<LoadingChatSection />
|
||||
|
||||
<div className="border-t border-gray-800 bg-gray-950 p-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 rounded-md border border-gray-700 bg-gray-900 p-3">
|
||||
<div className="space-y-2">
|
||||
<div className="h-3 w-1/3 animate-pulse rounded bg-gray-700"></div>
|
||||
<div className="h-3 w-1/2 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-10 w-10 animate-pulse self-end rounded bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="mt-2 h-3 w-32 animate-pulse rounded bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-1 flex-col">
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
<LoadingActionsSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export function useThreads<State extends Record<string, any>>(
|
|||
) {
|
||||
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||||
const [threads, setThreads] = useState<Thread<State>[] | null>(null);
|
||||
const [threadsLoading, setThreadsLoading] = useState(false);
|
||||
|
||||
const getThread = useCallback(
|
||||
async (threadId: string): Promise<Thread<State> | null> => {
|
||||
|
|
@ -26,6 +27,7 @@ export function useThreads<State extends Record<string, any>>(
|
|||
|
||||
const getThreads = useCallback(async (): Promise<Thread<State>[] | null> => {
|
||||
if (!apiUrl) return null;
|
||||
setThreadsLoading(true);
|
||||
const client = createClient(apiUrl);
|
||||
|
||||
try {
|
||||
|
|
@ -41,6 +43,8 @@ export function useThreads<State extends Record<string, any>>(
|
|||
} catch (error) {
|
||||
console.error("Failed to fetch threads:", error);
|
||||
return null;
|
||||
} finally {
|
||||
setThreadsLoading(false);
|
||||
}
|
||||
}, [apiUrl]);
|
||||
|
||||
|
|
@ -50,5 +54,5 @@ export function useThreads<State extends Record<string, any>>(
|
|||
});
|
||||
}, [getThreads]);
|
||||
|
||||
return { threads, setThreads, getThread, getThreads };
|
||||
return { threads, setThreads, getThread, getThreads, threadsLoading };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue