feat: Add loading state to threads page (#234)

* feat: Add loading state to threads page

* format

* cr
This commit is contained in:
Brace Sproul 2025-06-18 15:15:11 -07:00 • committed by GitHub
parent 0546a44d99
commit b752b2326e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 170 additions and 11 deletions

View file

@ -0,0 +1,5 @@
import { ThreadViewLoading } from "@/components/v2/thread-view-loading";
export default function Loading() {
return <ThreadViewLoading />;
}

View file

@ -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

View file

@ -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} />

View 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>
);
}

View file

@ -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 };
}