Skip to content

Commit 02bcf32

Browse files
authored
Add push to HF hub feature (#32)
* organise utilites * fix push to hub API * fix frontend for push to hub * minor version bump --------- Co-authored-by: RETR0-OS <[email protected]>
1 parent 0812458 commit 02bcf32

34 files changed

+646
-100
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from "react";
2+
3+
const ErrorDialog = ({ isOpen, onClose, title = "Error", message, details = null }) => {
4+
if (!isOpen) return null;
5+
6+
return (
7+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
8+
<div className="bg-gray-900 rounded-2xl shadow-2xl max-w-md w-full">
9+
<div className="p-6 border-b border-gray-700">
10+
<div className="flex items-center justify-between">
11+
<div className="flex items-center">
12+
<svg
13+
className="w-6 h-6 text-red-400 mr-3"
14+
fill="none"
15+
stroke="currentColor"
16+
viewBox="0 0 24 24"
17+
>
18+
<path
19+
strokeLinecap="round"
20+
strokeLinejoin="round"
21+
strokeWidth="2"
22+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
23+
/>
24+
</svg>
25+
<h2 className="text-xl font-bold text-red-400">{title}</h2>
26+
</div>
27+
<button
28+
onClick={onClose}
29+
className="text-gray-400 hover:text-white transition-colors"
30+
>
31+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
32+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
33+
</svg>
34+
</button>
35+
</div>
36+
</div>
37+
38+
<div className="p-6">
39+
<p className="text-gray-200 mb-4">{message}</p>
40+
41+
{details && (
42+
<div className="mt-4">
43+
<details className="cursor-pointer">
44+
<summary className="text-orange-400 font-medium hover:text-orange-300 transition-colors">
45+
Show Details
46+
</summary>
47+
<div className="mt-2 p-3 bg-gray-800 rounded-lg text-sm text-gray-300 break-all">
48+
{details}
49+
</div>
50+
</details>
51+
</div>
52+
)}
53+
</div>
54+
55+
<div className="p-6 border-t border-gray-700">
56+
<button
57+
onClick={onClose}
58+
className="w-full px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors font-medium"
59+
>
60+
Close
61+
</button>
62+
</div>
63+
</div>
64+
</div>
65+
);
66+
};
67+
68+
export default ErrorDialog;
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import React, { useState } from "react";
2+
import { config } from "../services/api";
3+
import ErrorDialog from "./ErrorDialog";
4+
5+
// Large, prominent, centered modal for status/success
6+
const StatusModal = ({ isOpen, message, onClose, isSuccess = false }) => {
7+
if (!isOpen) return null;
8+
9+
// Extract HuggingFace URL from message if present
10+
const urlRegex = /(https:\/\/huggingface\.co\/[^\s]+)/g;
11+
const urls = message ? message.match(urlRegex) : [];
12+
13+
// Split message by URLs to render text and links separately
14+
const renderMessage = () => {
15+
if (!urls || urls.length === 0) {
16+
return <p className="text-orange-300 text-xl mb-8 text-center">{message}</p>;
17+
}
18+
19+
let parts = message.split(urlRegex);
20+
return (
21+
<div className="text-orange-300 text-xl mb-8 text-center">
22+
{parts.map((part, index) => {
23+
if (urls.includes(part)) {
24+
return (
25+
<a
26+
key={index}
27+
href={part}
28+
target="_blank"
29+
rel="noopener noreferrer"
30+
className="text-blue-400 underline hover:text-blue-300 break-all"
31+
>
32+
{part}
33+
</a>
34+
);
35+
}
36+
return <span key={index}>{part}</span>;
37+
})}
38+
</div>
39+
);
40+
};
41+
42+
return (
43+
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
44+
<div className="bg-gray-900 rounded-3xl shadow-2xl w-full max-w-2xl p-10 flex flex-col items-center border-4 border-orange-500">
45+
{isSuccess && <h2 className="text-3xl font-bold text-orange-400 mb-6 text-center">Success!</h2>}
46+
{renderMessage()}
47+
{onClose && (
48+
<button
49+
onClick={onClose}
50+
className="px-8 py-3 bg-orange-500 text-white rounded-xl hover:bg-orange-600 transition-colors font-bold text-lg shadow-lg"
51+
>
52+
Close
53+
</button>
54+
)}
55+
</div>
56+
</div>
57+
);
58+
};
59+
60+
const PushToHubForm = ({ model, onClose, onPush }) => {
61+
const [repoName, setRepoName] = useState("");
62+
const [isPrivate, setIsPrivate] = useState(true);
63+
const [loading, setLoading] = useState(false);
64+
const [error, setError] = useState("");
65+
const [validationError, setValidationError] = useState("");
66+
const [showErrorDialog, setShowErrorDialog] = useState(false);
67+
const [errorDetails, setErrorDetails] = useState(null);
68+
const [showSuccessModal, setShowSuccessModal] = useState(false);
69+
const [successMessage, setSuccessMessage] = useState("");
70+
71+
// Validate repo name format
72+
const validateRepoName = (name) => {
73+
const pattern = /^([A-Za-z_0-9-]+\/[A-Za-z_0-9-]+)$/;
74+
return pattern.test(name);
75+
};
76+
77+
const handleRepoNameChange = (e) => {
78+
const value = e.target.value;
79+
setRepoName(value);
80+
81+
if (value && !validateRepoName(value)) {
82+
setValidationError("Repository name must be in format 'username/repo-name' (letters, numbers, hyphens and underscores only)");
83+
} else {
84+
setValidationError("");
85+
}
86+
};
87+
88+
const handleSubmit = async (e) => {
89+
e.preventDefault();
90+
91+
if (!repoName.trim()) {
92+
setError("Repository name is required");
93+
setShowErrorDialog(true);
94+
return;
95+
}
96+
97+
if (!validateRepoName(repoName)) {
98+
setError("Invalid repository name format");
99+
setShowErrorDialog(true);
100+
return;
101+
}
102+
103+
setLoading(true);
104+
setError("");
105+
106+
try {
107+
const response = await fetch(config.baseURL + "/hub/push", {
108+
method: "POST",
109+
headers: {
110+
"Content-Type": "application/json",
111+
},
112+
body: JSON.stringify({
113+
repo_name: repoName,
114+
model_path: model.model_path,
115+
private: isPrivate,
116+
}),
117+
});
118+
119+
const data = await response.json();
120+
121+
if (!response.ok) {
122+
setLoading(false); // Stop loading immediately on error
123+
const errorMessage = data.error || "Failed to push model to hub";
124+
setError(errorMessage);
125+
setErrorDetails(data.details || `HTTP ${response.status}: ${response.statusText}`);
126+
setShowErrorDialog(true);
127+
return;
128+
}
129+
130+
// Success case - show success modal after a brief delay
131+
setTimeout(() => {
132+
setLoading(false);
133+
setSuccessMessage(data.message || "Model pushed to Hub successfully!");
134+
setShowSuccessModal(true);
135+
}, 2000);
136+
137+
onPush && onPush(data.message);
138+
} catch (err) {
139+
setLoading(false); // Stop loading immediately on network error
140+
setError("Network error or unexpected failure");
141+
setErrorDetails(err.message);
142+
setShowErrorDialog(true);
143+
}
144+
};
145+
146+
const handleCloseErrorDialog = () => {
147+
setShowErrorDialog(false);
148+
setError("");
149+
setErrorDetails(null);
150+
};
151+
152+
return (
153+
<>
154+
{/* Loading modal */}
155+
<StatusModal
156+
isOpen={loading}
157+
message="Pushing to Hub. This might take a while."
158+
/>
159+
160+
{/* Success modal */}
161+
<StatusModal
162+
isOpen={showSuccessModal}
163+
message={successMessage}
164+
isSuccess={true}
165+
onClose={() => {
166+
setShowSuccessModal(false);
167+
onClose();
168+
}}
169+
/>
170+
171+
{/* Main form modal */}
172+
{!loading && !showSuccessModal && (
173+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
174+
<div className="bg-gray-900 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
175+
<div className="p-6 border-b border-gray-700">
176+
<h2 className="text-2xl font-bold text-orange-400">Push Model to HuggingFace Hub</h2>
177+
<button
178+
onClick={onClose}
179+
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
180+
>
181+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
182+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
183+
</svg>
184+
</button>
185+
</div>
186+
187+
<form onSubmit={handleSubmit} className="p-6 space-y-6">
188+
{/* Repository Name */}
189+
<div>
190+
<label className="block text-orange-400 font-semibold mb-2">
191+
Repository Name *
192+
</label>
193+
<input
194+
type="text"
195+
value={repoName}
196+
onChange={handleRepoNameChange}
197+
placeholder="username/model-name or model-name"
198+
className="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
199+
required
200+
/>
201+
{validationError && (
202+
<p className="text-red-400 text-sm mt-1">{validationError}</p>
203+
)}
204+
<p className="text-gray-400 text-sm mt-1">
205+
Format: 'username/repo-name' or 'repo-name' (letters and underscores only)
206+
</p>
207+
</div>
208+
209+
{/* Privacy Setting */}
210+
<div>
211+
<label className="flex items-center space-x-3">
212+
<input
213+
type="checkbox"
214+
checked={isPrivate}
215+
onChange={(e) => setIsPrivate(e.target.checked)}
216+
className="w-5 h-5 text-orange-500 bg-gray-800 border-gray-600 rounded focus:ring-orange-500 focus:ring-2"
217+
/>
218+
<span className="text-orange-400 font-semibold">Make repository private</span>
219+
</label>
220+
<p className="text-gray-400 text-sm mt-1 ml-8">
221+
{isPrivate ? "Repository will be private" : "Repository will be public"}
222+
</p>
223+
</div>
224+
225+
{/* Read-only Model Information */}
226+
<div className="space-y-4 border-t border-gray-700 pt-6">
227+
<h3 className="text-lg font-semibold text-orange-400">Model Information</h3>
228+
229+
<div>
230+
<label className="block text-gray-300 font-medium mb-1">Model Name</label>
231+
<div className="px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200">
232+
{model.model_name}
233+
</div>
234+
</div>
235+
236+
<div>
237+
<label className="block text-gray-300 font-medium mb-1">Base Model</label>
238+
<div className="px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200 break-all">
239+
{model.base_model}
240+
</div>
241+
</div>
242+
243+
<div>
244+
<label className="block text-gray-300 font-medium mb-1">Task Type</label>
245+
<div className="px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200">
246+
{model.task}
247+
</div>
248+
</div>
249+
250+
<div>
251+
<label className="block text-gray-300 font-medium mb-1">Local Model Path</label>
252+
<div className="px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200 break-all text-sm">
253+
{model.model_path}
254+
</div>
255+
</div>
256+
257+
<div>
258+
<label className="block text-gray-300 font-medium mb-1">Description</label>
259+
<div className="px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200">
260+
{model.description || "No description provided"}
261+
</div>
262+
</div>
263+
</div>
264+
265+
{/* Error Display */}
266+
{error && (
267+
<div className="text-red-400 bg-red-900/20 border border-red-700 rounded-lg p-3">
268+
{error}
269+
</div>
270+
)}
271+
272+
{/* Action Buttons */}
273+
<div className="flex gap-4 pt-4">
274+
<button
275+
type="button"
276+
onClick={onClose}
277+
className="flex-1 px-6 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
278+
>
279+
Cancel
280+
</button>
281+
<button
282+
type="submit"
283+
disabled={loading || !!validationError || !repoName.trim()}
284+
className="flex-1 px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
285+
>
286+
{loading ? (
287+
<>
288+
<svg className="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
289+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
290+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
291+
</svg>
292+
Pushing...
293+
</>
294+
) : (
295+
"Push to Hub"
296+
)}
297+
</button>
298+
</div>
299+
</form>
300+
</div>
301+
</div>
302+
)}
303+
304+
<ErrorDialog
305+
isOpen={showErrorDialog}
306+
onClose={handleCloseErrorDialog}
307+
title="Push to Hub Error"
308+
message={error}
309+
details={errorDetails}
310+
/>
311+
</>
312+
);
313+
};
314+
315+
export default PushToHubForm;

0 commit comments

Comments
 (0)