akseljoonas HF Staff commited on
Commit
5af3ab5
Β·
1 Parent(s): ca13fee

sync: org onboarding, model updates, sandbox cleanup, prompt fixes

Browse files
agent/core/session.py CHANGED
@@ -19,12 +19,14 @@ logger = logging.getLogger(__name__)
19
  # on network calls for certain providers (known litellm issue).
20
  _MAX_TOKENS_MAP: dict[str, int] = {
21
  # Anthropic
 
22
  "anthropic/claude-opus-4-5-20251101": 200_000,
23
  "anthropic/claude-sonnet-4-5-20250929": 200_000,
24
  "anthropic/claude-sonnet-4-20250514": 200_000,
25
  "anthropic/claude-haiku-3-5-20241022": 200_000,
26
  "anthropic/claude-3-5-sonnet-20241022": 200_000,
27
  "anthropic/claude-3-opus-20240229": 200_000,
 
28
  "huggingface/novita/minimax/minimax-m2.1": 196_608,
29
  "huggingface/novita/moonshotai/kimi-k2.5": 262_144,
30
  "huggingface/novita/zai-org/glm-5": 200_000,
 
19
  # on network calls for certain providers (known litellm issue).
20
  _MAX_TOKENS_MAP: dict[str, int] = {
21
  # Anthropic
22
+ "anthropic/claude-opus-4-6": 200_000,
23
  "anthropic/claude-opus-4-5-20251101": 200_000,
24
  "anthropic/claude-sonnet-4-5-20250929": 200_000,
25
  "anthropic/claude-sonnet-4-20250514": 200_000,
26
  "anthropic/claude-haiku-3-5-20241022": 200_000,
27
  "anthropic/claude-3-5-sonnet-20241022": 200_000,
28
  "anthropic/claude-3-opus-20240229": 200_000,
29
+ "huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5": 200_000,
30
  "huggingface/novita/minimax/minimax-m2.1": 196_608,
31
  "huggingface/novita/moonshotai/kimi-k2.5": 262_144,
32
  "huggingface/novita/zai-org/glm-5": 200_000,
agent/core/tools.py CHANGED
@@ -62,13 +62,7 @@ warnings.filterwarnings(
62
  "ignore", category=DeprecationWarning, module="aiohttp.connector"
63
  )
64
 
65
- NOT_ALLOWED_TOOL_NAMES = [
66
- "hf_jobs",
67
- "hf_doc_search",
68
- "hf_doc_fetch",
69
- "hf_whoami",
70
- "paper_search",
71
- ]
72
 
73
 
74
  def convert_mcp_content_to_string(content: list) -> str:
 
62
  "ignore", category=DeprecationWarning, module="aiohttp.connector"
63
  )
64
 
65
+ NOT_ALLOWED_TOOL_NAMES = ["hf_jobs", "hf_doc_search", "hf_doc_fetch", "hf_whoami"]
 
 
 
 
 
 
66
 
67
 
68
  def convert_mcp_content_to_string(content: list) -> str:
agent/prompts/system_prompt_v3.yaml CHANGED
@@ -16,8 +16,8 @@ system_prompt: |
16
 
17
  Skip research only for trivial non-code operations.
18
 
19
- When the user asks what is best, optimal, or most effective β€” start with research papers.
20
- Inspect specific resources before using them. Validate dataset format before any training job.
21
 
22
  # Mistakes you WILL make without research
23
 
@@ -52,7 +52,6 @@ system_prompt: |
52
  SFT: "messages", "text", or "prompt"/"completion"
53
  DPO: "prompt", "chosen", "rejected"
54
  GRPO: "prompt"
55
- All datasets for training should be in conversational ChatML format for HF library compatibility.
56
 
57
  # When submitting a training job
58
 
 
16
 
17
  Skip research only for trivial non-code operations.
18
 
19
+ For open-ended research tasks (improving model performance, finding the best approach for a task, exploring a field, implementing a paper's method):
20
+ hf_papers(trending/search) β†’ hf_papers(read_paper) β†’ hf_papers(find_all_resources) β†’ hf_inspect_dataset
21
 
22
  # Mistakes you WILL make without research
23
 
 
52
  SFT: "messages", "text", or "prompt"/"completion"
53
  DPO: "prompt", "chosen", "rejected"
54
  GRPO: "prompt"
 
55
 
56
  # When submitting a training job
57
 
agent/tools/dataset_tools.py CHANGED
@@ -10,7 +10,6 @@ from typing import Any, TypedDict
10
 
11
  import httpx
12
 
13
- from agent.core.session import Session
14
  from agent.tools.types import ToolResult
15
 
16
  BASE_URL = "https://datasets-server.huggingface.co"
@@ -424,17 +423,16 @@ HF_INSPECT_DATASET_TOOL_SPEC = {
424
  }
425
 
426
 
427
- async def hf_inspect_dataset_handler(
428
- arguments: dict[str, Any], session: Session = None
429
- ) -> tuple[str, bool]:
430
  """Handler for agent tool router"""
431
  try:
 
432
  result = await inspect_dataset(
433
  dataset=arguments["dataset"],
434
  config=arguments.get("config"),
435
  split=arguments.get("split"),
436
  sample_rows=min(arguments.get("sample_rows", 3), 10),
437
- hf_token=session.hf_token,
438
  )
439
  return result["formatted"], not result.get("isError", False)
440
  except Exception as e:
 
10
 
11
  import httpx
12
 
 
13
  from agent.tools.types import ToolResult
14
 
15
  BASE_URL = "https://datasets-server.huggingface.co"
 
423
  }
424
 
425
 
426
+ async def hf_inspect_dataset_handler(arguments: dict[str, Any], session=None) -> tuple[str, bool]:
 
 
427
  """Handler for agent tool router"""
428
  try:
429
+ hf_token = session.hf_token if session else None
430
  result = await inspect_dataset(
431
  dataset=arguments["dataset"],
432
  config=arguments.get("config"),
433
  split=arguments.get("split"),
434
  sample_rows=min(arguments.get("sample_rows", 3), 10),
435
+ hf_token=hf_token,
436
  )
437
  return result["formatted"], not result.get("isError", False)
438
  except Exception as e:
backend/dependencies.py CHANGED
@@ -22,6 +22,9 @@ AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", ""))
22
  _token_cache: dict[str, tuple[dict[str, Any], float]] = {}
23
  TOKEN_CACHE_TTL = 300 # 5 minutes
24
 
 
 
 
25
  DEV_USER: dict[str, Any] = {
26
  "user_id": "dev",
27
  "username": "dev",
@@ -80,6 +83,31 @@ async def _extract_user_from_token(token: str) -> dict[str, Any] | None:
80
  return None
81
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  async def get_current_user(request: Request) -> dict[str, Any]:
84
  """FastAPI dependency: extract and validate the current user.
85
 
 
22
  _token_cache: dict[str, tuple[dict[str, Any], float]] = {}
23
  TOKEN_CACHE_TTL = 300 # 5 minutes
24
 
25
+ # Org membership cache: key -> expiry_time (only caches positive results)
26
+ _org_member_cache: dict[str, float] = {}
27
+
28
  DEV_USER: dict[str, Any] = {
29
  "user_id": "dev",
30
  "username": "dev",
 
83
  return None
84
 
85
 
86
+ async def check_org_membership(token: str, org_name: str) -> bool:
87
+ """Check if the token owner belongs to an HF org. Only caches positive results."""
88
+ now = time.time()
89
+ key = token + org_name
90
+ cached = _org_member_cache.get(key)
91
+ if cached and cached > now:
92
+ return True
93
+
94
+ async with httpx.AsyncClient(timeout=10.0) as client:
95
+ try:
96
+ resp = await client.get(
97
+ f"{OPENID_PROVIDER_URL}/api/whoami-v2",
98
+ headers={"Authorization": f"Bearer {token}"},
99
+ )
100
+ if resp.status_code != 200:
101
+ return False
102
+ orgs = {o.get("name") for o in resp.json().get("orgs", [])}
103
+ if org_name in orgs:
104
+ _org_member_cache[key] = now + TOKEN_CACHE_TTL
105
+ return True
106
+ return False
107
+ except httpx.HTTPError:
108
+ return False
109
+
110
+
111
  async def get_current_user(request: Request) -> dict[str, Any]:
112
  """FastAPI dependency: extract and validate the current user.
113
 
backend/routes/agent.py CHANGED
@@ -37,15 +37,15 @@ router = APIRouter(prefix="/api", tags=["agent"])
37
 
38
  AVAILABLE_MODELS = [
39
  {
40
- "id": "huggingface/novita/minimax/minimax-m2.1",
41
- "label": "MiniMax M2.1",
42
- "provider": "huggingface",
43
  "recommended": True,
44
  },
45
  {
46
- "id": "anthropic/claude-opus-4-5-20251101",
47
- "label": "Claude Opus 4.5",
48
- "provider": "anthropic",
49
  "recommended": True,
50
  },
51
  {
 
37
 
38
  AVAILABLE_MODELS = [
39
  {
40
+ "id": "anthropic/claude-opus-4-6",
41
+ "label": "Claude Opus 4.6",
42
+ "provider": "anthropic",
43
  "recommended": True,
44
  },
45
  {
46
+ "id": "huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5",
47
+ "label": "MiniMax M2.5",
48
+ "provider": "huggingface",
49
  "recommended": True,
50
  },
51
  {
backend/routes/auth.py CHANGED
@@ -142,7 +142,7 @@ async def oauth_callback(
142
  httponly=True,
143
  secure=is_production, # Secure flag only in production (HTTPS)
144
  samesite="lax",
145
- max_age=3600 * 24, # 24 hours
146
  path="/",
147
  )
148
  return response
 
142
  httponly=True,
143
  secure=is_production, # Secure flag only in production (HTTPS)
144
  samesite="lax",
145
+ max_age=3600 * 24 * 7, # 7 days
146
  path="/",
147
  )
148
  return response
backend/session_manager.py CHANGED
@@ -164,6 +164,17 @@ class SessionManager:
164
  logger.info(f"Created session {session_id} for user {user_id}")
165
  return session_id
166
 
 
 
 
 
 
 
 
 
 
 
 
167
  async def _run_session(
168
  self,
169
  session_id: str,
@@ -218,6 +229,8 @@ class SessionManager:
218
  except asyncio.CancelledError:
219
  pass
220
 
 
 
221
  async with self._lock:
222
  if session_id in self.sessions:
223
  self.sessions[session_id].is_active = False
@@ -309,6 +322,9 @@ class SessionManager:
309
 
310
  ws_manager.clear_buffer(session_id)
311
 
 
 
 
312
  # Cancel the task if running
313
  if agent_session.task and not agent_session.task.done():
314
  agent_session.task.cancel()
 
164
  logger.info(f"Created session {session_id} for user {user_id}")
165
  return session_id
166
 
167
+ @staticmethod
168
+ async def _cleanup_sandbox(session: Session) -> None:
169
+ """Delete the sandbox Space if one was created for this session."""
170
+ sandbox = getattr(session, "sandbox", None)
171
+ if sandbox and getattr(sandbox, "_owns_space", False):
172
+ try:
173
+ logger.info(f"Deleting sandbox {sandbox.space_id}...")
174
+ await asyncio.to_thread(sandbox.delete)
175
+ except Exception as e:
176
+ logger.warning(f"Failed to delete sandbox {sandbox.space_id}: {e}")
177
+
178
  async def _run_session(
179
  self,
180
  session_id: str,
 
229
  except asyncio.CancelledError:
230
  pass
231
 
232
+ await self._cleanup_sandbox(session)
233
+
234
  async with self._lock:
235
  if session_id in self.sessions:
236
  self.sessions[session_id].is_active = False
 
322
 
323
  ws_manager.clear_buffer(session_id)
324
 
325
+ # Clean up sandbox Space before cancelling the task
326
+ await self._cleanup_sandbox(agent_session.session)
327
+
328
  # Cancel the task if running
329
  if agent_session.task and not agent_session.task.done():
330
  agent_session.task.cancel()
configs/main_agent_config.json CHANGED
@@ -1,12 +1,11 @@
1
  {
2
- "model_name": "huggingface/novita/moonshotai/kimi-k2.5",
3
  "save_sessions": true,
4
  "session_dataset_repo": "akseljoonas/hf-agent-sessions",
5
  "yolo_mode": false,
6
  "confirm_cpu_jobs": true,
7
  "auto_file_upload": true,
8
- "mcpServers": {},
9
- "_mcpServers_disabled": {
10
  "hf-mcp-server": {
11
  "transport": "http",
12
  "url": "https://huggingface.co/mcp?login"
 
1
  {
2
+ "model_name": "anthropic/claude-opus-4-6",
3
  "save_sessions": true,
4
  "session_dataset_repo": "akseljoonas/hf-agent-sessions",
5
  "yolo_mode": false,
6
  "confirm_cpu_jobs": true,
7
  "auto_file_upload": true,
8
+ "mcpServers": {
 
9
  "hf-mcp-server": {
10
  "transport": "http",
11
  "url": "https://huggingface.co/mcp?login"
frontend/src/components/Chat/ActivityStatusBar.tsx CHANGED
@@ -9,6 +9,7 @@ const shimmer = keyframes`
9
  `;
10
 
11
  const TOOL_LABELS: Record<string, string> = {
 
12
  hf_jobs: 'Running job',
13
  hf_repo_files: 'Uploading file',
14
  hf_repo_git: 'Git operation',
 
9
  `;
10
 
11
  const TOOL_LABELS: Record<string, string> = {
12
+ sandbox_create: 'Creating sandbox, this might take 1-2 minutes',
13
  hf_jobs: 'Running job',
14
  hf_repo_files: 'Uploading file',
15
  hf_repo_git: 'Git operation',
frontend/src/components/Chat/ChatInput.tsx CHANGED
@@ -21,22 +21,22 @@ const getHfAvatarUrl = (modelId: string) => {
21
  };
22
 
23
  const MODEL_OPTIONS: ModelOption[] = [
24
- {
25
- id: 'minimax-m2.1',
26
- name: 'MiniMax M2.1',
27
- description: 'Via Novita',
28
- modelPath: 'huggingface/novita/minimax/minimax-m2.1',
29
- avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.1'),
30
- recommended: true,
31
- },
32
  {
33
  id: 'claude-opus',
34
- name: 'Claude Opus 4.5',
35
  description: 'Anthropic',
36
- modelPath: 'anthropic/claude-opus-4-5-20251101',
37
  avatarUrl: 'https://huggingface.co/api/avatars/Anthropic',
38
  recommended: true,
39
  },
 
 
 
 
 
 
 
 
40
  {
41
  id: 'kimi-k2.5',
42
  name: 'Kimi K2.5',
 
21
  };
22
 
23
  const MODEL_OPTIONS: ModelOption[] = [
 
 
 
 
 
 
 
 
24
  {
25
  id: 'claude-opus',
26
+ name: 'Claude Opus 4.6',
27
  description: 'Anthropic',
28
+ modelPath: 'anthropic/claude-opus-4-6',
29
  avatarUrl: 'https://huggingface.co/api/avatars/Anthropic',
30
  recommended: true,
31
  },
32
+ {
33
+ id: 'minimax-m2.5',
34
+ name: 'MiniMax M2.5',
35
+ description: 'Via Fireworks',
36
+ modelPath: 'huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5',
37
+ avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.5'),
38
+ recommended: true,
39
+ },
40
  {
41
  id: 'kimi-k2.5',
42
  name: 'Kimi K2.5',
frontend/src/components/SessionChat.tsx CHANGED
@@ -94,9 +94,18 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
94
  store.setActivityStatus({ type: 'tool', toolName: 'running' });
95
  store.setProcessing(true);
96
  } else {
97
- // No pending approval β€” reset to idle
98
- store.setActivityStatus({ type: 'idle' });
99
- store.setProcessing(false);
 
 
 
 
 
 
 
 
 
100
  }
101
  }
102
  prevActiveRef.current = isActive;
 
94
  store.setActivityStatus({ type: 'tool', toolName: 'running' });
95
  store.setProcessing(true);
96
  } else {
97
+ // Check if any tools are still running (non-approval tools like bash, read, etc.)
98
+ const runningTool = lastAssistant?.parts.find(
99
+ (p) => p.type === 'dynamic-tool' && (p.state === 'input-available' || p.state === 'input-streaming')
100
+ );
101
+ if (runningTool && runningTool.type === 'dynamic-tool') {
102
+ const desc = (runningTool.input as Record<string, unknown>)?.description as string | undefined;
103
+ store.setActivityStatus({ type: 'tool', toolName: runningTool.toolName, description: desc });
104
+ store.setProcessing(true);
105
+ } else {
106
+ store.setActivityStatus({ type: 'idle' });
107
+ store.setProcessing(false);
108
+ }
109
  }
110
  }
111
  prevActiveRef.current = isActive;
frontend/src/components/WelcomeScreen/WelcomeScreen.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useCallback } from 'react';
2
  import {
3
  Box,
4
  Typography,
@@ -7,6 +7,7 @@ import {
7
  Alert,
8
  } from '@mui/material';
9
  import OpenInNewIcon from '@mui/icons-material/OpenInNew';
 
10
  import { useSessionStore } from '@/store/sessionStore';
11
  import { useAgentStore } from '@/store/agentStore';
12
  import { apiFetch } from '@/utils/api';
@@ -15,29 +16,43 @@ import { isInIframe, triggerLogin } from '@/hooks/useAuth';
15
  /** HF brand orange */
16
  const HF_ORANGE = '#FF9D00';
17
 
 
 
 
 
 
 
 
 
 
 
 
18
  export default function WelcomeScreen() {
19
  const { createSession } = useSessionStore();
20
  const { setPlan, clearPanel, user } = useAgentStore();
21
  const [isCreating, setIsCreating] = useState(false);
22
  const [error, setError] = useState<string | null>(null);
 
 
23
 
24
  const inIframe = isInIframe();
25
  const isAuthenticated = user?.authenticated;
26
  const isDevUser = user?.username === 'dev';
27
 
28
- const handleStart = useCallback(async () => {
29
- if (isCreating) return;
 
 
 
 
 
 
30
 
31
- // Not authenticated and not dev β†’ need to login
32
- if (!isAuthenticated && !isDevUser) {
33
- // In iframe: can't redirect (cookies blocked) β€” user needs to open in new tab
34
- // This shouldn't happen because we show a different button in iframe
35
- // But just in case:
36
- if (inIframe) return;
37
- triggerLogin();
38
- return;
39
- }
40
 
 
41
  setIsCreating(true);
42
  setError(null);
43
 
@@ -65,7 +80,19 @@ export default function WelcomeScreen() {
65
  } finally {
66
  setIsCreating(false);
67
  }
68
- }, [isCreating, createSession, setPlan, clearPanel, isAuthenticated, isDevUser, inIframe]);
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  // Build the direct Space URL for the "open in new tab" link
71
  const spaceHost = typeof window !== 'undefined'
@@ -74,6 +101,52 @@ export default function WelcomeScreen() {
74
  : `https://smolagents-ml-agent.hf.space`
75
  : '';
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return (
78
  <Box
79
  sx={{
@@ -109,112 +182,112 @@ export default function WelcomeScreen() {
109
  HF Agent
110
  </Typography>
111
 
112
- {/* Description */}
113
- <Typography
114
- variant="body1"
115
- sx={{
116
- color: 'var(--muted-text)',
117
- maxWidth: 520,
118
- mb: 5,
119
- lineHeight: 1.8,
120
- fontSize: '0.95rem',
121
- textAlign: 'center',
122
- px: 2,
123
- '& strong': { color: 'var(--text)', fontWeight: 600 },
124
- }}
125
- >
126
- A general-purpose AI agent for <strong>machine learning engineering</strong>.
127
- It browses <strong>Hugging Face documentation</strong>, manages{' '}
128
- <strong>repositories</strong>, launches <strong>training jobs</strong>,
129
- and explores <strong>datasets</strong> β€” all through natural conversation.
130
- </Typography>
131
 
132
- {/* Action button β€” depends on context */}
133
- {inIframe && !isAuthenticated && !isDevUser ? (
134
- // In iframe + not logged in β†’ link to open Space directly
135
- <Button
136
- variant="contained"
137
- size="large"
138
- component="a"
139
- href={spaceHost}
140
- target="_blank"
141
- rel="noopener noreferrer"
142
- endIcon={<OpenInNewIcon />}
143
- sx={{
144
- px: 5,
145
- py: 1.5,
146
- fontSize: '1rem',
147
- fontWeight: 700,
148
- textTransform: 'none',
149
- borderRadius: '12px',
150
- bgcolor: HF_ORANGE,
151
- color: '#000',
152
- boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
153
- textDecoration: 'none',
154
- '&:hover': {
155
- bgcolor: '#FFB340',
156
- boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
157
- },
158
- }}
159
- >
160
- Open HF Agent
161
- </Button>
162
- ) : !isAuthenticated && !isDevUser ? (
163
- // Direct access + not logged in β†’ sign in button
164
- <Button
165
- variant="contained"
166
- size="large"
167
- onClick={() => triggerLogin()}
168
- sx={{
169
- px: 5,
170
- py: 1.5,
171
- fontSize: '1rem',
172
- fontWeight: 700,
173
- textTransform: 'none',
174
- borderRadius: '12px',
175
- bgcolor: HF_ORANGE,
176
- color: '#000',
177
- boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
178
- '&:hover': {
179
- bgcolor: '#FFB340',
180
- boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
181
- },
182
- }}
183
- >
184
- Sign in with Hugging Face
185
- </Button>
186
- ) : (
187
- // Authenticated or dev β†’ start session
188
- <Button
189
- variant="contained"
190
- size="large"
191
- onClick={handleStart}
192
- disabled={isCreating}
193
- startIcon={
194
- isCreating ? <CircularProgress size={20} color="inherit" /> : null
195
- }
196
- sx={{
197
- px: 5,
198
- py: 1.5,
199
- fontSize: '1rem',
200
- fontWeight: 700,
201
- textTransform: 'none',
202
- borderRadius: '12px',
203
- bgcolor: HF_ORANGE,
204
- color: '#000',
205
- boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
206
- '&:hover': {
207
- bgcolor: '#FFB340',
208
- boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
209
- },
210
- '&.Mui-disabled': {
211
- bgcolor: 'rgba(255, 157, 0, 0.35)',
212
- color: 'rgba(0,0,0,0.45)',
213
- },
214
- }}
215
- >
216
- {isCreating ? 'Initializing...' : 'Start Session'}
217
- </Button>
 
218
  )}
219
 
220
  {/* Error */}
 
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
  import {
3
  Box,
4
  Typography,
 
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';
 
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
  } 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'
 
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
152
  sx={{
 
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 */}