@@ -236,8 +241,21 @@ function UploadButton({ apiKey, maxImages, onSelect, onClear }) {
}
triggerContent = (
<>
-
+ )}
+ {!uploading && badge}
>
);
} else {
@@ -382,27 +400,40 @@ function UploadButton({ apiKey, maxImages, onSelect, onClear }) {
handleCellClick(entry)}
+ onClick={() => entry.url && handleCellClick(entry)}
className={`relative rounded-xl overflow-hidden border-2 cursor-pointer group/cell aspect-square transition-all ${
isSelected ? "border-primary shadow-glow" : "border-white/10 hover:border-white/30"
- } ${atMax ? "opacity-40 cursor-not-allowed" : ""}`}
+ } ${atMax ? "opacity-40 cursor-not-allowed" : ""} ${!entry.url ? "cursor-wait" : ""}`}
>
-

+ {entry.url ? (
+

+ ) : (
+
+ )}
{/* Hover overlay with delete */}
-
-
-
+ {entry.url && (
+
+
+
+ )}
{/* Selection badge */}
{isSelected && (
diff --git a/packages/studio/src/components/LipSyncStudio.jsx b/packages/studio/src/components/LipSyncStudio.jsx
index c58626a..6355299 100644
--- a/packages/studio/src/components/LipSyncStudio.jsx
+++ b/packages/studio/src/components/LipSyncStudio.jsx
@@ -19,7 +19,7 @@ const UPLOAD_STATE = {
READY: 'ready',
};
-function MediaPickerButton({ accept, label, icon, onUpload, onClear, uploadState, fileName, apiKey }) {
+function MediaPickerButton({ accept, label, icon, onUpload, onClear, uploadState, progress, fileName, previewUrl, isVideo, apiKey }) {
const inputRef = useRef(null);
const handleClick = (e) => {
@@ -72,18 +72,29 @@ function MediaPickerButton({ accept, label, icon, onUpload, onClear, uploadState
)}
- {/* Uploading spinner */}
+ {/* Uploading indicator */}
{uploadState === UPLOAD_STATE.UPLOADING && (
-
-
◌
+
)}
{/* Ready state */}
{uploadState === UPLOAD_STATE.READY && (
- {icon}
-
READY
+ {previewUrl ? (
+ isVideo ? (
+
+ ) : (
+

+ )
+ ) : (
+ <>
+ {icon}
+
READY
+ >
+ )}
)}
@@ -233,6 +244,11 @@ export default function LipSyncStudio({ apiKey, onGenerationComplete, historyIte
const [audioName, setAudioName] = useState('');
const [audioUrl, setAudioUrl] = useState(null);
+ // ── Individual progress states ──
+ const [imageProgress, setImageProgress] = useState(0);
+ const [videoProgress, setVideoProgress] = useState(0);
+ const [audioProgress, setAudioProgress] = useState(0);
+
// ── Prompt ──────────────────────────────────────────────────────────────
const [prompt, setPrompt] = useState('');
@@ -273,41 +289,68 @@ export default function LipSyncStudio({ apiKey, onGenerationComplete, historyIte
// ── Upload handlers ─────────────────────────────────────────────────────
const handleImageUpload = useCallback(async (file) => {
+ if (file.size > 10 * 1024 * 1024) {
+ alert("Image exceeds 10MB limit.");
+ return;
+ }
setImageState(UPLOAD_STATE.UPLOADING);
+ setImageProgress(0);
try {
- const url = await uploadFile(apiKey, file);
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setImageProgress(pct);
+ });
setImageUrl(url);
setImageName(file.name);
setImageState(UPLOAD_STATE.READY);
} catch (err) {
setImageState(UPLOAD_STATE.IDLE);
alert(`Image upload failed: ${err.message}`);
+ } finally {
+ setImageProgress(0);
}
}, [apiKey]);
const handleVideoPick = useCallback(async (file) => {
+ if (file.size > 50 * 1024 * 1024) {
+ alert("Video exceeds 50MB limit.");
+ return;
+ }
setVideoState(UPLOAD_STATE.UPLOADING);
+ setVideoProgress(0);
try {
- const url = await uploadFile(apiKey, file);
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setVideoProgress(pct);
+ });
setVideoUrl(url);
setVideoName(file.name);
setVideoState(UPLOAD_STATE.READY);
} catch (err) {
setVideoState(UPLOAD_STATE.IDLE);
alert(`Video upload failed: ${err.message}`);
+ } finally {
+ setVideoProgress(0);
}
}, [apiKey]);
const handleAudioPick = useCallback(async (file) => {
+ if (file.size > 10 * 1024 * 1024) {
+ alert("Audio file exceeds 10MB limit.");
+ return;
+ }
setAudioState(UPLOAD_STATE.UPLOADING);
+ setAudioProgress(0);
try {
- const url = await uploadFile(apiKey, file);
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setAudioProgress(pct);
+ });
setAudioUrl(url);
setAudioName(file.name);
setAudioState(UPLOAD_STATE.READY);
} catch (err) {
setAudioState(UPLOAD_STATE.IDLE);
alert(`Audio upload failed: ${err.message}`);
+ } finally {
+ setAudioProgress(0);
}
}, [apiKey]);
@@ -541,9 +584,16 @@ export default function LipSyncStudio({ apiKey, onGenerationComplete, historyIte
}
onUpload={handleImageUpload}
- onClear={() => { setImageUrl(null); setImageState(UPLOAD_STATE.IDLE); setImageName(''); }}
+ onClear={() => {
+ setImageUrl(null);
+ setImageState(UPLOAD_STATE.IDLE);
+ setImageName('');
+ }}
uploadState={imageState}
+ progress={imageProgress}
fileName={imageName}
+ previewUrl={imageUrl}
+ isVideo={false}
apiKey={apiKey}
/>
)}
@@ -555,9 +605,16 @@ export default function LipSyncStudio({ apiKey, onGenerationComplete, historyIte
label="Video"
icon={
}
onUpload={handleVideoPick}
- onClear={() => { setVideoUrl(null); setVideoState(UPLOAD_STATE.IDLE); setVideoName(''); }}
+ onClear={() => {
+ setVideoUrl(null);
+ setVideoState(UPLOAD_STATE.IDLE);
+ setVideoName('');
+ }}
uploadState={videoState}
+ progress={videoProgress}
fileName={videoName}
+ previewUrl={videoUrl}
+ isVideo={true}
apiKey={apiKey}
/>
)}
@@ -570,7 +627,10 @@ export default function LipSyncStudio({ apiKey, onGenerationComplete, historyIte
onUpload={handleAudioPick}
onClear={() => { setAudioUrl(null); setAudioState(UPLOAD_STATE.IDLE); setAudioName(''); }}
uploadState={audioState}
+ progress={audioProgress}
fileName={audioName}
+ previewUrl={null}
+ isVideo={false}
apiKey={apiKey}
/>
diff --git a/packages/studio/src/components/VideoStudio.jsx b/packages/studio/src/components/VideoStudio.jsx
index 06f4c4c..24d6d30 100644
--- a/packages/studio/src/components/VideoStudio.jsx
+++ b/packages/studio/src/components/VideoStudio.jsx
@@ -192,6 +192,10 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
const [selectedQuality, setSelectedQuality] = useState(defaultModel.inputs?.quality?.default || '');
const [selectedMode, setSelectedMode] = useState('');
+ // ── upload progress ──
+ const [imageProgress, setImageProgress] = useState(0);
+ const [videoProgress, setVideoProgress] = useState(0);
+
// ── control visibility ──
const [showAr, setShowAr] = useState(true);
const [showDuration, setShowDuration] = useState(true);
@@ -201,7 +205,6 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
// ── uploads ──
const [uploadedImageUrl, setUploadedImageUrl] = useState(null);
- const [uploadedImagePreview, setUploadedImagePreview] = useState(null);
const [imageUploading, setImageUploading] = useState(false);
const [uploadedVideoUrl, setUploadedVideoUrl] = useState(null);
const [videoUploading, setVideoUploading] = useState(false);
@@ -323,11 +326,18 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
const handleImageFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
+ if (file.size > 10 * 1024 * 1024) {
+ alert("Image exceeds 10MB limit.");
+ return;
+ }
setImageUploading(true);
+ setImageProgress(0);
+
try {
- const url = await uploadFile(apiKey, file);
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setImageProgress(pct);
+ });
setUploadedImageUrl(url);
- setUploadedImagePreview(URL.createObjectURL(file));
// Clear v2v if active
setUploadedVideoUrl(null);
@@ -347,13 +357,13 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
alert(`Image upload failed: ${err.message}`);
} finally {
setImageUploading(false);
+ setImageProgress(0);
if (imageFileInputRef.current) imageFileInputRef.current.value = '';
}
};
const clearImageUpload = () => {
setUploadedImageUrl(null);
- setUploadedImagePreview(null);
setImageMode(false);
const first = t2vModels[0];
setSelectedModel(first.id);
@@ -366,16 +376,22 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
const handleVideoFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
+ if (file.size > 50 * 1024 * 1024) {
+ alert("Video exceeds 50MB limit.");
+ return;
+ }
setVideoUploading(true);
+ setVideoProgress(0);
try {
- const url = await uploadFile(apiKey, file);
+ const url = await uploadFile(apiKey, file, (pct) => {
+ setVideoProgress(pct);
+ });
setUploadedVideoUrl(url);
setUploadedVideoName(file.name);
// Clear image mode if active
if (imageMode) {
setUploadedImageUrl(null);
- setUploadedImagePreview(null);
setImageMode(false);
}
setV2vMode(true);
@@ -390,6 +406,7 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
alert(`Video upload failed: ${err.message}`);
} finally {
setVideoUploading(false);
+ setVideoProgress(0);
if (videoFileInputRef.current) videoFileInputRef.current.value = '';
}
};
@@ -763,10 +780,15 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
className={`w-10 h-10 shrink-0 rounded-xl border transition-all flex items-center justify-center relative overflow-hidden ${uploadedImageUrl ? 'border-primary/60 bg-primary/10' : 'bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40'} group`}
>
{imageUploading ? (
-
◌
- ) : uploadedImageUrl ? (
-

- ) : (
+
+ ) : null}
+
+ {uploadedImageUrl ? (
+

+ ) : !imageUploading && (