Skip to content
Source
Sign inSign up

HostedEmptyState.tsx

163 lines5.7 KB

1import { useAuth } from "@clerk/react";
2import type { EnvironmentId } from "@t3tools/contracts";
3import { Link } from "@tanstack/react-router";
4import { CheckIcon } from "lucide-react";
5import { type ReactNode, useState } from "react";
6​
7import { APP_DISPLAY_NAME, DESKTOP_DOWNLOAD_URL } from "~/branding";
8import { hasCloudPublicConfig } from "~/cloud/publicConfig";
9import { isElectron } from "../env";
10import { isLocalEnvironmentDisabled } from "../localEnvironment";
11import { useRelayEnvironmentDiscovery } from "../state/environments";
12import { InstallT3Commands } from "./InstallT3Commands";
13import { useT3ConnectAuthPrompt } from "./clerk/useT3ConnectAuthPrompt";
14import { CloudEnvironmentConnectRows } from "./cloud/CloudEnvironmentConnectList";
15import { Button, InlineButton } from "./ui/button";
16import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty";
17import { SidebarInset } from "./ui/sidebar";
18import { WorkspacePageHeader } from "./WorkspacePageHeader";
19​
20/** Home screen for the hosted app before any computer is added. */
21export function HostedEmptyState() {
22 const cloudEnabled = hasCloudPublicConfig();
23 const localEnvironmentOff = isLocalEnvironmentDisabled();
24​
25 return (
26 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none">
27 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
28 <WorkspacePageHeader electron={isElectron} className="border-b border-border">
29 <div className="flex items-center gap-2">
30 <span className="text-label text-foreground md:text-muted-foreground/60">
31 {APP_DISPLAY_NAME}
32 </span>
33 </div>
34 </WorkspacePageHeader>
35​
36 <Empty className="flex-1">
37 <EmptyHeader>
38 <EmptyTitle>
39 {localEnvironmentOff ? "This computer is off" : "No computer connected"}
40 </EmptyTitle>
41 <EmptyDescription>
42 {localEnvironmentOff
43 ? "Turn it back on, or connect a different computer."
44 : cloudEnabled
45 ? "Your agents run on your computer."
46 : "Your agents run on your computer. Start T3 Code there first."}
47 </EmptyDescription>
48 </EmptyHeader>
49 <EmptyContent>
50 {localEnvironmentOff ? (
51 <Button render={<Link to="/settings/connections" />} size="sm">
52 Open Environments
53 </Button>
54 ) : (
55 <ol className="w-full space-y-4 text-left">
56 {cloudEnabled ? <T3ConnectSteps /> : <PairingSteps />}
57 </ol>
58 )}
59 </EmptyContent>
60 </Empty>
61 </div>
62 </SidebarInset>
63 );
65​
66function T3ConnectSteps() {
67 const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false });
68 const { openAuthPrompt } = useT3ConnectAuthPrompt();
69 // Adding the first computer unmounts this screen. Wait for all health checks
70 // so every compatible registration is queued before that happens.
71 const [autoAddedComputers] = useState(() => new Set<EnvironmentId>());
72 const [selectedIds, setSelectedIds] = useState<ReadonlySet<EnvironmentId>>(() => new Set());
73 const discovered = [...useRelayEnvironmentDiscovery().environments.values()];
74 const discoverySettled =
75 discovered.length > 0 && discovered.every(({ availability }) => availability !== "checking");
76 return (
77 <>
78 <SetupStep number={1}>
79 {isSignedIn ? (
80 <p className="flex items-center gap-1.5 text-muted-foreground">
81 <CheckIcon className="size-4" />
82 Signed in
83 </p>
84 ) : (
85 <Button size="sm" disabled={!isLoaded} onClick={openAuthPrompt}>
86 Sign in
87 </Button>
88 )}
89 </SetupStep>
90 <SetupStep number={2}>
91 <RunOnComputer command="t3 connect" />
92 {isSignedIn ? (
93 <div className="mt-4 space-y-1.5">
94 <CloudEnvironmentConnectRows
95 primaryEnvironmentId={null}
96 savedEnvironments={[]}
97 refreshWhileEmpty
98 selection={{
99 ...(discoverySettled ? { autoSelectedComputers: autoAddedComputers } : {}),
100 selectedIds,
101 onChange: (environmentId, selected) =>
102 setSelectedIds((current) => {
103 const next = new Set(current);
104 if (selected) next.add(environmentId);
105 else next.delete(environmentId);
106 return next;
107 }),
108 }}
109 empty={<p className="text-meta text-muted-foreground">Waiting for your computer…</p>}
110 />
111 </div>
112 ) : null}
113 </SetupStep>
114 </>
115 );
117​
118function PairingSteps() {
119 return (
120 <>
121 <SetupStep number={1}>
122 <RunOnComputer command="t3 pair" />
123 </SetupStep>
124 <SetupStep number={2}>
125 <Button render={<Link to="/settings/connections" />} size="sm">
126 Add pairing link
127 </Button>
128 </SetupStep>
129 </>
130 );
132​
133function SetupStep({
134 number,
135 children,
136}: {
137 readonly number: number;
138 readonly children: ReactNode;
139}) {
140 return (
141 <li className="flex gap-3">
142 <span className="w-2 shrink-0 pt-1 text-muted-foreground">{number}</span>
143 <div className="min-w-0 flex-1">{children}</div>
144 </li>
145 );
147​
148function RunOnComputer({ command }: { readonly command: string }) {
149 return (
150 <>
151 <p>Run on your computer:</p>
152 <InstallT3Commands command={command} className="mt-2" />
153 <div className="mt-2 text-meta">
154 <InlineButton
155 tone="muted"
156 render={<a href={DESKTOP_DOWNLOAD_URL} target="_blank" rel="noreferrer noopener" />}
157 >
158 Or download the desktop app
159 </InlineButton>
160 </div>
161 </>
162 );