Spaces:
Running
Running
update
Browse files- frontend/src/app/page.tsx +2 -0
- frontend/src/components/LandingPage.tsx +176 -23
frontend/src/app/page.tsx
CHANGED
|
@@ -869,6 +869,8 @@ export default function Home() {
|
|
| 869 |
initialLanguage={selectedLanguage}
|
| 870 |
initialModel={selectedModel}
|
| 871 |
onAuthChange={checkAuth}
|
|
|
|
|
|
|
| 872 |
/>
|
| 873 |
</div>
|
| 874 |
);
|
|
|
|
| 869 |
initialLanguage={selectedLanguage}
|
| 870 |
initialModel={selectedModel}
|
| 871 |
onAuthChange={checkAuth}
|
| 872 |
+
setPendingPR={setPendingPR}
|
| 873 |
+
pendingPRRef={pendingPRRef}
|
| 874 |
/>
|
| 875 |
</div>
|
| 876 |
);
|
frontend/src/components/LandingPage.tsx
CHANGED
|
@@ -21,6 +21,8 @@ interface LandingPageProps {
|
|
| 21 |
initialLanguage?: Language;
|
| 22 |
initialModel?: string;
|
| 23 |
onAuthChange?: () => void;
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
export default function LandingPage({
|
|
@@ -29,7 +31,9 @@ export default function LandingPage({
|
|
| 29 |
isAuthenticated,
|
| 30 |
initialLanguage = 'html',
|
| 31 |
initialModel = 'deepseek-ai/DeepSeek-V3.2-Exp',
|
| 32 |
-
onAuthChange
|
|
|
|
|
|
|
| 33 |
}: LandingPageProps) {
|
| 34 |
const [prompt, setPrompt] = useState('');
|
| 35 |
const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLanguage);
|
|
@@ -62,6 +66,8 @@ export default function LandingPage({
|
|
| 62 |
const [importUrl, setImportUrl] = useState('');
|
| 63 |
const [isImporting, setIsImporting] = useState(false);
|
| 64 |
const [importError, setImportError] = useState('');
|
|
|
|
|
|
|
| 65 |
|
| 66 |
// Redesign project state
|
| 67 |
const [redesignUrl, setRedesignUrl] = useState('');
|
|
@@ -231,6 +237,31 @@ export default function LandingPage({
|
|
| 231 |
return lang.charAt(0).toUpperCase() + lang.slice(1);
|
| 232 |
};
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
const handleImportProject = async () => {
|
| 235 |
if (!importUrl.trim()) {
|
| 236 |
setImportError('Please enter a valid URL');
|
|
@@ -248,41 +279,90 @@ export default function LandingPage({
|
|
| 248 |
try {
|
| 249 |
console.log('[Import] ========== STARTING IMPORT ==========');
|
| 250 |
console.log('[Import] Import URL:', importUrl);
|
|
|
|
| 251 |
|
| 252 |
-
// Extract space ID from URL
|
| 253 |
const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 254 |
console.log('[Import] Space regex match result:', spaceMatch);
|
| 255 |
|
| 256 |
if (spaceMatch) {
|
| 257 |
-
// This is a HuggingFace Space - duplicate it
|
| 258 |
const fromSpaceId = spaceMatch[1];
|
| 259 |
-
console.log('[Import] β
Detected HF Space
|
| 260 |
-
console.log('[Import] Calling apiClient.duplicateSpace...');
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
|
| 265 |
-
if (
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
console.log('[Import] Calling onImport with duplicated space URL:', duplicateResult.space_url);
|
| 275 |
-
// Pass the duplicated space URL so it's tracked for future deployments
|
| 276 |
-
onImport(importResult.code, importResult.language || 'html', duplicateResult.space_url);
|
| 277 |
|
| 278 |
-
|
| 279 |
-
alert(`β
Space duplicated successfully!\n\nYour space: ${duplicateResult.space_url}\n\nThe code has been loaded in the editor. Any changes you deploy will update this duplicated space.`);
|
| 280 |
}
|
| 281 |
|
| 282 |
setShowImportDialog(false);
|
| 283 |
setImportUrl('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
} else {
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
} else {
|
| 288 |
// Not a Space URL - fall back to regular import
|
|
@@ -726,15 +806,86 @@ Note: After generating the redesign, I will create a Pull Request on the origina
|
|
| 726 |
<input
|
| 727 |
type="text"
|
| 728 |
value={importUrl}
|
| 729 |
-
onChange={(e) =>
|
|
|
|
|
|
|
|
|
|
| 730 |
onKeyPress={(e) => e.key === 'Enter' && handleImportProject()}
|
| 731 |
placeholder="https://huggingface.co/spaces/..."
|
| 732 |
-
className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-
|
| 733 |
disabled={isImporting}
|
| 734 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
{importError && (
|
| 736 |
<p className="text-xs text-red-400 mb-2">{importError}</p>
|
| 737 |
)}
|
|
|
|
| 738 |
<div className="flex gap-2">
|
| 739 |
<button
|
| 740 |
onClick={handleImportProject}
|
|
@@ -748,6 +899,8 @@ Note: After generating the redesign, I will create a Pull Request on the origina
|
|
| 748 |
setShowImportDialog(false);
|
| 749 |
setImportUrl('');
|
| 750 |
setImportError('');
|
|
|
|
|
|
|
| 751 |
}}
|
| 752 |
className="px-3 py-2 bg-[#2d2d30] text-[#f5f5f7] rounded-lg text-xs hover:bg-[#3d3d3f] font-medium"
|
| 753 |
>
|
|
|
|
| 21 |
initialLanguage?: Language;
|
| 22 |
initialModel?: string;
|
| 23 |
onAuthChange?: () => void;
|
| 24 |
+
setPendingPR?: (pr: { repoId: string; language: Language } | null) => void;
|
| 25 |
+
pendingPRRef?: React.MutableRefObject<{ repoId: string; language: Language } | null>;
|
| 26 |
}
|
| 27 |
|
| 28 |
export default function LandingPage({
|
|
|
|
| 31 |
isAuthenticated,
|
| 32 |
initialLanguage = 'html',
|
| 33 |
initialModel = 'deepseek-ai/DeepSeek-V3.2-Exp',
|
| 34 |
+
onAuthChange,
|
| 35 |
+
setPendingPR,
|
| 36 |
+
pendingPRRef
|
| 37 |
}: LandingPageProps) {
|
| 38 |
const [prompt, setPrompt] = useState('');
|
| 39 |
const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLanguage);
|
|
|
|
| 66 |
const [importUrl, setImportUrl] = useState('');
|
| 67 |
const [isImporting, setIsImporting] = useState(false);
|
| 68 |
const [importError, setImportError] = useState('');
|
| 69 |
+
const [importAction, setImportAction] = useState<'duplicate' | 'update' | 'pr'>('duplicate'); // Default to duplicate
|
| 70 |
+
const [isSpaceOwner, setIsSpaceOwner] = useState(false); // Track if user owns the space
|
| 71 |
|
| 72 |
// Redesign project state
|
| 73 |
const [redesignUrl, setRedesignUrl] = useState('');
|
|
|
|
| 237 |
return lang.charAt(0).toUpperCase() + lang.slice(1);
|
| 238 |
};
|
| 239 |
|
| 240 |
+
// Check if user owns the imported space
|
| 241 |
+
const checkSpaceOwnership = (url: string) => {
|
| 242 |
+
if (!url || !userInfo?.preferred_username) {
|
| 243 |
+
setIsSpaceOwner(false);
|
| 244 |
+
return;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
const spaceMatch = url.match(/huggingface\.co\/spaces\/([^\/\s\)]+)\/[^\/\s\)]+/);
|
| 248 |
+
if (spaceMatch) {
|
| 249 |
+
const spaceOwner = spaceMatch[1];
|
| 250 |
+
const isOwner = spaceOwner === userInfo.preferred_username;
|
| 251 |
+
setIsSpaceOwner(isOwner);
|
| 252 |
+
console.log('[Import] Space owner:', spaceOwner, '| Current user:', userInfo.preferred_username, '| Is owner:', isOwner);
|
| 253 |
+
|
| 254 |
+
// Auto-select update mode if owner, otherwise duplicate
|
| 255 |
+
if (isOwner) {
|
| 256 |
+
setImportAction('update');
|
| 257 |
+
} else {
|
| 258 |
+
setImportAction('duplicate');
|
| 259 |
+
}
|
| 260 |
+
} else {
|
| 261 |
+
setIsSpaceOwner(false);
|
| 262 |
+
}
|
| 263 |
+
};
|
| 264 |
+
|
| 265 |
const handleImportProject = async () => {
|
| 266 |
if (!importUrl.trim()) {
|
| 267 |
setImportError('Please enter a valid URL');
|
|
|
|
| 279 |
try {
|
| 280 |
console.log('[Import] ========== STARTING IMPORT ==========');
|
| 281 |
console.log('[Import] Import URL:', importUrl);
|
| 282 |
+
console.log('[Import] Action:', importAction);
|
| 283 |
|
| 284 |
+
// Extract space ID from URL
|
| 285 |
const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 286 |
console.log('[Import] Space regex match result:', spaceMatch);
|
| 287 |
|
| 288 |
if (spaceMatch) {
|
|
|
|
| 289 |
const fromSpaceId = spaceMatch[1];
|
| 290 |
+
console.log('[Import] β
Detected HF Space:', fromSpaceId);
|
|
|
|
| 291 |
|
| 292 |
+
// Import the code first (always needed to load in editor)
|
| 293 |
+
const importResult = await apiClient.importProject(importUrl);
|
| 294 |
|
| 295 |
+
if (importResult.status !== 'success') {
|
| 296 |
+
setImportError(importResult.message || 'Failed to import project');
|
| 297 |
+
setIsImporting(false);
|
| 298 |
+
return;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
// Handle different import actions
|
| 302 |
+
if (importAction === 'update' && isSpaceOwner) {
|
| 303 |
+
// Option 1: Update existing space directly (for owners)
|
| 304 |
+
console.log('[Import] Owner update - loading code for direct update to:', fromSpaceId);
|
| 305 |
|
| 306 |
+
if (onImport && importResult.code) {
|
| 307 |
+
// Pass the original space URL so future deployments update it
|
| 308 |
+
onImport(importResult.code, importResult.language || 'html', importUrl);
|
|
|
|
|
|
|
|
|
|
| 309 |
|
| 310 |
+
alert(`β
Code loaded!\n\nYou can now make changes and deploy them directly to: ${importUrl}\n\nThe code has been loaded in the editor.`);
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
setShowImportDialog(false);
|
| 314 |
setImportUrl('');
|
| 315 |
+
|
| 316 |
+
} else if (importAction === 'pr') {
|
| 317 |
+
// Option 2: Create Pull Request
|
| 318 |
+
console.log('[Import] PR mode - loading code to create PR to:', fromSpaceId);
|
| 319 |
+
|
| 320 |
+
if (onImport && importResult.code) {
|
| 321 |
+
// Load code in editor with the original space for PR tracking
|
| 322 |
+
onImport(importResult.code, importResult.language || 'html', importUrl);
|
| 323 |
+
|
| 324 |
+
// Set pending PR state so any future code generation creates a PR
|
| 325 |
+
if (setPendingPR && pendingPRRef) {
|
| 326 |
+
const prInfo = { repoId: fromSpaceId, language: (importResult.language || 'html') as Language };
|
| 327 |
+
setPendingPR(prInfo);
|
| 328 |
+
pendingPRRef.current = prInfo;
|
| 329 |
+
console.log('[Import PR] Set pending PR:', prInfo);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
// Show success message
|
| 333 |
+
alert(`β
Code loaded in PR mode!\n\nYou can now:\nβ’ Make manual edits in the editor\nβ’ Generate new features with AI\n\nWhen you deploy, a Pull Request will be created to: ${fromSpaceId}`);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
setShowImportDialog(false);
|
| 337 |
+
setImportUrl('');
|
| 338 |
+
|
| 339 |
} else {
|
| 340 |
+
// Option 3: Duplicate space (default)
|
| 341 |
+
console.log('[Import] Duplicate mode - will duplicate:', fromSpaceId);
|
| 342 |
+
|
| 343 |
+
const duplicateResult = await apiClient.duplicateSpace(fromSpaceId);
|
| 344 |
+
console.log('[Import] Duplicate API response:', duplicateResult);
|
| 345 |
+
|
| 346 |
+
if (duplicateResult.success) {
|
| 347 |
+
console.log('[Import] ========== DUPLICATE SUCCESS ==========');
|
| 348 |
+
console.log('[Import] Duplicated space URL:', duplicateResult.space_url);
|
| 349 |
+
console.log('[Import] Duplicated space ID:', duplicateResult.space_id);
|
| 350 |
+
console.log('[Import] ==========================================');
|
| 351 |
+
|
| 352 |
+
if (onImport && importResult.code) {
|
| 353 |
+
console.log('[Import] Calling onImport with duplicated space URL:', duplicateResult.space_url);
|
| 354 |
+
// Pass the duplicated space URL so it's tracked for future deployments
|
| 355 |
+
onImport(importResult.code, importResult.language || 'html', duplicateResult.space_url);
|
| 356 |
+
|
| 357 |
+
// Show success message with link to duplicated space
|
| 358 |
+
alert(`β
Space duplicated successfully!\n\nYour space: ${duplicateResult.space_url}\n\nThe code has been loaded in the editor. Any changes you deploy will update this duplicated space.`);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
setShowImportDialog(false);
|
| 362 |
+
setImportUrl('');
|
| 363 |
+
} else {
|
| 364 |
+
setImportError(duplicateResult.message || 'Failed to duplicate space');
|
| 365 |
+
}
|
| 366 |
}
|
| 367 |
} else {
|
| 368 |
// Not a Space URL - fall back to regular import
|
|
|
|
| 806 |
<input
|
| 807 |
type="text"
|
| 808 |
value={importUrl}
|
| 809 |
+
onChange={(e) => {
|
| 810 |
+
setImportUrl(e.target.value);
|
| 811 |
+
checkSpaceOwnership(e.target.value);
|
| 812 |
+
}}
|
| 813 |
onKeyPress={(e) => e.key === 'Enter' && handleImportProject()}
|
| 814 |
placeholder="https://huggingface.co/spaces/..."
|
| 815 |
+
className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-3"
|
| 816 |
disabled={isImporting}
|
| 817 |
/>
|
| 818 |
+
|
| 819 |
+
{/* Import Action Options */}
|
| 820 |
+
{importUrl.includes('huggingface.co/spaces/') && (
|
| 821 |
+
<div className="mb-3 space-y-2">
|
| 822 |
+
<p className="text-[10px] font-medium text-[#86868b] mb-2">Import Mode:</p>
|
| 823 |
+
|
| 824 |
+
{/* Update Space (only for owners) */}
|
| 825 |
+
{isSpaceOwner && (
|
| 826 |
+
<label className="flex items-start gap-2 cursor-pointer group">
|
| 827 |
+
<input
|
| 828 |
+
type="radio"
|
| 829 |
+
checked={importAction === 'update'}
|
| 830 |
+
onChange={() => setImportAction('update')}
|
| 831 |
+
className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
|
| 832 |
+
disabled={isImporting}
|
| 833 |
+
/>
|
| 834 |
+
<div>
|
| 835 |
+
<span className="text-[11px] text-[#f5f5f7] font-medium">Update your space directly</span>
|
| 836 |
+
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 837 |
+
β
You own this space - changes will update it
|
| 838 |
+
</p>
|
| 839 |
+
</div>
|
| 840 |
+
</label>
|
| 841 |
+
)}
|
| 842 |
+
|
| 843 |
+
{/* Duplicate Space */}
|
| 844 |
+
<label className="flex items-start gap-2 cursor-pointer group">
|
| 845 |
+
<input
|
| 846 |
+
type="radio"
|
| 847 |
+
checked={importAction === 'duplicate'}
|
| 848 |
+
onChange={() => setImportAction('duplicate')}
|
| 849 |
+
className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
|
| 850 |
+
disabled={isImporting}
|
| 851 |
+
/>
|
| 852 |
+
<div>
|
| 853 |
+
<span className="text-[11px] text-[#f5f5f7] font-medium">Duplicate to your account</span>
|
| 854 |
+
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 855 |
+
Create a copy you can freely modify
|
| 856 |
+
</p>
|
| 857 |
+
</div>
|
| 858 |
+
</label>
|
| 859 |
+
|
| 860 |
+
{/* Create PR */}
|
| 861 |
+
<label className="flex items-start gap-2 cursor-pointer group">
|
| 862 |
+
<input
|
| 863 |
+
type="radio"
|
| 864 |
+
checked={importAction === 'pr'}
|
| 865 |
+
onChange={() => setImportAction('pr')}
|
| 866 |
+
className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
|
| 867 |
+
disabled={isImporting}
|
| 868 |
+
/>
|
| 869 |
+
<div>
|
| 870 |
+
<span className="text-[11px] text-[#f5f5f7] font-medium">Create Pull Request</span>
|
| 871 |
+
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 872 |
+
Propose changes to the original space
|
| 873 |
+
</p>
|
| 874 |
+
</div>
|
| 875 |
+
</label>
|
| 876 |
+
|
| 877 |
+
{importAction === 'pr' && (
|
| 878 |
+
<p className="text-[10px] text-[#86868b] ml-6 mt-1">
|
| 879 |
+
β οΈ Requires space owner to enable PRs
|
| 880 |
+
</p>
|
| 881 |
+
)}
|
| 882 |
+
</div>
|
| 883 |
+
)}
|
| 884 |
+
|
| 885 |
{importError && (
|
| 886 |
<p className="text-xs text-red-400 mb-2">{importError}</p>
|
| 887 |
)}
|
| 888 |
+
|
| 889 |
<div className="flex gap-2">
|
| 890 |
<button
|
| 891 |
onClick={handleImportProject}
|
|
|
|
| 899 |
setShowImportDialog(false);
|
| 900 |
setImportUrl('');
|
| 901 |
setImportError('');
|
| 902 |
+
setIsSpaceOwner(false);
|
| 903 |
+
setImportAction('duplicate');
|
| 904 |
}}
|
| 905 |
className="px-3 py-2 bg-[#2d2d30] text-[#f5f5f7] rounded-lg text-xs hover:bg-[#3d3d3f] font-medium"
|
| 906 |
>
|