Twan07 commited on
Commit
540f559
·
verified ·
1 Parent(s): a263b2b

Upload 5 files

Browse files
components/ConfigPanel.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Key, FolderGit2, CheckCircle2, AlertCircle, Database, Box, Layout } from 'lucide-react';
3
+ import { HFConfig, RepoType } from '../types';
4
+
5
+ interface ConfigPanelProps {
6
+ config: HFConfig;
7
+ onConfigChange: (newConfig: HFConfig) => void;
8
+ }
9
+
10
+ export const ConfigPanel: React.FC<ConfigPanelProps> = ({ config, onConfigChange }) => {
11
+ const [showToken, setShowToken] = useState(false);
12
+
13
+ const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
14
+ onConfigChange({ ...config, token: e.target.value });
15
+ };
16
+
17
+ const handleRepoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
18
+ onConfigChange({ ...config, repo: e.target.value });
19
+ };
20
+
21
+ const handleTypeChange = (type: RepoType) => {
22
+ onConfigChange({ ...config, repoType: type });
23
+ };
24
+
25
+ const isValid = config.token.length > 0 && config.repo.includes('/');
26
+
27
+ return (
28
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
29
+ <h2 className="text-xl font-semibold mb-4 text-gray-800 flex items-center gap-2">
30
+ <Key className="w-5 h-5 text-yellow-500" />
31
+ Authentication & Target
32
+ </h2>
33
+
34
+ <div className="space-y-5">
35
+ {/* Repo Type Selector */}
36
+ <div>
37
+ <label className="block text-sm font-medium text-gray-700 mb-2">
38
+ Repository Type
39
+ </label>
40
+ <div className="flex bg-gray-100 p-1 rounded-lg">
41
+ <button
42
+ onClick={() => handleTypeChange('model')}
43
+ className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all ${
44
+ config.repoType === 'model'
45
+ ? 'bg-white text-gray-900 shadow-sm'
46
+ : 'text-gray-500 hover:text-gray-700'
47
+ }`}
48
+ >
49
+ <Box className="w-4 h-4" />
50
+ Model
51
+ </button>
52
+ <button
53
+ onClick={() => handleTypeChange('dataset')}
54
+ className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all ${
55
+ config.repoType === 'dataset'
56
+ ? 'bg-white text-red-600 shadow-sm'
57
+ : 'text-gray-500 hover:text-gray-700'
58
+ }`}
59
+ >
60
+ <Database className="w-4 h-4" />
61
+ Dataset
62
+ </button>
63
+ <button
64
+ onClick={() => handleTypeChange('space')}
65
+ className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all ${
66
+ config.repoType === 'space'
67
+ ? 'bg-white text-blue-600 shadow-sm'
68
+ : 'text-gray-500 hover:text-gray-700'
69
+ }`}
70
+ >
71
+ <Layout className="w-4 h-4" />
72
+ Space
73
+ </button>
74
+ </div>
75
+ </div>
76
+
77
+ {/* Token Input */}
78
+ <div>
79
+ <label className="block text-sm font-medium text-gray-700 mb-1">
80
+ Hugging Face Access Token (Write)
81
+ </label>
82
+ <div className="relative">
83
+ <input
84
+ type={showToken ? "text" : "password"}
85
+ value={config.token}
86
+ onChange={handleTokenChange}
87
+ placeholder="hf_..."
88
+ className="w-full pl-10 pr-12 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-yellow-400 focus:border-transparent outline-none transition-all"
89
+ />
90
+ <Key className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" />
91
+ <button
92
+ type="button"
93
+ onClick={() => setShowToken(!showToken)}
94
+ className="absolute right-3 top-2.5 text-xs font-semibold text-gray-500 hover:text-gray-700"
95
+ >
96
+ {showToken ? "HIDE" : "SHOW"}
97
+ </button>
98
+ </div>
99
+ <p className="mt-1 text-xs text-gray-500">
100
+ Get your token from <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">HF Settings</a>. Ensure it has <strong>WRITE</strong> permissions.
101
+ </p>
102
+ </div>
103
+
104
+ {/* Repo Input */}
105
+ <div>
106
+ <label className="block text-sm font-medium text-gray-700 mb-1">
107
+ Repository ID
108
+ </label>
109
+ <div className="relative">
110
+ <input
111
+ type="text"
112
+ value={config.repo}
113
+ onChange={handleRepoChange}
114
+ placeholder={config.repoType === 'space' ? "username/space-name" : (config.repoType === 'dataset' ? "username/dataset-name" : "username/model-name")}
115
+ className="w-full pl-10 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-yellow-400 focus:border-transparent outline-none transition-all"
116
+ />
117
+ <FolderGit2 className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" />
118
+ </div>
119
+ <p className="mt-1 text-xs text-gray-500">
120
+ Targeting: <span className="font-semibold capitalize">{config.repoType}</span>. Example: <code>jdoe/my-{config.repoType}</code>
121
+ </p>
122
+ </div>
123
+
124
+ <div className={`flex items-center gap-2 text-sm p-3 rounded-lg ${isValid ? 'bg-green-50 text-green-700 border border-green-100' : 'bg-gray-50 text-gray-500 border border-gray-200'}`}>
125
+ {isValid ? <CheckCircle2 className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />}
126
+ {isValid ? `Ready to upload to ${config.repoType}` : "Enter a valid token and repo ID to start."}
127
+ </div>
128
+ </div>
129
+ </div>
130
+ );
131
+ };
components/FilePreview.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { X, FileText, Image as ImageIcon, Code } from 'lucide-react';
3
+
4
+ interface FilePreviewProps {
5
+ isOpen: boolean;
6
+ fileName: string;
7
+ blob: Blob | null;
8
+ onClose: () => void;
9
+ }
10
+
11
+ export const FilePreview: React.FC<FilePreviewProps> = ({ isOpen, fileName, blob, onClose }) => {
12
+ const [content, setContent] = useState<string | null>(null);
13
+ const [objectUrl, setObjectUrl] = useState<string | null>(null);
14
+
15
+ useEffect(() => {
16
+ if (!blob) return;
17
+
18
+ const type = blob.type;
19
+
20
+ // Handle Images
21
+ if (type.startsWith('image/')) {
22
+ const url = URL.createObjectURL(blob);
23
+ setObjectUrl(url);
24
+ return () => URL.revokeObjectURL(url);
25
+ }
26
+
27
+ // Handle Text/JSON/Code
28
+ if (type.startsWith('text/') || type.includes('json') || type.includes('javascript') || type.includes('xml')) {
29
+ blob.text().then(text => setContent(text));
30
+ } else {
31
+ // Fallback for unknown text-like files
32
+ blob.text().then(text => setContent(text));
33
+ }
34
+ }, [blob]);
35
+
36
+ if (!isOpen || !blob) return null;
37
+
38
+ const isImage = blob.type.startsWith('image/');
39
+
40
+ return (
41
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
42
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden">
43
+ {/* Header */}
44
+ <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
45
+ <div className="flex items-center gap-3">
46
+ <div className="p-2 bg-yellow-100 rounded-lg text-yellow-600">
47
+ {isImage ? <ImageIcon className="w-5 h-5" /> : <FileText className="w-5 h-5" />}
48
+ </div>
49
+ <div>
50
+ <h3 className="font-semibold text-gray-900 truncate max-w-md">{fileName}</h3>
51
+ <p className="text-xs text-gray-500 uppercase tracking-wider">{blob.type || 'Unknown Type'}</p>
52
+ </div>
53
+ </div>
54
+ <button
55
+ onClick={onClose}
56
+ className="p-2 text-gray-400 hover:text-gray-900 hover:bg-gray-200 rounded-full transition-colors"
57
+ >
58
+ <X className="w-5 h-5" />
59
+ </button>
60
+ </div>
61
+
62
+ {/* Content */}
63
+ <div className="flex-1 overflow-auto p-6 bg-gray-50/50 flex items-center justify-center min-h-[300px]">
64
+ {isImage && objectUrl && (
65
+ <img src={objectUrl} alt={fileName} className="max-w-full max-h-full rounded-lg shadow-sm object-contain" />
66
+ )}
67
+
68
+ {!isImage && content && (
69
+ <div className="w-full h-full bg-white border border-gray-200 rounded-lg p-4 overflow-auto shadow-inner">
70
+ <pre className="text-sm font-mono text-gray-800 whitespace-pre-wrap break-words">
71
+ {content.slice(0, 50000)}
72
+ {content.length > 50000 && <span className="text-gray-400 block mt-2 italic">...Content truncated...</span>}
73
+ </pre>
74
+ </div>
75
+ )}
76
+
77
+ {!isImage && !content && (
78
+ <div className="text-center text-gray-500">
79
+ <p>Binary file or loading...</p>
80
+ </div>
81
+ )}
82
+ </div>
83
+
84
+ {/* Footer */}
85
+ <div className="px-6 py-4 border-t border-gray-100 bg-white flex justify-end">
86
+ <button
87
+ onClick={onClose}
88
+ className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium rounded-lg transition-colors"
89
+ >
90
+ Close Preview
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
components/FileUploader.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useState } from 'react';
2
+ import { UploadCloud, FileUp } from 'lucide-react';
3
+ import { FileItem, UploadStatus } from '../types';
4
+
5
+ const generateId = () => Math.random().toString(36).substring(2, 15);
6
+
7
+ interface FileUploaderProps {
8
+ onFilesAdded: (files: FileItem[]) => void;
9
+ disabled: boolean;
10
+ }
11
+
12
+ const sanitizeFileName = (fileName: string): string => {
13
+ const timestamp = Date.now();
14
+ const lastDotIndex = fileName.lastIndexOf('.');
15
+ const name = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;
16
+ const ext = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : '';
17
+
18
+ let cleanName = name;
19
+ cleanName = cleanName.replace(/^\d+[-_.\s]*/, '');
20
+ cleanName = cleanName.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
21
+ .replace(/đ/g, 'd').replace(/Đ/g, 'D');
22
+ cleanName = cleanName.replace(/[^a-zA-Z0-9]/g, '-');
23
+ cleanName = cleanName.replace(/-+/g, '-').replace(/^-|-$/g, '');
24
+ if (cleanName.length === 0) cleanName = 'file';
25
+
26
+ return `${timestamp}-${cleanName}${ext}`.toLowerCase();
27
+ };
28
+
29
+ export const FileUploader: React.FC<FileUploaderProps> = ({ onFilesAdded, disabled }) => {
30
+ const [isDragging, setIsDragging] = useState(false);
31
+
32
+ const handleDragOver = useCallback((e: React.DragEvent) => {
33
+ e.preventDefault();
34
+ if (!disabled) setIsDragging(true);
35
+ }, [disabled]);
36
+
37
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
38
+ e.preventDefault();
39
+ setIsDragging(false);
40
+ }, []);
41
+
42
+ const handleDrop = useCallback((e: React.DragEvent) => {
43
+ e.preventDefault();
44
+ setIsDragging(false);
45
+ if (disabled) return;
46
+
47
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
48
+ processFiles(e.dataTransfer.files);
49
+ }
50
+ }, [disabled]);
51
+
52
+ const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ if (e.target.files && e.target.files.length > 0) {
54
+ processFiles(e.target.files);
55
+ e.target.value = ''; // Reset input
56
+ }
57
+ };
58
+
59
+ const processFiles = (fileList: FileList) => {
60
+ const newFiles: FileItem[] = Array.from(fileList).map(file => {
61
+ const cleanPath = sanitizeFileName(file.name);
62
+ return {
63
+ id: generateId(),
64
+ file,
65
+ path: cleanPath,
66
+ status: UploadStatus.IDLE
67
+ };
68
+ });
69
+ onFilesAdded(newFiles);
70
+ };
71
+
72
+ return (
73
+ <div
74
+ onDragOver={handleDragOver}
75
+ onDragLeave={handleDragLeave}
76
+ onDrop={handleDrop}
77
+ className={`
78
+ relative group border-2 border-dashed rounded-2xl p-10 text-center transition-all duration-300 ease-out overflow-hidden
79
+ ${disabled ? 'opacity-60 cursor-not-allowed border-gray-200 bg-gray-50' : 'cursor-pointer'}
80
+ ${isDragging
81
+ ? 'border-indigo-500 bg-indigo-50/50 scale-[1.01] shadow-lg'
82
+ : 'border-gray-200 hover:border-indigo-400 hover:bg-gray-50'}
83
+ `}
84
+ >
85
+ <input
86
+ type="file"
87
+ multiple
88
+ onChange={handleFileInput}
89
+ disabled={disabled}
90
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed z-20"
91
+ />
92
+
93
+ {/* Decorative Background Glow */}
94
+ <div className={`absolute inset-0 bg-gradient-to-tr from-indigo-100/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none`} />
95
+
96
+ <div className="relative z-10 flex flex-col items-center justify-center space-y-4">
97
+ <div className={`
98
+ p-5 rounded-2xl shadow-sm transition-all duration-300
99
+ ${isDragging ? 'bg-indigo-100 text-indigo-600 scale-110' : 'bg-white border border-gray-100 text-gray-400 group-hover:text-indigo-500 group-hover:scale-105 group-hover:shadow-md'}
100
+ `}>
101
+ <UploadCloud className="w-10 h-10" strokeWidth={1.5} />
102
+ </div>
103
+
104
+ <div>
105
+ <p className="text-xl font-semibold text-gray-700 group-hover:text-indigo-900 transition-colors">
106
+ {isDragging ? 'Drop files instantly' : 'Click or Drag files here'}
107
+ </p>
108
+ <p className="text-sm text-gray-400 mt-2 max-w-sm mx-auto">
109
+ Supports images, JSON, CSV, and Parquet.
110
+ <span className="block mt-1 text-xs text-indigo-400 opacity-80">Files are auto-renamed with timestamps</span>
111
+ </p>
112
+ </div>
113
+
114
+ <div className="pt-2">
115
+ <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-md bg-gray-100 text-gray-500 text-xs font-medium group-hover:bg-indigo-50 group-hover:text-indigo-600 transition-colors">
116
+ <FileUp className="w-3 h-3" />
117
+ Bulk Upload Supported
118
+ </span>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ );
123
+ };
components/RemoteFileList.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { RemoteFile } from '../types';
3
+ import { File, Eye, Download, Loader2, Database } from 'lucide-react';
4
+
5
+ interface RemoteFileListProps {
6
+ files: RemoteFile[];
7
+ isLoading: boolean;
8
+ onPreview: (file: RemoteFile) => void;
9
+ }
10
+
11
+ export const RemoteFileList: React.FC<RemoteFileListProps> = ({ files, isLoading, onPreview }) => {
12
+ if (isLoading) {
13
+ return (
14
+ <div className="flex flex-col items-center justify-center p-8 bg-white rounded-xl shadow-sm border border-gray-100 h-64">
15
+ <Loader2 className="w-8 h-8 text-blue-500 animate-spin mb-3" />
16
+ <p className="text-gray-500 font-medium">Fetching dataset from Hugging Face...</p>
17
+ </div>
18
+ );
19
+ }
20
+
21
+ if (files.length === 0) {
22
+ return (
23
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center">
24
+ <div className="bg-gray-100 p-3 rounded-full w-fit mx-auto mb-3">
25
+ <Database className="w-6 h-6 text-gray-400" />
26
+ </div>
27
+ <h3 className="text-gray-900 font-medium">No files found</h3>
28
+ <p className="text-gray-500 text-sm mt-1">The repository seems empty or files are loading.</p>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
35
+ <div className="px-6 py-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
36
+ <h3 className="font-semibold text-gray-800 flex items-center gap-2">
37
+ <Database className="w-4 h-4 text-blue-500" />
38
+ Server Files ({files.length})
39
+ </h3>
40
+ </div>
41
+ <div className="max-h-[400px] overflow-y-auto divide-y divide-gray-100">
42
+ {files.map((file) => (
43
+ <div key={file.path} className="px-6 py-3 hover:bg-blue-50/50 transition-colors flex items-center justify-between group">
44
+ <div className="flex items-center gap-3 min-w-0">
45
+ <File className="w-4 h-4 text-gray-400" />
46
+ <div className="min-w-0">
47
+ <p className="text-sm font-medium text-gray-700 truncate" title={file.path}>
48
+ {file.path}
49
+ </p>
50
+ <p className="text-xs text-gray-400">
51
+ {(file.size / 1024).toFixed(1)} KB
52
+ </p>
53
+ </div>
54
+ </div>
55
+ <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
56
+ <button
57
+ onClick={() => onPreview(file)}
58
+ className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
59
+ title="Preview File"
60
+ >
61
+ <Eye className="w-4 h-4" />
62
+ </button>
63
+ <a
64
+ href={file.url}
65
+ target="_blank"
66
+ rel="noopener noreferrer"
67
+ className="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
68
+ title="Open on Hugging Face"
69
+ >
70
+ <Download className="w-4 h-4" />
71
+ </a>
72
+ </div>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ </div>
77
+ );
78
+ };
components/UploadList.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { FileItem, UploadStatus } from '../types';
3
+ import { FileText, Loader2, CheckCircle2, AlertTriangle, ExternalLink, X, Edit2 } from 'lucide-react';
4
+
5
+ interface UploadListProps {
6
+ files: FileItem[];
7
+ onRemove: (id: string) => void;
8
+ onPathChange: (id: string, newPath: string) => void;
9
+ }
10
+
11
+ export const UploadList: React.FC<UploadListProps> = ({ files, onRemove, onPathChange }) => {
12
+ if (files.length === 0) return null;
13
+
14
+ return (
15
+ <div className="bg-white/60 backdrop-blur-md rounded-2xl shadow-sm border border-gray-100 overflow-hidden ring-1 ring-gray-200/50">
16
+ <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
17
+ <h3 className="font-semibold text-gray-700 flex items-center gap-2">
18
+ <div className="w-2 h-2 rounded-full bg-indigo-500" />
19
+ Upload Queue
20
+ <span className="bg-gray-200 text-gray-600 text-xs py-0.5 px-2 rounded-full ml-1">{files.length}</span>
21
+ </h3>
22
+ <span className="text-xs text-gray-400 font-medium">Auto-saving path changes</span>
23
+ </div>
24
+
25
+ <div className="max-h-[400px] overflow-y-auto custom-scrollbar divide-y divide-gray-100">
26
+ {files.map((item) => (
27
+ <div key={item.id} className="p-4 hover:bg-white transition-colors flex items-center gap-4 group relative">
28
+
29
+ {/* Icon Box */}
30
+ <div className={`
31
+ p-3 rounded-xl flex-shrink-0 transition-colors
32
+ ${item.status === UploadStatus.SUCCESS ? 'bg-green-50 text-green-600' :
33
+ item.status === UploadStatus.ERROR ? 'bg-red-50 text-red-600' : 'bg-indigo-50 text-indigo-600'}
34
+ `}>
35
+ <FileText className="w-5 h-5" />
36
+ </div>
37
+
38
+ {/* File Info & Input */}
39
+ <div className="flex-1 min-w-0 space-y-1">
40
+ <div className="flex items-center gap-2">
41
+ <span className="font-medium text-sm text-gray-700 truncate max-w-[180px] md:max-w-xs" title={item.file.name}>
42
+ {item.file.name}
43
+ </span>
44
+ <span className="text-[10px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded">
45
+ {(item.file.size / 1024).toFixed(1)} KB
46
+ </span>
47
+ </div>
48
+
49
+ {item.status === UploadStatus.IDLE && (
50
+ <div className="relative max-w-sm">
51
+ <input
52
+ type="text"
53
+ value={item.path}
54
+ onChange={(e) => onPathChange(item.id, e.target.value)}
55
+ className="w-full text-xs py-1.5 pl-2 pr-7 border border-transparent hover:border-gray-200 focus:border-indigo-400 focus:bg-white rounded bg-transparent transition-all outline-none text-gray-600 font-mono"
56
+ placeholder="path/to/file.ext"
57
+ />
58
+ <Edit2 className="w-3 h-3 text-gray-300 absolute right-2 top-2 pointer-events-none" />
59
+ </div>
60
+ )}
61
+
62
+ {item.status === UploadStatus.UPLOADING && (
63
+ <div className="w-full bg-gray-100 rounded-full h-1.5 mt-2 overflow-hidden">
64
+ <div className="bg-indigo-500 h-1.5 rounded-full animate-[progress_1s_ease-in-out_infinite]" style={{width: '70%'}}></div>
65
+ </div>
66
+ )}
67
+
68
+ {item.status === UploadStatus.SUCCESS && (
69
+ <a
70
+ href={item.url}
71
+ target="_blank"
72
+ rel="noopener noreferrer"
73
+ className="inline-flex items-center gap-1 text-xs font-medium text-green-600 hover:text-green-700 hover:underline"
74
+ >
75
+ View on Hub <ExternalLink className="w-3 h-3" />
76
+ </a>
77
+ )}
78
+
79
+ {item.status === UploadStatus.ERROR && (
80
+ <span className="text-xs font-medium text-red-500 truncate block" title={item.error}>
81
+ Error: {item.error}
82
+ </span>
83
+ )}
84
+ </div>
85
+
86
+ {/* Actions / Status Icons */}
87
+ <div className="flex items-center gap-2 pl-2 border-l border-gray-50">
88
+ {item.status === UploadStatus.UPLOADING && (
89
+ <Loader2 className="w-5 h-5 text-indigo-500 animate-spin" />
90
+ )}
91
+ {item.status === UploadStatus.SUCCESS && (
92
+ <div className="flex items-center gap-1 text-green-600 bg-green-50 px-2 py-1 rounded-lg text-xs font-medium">
93
+ <CheckCircle2 className="w-4 h-4" />
94
+ <span>Done</span>
95
+ </div>
96
+ )}
97
+ {item.status === UploadStatus.ERROR && (
98
+ <div className="flex items-center gap-1 text-red-600 bg-red-50 px-2 py-1 rounded-lg text-xs font-medium">
99
+ <AlertTriangle className="w-4 h-4" />
100
+ <span>Failed</span>
101
+ </div>
102
+ )}
103
+
104
+ {item.status !== UploadStatus.UPLOADING && item.status !== UploadStatus.SUCCESS && (
105
+ <button
106
+ onClick={() => onRemove(item.id)}
107
+ className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
108
+ title="Remove file"
109
+ >
110
+ <X className="w-4 h-4" />
111
+ </button>
112
+ )}
113
+ </div>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ </div>
118
+ );
119
+ };