1import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts";
2import { Globe, History, RadioTower } from "lucide-react";
3
4import type { BrowserHistoryEntry } from "~/browserHistoryStore";
5import { Empty, EmptyDescription, EmptyMedia, EmptyTitle } from "~/components/ui/empty";
6import { DiscoveryList } from "../ui/discovery-list";
7
8import { PreviewLocalServerCard } from "./PreviewLocalServerCard";
9import { PreviewRecentUrlCard } from "./PreviewRecentUrlCard";
10import { useDiscoveredLocalServers } from "./useDiscoveredLocalServers";
11
12interface Props {
13 threadRef: ScopedThreadRef;
14 environmentId: EnvironmentId;
15 configuredUrls?: ReadonlyArray<string> | undefined;
16 recentEntries: ReadonlyArray<BrowserHistoryEntry>;
17 onRemoveRecent: (url: string) => void;
18 onOpenUrl: (url: string) => void;
19}
20
21export function PreviewEmptyState({
22 threadRef,
23 environmentId,
24 configuredUrls,
25 recentEntries,
26 onRemoveRecent,
27 onOpenUrl,
28}: Props) {
29 const servers = useDiscoveredLocalServers({
30 environmentId,
31 configuredUrls,
32 });
33 const recents = recentEntries.filter((entry) => URL.canParse(entry.url)).slice(0, 8);
34
35 if (servers.length === 0 && recents.length === 0) {
36 return (
37 <Empty>
38 <EmptyMedia variant="icon">
39 <Globe className="size-4.5 text-muted-foreground" />
40 </EmptyMedia>
41 <EmptyTitle>No preview yet</EmptyTitle>
42 <EmptyDescription>
43 Type a URL above, or run a dev script. Browser-ready localhost servers will show up here
44 automatically.
45 </EmptyDescription>
46 </Empty>
47 );
48 }
49
50 return (
51 <div className="scrollbar-gutter-both flex h-full min-h-0 overflow-y-auto px-5 py-8">
52 <div className="mx-auto flex w-full max-w-xl flex-col gap-6">
53 {recents.length > 0 ? (
54 <div className="flex flex-col gap-3">
55 <div className="flex items-center gap-2 text-sm text-muted-foreground">
56 <History className="size-4 shrink-0" />
57 <h2 className="font-medium">Recently used</h2>
58 </div>
59 <DiscoveryList>
60 {recents.map((entry) => (
61 <PreviewRecentUrlCard
62 key={entry.url}
63 threadRef={threadRef}
64 entry={entry}
65 onOpen={() => onOpenUrl(entry.url)}
66 onRemove={() => onRemoveRecent(entry.url)}
67 />
68 ))}
69 </DiscoveryList>
70 </div>
71 ) : null}
72 {servers.length > 0 ? (
73 <div className="flex flex-col gap-3">
74 <div className="flex items-center gap-2 text-sm text-muted-foreground">
75 <RadioTower className="size-4 shrink-0" />
76 <h2 className="font-medium">Local servers</h2>
77 </div>
78 <DiscoveryList>
79 {servers.map((server) => (
80 <PreviewLocalServerCard
81 key={`${server.host}:${server.port}`}
82 threadRef={threadRef}
83 server={server}
84 onOpen={() => onOpenUrl(server.requestedUrl)}
85 />
86 ))}
87 </DiscoveryList>
88 <p className="px-1 text-xs text-muted-foreground">
89 Select a live local server to open it in this browser tab.
90 </p>
91 </div>
92 ) : null}
93 </div>
94 </div>
95 );
96}