Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Simplify OAuth: server-side cookies + iframe detection
Browse files- Removed @huggingface/hub (unnecessary complexity)
- Server-side OAuth via /auth/login (cookies work on direct access)
- In iframe: show "Open ML Agent" button linking to direct Space URL
- In dev mode: bypass auth entirely (no OAUTH_CLIENT_ID)
- 3 states on welcome screen: iframeβlink, directβsign in, authedβstart
Co-authored-by: Cursor <cursoragent@cursor.com>
- frontend/package-lock.json +0 -93
- frontend/package.json +0 -1
- frontend/src/components/WelcomeScreen/WelcomeScreen.tsx +115 -61
- frontend/src/hooks/useAuth.ts +34 -90
- frontend/src/utils/api.ts +10 -25
frontend/package-lock.json
CHANGED
|
@@ -10,7 +10,6 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"@emotion/react": "^11.13.0",
|
| 12 |
"@emotion/styled": "^11.13.0",
|
| 13 |
-
"@huggingface/hub": "^2.8.1",
|
| 14 |
"@mui/icons-material": "^6.1.0",
|
| 15 |
"@mui/material": "^6.1.0",
|
| 16 |
"react": "^18.3.1",
|
|
@@ -1020,30 +1019,6 @@
|
|
| 1020 |
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1021 |
}
|
| 1022 |
},
|
| 1023 |
-
"node_modules/@huggingface/hub": {
|
| 1024 |
-
"version": "2.8.1",
|
| 1025 |
-
"resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.8.1.tgz",
|
| 1026 |
-
"integrity": "sha512-VAsXdMiIHPteXQJhrwaBEiePTWiJ0zBSymHdnX4J+AijjNN0h3RzGfkKemXcu75gu/TmRLFY9l8+2Tkdmpis0w==",
|
| 1027 |
-
"license": "MIT",
|
| 1028 |
-
"dependencies": {
|
| 1029 |
-
"@huggingface/tasks": "^0.19.82"
|
| 1030 |
-
},
|
| 1031 |
-
"bin": {
|
| 1032 |
-
"hfjs": "dist/cli.js"
|
| 1033 |
-
},
|
| 1034 |
-
"engines": {
|
| 1035 |
-
"node": ">=18"
|
| 1036 |
-
},
|
| 1037 |
-
"optionalDependencies": {
|
| 1038 |
-
"cli-progress": "^3.12.0"
|
| 1039 |
-
}
|
| 1040 |
-
},
|
| 1041 |
-
"node_modules/@huggingface/tasks": {
|
| 1042 |
-
"version": "0.19.84",
|
| 1043 |
-
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.84.tgz",
|
| 1044 |
-
"integrity": "sha512-nn8DtUW7EZb4QcV+AdNbggPDbvaN32+FldkJakDVml0Aa18fSWjxePdxaRejfTlJYX9oGanOjQKj++NDVRYFXw==",
|
| 1045 |
-
"license": "MIT"
|
| 1046 |
-
},
|
| 1047 |
"node_modules/@humanfs/core": {
|
| 1048 |
"version": "0.19.1",
|
| 1049 |
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
|
@@ -2242,16 +2217,6 @@
|
|
| 2242 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 2243 |
}
|
| 2244 |
},
|
| 2245 |
-
"node_modules/ansi-regex": {
|
| 2246 |
-
"version": "5.0.1",
|
| 2247 |
-
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
| 2248 |
-
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
| 2249 |
-
"license": "MIT",
|
| 2250 |
-
"optional": true,
|
| 2251 |
-
"engines": {
|
| 2252 |
-
"node": ">=8"
|
| 2253 |
-
}
|
| 2254 |
-
},
|
| 2255 |
"node_modules/ansi-styles": {
|
| 2256 |
"version": "4.3.0",
|
| 2257 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
@@ -2460,19 +2425,6 @@
|
|
| 2460 |
"url": "https://github.com/sponsors/wooorm"
|
| 2461 |
}
|
| 2462 |
},
|
| 2463 |
-
"node_modules/cli-progress": {
|
| 2464 |
-
"version": "3.12.0",
|
| 2465 |
-
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
| 2466 |
-
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
| 2467 |
-
"license": "MIT",
|
| 2468 |
-
"optional": true,
|
| 2469 |
-
"dependencies": {
|
| 2470 |
-
"string-width": "^4.2.3"
|
| 2471 |
-
},
|
| 2472 |
-
"engines": {
|
| 2473 |
-
"node": ">=4"
|
| 2474 |
-
}
|
| 2475 |
-
},
|
| 2476 |
"node_modules/clsx": {
|
| 2477 |
"version": "2.1.1",
|
| 2478 |
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
|
@@ -2638,13 +2590,6 @@
|
|
| 2638 |
"dev": true,
|
| 2639 |
"license": "ISC"
|
| 2640 |
},
|
| 2641 |
-
"node_modules/emoji-regex": {
|
| 2642 |
-
"version": "8.0.0",
|
| 2643 |
-
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
| 2644 |
-
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
| 2645 |
-
"license": "MIT",
|
| 2646 |
-
"optional": true
|
| 2647 |
-
},
|
| 2648 |
"node_modules/error-ex": {
|
| 2649 |
"version": "1.3.4",
|
| 2650 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
|
@@ -3325,16 +3270,6 @@
|
|
| 3325 |
"node": ">=0.10.0"
|
| 3326 |
}
|
| 3327 |
},
|
| 3328 |
-
"node_modules/is-fullwidth-code-point": {
|
| 3329 |
-
"version": "3.0.0",
|
| 3330 |
-
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
| 3331 |
-
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
| 3332 |
-
"license": "MIT",
|
| 3333 |
-
"optional": true,
|
| 3334 |
-
"engines": {
|
| 3335 |
-
"node": ">=8"
|
| 3336 |
-
}
|
| 3337 |
-
},
|
| 3338 |
"node_modules/is-glob": {
|
| 3339 |
"version": "4.0.3",
|
| 3340 |
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
|
@@ -5041,21 +4976,6 @@
|
|
| 5041 |
"url": "https://github.com/sponsors/wooorm"
|
| 5042 |
}
|
| 5043 |
},
|
| 5044 |
-
"node_modules/string-width": {
|
| 5045 |
-
"version": "4.2.3",
|
| 5046 |
-
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
| 5047 |
-
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
| 5048 |
-
"license": "MIT",
|
| 5049 |
-
"optional": true,
|
| 5050 |
-
"dependencies": {
|
| 5051 |
-
"emoji-regex": "^8.0.0",
|
| 5052 |
-
"is-fullwidth-code-point": "^3.0.0",
|
| 5053 |
-
"strip-ansi": "^6.0.1"
|
| 5054 |
-
},
|
| 5055 |
-
"engines": {
|
| 5056 |
-
"node": ">=8"
|
| 5057 |
-
}
|
| 5058 |
-
},
|
| 5059 |
"node_modules/stringify-entities": {
|
| 5060 |
"version": "4.0.4",
|
| 5061 |
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
|
@@ -5070,19 +4990,6 @@
|
|
| 5070 |
"url": "https://github.com/sponsors/wooorm"
|
| 5071 |
}
|
| 5072 |
},
|
| 5073 |
-
"node_modules/strip-ansi": {
|
| 5074 |
-
"version": "6.0.1",
|
| 5075 |
-
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
| 5076 |
-
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
| 5077 |
-
"license": "MIT",
|
| 5078 |
-
"optional": true,
|
| 5079 |
-
"dependencies": {
|
| 5080 |
-
"ansi-regex": "^5.0.1"
|
| 5081 |
-
},
|
| 5082 |
-
"engines": {
|
| 5083 |
-
"node": ">=8"
|
| 5084 |
-
}
|
| 5085 |
-
},
|
| 5086 |
"node_modules/strip-json-comments": {
|
| 5087 |
"version": "3.1.1",
|
| 5088 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"@emotion/react": "^11.13.0",
|
| 12 |
"@emotion/styled": "^11.13.0",
|
|
|
|
| 13 |
"@mui/icons-material": "^6.1.0",
|
| 14 |
"@mui/material": "^6.1.0",
|
| 15 |
"react": "^18.3.1",
|
|
|
|
| 1019 |
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1020 |
}
|
| 1021 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
"node_modules/@humanfs/core": {
|
| 1023 |
"version": "0.19.1",
|
| 1024 |
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
|
|
|
| 2217 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 2218 |
}
|
| 2219 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2220 |
"node_modules/ansi-styles": {
|
| 2221 |
"version": "4.3.0",
|
| 2222 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
|
|
| 2425 |
"url": "https://github.com/sponsors/wooorm"
|
| 2426 |
}
|
| 2427 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2428 |
"node_modules/clsx": {
|
| 2429 |
"version": "2.1.1",
|
| 2430 |
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
|
|
|
| 2590 |
"dev": true,
|
| 2591 |
"license": "ISC"
|
| 2592 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2593 |
"node_modules/error-ex": {
|
| 2594 |
"version": "1.3.4",
|
| 2595 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
|
|
|
| 3270 |
"node": ">=0.10.0"
|
| 3271 |
}
|
| 3272 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3273 |
"node_modules/is-glob": {
|
| 3274 |
"version": "4.0.3",
|
| 3275 |
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
|
|
|
| 4976 |
"url": "https://github.com/sponsors/wooorm"
|
| 4977 |
}
|
| 4978 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4979 |
"node_modules/stringify-entities": {
|
| 4980 |
"version": "4.0.4",
|
| 4981 |
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
|
|
|
| 4990 |
"url": "https://github.com/sponsors/wooorm"
|
| 4991 |
}
|
| 4992 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4993 |
"node_modules/strip-json-comments": {
|
| 4994 |
"version": "3.1.1",
|
| 4995 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
frontend/package.json
CHANGED
|
@@ -12,7 +12,6 @@
|
|
| 12 |
"dependencies": {
|
| 13 |
"@emotion/react": "^11.13.0",
|
| 14 |
"@emotion/styled": "^11.13.0",
|
| 15 |
-
"@huggingface/hub": "^2.8.1",
|
| 16 |
"@mui/icons-material": "^6.1.0",
|
| 17 |
"@mui/material": "^6.1.0",
|
| 18 |
"react": "^18.3.1",
|
|
|
|
| 12 |
"dependencies": {
|
| 13 |
"@emotion/react": "^11.13.0",
|
| 14 |
"@emotion/styled": "^11.13.0",
|
|
|
|
| 15 |
"@mui/icons-material": "^6.1.0",
|
| 16 |
"@mui/material": "^6.1.0",
|
| 17 |
"react": "^18.3.1",
|
frontend/src/components/WelcomeScreen/WelcomeScreen.tsx
CHANGED
|
@@ -5,12 +5,13 @@ import {
|
|
| 5 |
Button,
|
| 6 |
CircularProgress,
|
| 7 |
Alert,
|
|
|
|
| 8 |
} from '@mui/material';
|
|
|
|
| 9 |
import { useSessionStore } from '@/store/sessionStore';
|
| 10 |
import { useAgentStore } from '@/store/agentStore';
|
| 11 |
import { apiFetch } from '@/utils/api';
|
| 12 |
-
import {
|
| 13 |
-
import { logger } from '@/utils/logger';
|
| 14 |
|
| 15 |
/** HF brand orange */
|
| 16 |
const HF_ORANGE = '#FF9D00';
|
|
@@ -21,15 +22,20 @@ export default function WelcomeScreen() {
|
|
| 21 |
const [isCreating, setIsCreating] = useState(false);
|
| 22 |
const [error, setError] = useState<string | null>(null);
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
const handleStart = useCallback(async () => {
|
| 25 |
if (isCreating) return;
|
| 26 |
|
| 27 |
-
//
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
| 33 |
return;
|
| 34 |
}
|
| 35 |
|
|
@@ -44,8 +50,7 @@ export default function WelcomeScreen() {
|
|
| 44 |
return;
|
| 45 |
}
|
| 46 |
if (response.status === 401) {
|
| 47 |
-
|
| 48 |
-
await triggerLogin();
|
| 49 |
return;
|
| 50 |
}
|
| 51 |
if (!response.ok) {
|
|
@@ -57,11 +62,18 @@ export default function WelcomeScreen() {
|
|
| 57 |
setPlan([]);
|
| 58 |
setPanelContent(null);
|
| 59 |
} catch {
|
| 60 |
-
//
|
| 61 |
} finally {
|
| 62 |
setIsCreating(false);
|
| 63 |
}
|
| 64 |
-
}, [isCreating, createSession, setPlan, setPanelContent,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
return (
|
| 67 |
<Box
|
|
@@ -76,17 +88,12 @@ export default function WelcomeScreen() {
|
|
| 76 |
py: 8,
|
| 77 |
}}
|
| 78 |
>
|
| 79 |
-
{/* HF Logo
|
| 80 |
<Box
|
| 81 |
component="img"
|
| 82 |
src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 83 |
alt="Hugging Face"
|
| 84 |
-
sx={{
|
| 85 |
-
width: 96,
|
| 86 |
-
height: 96,
|
| 87 |
-
mb: 3,
|
| 88 |
-
display: 'block',
|
| 89 |
-
}}
|
| 90 |
/>
|
| 91 |
|
| 92 |
{/* Title */}
|
|
@@ -114,10 +121,7 @@ export default function WelcomeScreen() {
|
|
| 114 |
fontSize: '0.95rem',
|
| 115 |
textAlign: 'center',
|
| 116 |
px: 2,
|
| 117 |
-
'& strong': {
|
| 118 |
-
color: 'var(--text)',
|
| 119 |
-
fontWeight: 600,
|
| 120 |
-
},
|
| 121 |
}}
|
| 122 |
>
|
| 123 |
A general-purpose AI agent for <strong>machine learning engineering</strong>.
|
|
@@ -126,38 +130,93 @@ export default function WelcomeScreen() {
|
|
| 126 |
and explores <strong>datasets</strong> β all through natural conversation.
|
| 127 |
</Typography>
|
| 128 |
|
| 129 |
-
{/*
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
{/* Error */}
|
| 163 |
{error && (
|
|
@@ -180,12 +239,7 @@ export default function WelcomeScreen() {
|
|
| 180 |
{/* Footnote */}
|
| 181 |
<Typography
|
| 182 |
variant="caption"
|
| 183 |
-
sx={{
|
| 184 |
-
mt: 5,
|
| 185 |
-
color: 'var(--muted-text)',
|
| 186 |
-
opacity: 0.5,
|
| 187 |
-
fontSize: '0.7rem',
|
| 188 |
-
}}
|
| 189 |
>
|
| 190 |
Conversations are stored locally in your browser.
|
| 191 |
</Typography>
|
|
|
|
| 5 |
Button,
|
| 6 |
CircularProgress,
|
| 7 |
Alert,
|
| 8 |
+
Link,
|
| 9 |
} from '@mui/material';
|
| 10 |
+
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
| 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';
|
|
|
|
| 22 |
const [isCreating, setIsCreating] = useState(false);
|
| 23 |
const [error, setError] = useState<string | null>(null);
|
| 24 |
|
| 25 |
+
const inIframe = isInIframe();
|
| 26 |
+
const isAuthenticated = user?.authenticated;
|
| 27 |
+
const isDevUser = user?.username === 'dev';
|
| 28 |
+
|
| 29 |
const handleStart = useCallback(async () => {
|
| 30 |
if (isCreating) return;
|
| 31 |
|
| 32 |
+
// Not authenticated and not dev β need to login
|
| 33 |
+
if (!isAuthenticated && !isDevUser) {
|
| 34 |
+
// In iframe: can't redirect (cookies blocked) β user needs to open in new tab
|
| 35 |
+
// This shouldn't happen because we show a different button in iframe
|
| 36 |
+
// But just in case:
|
| 37 |
+
if (inIframe) return;
|
| 38 |
+
triggerLogin();
|
| 39 |
return;
|
| 40 |
}
|
| 41 |
|
|
|
|
| 50 |
return;
|
| 51 |
}
|
| 52 |
if (response.status === 401) {
|
| 53 |
+
triggerLogin();
|
|
|
|
| 54 |
return;
|
| 55 |
}
|
| 56 |
if (!response.ok) {
|
|
|
|
| 62 |
setPlan([]);
|
| 63 |
setPanelContent(null);
|
| 64 |
} catch {
|
| 65 |
+
// Redirect may throw β ignore
|
| 66 |
} finally {
|
| 67 |
setIsCreating(false);
|
| 68 |
}
|
| 69 |
+
}, [isCreating, createSession, setPlan, setPanelContent, isAuthenticated, isDevUser, inIframe]);
|
| 70 |
+
|
| 71 |
+
// Build the direct Space URL for the "open in new tab" link
|
| 72 |
+
const spaceHost = typeof window !== 'undefined'
|
| 73 |
+
? window.location.hostname.includes('.hf.space')
|
| 74 |
+
? window.location.origin
|
| 75 |
+
: `https://smolagents-ml-agent.hf.space`
|
| 76 |
+
: '';
|
| 77 |
|
| 78 |
return (
|
| 79 |
<Box
|
|
|
|
| 88 |
py: 8,
|
| 89 |
}}
|
| 90 |
>
|
| 91 |
+
{/* HF Logo */}
|
| 92 |
<Box
|
| 93 |
component="img"
|
| 94 |
src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 95 |
alt="Hugging Face"
|
| 96 |
+
sx={{ width: 96, height: 96, mb: 3, display: 'block' }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
/>
|
| 98 |
|
| 99 |
{/* Title */}
|
|
|
|
| 121 |
fontSize: '0.95rem',
|
| 122 |
textAlign: 'center',
|
| 123 |
px: 2,
|
| 124 |
+
'& strong': { color: 'var(--text)', fontWeight: 600 },
|
|
|
|
|
|
|
|
|
|
| 125 |
}}
|
| 126 |
>
|
| 127 |
A general-purpose AI agent for <strong>machine learning engineering</strong>.
|
|
|
|
| 130 |
and explores <strong>datasets</strong> β all through natural conversation.
|
| 131 |
</Typography>
|
| 132 |
|
| 133 |
+
{/* Action button β depends on context */}
|
| 134 |
+
{inIframe && !isAuthenticated && !isDevUser ? (
|
| 135 |
+
// In iframe + not logged in β link to open Space directly
|
| 136 |
+
<Button
|
| 137 |
+
variant="contained"
|
| 138 |
+
size="large"
|
| 139 |
+
component="a"
|
| 140 |
+
href={spaceHost}
|
| 141 |
+
target="_blank"
|
| 142 |
+
rel="noopener noreferrer"
|
| 143 |
+
endIcon={<OpenInNewIcon />}
|
| 144 |
+
sx={{
|
| 145 |
+
px: 5,
|
| 146 |
+
py: 1.5,
|
| 147 |
+
fontSize: '1rem',
|
| 148 |
+
fontWeight: 700,
|
| 149 |
+
textTransform: 'none',
|
| 150 |
+
borderRadius: '12px',
|
| 151 |
+
bgcolor: HF_ORANGE,
|
| 152 |
+
color: '#000',
|
| 153 |
+
boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
|
| 154 |
+
textDecoration: 'none',
|
| 155 |
+
'&:hover': {
|
| 156 |
+
bgcolor: '#FFB340',
|
| 157 |
+
boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
|
| 158 |
+
},
|
| 159 |
+
}}
|
| 160 |
+
>
|
| 161 |
+
Open ML Agent
|
| 162 |
+
</Button>
|
| 163 |
+
) : !isAuthenticated && !isDevUser ? (
|
| 164 |
+
// Direct access + not logged in β sign in button
|
| 165 |
+
<Button
|
| 166 |
+
variant="contained"
|
| 167 |
+
size="large"
|
| 168 |
+
onClick={() => triggerLogin()}
|
| 169 |
+
sx={{
|
| 170 |
+
px: 5,
|
| 171 |
+
py: 1.5,
|
| 172 |
+
fontSize: '1rem',
|
| 173 |
+
fontWeight: 700,
|
| 174 |
+
textTransform: 'none',
|
| 175 |
+
borderRadius: '12px',
|
| 176 |
+
bgcolor: HF_ORANGE,
|
| 177 |
+
color: '#000',
|
| 178 |
+
boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
|
| 179 |
+
'&:hover': {
|
| 180 |
+
bgcolor: '#FFB340',
|
| 181 |
+
boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
|
| 182 |
+
},
|
| 183 |
+
}}
|
| 184 |
+
>
|
| 185 |
+
Sign in with Hugging Face
|
| 186 |
+
</Button>
|
| 187 |
+
) : (
|
| 188 |
+
// Authenticated or dev β start session
|
| 189 |
+
<Button
|
| 190 |
+
variant="contained"
|
| 191 |
+
size="large"
|
| 192 |
+
onClick={handleStart}
|
| 193 |
+
disabled={isCreating}
|
| 194 |
+
startIcon={
|
| 195 |
+
isCreating ? <CircularProgress size={20} color="inherit" /> : null
|
| 196 |
+
}
|
| 197 |
+
sx={{
|
| 198 |
+
px: 5,
|
| 199 |
+
py: 1.5,
|
| 200 |
+
fontSize: '1rem',
|
| 201 |
+
fontWeight: 700,
|
| 202 |
+
textTransform: 'none',
|
| 203 |
+
borderRadius: '12px',
|
| 204 |
+
bgcolor: HF_ORANGE,
|
| 205 |
+
color: '#000',
|
| 206 |
+
boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
|
| 207 |
+
'&:hover': {
|
| 208 |
+
bgcolor: '#FFB340',
|
| 209 |
+
boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
|
| 210 |
+
},
|
| 211 |
+
'&.Mui-disabled': {
|
| 212 |
+
bgcolor: 'rgba(255, 157, 0, 0.35)',
|
| 213 |
+
color: 'rgba(0,0,0,0.45)',
|
| 214 |
+
},
|
| 215 |
+
}}
|
| 216 |
+
>
|
| 217 |
+
{isCreating ? 'Initializing...' : 'Start Session'}
|
| 218 |
+
</Button>
|
| 219 |
+
)}
|
| 220 |
|
| 221 |
{/* Error */}
|
| 222 |
{error && (
|
|
|
|
| 239 |
{/* Footnote */}
|
| 240 |
<Typography
|
| 241 |
variant="caption"
|
| 242 |
+
sx={{ mt: 5, color: 'var(--muted-text)', opacity: 0.5, fontSize: '0.7rem' }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
>
|
| 244 |
Conversations are stored locally in your browser.
|
| 245 |
</Typography>
|
frontend/src/hooks/useAuth.ts
CHANGED
|
@@ -1,71 +1,34 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
*
|
|
|
|
|
|
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { useEffect } from 'react';
|
| 9 |
-
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
|
| 10 |
import { useAgentStore } from '@/store/agentStore';
|
| 11 |
import { logger } from '@/utils/logger';
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
/** Get the stored HF access token (or null). */
|
| 16 |
-
export function getStoredToken(): string | null {
|
| 17 |
-
try {
|
| 18 |
-
return localStorage.getItem(TOKEN_KEY);
|
| 19 |
-
} catch {
|
| 20 |
-
return null;
|
| 21 |
-
}
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
/** Clear the stored token (logout). */
|
| 25 |
-
export function clearStoredToken(): void {
|
| 26 |
try {
|
| 27 |
-
|
| 28 |
} catch {
|
| 29 |
-
//
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
| 33 |
-
/** Redirect to
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
* 2. Fall back to server-side /auth/login (works on direct access, handles cookies)
|
| 37 |
-
* 3. In iframe context, open in new tab to avoid cookie issues */
|
| 38 |
-
export async function triggerLogin(): Promise<void> {
|
| 39 |
-
let url: string;
|
| 40 |
-
|
| 41 |
-
try {
|
| 42 |
-
// Client-side OAuth β needs window.huggingface (HF iframe only)
|
| 43 |
-
url = await oauthLoginUrl({
|
| 44 |
-
scopes: 'openid profile read-repos write-repos manage-repos inference-api jobs',
|
| 45 |
-
});
|
| 46 |
-
} catch {
|
| 47 |
-
// Fallback: server-side OAuth (works on direct access)
|
| 48 |
-
url = '/auth/login';
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
// In an iframe, open in a new tab (cookies blocked otherwise)
|
| 52 |
-
let inIframe = false;
|
| 53 |
-
try {
|
| 54 |
-
inIframe = window.top !== window.self;
|
| 55 |
-
} catch {
|
| 56 |
-
inIframe = true; // SecurityError = cross-origin iframe
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
if (inIframe) {
|
| 60 |
-
window.open(url, '_blank');
|
| 61 |
-
} else {
|
| 62 |
-
window.location.href = url;
|
| 63 |
-
}
|
| 64 |
}
|
| 65 |
|
| 66 |
/**
|
| 67 |
-
* Hook: on mount, check
|
| 68 |
-
* Sets
|
| 69 |
*/
|
| 70 |
export function useAuth() {
|
| 71 |
const setUser = useAgentStore((s) => s.setUser);
|
|
@@ -73,41 +36,12 @@ export function useAuth() {
|
|
| 73 |
useEffect(() => {
|
| 74 |
let cancelled = false;
|
| 75 |
|
| 76 |
-
async function
|
| 77 |
-
// 1. Check if we're returning from an OAuth redirect
|
| 78 |
-
const oauthResult = await oauthHandleRedirectIfPresent();
|
| 79 |
-
|
| 80 |
-
if (oauthResult) {
|
| 81 |
-
// Store the access token
|
| 82 |
-
localStorage.setItem(TOKEN_KEY, oauthResult.accessToken);
|
| 83 |
-
logger.log('OAuth login successful:', oauthResult.userInfo?.name);
|
| 84 |
-
|
| 85 |
-
if (!cancelled) {
|
| 86 |
-
setUser({
|
| 87 |
-
authenticated: true,
|
| 88 |
-
username: oauthResult.userInfo?.name || oauthResult.userInfo?.preferred_username || 'user',
|
| 89 |
-
name: oauthResult.userInfo?.name,
|
| 90 |
-
picture: oauthResult.userInfo?.picture,
|
| 91 |
-
});
|
| 92 |
-
}
|
| 93 |
-
return;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
// 2. Check for existing token in localStorage
|
| 97 |
-
const token = getStoredToken();
|
| 98 |
-
if (!token) {
|
| 99 |
-
// Not logged in β welcome screen will handle login trigger
|
| 100 |
-
if (!cancelled) setUser(null);
|
| 101 |
-
return;
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
// 3. Validate the stored token
|
| 105 |
try {
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
const data = await res.json();
|
| 111 |
if (!cancelled && data.authenticated) {
|
| 112 |
setUser({
|
| 113 |
authenticated: true,
|
|
@@ -115,19 +49,29 @@ export function useAuth() {
|
|
| 115 |
name: data.name,
|
| 116 |
picture: data.picture,
|
| 117 |
});
|
|
|
|
| 118 |
return;
|
| 119 |
}
|
| 120 |
}
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
if (!cancelled) setUser(null);
|
| 124 |
} catch {
|
| 125 |
-
// Backend unreachable
|
| 126 |
if (!cancelled) setUser({ authenticated: true, username: 'dev' });
|
| 127 |
}
|
| 128 |
}
|
| 129 |
|
| 130 |
-
|
| 131 |
return () => { cancelled = true; };
|
| 132 |
}, [setUser]);
|
| 133 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Authentication hook β simple server-side OAuth.
|
| 3 |
*
|
| 4 |
+
* - Hors iframe: /auth/login redirect (cookies work fine)
|
| 5 |
+
* - Dans iframe: show "Open in full page" link
|
| 6 |
+
*
|
| 7 |
+
* Token is stored via HttpOnly cookie by the backend.
|
| 8 |
+
* In dev mode (no OAUTH_CLIENT_ID), auth is bypassed.
|
| 9 |
*/
|
| 10 |
|
| 11 |
import { useEffect } from 'react';
|
|
|
|
| 12 |
import { useAgentStore } from '@/store/agentStore';
|
| 13 |
import { logger } from '@/utils/logger';
|
| 14 |
|
| 15 |
+
/** Check if we're running inside an iframe. */
|
| 16 |
+
export function isInIframe(): boolean {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
try {
|
| 18 |
+
return window.top !== window.self;
|
| 19 |
} catch {
|
| 20 |
+
return true; // SecurityError = cross-origin iframe
|
| 21 |
}
|
| 22 |
}
|
| 23 |
|
| 24 |
+
/** Redirect to the server-side OAuth login. */
|
| 25 |
+
export function triggerLogin(): void {
|
| 26 |
+
window.location.href = '/auth/login';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
/**
|
| 30 |
+
* Hook: on mount, check if user is authenticated.
|
| 31 |
+
* Sets user in the agent store.
|
| 32 |
*/
|
| 33 |
export function useAuth() {
|
| 34 |
const setUser = useAgentStore((s) => s.setUser);
|
|
|
|
| 36 |
useEffect(() => {
|
| 37 |
let cancelled = false;
|
| 38 |
|
| 39 |
+
async function checkAuth() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
try {
|
| 41 |
+
// Check if user is already authenticated (cookie-based)
|
| 42 |
+
const response = await fetch('/auth/me', { credentials: 'include' });
|
| 43 |
+
if (response.ok) {
|
| 44 |
+
const data = await response.json();
|
|
|
|
| 45 |
if (!cancelled && data.authenticated) {
|
| 46 |
setUser({
|
| 47 |
authenticated: true,
|
|
|
|
| 49 |
name: data.name,
|
| 50 |
picture: data.picture,
|
| 51 |
});
|
| 52 |
+
logger.log('Authenticated as', data.username);
|
| 53 |
return;
|
| 54 |
}
|
| 55 |
}
|
| 56 |
+
|
| 57 |
+
// Not authenticated β check if auth is enabled
|
| 58 |
+
const statusRes = await fetch('/auth/status', { credentials: 'include' });
|
| 59 |
+
const statusData = await statusRes.json();
|
| 60 |
+
if (!statusData.auth_enabled) {
|
| 61 |
+
// Dev mode β no OAuth configured
|
| 62 |
+
if (!cancelled) setUser({ authenticated: true, username: 'dev' });
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Auth enabled but not logged in β welcome screen will handle it
|
| 67 |
if (!cancelled) setUser(null);
|
| 68 |
} catch {
|
| 69 |
+
// Backend unreachable β assume dev mode
|
| 70 |
if (!cancelled) setUser({ authenticated: true, username: 'dev' });
|
| 71 |
}
|
| 72 |
}
|
| 73 |
|
| 74 |
+
checkAuth();
|
| 75 |
return () => { cancelled = true; };
|
| 76 |
}, [setUser]);
|
| 77 |
}
|
frontend/src/utils/api.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
/**
|
| 2 |
* Centralized API utilities.
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
* WebSocket URLs include the token as a query parameter.
|
| 7 |
*/
|
| 8 |
|
| 9 |
-
import {
|
| 10 |
|
| 11 |
-
/** Wrapper around fetch
|
| 12 |
export async function apiFetch(
|
| 13 |
path: string,
|
| 14 |
options: RequestInit = {}
|
|
@@ -18,45 +17,31 @@ export async function apiFetch(
|
|
| 18 |
...(options.headers as Record<string, string>),
|
| 19 |
};
|
| 20 |
|
| 21 |
-
// Inject Bearer token if available
|
| 22 |
-
const token = getStoredToken();
|
| 23 |
-
if (token) {
|
| 24 |
-
headers['Authorization'] = `Bearer ${token}`;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
const response = await fetch(path, {
|
| 28 |
...options,
|
| 29 |
headers,
|
| 30 |
-
credentials: 'include', //
|
| 31 |
});
|
| 32 |
|
| 33 |
-
// Handle 401 β
|
| 34 |
if (response.status === 401) {
|
| 35 |
try {
|
| 36 |
-
const authStatus = await fetch('/auth/status');
|
| 37 |
const data = await authStatus.json();
|
| 38 |
if (data.auth_enabled) {
|
| 39 |
-
|
| 40 |
throw new Error('Authentication required β redirecting to login.');
|
| 41 |
}
|
| 42 |
} catch (e) {
|
| 43 |
if (e instanceof Error && e.message.includes('redirecting')) throw e;
|
| 44 |
-
// auth/status failed β ignore
|
| 45 |
}
|
| 46 |
}
|
| 47 |
|
| 48 |
return response;
|
| 49 |
}
|
| 50 |
|
| 51 |
-
/** Build the WebSocket URL for a session
|
| 52 |
export function getWebSocketUrl(sessionId: string): string {
|
| 53 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
// Pass token as query param (WebSocket can't set custom headers from browser)
|
| 57 |
-
const token = getStoredToken();
|
| 58 |
-
if (token) {
|
| 59 |
-
return `${base}?token=${encodeURIComponent(token)}`;
|
| 60 |
-
}
|
| 61 |
-
return base;
|
| 62 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* Centralized API utilities.
|
| 3 |
*
|
| 4 |
+
* In production: HttpOnly cookie (hf_access_token) is sent automatically.
|
| 5 |
+
* In development: auth is bypassed on the backend.
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { triggerLogin } from '@/hooks/useAuth';
|
| 9 |
|
| 10 |
+
/** Wrapper around fetch with credentials and common headers. */
|
| 11 |
export async function apiFetch(
|
| 12 |
path: string,
|
| 13 |
options: RequestInit = {}
|
|
|
|
| 17 |
...(options.headers as Record<string, string>),
|
| 18 |
};
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
const response = await fetch(path, {
|
| 21 |
...options,
|
| 22 |
headers,
|
| 23 |
+
credentials: 'include', // Send cookies with every request
|
| 24 |
});
|
| 25 |
|
| 26 |
+
// Handle 401 β redirect to login
|
| 27 |
if (response.status === 401) {
|
| 28 |
try {
|
| 29 |
+
const authStatus = await fetch('/auth/status', { credentials: 'include' });
|
| 30 |
const data = await authStatus.json();
|
| 31 |
if (data.auth_enabled) {
|
| 32 |
+
triggerLogin();
|
| 33 |
throw new Error('Authentication required β redirecting to login.');
|
| 34 |
}
|
| 35 |
} catch (e) {
|
| 36 |
if (e instanceof Error && e.message.includes('redirecting')) throw e;
|
|
|
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
| 40 |
return response;
|
| 41 |
}
|
| 42 |
|
| 43 |
+
/** Build the WebSocket URL for a session. */
|
| 44 |
export function getWebSocketUrl(sessionId: string): string {
|
| 45 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 46 |
+
return `${protocol}//${window.location.host}/api/ws/${sessionId}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|