akseljoonas HF Staff commited on
Commit
6d7f53f
·
1 Parent(s): 23be8d5

Replace multi-screen onboarding with single checklist view

Browse files

Replaces the 4 separate onboarding screens with a unified checklist
showing all steps (sign in, join org, start session) with live status.
Adds org membership polling via new GET /auth/org-membership endpoint
that reuses existing check_org_membership(). Join link opens as popup
and auto-closes when membership is detected.

backend/routes/auth.py CHANGED
@@ -10,7 +10,7 @@ import time
10
  from urllib.parse import urlencode
11
 
12
  import httpx
13
- from dependencies import AUTH_ENABLED, get_current_user
14
  from fastapi import APIRouter, Depends, HTTPException, Request
15
  from fastapi.responses import RedirectResponse
16
 
@@ -169,3 +169,20 @@ async def get_me(user: dict = Depends(get_current_user)) -> dict:
169
  Uses the shared auth dependency which handles cookie + Bearer token.
170
  """
171
  return user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  from urllib.parse import urlencode
11
 
12
  import httpx
13
+ from dependencies import AUTH_ENABLED, check_org_membership, get_current_user
14
  from fastapi import APIRouter, Depends, HTTPException, Request
15
  from fastapi.responses import RedirectResponse
16
 
 
169
  Uses the shared auth dependency which handles cookie + Bearer token.
170
  """
171
  return user
