Skip to content

Commit 4c17438

Browse files
authored
Merge pull request #811 from transformerlab/fix/remote-tasks-launch
Create tasks and launch separately , also adds upload directory and edit tasks
2 parents cc54ebd + f67a55f commit 4c17438

File tree

6 files changed

+701
-56
lines changed

6 files changed

+701
-56
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import React, { useRef, useState } from 'react';
2+
import {
3+
Button,
4+
FormControl,
5+
FormLabel,
6+
Sheet,
7+
Stack,
8+
Typography,
9+
Alert,
10+
CircularProgress,
11+
} from '@mui/joy';
12+
import { UploadIcon, FileIcon, XIcon } from 'lucide-react';
13+
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
14+
15+
interface DirectoryUploadProps {
16+
onUploadComplete?: (uploadedDirPath: string) => void;
17+
onUploadError?: (error: string) => void;
18+
disabled?: boolean;
19+
}
20+
21+
interface UploadedFile {
22+
name: string;
23+
path: string;
24+
size: number;
25+
}
26+
27+
export default function DirectoryUpload({
28+
onUploadComplete = () => {},
29+
onUploadError = () => {},
30+
disabled = false,
31+
}: DirectoryUploadProps) {
32+
const [isUploading, setIsUploading] = useState(false);
33+
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
34+
const [uploadedDirPath, setUploadedDirPath] = useState<string>('');
35+
const [uploadError, setUploadError] = useState<string>('');
36+
const fileInputRef = useRef<HTMLInputElement>(null);
37+
38+
const handleFileSelect = async (
39+
event: React.ChangeEvent<HTMLInputElement>,
40+
) => {
41+
const { files } = event.target;
42+
if (!files || files.length === 0) return;
43+
44+
setIsUploading(true);
45+
setUploadError('');
46+
47+
try {
48+
const formData = new FormData();
49+
50+
// Add all files to formData
51+
Array.from(files).forEach((file) => {
52+
formData.append('dir_files', file);
53+
});
54+
55+
// Add directory name (use the first file's directory path or a default name)
56+
const firstFile = files[0];
57+
const webkitRelativePath = firstFile.webkitRelativePath || '';
58+
const dirName = webkitRelativePath.split('/')[0] || 'uploaded_directory';
59+
formData.append('dir_name', dirName);
60+
61+
// Make the upload request using authenticated fetch
62+
const response = await chatAPI.authenticatedFetch(
63+
chatAPI.Endpoints.Jobs.UploadRemote(),
64+
{
65+
method: 'POST',
66+
body: formData,
67+
},
68+
);
69+
70+
if (!response.ok) {
71+
const errorText = await response.text();
72+
throw new Error(
73+
`Upload failed: ${response.status} ${response.statusText} - ${errorText}`,
74+
);
75+
}
76+
77+
const result = await response.json();
78+
79+
if (result.status === 'success') {
80+
const uploadedDirPathResult =
81+
result.data.uploaded_files.dir_files.uploaded_dir;
82+
setUploadedDirPath(uploadedDirPathResult);
83+
84+
// Update uploaded files list
85+
const filesList: UploadedFile[] = Array.from(files).map((file) => ({
86+
name: file.name,
87+
path: file.webkitRelativePath || file.name,
88+
size: file.size,
89+
}));
90+
setUploadedFiles(filesList);
91+
92+
onUploadComplete(uploadedDirPathResult);
93+
} else {
94+
const errorMessage = result.message || 'Upload failed';
95+
setUploadError(errorMessage);
96+
onUploadError(errorMessage);
97+
}
98+
} catch (error) {
99+
const errorMessage =
100+
error instanceof Error ? error.message : 'Upload failed';
101+
setUploadError(errorMessage);
102+
onUploadError(errorMessage);
103+
} finally {
104+
setIsUploading(false);
105+
}
106+
};
107+
108+
const handleDirectorySelect = () => {
109+
if (fileInputRef.current) {
110+
fileInputRef.current.click();
111+
}
112+
};
113+
114+
const handleRemoveUpload = () => {
115+
setUploadedFiles([]);
116+
setUploadedDirPath('');
117+
setUploadError('');
118+
if (fileInputRef.current) {
119+
fileInputRef.current.value = '';
120+
}
121+
};
122+
123+
const formatFileSize = (bytes: number): string => {
124+
if (bytes === 0) return '0 Bytes';
125+
const k = 1024;
126+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
127+
const i = Math.floor(Math.log(bytes) / Math.log(k));
128+
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
129+
};
130+
131+
return (
132+
<FormControl>
133+
<FormLabel>Directory Upload (Optional)</FormLabel>
134+
<input
135+
ref={fileInputRef}
136+
type="file"
137+
// eslint-disable-next-line react/jsx-props-no-spreading
138+
{...({ webkitdirectory: '' } as any)}
139+
multiple
140+
style={{ display: 'none' }}
141+
onChange={handleFileSelect}
142+
disabled={disabled || isUploading}
143+
/>
144+
145+
{uploadedFiles.length === 0 ? (
146+
<Sheet
147+
variant="soft"
148+
sx={{
149+
p: 2,
150+
borderRadius: 'md',
151+
border: '2px dashed',
152+
borderColor: 'neutral.300',
153+
textAlign: 'center',
154+
cursor: disabled || isUploading ? 'not-allowed' : 'pointer',
155+
opacity: disabled || isUploading ? 0.6 : 1,
156+
}}
157+
onClick={handleDirectorySelect}
158+
>
159+
<Stack spacing={1} alignItems="center">
160+
{isUploading ? (
161+
<>
162+
<CircularProgress size="sm" />
163+
<Typography level="body-sm">Uploading...</Typography>
164+
</>
165+
) : (
166+
<>
167+
<UploadIcon size={24} />
168+
<Typography level="body-sm">
169+
Click to select a directory to upload
170+
</Typography>
171+
<Typography level="body-xs" color="neutral">
172+
All files in the directory will be uploaded
173+
</Typography>
174+
</>
175+
)}
176+
</Stack>
177+
</Sheet>
178+
) : (
179+
<Sheet variant="soft" sx={{ p: 2, borderRadius: 'md' }}>
180+
<Stack spacing={1}>
181+
<Stack
182+
direction="row"
183+
justifyContent="space-between"
184+
alignItems="center"
185+
>
186+
<Typography level="title-sm">Uploaded Directory</Typography>
187+
{!disabled && (
188+
<Button
189+
size="sm"
190+
variant="plain"
191+
color="danger"
192+
onClick={handleRemoveUpload}
193+
startDecorator={<XIcon size={16} />}
194+
>
195+
Remove
196+
</Button>
197+
)}
198+
</Stack>
199+
200+
<Stack spacing={0.5}>
201+
<Typography level="body-xs" color="neutral">
202+
Files ({uploadedFiles.length}):
203+
</Typography>
204+
<Sheet
205+
variant="outlined"
206+
sx={{
207+
p: 1,
208+
maxHeight: '150px',
209+
overflow: 'auto',
210+
borderRadius: 'sm',
211+
}}
212+
>
213+
{uploadedFiles.slice(0, 10).map((file) => (
214+
<Stack
215+
key={file.path}
216+
direction="row"
217+
spacing={1}
218+
alignItems="center"
219+
sx={{ py: 0.5 }}
220+
>
221+
<FileIcon size={14} />
222+
<Typography level="body-xs" sx={{ flex: 1 }}>
223+
{file.path}
224+
</Typography>
225+
<Typography level="body-xs" color="neutral">
226+
{formatFileSize(file.size)}
227+
</Typography>
228+
</Stack>
229+
))}
230+
{uploadedFiles.length > 10 && (
231+
<Typography
232+
level="body-xs"
233+
color="neutral"
234+
sx={{ textAlign: 'center', py: 1 }}
235+
>
236+
... and {uploadedFiles.length - 10} more files
237+
</Typography>
238+
)}
239+
</Sheet>
240+
</Stack>
241+
</Stack>
242+
</Sheet>
243+
)}
244+
{uploadError && (
245+
<Alert color="danger" sx={{ mt: 1 }}>
246+
{uploadError}
247+
</Alert>
248+
)}
249+
</FormControl>
250+
);
251+
}

0 commit comments

Comments
 (0)