akhaliq HF Staff commited on
Commit
c8820b9
Β·
1 Parent(s): b301cbe
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 for duplication
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 - will duplicate:', fromSpaceId);
260
- console.log('[Import] Calling apiClient.duplicateSpace...');
261
 
262
- const duplicateResult = await apiClient.duplicateSpace(fromSpaceId);
263
- console.log('[Import] Duplicate API response:', duplicateResult);
264
 
265
- if (duplicateResult.success) {
266
- console.log('[Import] ========== DUPLICATE SUCCESS ==========');
267
- console.log('[Import] Duplicated space URL:', duplicateResult.space_url);
268
- console.log('[Import] Duplicated space ID:', duplicateResult.space_id);
269
- console.log('[Import] ==========================================');
 
 
 
 
 
270
 
271
- // Also load the code in the editor
272
- const importResult = await apiClient.importProject(importUrl);
273
- if (importResult.status === 'success' && onImport && importResult.code) {
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
- // Show success message with link to duplicated space
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
- setImportError(duplicateResult.message || 'Failed to duplicate space');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) => setImportUrl(e.target.value)}
 
 
 
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-2"
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
  >