feat: Implement a loading and empty state for recent & running threads (#257)

* feat: Implement a loading and empty state for recent & running threads

* cr
This commit is contained in:
Brace Sproul 2025-06-19 12:28:06 -07:00 • committed by GitHub
parent 3cd44ed86b
commit a93614ffa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 12 deletions

View file

@ -9,7 +9,7 @@ import { Toaster } from "@/components/ui/sonner";
import { Suspense } from "react";
export default function ChatPage() {
const { threads } = useThreads<GraphState>(
const { threads, threadsLoading } = useThreads<GraphState>(
process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID,
);
@ -22,7 +22,10 @@ export default function ChatPage() {
<Suspense>
<Toaster />
<GitHubAppProvider>
<DefaultView threads={displayThreads} />
<DefaultView
threads={displayThreads}
threadsLoading={threadsLoading}
/>
</GitHubAppProvider>
</Suspense>
</div>

View file

@ -17,6 +17,7 @@ import {
GitPullRequest,
Bug,
FilePlus2,
Archive,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { ThreadDisplayInfo } from "./types";
@ -33,13 +34,14 @@ import { Label } from "../ui/label";
import { ContentBlocksPreview } from "../thread/ContentBlocksPreview";
import { TooltipIconButton } from "../ui/tooltip-icon-button";
import { ThemeToggle } from "../theme-toggle";
import { ThreadCard } from "./thread-card";
import { ThreadCard, ThreadCardLoading } from "./thread-card";
interface DefaultViewProps {
threads: ThreadDisplayInfo[];
threadsLoading: boolean;
}
export function DefaultView({ threads }: DefaultViewProps) {
export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
const router = useRouter();
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
const assistantId: string | undefined =
@ -149,14 +151,31 @@ export function DefaultView({ threads }: DefaultViewProps) {
</Button>
</div>
<div className="grid gap-3 md:grid-cols-2">
{threads.slice(0, 4).map((thread) => (
<ThreadCard
key={thread.id}
thread={thread}
/>
))}
</div>
{threadsLoading || threads.length ? (
<div className="grid gap-3 md:grid-cols-2">
{threadsLoading && threads.length === 0 && (
<>
<ThreadCardLoading />
<ThreadCardLoading />
<ThreadCardLoading />
<ThreadCardLoading />
</>
)}
{threads.slice(0, 4).map((thread) => (
<ThreadCard
key={thread.id}
thread={thread}
/>
))}
</div>
) : (
<div className="flex items-center justify-center py-8">
<span className="text-muted-foreground flex items-center gap-2">
<Archive className="size-4" />
<span className="text-sm">No threads found</span>
</span>
</div>
)}
</div>
{/* Quick Actions */}

View file

@ -11,6 +11,7 @@ import { ThreadDisplayInfo } from "./types";
import { useRouter } from "next/navigation";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Skeleton } from "../ui/skeleton";
export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
const router = useRouter();
@ -136,3 +137,37 @@ export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
</Card>
);
}
export function ThreadCardLoading() {
return (
<Card className="border-border bg-card px-0 py-3 dark:bg-gray-950">
<CardHeader>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="text-foreground truncate text-sm font-medium">
<Skeleton className="h-5 w-48" />
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-3 w-32" />
</div>
</div>
<Skeleton className="h-6 w-24 rounded-full" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-1" />
<Skeleton className="h-3 w-24" />
</div>
<div className="flex items-center gap-1">
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-5 w-5 rounded-full" />
</div>
</div>
</CardContent>
</Card>
);
}