|  | 
|  | 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