Spaces:
Running
Running
| import React, { useState } from "react"; | |
| import { useNavigate, useLocation } from "react-router-dom"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { useToast } from "@/hooks/use-toast"; | |
| import { Checkbox } from "@/components/ui/checkbox"; | |
| import { | |
| ArrowLeft, | |
| Upload as UploadIcon, | |
| Database, | |
| Tag, | |
| Eye, | |
| EyeOff, | |
| ExternalLink, | |
| CheckCircle, | |
| AlertCircle, | |
| Loader2, | |
| } from "lucide-react"; | |
| import { useApi } from "@/contexts/ApiContext"; | |
| interface DatasetInfo { | |
| dataset_repo_id: string; | |
| single_task: string; | |
| num_episodes: number; | |
| saved_episodes?: number; | |
| session_elapsed_seconds?: number; | |
| fps?: number; | |
| total_frames?: number; | |
| robot_type?: string; | |
| } | |
| interface UploadConfig { | |
| tags: string[]; | |
| private: boolean; | |
| } | |
| const Upload = () => { | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| const { toast } = useToast(); | |
| const { baseUrl, fetchWithHeaders } = useApi(); | |
| // Get initial dataset info from navigation state | |
| const initialDatasetInfo = location.state?.datasetInfo as DatasetInfo; | |
| // State for actual dataset info (will be loaded from backend) | |
| const [datasetInfo, setDatasetInfo] = useState<DatasetInfo | null>(null); | |
| const [isLoadingDatasetInfo, setIsLoadingDatasetInfo] = useState(true); | |
| // Upload configuration state | |
| const [uploadConfig, setUploadConfig] = useState<UploadConfig>({ | |
| tags: ["robotics", "lerobot"], | |
| private: false, | |
| }); | |
| const [tagsInput, setTagsInput] = useState(uploadConfig.tags.join(", ")); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const [uploadSuccess, setUploadSuccess] = useState(false); | |
| // Load actual dataset information from backend | |
| React.useEffect(() => { | |
| const loadDatasetInfo = async () => { | |
| if (!initialDatasetInfo?.dataset_repo_id) { | |
| toast({ | |
| title: "No Dataset Information", | |
| description: "Please complete a recording session first.", | |
| variant: "destructive", | |
| }); | |
| navigate("/"); | |
| return; | |
| } | |
| try { | |
| const response = await fetchWithHeaders(`${baseUrl}/dataset-info`, { | |
| method: "POST", | |
| body: JSON.stringify({ | |
| dataset_repo_id: initialDatasetInfo.dataset_repo_id, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| // Merge the loaded dataset info with any session info we have | |
| setDatasetInfo({ | |
| ...data, | |
| saved_episodes: data.num_episodes, // Use actual episodes from dataset | |
| session_elapsed_seconds: | |
| initialDatasetInfo.session_elapsed_seconds || 0, | |
| }); | |
| } else { | |
| // Fallback to initial dataset info if backend fails | |
| toast({ | |
| title: "Warning", | |
| description: | |
| "Could not load complete dataset information. Using session data.", | |
| variant: "destructive", | |
| }); | |
| setDatasetInfo(initialDatasetInfo); | |
| } | |
| } catch (error) { | |
| console.error("Error loading dataset info:", error); | |
| // Fallback to initial dataset info | |
| toast({ | |
| title: "Warning", | |
| description: "Could not connect to backend. Using session data.", | |
| variant: "destructive", | |
| }); | |
| setDatasetInfo(initialDatasetInfo); | |
| } finally { | |
| setIsLoadingDatasetInfo(false); | |
| } | |
| }; | |
| loadDatasetInfo(); | |
| }, [initialDatasetInfo, navigate, toast]); | |
| const formatDuration = (seconds: number): string => { | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = seconds % 60; | |
| if (hours > 0) { | |
| return `${hours}h ${minutes}m ${secs}s`; | |
| } else if (minutes > 0) { | |
| return `${minutes}m ${secs}s`; | |
| } else { | |
| return `${secs}s`; | |
| } | |
| }; | |
| const handleUploadToHub = async () => { | |
| if (!datasetInfo) return; | |
| setIsUploading(true); | |
| try { | |
| // Parse tags from input | |
| const tags = tagsInput | |
| .split(",") | |
| .map((tag) => tag.trim()) | |
| .filter((tag) => tag.length > 0); | |
| const response = await fetchWithHeaders(`${baseUrl}/upload-dataset`, { | |
| method: "POST", | |
| body: JSON.stringify({ | |
| dataset_repo_id: datasetInfo.dataset_repo_id, | |
| tags, | |
| private: uploadConfig.private, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| setUploadSuccess(true); | |
| toast({ | |
| title: "Upload Successful!", | |
| description: `Dataset ${datasetInfo.dataset_repo_id} has been uploaded to HuggingFace Hub.`, | |
| }); | |
| } else { | |
| toast({ | |
| title: "Upload Failed", | |
| description: | |
| data.message || "Failed to upload dataset to HuggingFace Hub.", | |
| variant: "destructive", | |
| }); | |
| } | |
| } catch (error) { | |
| console.error("Error uploading dataset:", error); | |
| toast({ | |
| title: "Connection Error", | |
| description: "Could not connect to the backend server.", | |
| variant: "destructive", | |
| }); | |
| } finally { | |
| setIsUploading(false); | |
| } | |
| }; | |
| const handleSkipUpload = () => { | |
| toast({ | |
| title: "Upload Skipped", | |
| description: "Dataset saved locally. You can upload it manually later.", | |
| }); | |
| navigate("/"); | |
| }; | |
| // Show loading state while fetching dataset info | |
| if (isLoadingDatasetInfo || !datasetInfo) { | |
| return ( | |
| <div className="min-h-screen bg-black text-white flex items-center justify-center"> | |
| <div className="text-center"> | |
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div> | |
| <p className="text-lg">Loading dataset information...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen bg-black text-white p-8"> | |
| <div className="max-w-4xl mx-auto"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between mb-8"> | |
| <Button | |
| onClick={() => navigate("/")} | |
| variant="outline" | |
| className="border-gray-500 hover:border-gray-200 text-gray-300 hover:text-white" | |
| > | |
| <ArrowLeft className="w-4 h-4 mr-2" /> | |
| Back to Home | |
| </Button> | |
| <div className="flex items-center gap-3"> | |
| {uploadSuccess ? ( | |
| <CheckCircle className="w-8 h-8 text-green-500" /> | |
| ) : ( | |
| <Database className="w-8 h-8 text-blue-500" /> | |
| )} | |
| <h1 className="text-3xl font-bold"> | |
| {uploadSuccess ? "Upload Complete" : "Dataset Upload"} | |
| </h1> | |
| </div> | |
| </div> | |
| {/* Success State */} | |
| {uploadSuccess && ( | |
| <div className="bg-green-900/20 border border-green-600 rounded-lg p-6 mb-8"> | |
| <div className="flex items-center gap-3 mb-4"> | |
| <CheckCircle className="w-6 h-6 text-green-500" /> | |
| <h2 className="text-xl font-semibold text-green-400"> | |
| Successfully Uploaded! | |
| </h2> | |
| </div> | |
| <p className="text-gray-300 mb-4"> | |
| Your dataset has been uploaded to HuggingFace Hub and is now | |
| available for training and sharing. | |
| </p> | |
| <div className="flex flex-col sm:flex-row gap-4"> | |
| <Button | |
| onClick={() => | |
| window.open( | |
| `https://huggingface.co/datasets/${datasetInfo.dataset_repo_id}`, | |
| "_blank" | |
| ) | |
| } | |
| className="bg-blue-500 hover:bg-blue-600 text-white" | |
| > | |
| <ExternalLink className="w-4 h-4 mr-2" /> | |
| View on HuggingFace Hub | |
| </Button> | |
| <Button | |
| onClick={() => | |
| navigate("/training", { | |
| state: { datasetRepoId: datasetInfo.dataset_repo_id }, | |
| }) | |
| } | |
| className="bg-purple-500 hover:bg-purple-600 text-white" | |
| > | |
| Start Training | |
| </Button> | |
| <Button | |
| onClick={() => navigate("/")} | |
| variant="outline" | |
| className="border-gray-600 text-gray-300 hover:bg-gray-800 hover:text-white" | |
| > | |
| Return to Home | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Upload Form */} | |
| {!uploadSuccess && ( | |
| <> | |
| {/* Dataset Summary */} | |
| <div className="bg-gray-900 rounded-lg p-6 border border-gray-700 mb-8"> | |
| <h2 className="text-xl font-semibold text-white mb-4"> | |
| Dataset Summary | |
| </h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div className="space-y-3"> | |
| <div> | |
| <span className="text-gray-400">Repository ID:</span> | |
| <p className="text-white font-mono text-lg"> | |
| {datasetInfo.dataset_repo_id} | |
| </p> | |
| </div> | |
| <div> | |
| <span className="text-gray-400">Task:</span> | |
| <p className="text-white">{datasetInfo.single_task}</p> | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <div> | |
| <span className="text-gray-400">Episodes Recorded:</span> | |
| <p className="text-white text-2xl font-bold text-green-400"> | |
| {datasetInfo.saved_episodes || datasetInfo.num_episodes} | |
| </p> | |
| {datasetInfo.total_frames && ( | |
| <p className="text-gray-400 text-sm"> | |
| {datasetInfo.total_frames} total frames | |
| </p> | |
| )} | |
| </div> | |
| <div> | |
| <span className="text-gray-400">Session Duration:</span> | |
| <p className="text-white"> | |
| {formatDuration(datasetInfo.session_elapsed_seconds || 0)} | |
| </p> | |
| {datasetInfo.fps && ( | |
| <p className="text-gray-400 text-sm"> | |
| {datasetInfo.fps} FPS | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Upload Configuration */} | |
| <div className="bg-gray-900 rounded-lg p-6 border border-gray-700 mb-8"> | |
| <h2 className="text-xl font-semibold text-white mb-6"> | |
| Upload Configuration | |
| </h2> | |
| <div className="space-y-6"> | |
| {/* Tags */} | |
| <div> | |
| <Label htmlFor="tags" className="text-gray-300 mb-2 block"> | |
| Tags (comma-separated) | |
| </Label> | |
| <Input | |
| id="tags" | |
| value={tagsInput} | |
| onChange={(e) => setTagsInput(e.target.value)} | |
| placeholder="robotics, lerobot, manipulation" | |
| className="bg-gray-800 border-gray-600 text-white" | |
| /> | |
| <p className="text-sm text-gray-500 mt-1"> | |
| Tags help others discover your dataset on HuggingFace Hub | |
| </p> | |
| </div> | |
| {/* Privacy Setting */} | |
| <div className="flex items-center space-x-3"> | |
| <Checkbox | |
| id="private" | |
| checked={uploadConfig.private} | |
| onCheckedChange={(checked) => | |
| setUploadConfig({ | |
| ...uploadConfig, | |
| private: checked as boolean, | |
| }) | |
| } | |
| /> | |
| <div className="flex items-center gap-2"> | |
| {uploadConfig.private ? ( | |
| <EyeOff className="w-4 h-4 text-gray-400" /> | |
| ) : ( | |
| <Eye className="w-4 h-4 text-gray-400" /> | |
| )} | |
| <Label htmlFor="private" className="text-gray-300"> | |
| Make dataset private | |
| </Label> | |
| </div> | |
| </div> | |
| <p className="text-sm text-gray-500 ml-6"> | |
| {uploadConfig.private | |
| ? "Only you will be able to access this dataset" | |
| : "Dataset will be publicly accessible on HuggingFace Hub"} | |
| </p> | |
| </div> | |
| </div> | |
| {/* Action Buttons */} | |
| <div className="flex flex-col sm:flex-row gap-4 justify-center"> | |
| <Button | |
| onClick={handleUploadToHub} | |
| disabled={isUploading} | |
| className="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-4 px-8 text-lg" | |
| > | |
| {isUploading ? ( | |
| <> | |
| <Loader2 className="w-5 h-5 mr-2 animate-spin" /> | |
| Uploading to Hub... | |
| </> | |
| ) : ( | |
| <> | |
| <UploadIcon className="w-5 h-5 mr-2" /> | |
| Upload to HuggingFace Hub | |
| </> | |
| )} | |
| </Button> | |
| <Button | |
| onClick={handleSkipUpload} | |
| disabled={isUploading} | |
| variant="outline" | |
| className="border-gray-600 text-gray-300 hover:bg-gray-800 hover:text-white py-4 px-8 text-lg" | |
| > | |
| Skip Upload | |
| </Button> | |
| </div> | |
| {/* Info Box */} | |
| <div className="mt-8 p-4 bg-blue-900/20 border border-blue-600 rounded-lg"> | |
| <div className="flex items-start gap-3"> | |
| <AlertCircle className="w-5 h-5 text-blue-400 mt-0.5" /> | |
| <div> | |
| <h3 className="font-semibold text-blue-400 mb-2"> | |
| About HuggingFace Hub Upload | |
| </h3> | |
| <ul className="text-sm text-gray-300 space-y-1"> | |
| <li> | |
| • Your dataset will be uploaded to HuggingFace Hub for | |
| sharing and collaboration | |
| </li> | |
| <li> | |
| • You need to be logged in to HuggingFace CLI on the | |
| server | |
| </li> | |
| <li> | |
| • Uploaded datasets can be used for training models and | |
| sharing with the community | |
| </li> | |
| <li> | |
| • You can always upload manually later using the | |
| HuggingFace CLI | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Upload; | |