172
+
173
+
174
+ ORG_NAME = "ml-agent-explorers"
175
+
176
+
177
+ @router.get("/org-membership")
178
+ async def org_membership(
179
+ request: Request, user: dict = Depends(get_current_user)
180
+ ) -> dict:
181
+ """Check if the authenticated user belongs to the ml-agent-explorers org."""
182
+ if not AUTH_ENABLED:
183
+ return {"is_member": True}
184
+ token = request.cookies.get("hf_access_token") or ""
185
+ if not token:
186
+ return {"is_member": False}
187
+ is_member = await check_org_membership(token, ORG_NAME)
188
+ return {"is_member": is_member}
frontend/src/components/WelcomeScreen/WelcomeScreen.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect, useRef } from 'react';
2
  import {
3
  Box,
4
  Typography,
@@ -6,53 +6,210 @@ import {
6
  CircularProgress,
7
  Alert,
8
  } from '@mui/material';
 
9
  import OpenInNewIcon from '@mui/icons-material/OpenInNew';
10
  import GroupAddIcon from '@mui/icons-material/GroupAdd';
 
 
11
  import { useSessionStore } from '@/store/sessionStore';
12
  import { useAgentStore } from '@/store/agentStore';
13
  import { apiFetch } from '@/utils/api';
14
  import { isInIframe, triggerLogin } from '@/hooks/useAuth';
 
15
 
16
- /** HF brand orange */
17
  const HF_ORANGE = '#FF9D00';
 
 
18
 
19
- const ORG_JOIN_URL = 'https://huggingface.co/organizations/ml-agent-explorers/share/GzPMJUivoFPlfkvFtIqEouZKSytatKQSZT';
20
- const ORG_JOINED_KEY = 'hf-agent-org-joined';
 
21
 
22
- function hasJoinedOrg(): boolean {
23
- try { return localStorage.getItem(ORG_JOINED_KEY) === '1'; } catch { return false; }
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
- function markOrgJoined(): void {
27
- try { localStorage.setItem(ORG_JOINED_KEY, '1'); } catch { /* ignore */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  export default function WelcomeScreen() {
31
  const { createSession } = useSessionStore();
32
  const { setPlan, clearPanel, user } = useAgentStore();
33
  const [isCreating, setIsCreating] = useState(false);
34
  const [error, setError] = useState<string | null>(null);
35
- const [orgJoined, setOrgJoined] = useState(hasJoinedOrg);
36
- const joinLinkOpened = useRef(false);
37
 
38
  const inIframe = isInIframe();
39
- const isAuthenticated = user?.authenticated;
40
  const isDevUser = user?.username === 'dev';
 
 
 
 
41
 
42
- // Auto-advance when user returns from the join link
43
- useEffect(() => {
44
- const handleVisibility = () => {
45
- if (document.visibilityState !== 'visible' || !joinLinkOpened.current) return;
46
- joinLinkOpened.current = false;
47
- markOrgJoined();
48
- setOrgJoined(true);
49
- };
50
 
51
- document.addEventListener('visibilitychange', handleVisibility);
52
- return () => document.removeEventListener('visibilitychange', handleVisibility);
53
- }, []);
 
 
 
 
 
 
54
 
55
- const tryCreateSession = useCallback(async () => {
 
56
  setIsCreating(true);
57
  setError(null);
58
 
@@ -80,72 +237,21 @@ export default function WelcomeScreen() {
80
  } finally {
81
  setIsCreating(false);
82
  }
83
- }, [createSession, setPlan, clearPanel]);
84
-
85
- const handleStart = useCallback(async () => {
86
- if (isCreating) return;
87
 
88
- if (!isAuthenticated && !isDevUser) {
89
- if (inIframe) return;
90
- triggerLogin();
91
- return;
92
- }
93
 
94
- await tryCreateSession();
95
- }, [isCreating, isAuthenticated, isDevUser, inIframe, tryCreateSession]);
 
96
 
97
- // Build the direct Space URL for the "open in new tab" link
98
- const spaceHost = typeof window !== 'undefined'
99
- ? window.location.hostname.includes('.hf.space')
100
- ? window.location.origin
101
- : `https://smolagents-ml-agent.hf.space`
102
- : '';
103
-
104
- // Shared button style
105
- const primaryBtnSx = {
106
- px: 5,
107
- py: 1.5,
108
- fontSize: '1rem',
109
- fontWeight: 700,
110
- textTransform: 'none' as const,
111
- borderRadius: '12px',
112
- bgcolor: HF_ORANGE,
113
- color: '#000',
114
- boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
115
- textDecoration: 'none',
116
- '&:hover': {
117
- bgcolor: '#FFB340',
118
- boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
119
- },
120
- };
121
-
122
- // Description block (reused across screens)
123
- const description = (
124
- <Typography
125
- variant="body1"
126
- sx={{
127
- color: 'var(--muted-text)',
128
- maxWidth: 520,
129
- mb: 5,
130
- lineHeight: 1.8,
131
- fontSize: '0.95rem',
132
- textAlign: 'center',
133
- px: 2,
134
- '& strong': { color: 'var(--text)', fontWeight: 600 },
135
- }}
136
- >
137
- A general-purpose AI agent for <strong>machine learning engineering</strong>.
138
- It browses <strong>Hugging Face documentation</strong>, manages{' '}
139
- <strong>repositories</strong>, launches <strong>training jobs</strong>,
140
- and explores <strong>datasets</strong> — all through natural conversation.
141
- </Typography>
142
- );
143
-
144
- // Which screen to show
145
- const needsJoin = inIframe && !orgJoined;
146
- const showOpenAgent = inIframe && orgJoined;
147
- const showSignin = !inIframe && !isAuthenticated && !isDevUser;
148
- const showReady = !inIframe && (isAuthenticated || isDevUser);
149
 
150
  return (
151
  <Box
@@ -160,12 +266,12 @@ export default function WelcomeScreen() {
160
  py: 8,
161
  }}
162
  >
163
- {/* HF Logo */}
164
  <Box
165
  component="img"
166
  src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
167
  alt="Hugging Face"
168
- sx={{ width: 96, height: 96, mb: 3, display: 'block' }}
169
  />
170
 
171
  {/* Title */}
@@ -174,120 +280,128 @@ export default function WelcomeScreen() {
174
  sx={{
175
  fontWeight: 800,
176
  color: 'var(--text)',
177
- mb: 1.5,
178
  letterSpacing: '-0.02em',
179
- fontSize: { xs: '2rem', md: '2.8rem' },
180
  }}
181
  >
182
  HF Agent
183
  </Typography>
184
 
185
- {/* ── Iframe: join org (first visit only) ──────────────────── */}
186
- {needsJoin && (
187
- <>
188
- <Typography
189
- variant="body1"
190
- sx={{
191
- color: 'var(--muted-text)',
192
- maxWidth: 480,
193
- mb: 4,
194
- lineHeight: 1.8,
195
- fontSize: '0.95rem',
196
- textAlign: 'center',
197
- px: 2,
198
- '& strong': { color: 'var(--text)', fontWeight: 600 },
199
- }}
200
- >
201
- Under the hood, this agent uses GPUs, inference APIs, and other paid Hub goodies — but we made them all free for you. Just join <strong>ML Agent Explorers</strong> to get started!
202
- </Typography>
203
-
204
- <Button
205
- variant="contained"
206
- size="large"
207
- component="a"
208
- href={ORG_JOIN_URL}
209
- target="_blank"
210
- rel="noopener noreferrer"
211
- onClick={() => { joinLinkOpened.current = true; }}
212
- startIcon={<GroupAddIcon />}
213
- sx={primaryBtnSx}
214
- >
215
- Join ML Agent Explorers
216
- </Button>
217
- </>
218
- )}
219
-
220
- {/* ── Iframe: already joined → open Space ──────────────────── */}
221
- {showOpenAgent && (
222
- <>
223
- {description}
224
- <Button
225
- variant="contained"
226
- size="large"
227
- component="a"
228
- href={spaceHost}
229
- target="_blank"
230
- rel="noopener noreferrer"
231
- endIcon={<OpenInNewIcon />}
232
- sx={primaryBtnSx}
233
- >
234
- Open HF Agent
235
- </Button>
236
- </>
237
- )}
238
-
239
- {/* ── Direct: not logged in → sign in ──────────────────────── */}
240
- {showSignin && (
241
- <>
242
- {description}
243
- <Button
244
- variant="contained"
245
- size="large"
246
- onClick={() => triggerLogin()}
247
- sx={primaryBtnSx}
248
- >
249
- Sign in with Hugging Face
250
- </Button>
251
 
252
- <Typography
253
- variant="caption"
254
- sx={{
255
- mt: 2.5,
256
- color: 'var(--muted-text)',
257
- fontSize: '0.78rem',
258
- textAlign: 'center',
259
- maxWidth: 360,
260
- lineHeight: 1.6,
261
- }}
262
- >
263
- Make sure to enable access to the <strong>ml-agent-explorers</strong> org when prompted.
264
- </Typography>
265
- </>
266
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
- {/* ── Direct: authenticated start session ────────────────── */}
269
- {showReady && (
270
- <>
271
- {description}
272
- <Button
273
- variant="contained"
274
- size="large"
275
- onClick={handleStart}
276
- disabled={isCreating}
277
- startIcon={
278
- isCreating ? <CircularProgress size={20} color="inherit" /> : null
279
- }
280
- sx={{
281
- ...primaryBtnSx,
282
- '&.Mui-disabled': {
283
- bgcolor: 'rgba(255, 157, 0, 0.35)',
284
- color: 'rgba(0,0,0,0.45)',
285
- },
286
- }}
287
- >
288
- {isCreating ? 'Initializing...' : 'Start Session'}
289
- </Button>
290
- </>
291
  )}
292
 
293
  {/* Error */}
@@ -311,7 +425,7 @@ export default function WelcomeScreen() {
311
  {/* Footnote */}
312
  <Typography
313
  variant="caption"
314
- sx={{ mt: 5, color: 'var(--muted-text)', opacity: 0.5, fontSize: '0.7rem' }}
315
  >
316
  Conversations are stored locally in your browser.
317
  </Typography>
 
1
+ import { useState, useCallback, type ReactNode } from 'react';
2
  import {
3
  Box,
4
  Typography,
 
6
  CircularProgress,
7
  Alert,
8
  } from '@mui/material';
9
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
10
  import OpenInNewIcon from '@mui/icons-material/OpenInNew';
11
  import GroupAddIcon from '@mui/icons-material/GroupAdd';
12
+ import LoginIcon from '@mui/icons-material/Login';
13
+ import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
14
  import { useSessionStore } from '@/store/sessionStore';
15
  import { useAgentStore } from '@/store/agentStore';
16
  import { apiFetch } from '@/utils/api';
17
  import { isInIframe, triggerLogin } from '@/hooks/useAuth';
18
+ import { useOrgMembership } from '@/hooks/useOrgMembership';
19
 
 
20
  const HF_ORANGE = '#FF9D00';
21
+ const ORG_JOIN_URL =
22
+ 'https://huggingface.co/organizations/ml-agent-explorers/share/GzPMJUivoFPlfkvFtIqEouZKSytatKQSZT';
23
 
24
+ // ---------------------------------------------------------------------------
25
+ // ChecklistStep sub-component
26
+ // ---------------------------------------------------------------------------
27
 
28
+ type StepStatus = 'completed' | 'active' | 'locked';
29
+
30
+ interface ChecklistStepProps {
31
+ stepNumber: number;
32
+ title: string;
33
+ description: string;
34
+ status: StepStatus;
35
+ lockedReason?: string;
36
+ actionLabel?: string;
37
+ onAction?: () => void;
38
+ actionIcon?: ReactNode;
39
+ actionHref?: string;
40
+ loading?: boolean;
41
+ isLast?: boolean;
42
  }
43
 
44
+ function StepIndicator({ status, stepNumber }: { status: StepStatus; stepNumber: number }) {
45
+ if (status === 'completed') {
46
+ return <CheckCircleIcon sx={{ fontSize: 28, color: 'var(--accent-green)' }} />;
47
+ }
48
+ return (
49
+ <Box
50
+ sx={{
51
+ width: 28,
52
+ height: 28,
53
+ borderRadius: '50%',
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ fontSize: '0.8rem',
58
+ fontWeight: 700,
59
+ ...(status === 'active'
60
+ ? { bgcolor: HF_ORANGE, color: '#000' }
61
+ : { bgcolor: 'transparent', border: '2px solid var(--border)', color: 'var(--muted-text)' }),
62
+ }}
63
+ >
64
+ {stepNumber}
65
+ </Box>
66
+ );
67
  }
68
 
69
+ function ChecklistStep({
70
+ stepNumber,
71
+ title,
72
+ description,
73
+ status,
74
+ lockedReason,
75
+ actionLabel,
76
+ onAction,
77
+ actionIcon,
78
+ actionHref,
79
+ loading = false,
80
+ isLast = false,
81
+ }: ChecklistStepProps) {
82
+ const btnSx = {
83
+ px: 3,
84
+ py: 0.75,
85
+ fontSize: '0.85rem',
86
+ fontWeight: 700,
87
+ textTransform: 'none' as const,
88
+ borderRadius: '10px',
89
+ whiteSpace: 'nowrap' as const,
90
+ textDecoration: 'none',
91
+ ...(status === 'active'
92
+ ? {
93
+ bgcolor: HF_ORANGE,
94
+ color: '#000',
95
+ boxShadow: '0 2px 12px rgba(255, 157, 0, 0.25)',
96
+ '&:hover': { bgcolor: '#FFB340', boxShadow: '0 4px 20px rgba(255, 157, 0, 0.4)' },
97
+ }
98
+ : {
99
+ bgcolor: 'rgba(255,255,255,0.04)',
100
+ color: 'var(--muted-text)',
101
+ '&.Mui-disabled': { bgcolor: 'rgba(255,255,255,0.04)', color: 'var(--muted-text)' },
102
+ }),
103
+ };
104
+
105
+ return (
106
+ <Box
107
+ sx={{
108
+ display: 'flex',
109
+ alignItems: 'center',
110
+ gap: 2,
111
+ px: 3,
112
+ py: 2.5,
113
+ borderLeft: '3px solid',
114
+ borderLeftColor:
115
+ status === 'completed'
116
+ ? 'var(--accent-green)'
117
+ : status === 'active'
118
+ ? HF_ORANGE
119
+ : 'transparent',
120
+ ...(!isLast && { borderBottom: '1px solid var(--border)' }),
121
+ opacity: status === 'locked' ? 0.55 : 1,
122
+ transition: 'opacity 0.2s, border-color 0.2s',
123
+ }}
124
+ >
125
+ <StepIndicator status={status} stepNumber={stepNumber} />
126
+
127
+ <Box sx={{ flex: 1, minWidth: 0 }}>
128
+ <Typography
129
+ variant="subtitle2"
130
+ sx={{
131
+ fontWeight: 600,
132
+ fontSize: '0.92rem',
133
+ color: status === 'completed' ? 'var(--muted-text)' : 'var(--text)',
134
+ ...(status === 'completed' && { textDecoration: 'line-through', textDecorationColor: 'var(--muted-text)' }),
135
+ }}
136
+ >
137
+ {title}
138
+ </Typography>
139
+ <Typography variant="body2" sx={{ color: 'var(--muted-text)', fontSize: '0.8rem', mt: 0.25, lineHeight: 1.5 }}>
140
+ {status === 'locked' && lockedReason ? lockedReason : description}
141
+ </Typography>
142
+ </Box>
143
+
144
+ {status === 'completed' ? (
145
+ <Typography variant="caption" sx={{ color: 'var(--accent-green)', fontWeight: 600, fontSize: '0.78rem', whiteSpace: 'nowrap' }}>
146
+ Done
147
+ </Typography>
148
+ ) : actionLabel ? (
149
+ actionHref ? (
150
+ <Button
151
+ variant="contained"
152
+ size="small"
153
+ component="a"
154
+ href={actionHref}
155
+ target="_blank"
156
+ rel="noopener noreferrer"
157
+ disabled={status === 'locked'}
158
+ startIcon={actionIcon}
159
+ sx={btnSx}
160
+ onClick={onAction}
161
+ >
162
+ {actionLabel}
163
+ </Button>
164
+ ) : (
165
+ <Button
166
+ variant="contained"
167
+ size="small"
168
+ disabled={status === 'locked' || loading}
169
+ startIcon={loading ? <CircularProgress size={16} color="inherit" /> : actionIcon}
170
+ onClick={onAction}
171
+ sx={btnSx}
172
+ >
173
+ {loading ? 'Loading...' : actionLabel}
174
+ </Button>
175
+ )
176
+ ) : null}
177
+ </Box>
178
+ );
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // WelcomeScreen
183
+ // ---------------------------------------------------------------------------
184
+
185
  export default function WelcomeScreen() {
186
  const { createSession } = useSessionStore();
187
  const { setPlan, clearPanel, user } = useAgentStore();
188
  const [isCreating, setIsCreating] = useState(false);
189
  const [error, setError] = useState<string | null>(null);
 
 
190
 
191
  const inIframe = isInIframe();
192
+ const isAuthenticated = !!user?.authenticated;
193
  const isDevUser = user?.username === 'dev';
194
+ const isOrgMember = !!user?.orgMember;
195
+
196
+ // Poll for org membership once authenticated (skipped in dev mode)
197
+ const popupRef = useOrgMembership(isAuthenticated && !isDevUser && !isOrgMember);
198
 
199
+ // ---- Actions ----
 
 
 
 
 
 
 
200
 
201
+ const handleJoinOrg = useCallback(() => {
202
+ const popup = window.open(ORG_JOIN_URL, 'hf-org-join', 'noopener');
203
+ if (popup) {
204
+ popupRef.current = popup;
205
+ } else {
206
+ // Popup blocked — fall back to regular navigation
207
+ window.open(ORG_JOIN_URL, '_blank', 'noopener,noreferrer');
208
+ }
209
+ }, [popupRef]);
210
 
211
+ const handleStartSession = useCallback(async () => {
212
+ if (isCreating) return;
213
  setIsCreating(true);
214
  setError(null);
215
 
 
237
  } finally {
238
  setIsCreating(false);
239
  }
240
+ }, [isCreating, createSession, setPlan, clearPanel]);
 
 
 
241
 
242
+ // ---- Step status helpers ----
 
 
 
 
243
 
244
+ const signInStatus: StepStatus = isAuthenticated ? 'completed' : 'active';
245
+ const joinOrgStatus: StepStatus = isOrgMember ? 'completed' : isAuthenticated ? 'active' : 'locked';
246
+ const startStatus: StepStatus = isAuthenticated && isOrgMember ? 'active' : 'locked';
247
 
248
+ // Space URL for iframe "Open HF Agent" step
249
+ const spaceHost =
250
+ typeof window !== 'undefined'
251
+ ? window.location.hostname.includes('.hf.space')
252
+ ? window.location.origin
253
+ : 'https://smolagents-ml-agent.hf.space'
254
+ : '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  return (
257
  <Box
 
266
  py: 8,
267
  }}
268
  >
269
+ {/* Logo */}
270
  <Box
271
  component="img"
272
  src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
273
  alt="Hugging Face"
274
+ sx={{ width: 80, height: 80, mb: 2.5, display: 'block' }}
275
  />
276
 
277
  {/* Title */}
 
280
  sx={{
281
  fontWeight: 800,
282
  color: 'var(--text)',
283
+ mb: 1,
284
  letterSpacing: '-0.02em',
285
+ fontSize: { xs: '1.8rem', md: '2.4rem' },
286
  }}
287
  >
288
  HF Agent
289
  </Typography>
290
 
291
+ {/* Description */}
292
+ <Typography
293
+ variant="body1"
294
+ sx={{
295
+ color: 'var(--muted-text)',
296
+ maxWidth: 480,
297
+ mb: 4,
298
+ lineHeight: 1.7,
299
+ fontSize: '0.9rem',
300
+ textAlign: 'center',
301
+ px: 2,
302
+ '& strong': { color: 'var(--text)', fontWeight: 600 },
303
+ }}
304
+ >
305
+ A general-purpose AI agent for <strong>machine learning engineering</strong>.
306
+ It browses <strong>Hugging Face docs</strong>, manages <strong>repos</strong>,
307
+ launches <strong>training jobs</strong>, and explores <strong>datasets</strong>.
308
+ </Typography>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ {/* ── Checklist ──────────────────────────────────────────── */}
311
+ <Box
312
+ sx={{
313
+ width: '100%',
314
+ maxWidth: 520,
315
+ bgcolor: 'var(--surface)',
316
+ border: '1px solid var(--border)',
317
+ borderRadius: '12px',
318
+ overflow: 'hidden',
319
+ mx: 2,
320
+ }}
321
+ >
322
+ {isDevUser ? (
323
+ /* Dev mode: single step */
324
+ <ChecklistStep
325
+ stepNumber={1}
326
+ title="Start Session"
327
+ description="Launch an AI agent session for ML engineering."
328
+ status="active"
329
+ actionLabel="Start Session"
330
+ actionIcon={<RocketLaunchIcon sx={{ fontSize: 16 }} />}
331
+ onAction={handleStartSession}
332
+ loading={isCreating}
333
+ isLast
334
+ />
335
+ ) : inIframe ? (
336
+ /* Iframe: 2 steps */
337
+ <>
338
+ <ChecklistStep
339
+ stepNumber={1}
340
+ title="Join ML Agent Explorers"
341
+ description="Get free access to GPUs, inference APIs, and Hub resources."
342
+ status={isOrgMember ? 'completed' : 'active'}
343
+ actionLabel="Join Organization"
344
+ actionIcon={<GroupAddIcon sx={{ fontSize: 16 }} />}
345
+ onAction={handleJoinOrg}
346
+ />
347
+ <ChecklistStep
348
+ stepNumber={2}
349
+ title="Open HF Agent"
350
+ description="Open the agent in a full browser tab to get started."
351
+ status={isOrgMember ? 'active' : 'locked'}
352
+ lockedReason="Join the organization first."
353
+ actionLabel="Open HF Agent"
354
+ actionIcon={<OpenInNewIcon sx={{ fontSize: 16 }} />}
355
+ actionHref={spaceHost}
356
+ isLast
357
+ />
358
+ </>
359
+ ) : (
360
+ /* Direct access: 3 steps */
361
+ <>
362
+ <ChecklistStep
363
+ stepNumber={1}
364
+ title="Sign in with Hugging Face"
365
+ description="Authenticate to access GPU resources and model APIs."
366
+ status={signInStatus}
367
+ actionLabel="Sign in"
368
+ actionIcon={<LoginIcon sx={{ fontSize: 16 }} />}
369
+ onAction={() => triggerLogin()}
370
+ />
371
+ <ChecklistStep
372
+ stepNumber={2}
373
+ title="Join ML Agent Explorers"
374
+ description="Get free access to GPUs, inference APIs, and Hub resources."
375
+ status={joinOrgStatus}
376
+ lockedReason="Sign in first to continue."
377
+ actionLabel="Join Organization"
378
+ actionIcon={<GroupAddIcon sx={{ fontSize: 16 }} />}
379
+ onAction={handleJoinOrg}
380
+ />
381
+ <ChecklistStep
382
+ stepNumber={3}
383
+ title="Start Session"
384
+ description="Launch an AI agent session for ML engineering."
385
+ status={startStatus}
386
+ lockedReason="Complete the steps above to continue."
387
+ actionLabel="Start Session"
388
+ actionIcon={<RocketLaunchIcon sx={{ fontSize: 16 }} />}
389
+ onAction={handleStartSession}
390
+ loading={isCreating}
391
+ isLast
392
+ />
393
+ </>
394
+ )}
395
+ </Box>
396
 
397
+ {/* Polling hint when waiting for org join */}
398
+ {isAuthenticated && !isOrgMember && !isDevUser && !inIframe && (
399
+ <Typography
400
+ variant="caption"
401
+ sx={{ mt: 2, color: 'var(--muted-text)', fontSize: '0.75rem', textAlign: 'center' }}
402
+ >
403
+ This page updates automatically when you join the organization.
404
+ </Typography>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  )}
406
 
407
  {/* Error */}
 
425
  {/* Footnote */}
426
  <Typography
427
  variant="caption"
428
+ sx={{ mt: 4, color: 'var(--muted-text)', opacity: 0.5, fontSize: '0.7rem' }}
429
  >
430
  Conversations are stored locally in your browser.
431
  </Typography>
frontend/src/hooks/useOrgMembership.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Polls backend for org membership status.
3
+ * When membership is detected, updates the user in the agent store
4
+ * and closes any org-join popup that was opened.
5
+ */
6
+ import { useEffect, useRef } from 'react';
7
+ import { useAgentStore } from '@/store/agentStore';
8
+
9
+ const POLL_INTERVAL_MS = 3000;
10
+
11
+ /**
12
+ * @param enabled Only poll when true (user is authenticated but not yet confirmed as org member)
13
+ * @returns popupRef — assign `window.open()` result to `.current` so the hook can auto-close it
14
+ */
15
+ export function useOrgMembership(enabled: boolean) {
16
+ const user = useAgentStore((s) => s.user);
17
+ const setUser = useAgentStore((s) => s.setUser);
18
+ const popupRef = useRef<Window | null>(null);
19
+
20
+ useEffect(() => {
21
+ if (!enabled || user?.orgMember) return;
22
+
23
+ let cancelled = false;
24
+
25
+ const check = async () => {
26
+ try {
27
+ const res = await fetch('/auth/org-membership', { credentials: 'include' });
28
+ if (!res.ok || cancelled) return;
29
+ const data = await res.json();
30
+ if (cancelled) return;
31
+ if (data.is_member && user) {
32
+ setUser({ ...user, orgMember: true });
33
+ try { popupRef.current?.close(); } catch { /* cross-origin or already closed */ }
34
+ popupRef.current = null;
35
+ }
36
+ } catch { /* backend unreachable — skip */ }
37
+ };
38
+
39
+ check();
40
+ const id = setInterval(check, POLL_INTERVAL_MS);
41
+ return () => { cancelled = true; clearInterval(id); };
42
+ }, [enabled, user?.orgMember, user, setUser]);
43
+
44
+ return popupRef;
45
+ }
frontend/src/types/agent.ts CHANGED
@@ -29,4 +29,5 @@ export interface User {
29
  username?: string;
30
  name?: string;
31
  picture?: string;
 
32
  }
 
29
  username?: string;
30
  name?: string;
31
  picture?: string;
32
+ orgMember?: boolean;
33
  }