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

View file

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

View file

@ -11,6 +11,7 @@ import { ThreadDisplayInfo } from "./types";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Badge } from "../ui/badge"; import { Badge } from "../ui/badge";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Skeleton } from "../ui/skeleton";
export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) { export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
const router = useRouter(); const router = useRouter();
@ -136,3 +137,37 @@ export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
</Card> </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>
);
}