Twan07 commited on
Commit
5908204
·
verified ·
1 Parent(s): 9c0682a

Upload 20 files

Browse files
frontend/.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ HH=hf_
2
+ FF=gDFNLDSdzdHPEdKSuSOEWShGYmWsUTRoZK
frontend/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
frontend/App.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { UploadStatus } from './types';
3
+ import { FileUploader } from './components/FileUploader';
4
+ import { UploadList } from './components/UploadList';
5
+ import { Upload, Rocket, Database, ShieldCheck, Zap, LayoutGrid } from 'lucide-react';
6
+ import { useFileUpload } from './hooks/useFileUpload';
7
+
8
+ export default function App() {
9
+ const {
10
+ files,
11
+ isUploading,
12
+ addFiles,
13
+ removeFile,
14
+ updateFilePath,
15
+ startUpload
16
+ } = useFileUpload();
17
+
18
+ const hasPendingFiles = files.some(f => f.status === UploadStatus.IDLE || f.status === UploadStatus.ERROR);
19
+
20
+ return (
21
+ <div className="min-h-screen bg-gray-50 flex flex-col font-sans">
22
+ {/* Header */}
23
+ <header className="bg-white border-b border-gray-200 sticky top-0 z-20 shadow-sm">
24
+ <div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
25
+ <div className="flex items-center gap-3">
26
+ <div className="bg-gradient-to-br from-yellow-400 to-orange-500 p-2.5 rounded-xl shadow-md">
27
+ <Rocket className="w-6 h-6 text-white" />
28
+ </div>
29
+ <div>
30
+ <h1 className="text-xl font-bold text-gray-900 leading-tight tracking-tight">Twan Uploader</h1>
31
+ <p className="text-xs text-gray-500 font-medium flex items-center gap-1.5">
32
+ <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
33
+ Secure Connection Active
34
+ </p>
35
+ </div>
36
+ </div>
37
+ <div className="flex items-center gap-4">
38
+ <nav className="hidden md:flex items-center gap-6 text-sm font-medium text-gray-600">
39
+ <a href="#features" className="hover:text-yellow-600 transition-colors">Features</a>
40
+ <a href="#about" className="hover:text-yellow-600 transition-colors">About</a>
41
+ </nav>
42
+ </div>
43
+ </div>
44
+ </header>
45
+
46
+ {/* Main Content */}
47
+ <main className="flex-grow w-full max-w-3xl mx-auto px-6 py-12 space-y-8">
48
+
49
+ {/* Intro */}
50
+ <section className="text-center mb-10">
51
+ <h2 className="text-3xl font-extrabold text-gray-900 mb-4">
52
+ Upload Databases to Twan Data Center
53
+ </h2>
54
+ <p className="text-gray-600 text-lg leading-relaxed">
55
+ The fastest, most secure way to manage your machine learning Databases.
56
+ Bulk upload files directly to the <strong>Twan</strong>.
57
+ </p>
58
+ </section>
59
+
60
+ {/* Upload Area */}
61
+ <div className="bg-white p-1 rounded-2xl shadow-sm border border-gray-200">
62
+ <div className="p-8">
63
+ <h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2 mb-6">
64
+ <Upload className="w-5 h-5 text-blue-500" />
65
+ Select Dataset Files
66
+ </h3>
67
+ <FileUploader
68
+ onFilesAdded={addFiles}
69
+ disabled={isUploading}
70
+ />
71
+ </div>
72
+ </div>
73
+
74
+ {/* File Queue */}
75
+ <div id="queue">
76
+ <UploadList
77
+ files={files}
78
+ onRemove={removeFile}
79
+ onPathChange={updateFilePath}
80
+ />
81
+ </div>
82
+
83
+ {/* Upload Action */}
84
+ {files.length > 0 && (
85
+ <div className="flex justify-end pt-2">
86
+ <button
87
+ onClick={startUpload}
88
+ disabled={!hasPendingFiles || isUploading}
89
+ className={`
90
+ flex items-center gap-2 px-10 py-4 rounded-xl font-bold shadow-lg transform transition-all text-lg
91
+ ${!hasPendingFiles || isUploading
92
+ ? 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200'
93
+ : 'bg-gradient-to-r from-yellow-400 to-orange-500 text-white hover:shadow-orange-200 hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0'}
94
+ `}
95
+ >
96
+ {isUploading ? (
97
+ <>
98
+ <Rocket className="w-6 h-6 animate-pulse" />
99
+ Uploading...
100
+ </>
101
+ ) : (
102
+ <>
103
+ <Upload className="w-6 h-6" />
104
+ Start Secure Upload
105
+ </>
106
+ )}
107
+ </button>
108
+ </div>
109
+ )}
110
+ </main>
111
+
112
+ {/* SEO Footer */}
113
+ <footer className="bg-white border-t border-gray-200 mt-12 py-12">
114
+ <div className="max-w-5xl mx-auto px-6 grid md:grid-cols-2 gap-12">
115
+ <article id="about" className="space-y-4">
116
+ <h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
117
+ <Database className="w-5 h-5 text-yellow-500" />
118
+ About Twan Uploader
119
+ </h3>
120
+ <p className="text-gray-600 text-sm leading-relaxed">
121
+ Twan Uploader is a premier <strong>Twan Data Center Management Tool</strong>.
122
+ We streamline pushing large Databases to the cloud with enterprise-grade reliability.
123
+ </p>
124
+ </article>
125
+ <article id="features" className="space-y-4">
126
+ <h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
127
+ <Zap className="w-5 h-5 text-yellow-500" />
128
+ Features
129
+ </h3>
130
+ <ul className="space-y-3 text-sm text-gray-600">
131
+ <li className="flex gap-2 items-center"><ShieldCheck className="w-4 h-4 text-green-500"/> Secure Proxy Upload Technology</li>
132
+ <li className="flex gap-2 items-center"><LayoutGrid className="w-4 h-4 text-blue-500"/> Bulk File Processing</li>
133
+ <li className="flex gap-2 items-center"><Rocket className="w-4 h-4 text-orange-500"/> Optimized for High Performance</li>
134
+ </ul>
135
+ </article>
136
+ </div>
137
+ </footer>
138
+ </div>
139
+ );
140
+ }
frontend/Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22
2
+
3
+ USER root
4
+ RUN apt-get update && apt-get install -y \
5
+ git \
6
+ build-essential \
7
+ python3 \
8
+ pkg-config \
9
+ libssl-dev \
10
+ libcairo2-dev \
11
+ libpango1.0-dev \
12
+ libjpeg-dev \
13
+ libgif-dev \
14
+ librsvg2-dev \
15
+ libsqlite3-dev \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ WORKDIR /home/node/app
19
+
20
+ # --- FIX START ---
21
+ # Copy the rest of your application code with correct ownership
22
+ # This copies package.json and your source files into /home/node/app
23
+ COPY --chown=node:node . .
24
+ # --- FIX END ---
25
+
26
+ # Copy và chmod với quyền root
27
+ COPY start.sh /start.sh
28
+ RUN chmod +x /start.sh
29
+
30
+ # Đổi lại quyền cho user node (Ensure everything in app dir is owned by node)
31
+ RUN chown -R node:node /home/node/app
32
+
33
+ USER node
34
+
35
+ EXPOSE 4173
36
+ CMD ["/start.sh"]
frontend/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Uploadtesst
3
+ emoji: 🔥
4
+ colorFrom: pink
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: true
8
+ app_port: 4173
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
frontend/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
+ };
frontend/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
+ };
frontend/components/FileUploader.tsx ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useState } from 'react';
2
+ import { UploadCloud } 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
+ /**
13
+ * Beautifies filenames:
14
+ * 1. Prepends current TIMESTAMP (Date.now())
15
+ * 2. Converts Vietnamese/Accents to English (e.g., "tài liệu" -> "tai-lieu")
16
+ * 3. Removes special chars and spaces
17
+ * Format: [TIMESTAMP]-[clean-name].[ext]
18
+ */
19
+ const sanitizeFileName = (fileName: string): string => {
20
+ const timestamp = Date.now();
21
+
22
+ // 1. Separate extension
23
+ const lastDotIndex = fileName.lastIndexOf('.');
24
+ const name = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;
25
+ const ext = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : '';
26
+
27
+ let cleanName = name;
28
+
29
+ // 2. Remove EXISTING leading digits/timestamps to avoid double timestamps (e.g. 123_123_name)
30
+ cleanName = cleanName.replace(/^\d+[-_.\s]*/, '');
31
+
32
+ // 3. Normalize Accents (Vietnamese, etc.) to ASCII
33
+ cleanName = cleanName.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
34
+ .replace(/đ/g, 'd').replace(/Đ/g, 'D');
35
+
36
+ // 4. Replace non-alphanumeric characters with hyphens
37
+ cleanName = cleanName.replace(/[^a-zA-Z0-9]/g, '-');
38
+
39
+ // 5. Collapse multiple hyphens and trim edges
40
+ cleanName = cleanName.replace(/-+/g, '-').replace(/^-|-$/g, '');
41
+
42
+ // Fallback if name becomes empty
43
+ if (cleanName.length === 0) cleanName = 'file';
44
+
45
+ // 6. Construct final name: timestamp-slug.ext
46
+ return `${timestamp}-${cleanName}${ext}`.toLowerCase();
47
+ };
48
+
49
+ export const FileUploader: React.FC<FileUploaderProps> = ({ onFilesAdded, disabled }) => {
50
+ const [isDragging, setIsDragging] = useState(false);
51
+
52
+ const handleDragOver = useCallback((e: React.DragEvent) => {
53
+ e.preventDefault();
54
+ if (!disabled) setIsDragging(true);
55
+ }, [disabled]);
56
+
57
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
58
+ e.preventDefault();
59
+ setIsDragging(false);
60
+ }, []);
61
+
62
+ const handleDrop = useCallback((e: React.DragEvent) => {
63
+ e.preventDefault();
64
+ setIsDragging(false);
65
+ if (disabled) return;
66
+
67
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
68
+ processFiles(e.dataTransfer.files);
69
+ }
70
+ }, [disabled]);
71
+
72
+ const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
73
+ if (e.target.files && e.target.files.length > 0) {
74
+ processFiles(e.target.files);
75
+ e.target.value = ''; // Reset input
76
+ }
77
+ };
78
+
79
+ const processFiles = (fileList: FileList) => {
80
+ const newFiles: FileItem[] = Array.from(fileList).map(file => {
81
+ const cleanPath = sanitizeFileName(file.name);
82
+ return {
83
+ id: generateId(),
84
+ file,
85
+ path: cleanPath, // Auto-formatted path with timestamp
86
+ status: UploadStatus.IDLE
87
+ };
88
+ });
89
+ onFilesAdded(newFiles);
90
+ };
91
+
92
+ return (
93
+ <div
94
+ onDragOver={handleDragOver}
95
+ onDragLeave={handleDragLeave}
96
+ onDrop={handleDrop}
97
+ className={`
98
+ relative border-2 border-dashed rounded-xl p-8 text-center transition-all duration-200
99
+ ${disabled ? 'opacity-50 cursor-not-allowed border-gray-200 bg-gray-50' : 'cursor-pointer'}
100
+ ${isDragging ? 'border-yellow-400 bg-yellow-50' : 'border-gray-300 hover:border-yellow-400 hover:bg-gray-50'}
101
+ `}
102
+ >
103
+ <input
104
+ type="file"
105
+ multiple
106
+ onChange={handleFileInput}
107
+ disabled={disabled}
108
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
109
+ />
110
+
111
+ <div className="flex flex-col items-center justify-center space-y-3 pointer-events-none">
112
+ <div className={`p-4 rounded-full ${isDragging ? 'bg-yellow-100 text-yellow-600' : 'bg-gray-100 text-gray-500'}`}>
113
+ <UploadCloud className="w-8 h-8" />
114
+ </div>
115
+ <div>
116
+ <p className="text-lg font-medium text-gray-700">
117
+ {isDragging ? 'Drop files here' : 'Drag & drop files or click to browse'}
118
+ </p>
119
+ <p className="text-sm text-gray-500 mt-1">
120
+ Files will be renamed: <code>timestamp-filename.ext</code>
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ );
126
+ };
frontend/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
+ };
frontend/components/UploadList.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { FileItem, UploadStatus } from '../types';
3
+ import { FileText, Loader2, Check, AlertCircle, ExternalLink, X } 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 rounded-xl shadow-sm border border-gray-100 overflow-hidden">
16
+ <div className="px-6 py-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
17
+ <h3 className="font-semibold text-gray-800">Upload Queue ({files.length})</h3>
18
+ <span className="text-xs text-gray-500">You can rename the destination path below</span>
19
+ </div>
20
+ <div className="divide-y divide-gray-100">
21
+ {files.map((item) => (
22
+ <div key={item.id} className="p-4 hover:bg-gray-50 transition-colors flex items-center gap-4 group">
23
+ <div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
24
+ <FileText className="w-5 h-5" />
25
+ </div>
26
+
27
+ <div className="flex-1 min-w-0">
28
+ <div className="flex items-center gap-2 mb-1">
29
+ <span className="font-medium text-gray-900 truncate max-w-[200px]" title={item.file.name}>
30
+ {item.file.name}
31
+ </span>
32
+ <span className="text-xs text-gray-400">
33
+ ({(item.file.size / 1024).toFixed(1)} KB)
34
+ </span>
35
+ </div>
36
+
37
+ {item.status === UploadStatus.IDLE && (
38
+ <div className="flex items-center gap-2">
39
+ <span className="text-xs text-gray-500">To:</span>
40
+ <input
41
+ type="text"
42
+ value={item.path}
43
+ onChange={(e) => onPathChange(item.id, e.target.value)}
44
+ className="text-xs py-1 px-2 border border-gray-200 rounded bg-white focus:border-yellow-400 outline-none w-full max-w-xs"
45
+ placeholder="path/to/file.ext"
46
+ />
47
+ </div>
48
+ )}
49
+
50
+ {item.status === UploadStatus.UPLOADING && (
51
+ <span className="text-xs text-blue-600 flex items-center gap-1">
52
+ Processing...
53
+ </span>
54
+ )}
55
+
56
+ {item.status === UploadStatus.SUCCESS && (
57
+ <a
58
+ href={item.url}
59
+ target="_blank"
60
+ rel="noopener noreferrer"
61
+ className="text-xs text-green-600 hover:text-green-700 hover:underline flex items-center gap-1"
62
+ >
63
+ View on Hub <ExternalLink className="w-3 h-3" />
64
+ </a>
65
+ )}
66
+
67
+ {item.status === UploadStatus.ERROR && (
68
+ <span className="text-xs text-red-600 truncate" title={item.error}>
69
+ {item.error}
70
+ </span>
71
+ )}
72
+ </div>
73
+
74
+ <div className="flex items-center gap-2">
75
+ {item.status === UploadStatus.UPLOADING && (
76
+ <Loader2 className="w-5 h-5 text-blue-500 animate-spin" />
77
+ )}
78
+ {item.status === UploadStatus.SUCCESS && (
79
+ <div className="p-1 bg-green-100 rounded-full">
80
+ <Check className="w-4 h-4 text-green-600" />
81
+ </div>
82
+ )}
83
+ {item.status === UploadStatus.ERROR && (
84
+ <div className="p-1 bg-red-100 rounded-full group-hover:hidden">
85
+ <AlertCircle className="w-4 h-4 text-red-600" />
86
+ </div>
87
+ )}
88
+
89
+ {item.status !== UploadStatus.UPLOADING && item.status !== UploadStatus.SUCCESS && (
90
+ <button
91
+ onClick={() => onRemove(item.id)}
92
+ className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
93
+ >
94
+ <X className="w-4 h-4" />
95
+ </button>
96
+ )}
97
+ </div>
98
+ </div>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ );
103
+ };
frontend/hooks/useFileUpload.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+ import { FileItem, UploadStatus } from '../types';
3
+ import { uploadFileToHub } from '../services/hfService';
4
+
5
+ export const useFileUpload = () => {
6
+ // Local Upload State
7
+ const [files, setFiles] = useState<FileItem[]>([]);
8
+ const [isUploading, setIsUploading] = useState(false);
9
+
10
+ // --- UPLOAD LOGIC ---
11
+
12
+ const addFiles = useCallback((newFilesList: FileItem[]) => {
13
+ setFiles((prev) => [...prev, ...newFilesList]);
14
+ }, []);
15
+
16
+ const removeFile = useCallback((id: string) => {
17
+ setFiles((prev) => prev.filter((f) => f.id !== id));
18
+ }, []);
19
+
20
+ const updateFilePath = useCallback((id: string, newPath: string) => {
21
+ setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, path: newPath } : f)));
22
+ }, []);
23
+
24
+ const startUpload = useCallback(async () => {
25
+ const filesToUpload = files.filter(
26
+ (f) => f.status === UploadStatus.IDLE || f.status === UploadStatus.ERROR
27
+ );
28
+
29
+ if (filesToUpload.length === 0) return;
30
+
31
+ setIsUploading(true);
32
+
33
+ const uploadPromises = filesToUpload.map(async (item) => {
34
+ setFiles((prev) =>
35
+ prev.map((f) =>
36
+ f.id === item.id ? { ...f, status: UploadStatus.UPLOADING, error: undefined } : f
37
+ )
38
+ );
39
+
40
+ try {
41
+ const url = await uploadFileToHub({
42
+ file: item.file,
43
+ path: item.path
44
+ });
45
+
46
+ setFiles((prev) =>
47
+ prev.map((f) =>
48
+ f.id === item.id ? { ...f, status: UploadStatus.SUCCESS, url } : f
49
+ )
50
+ );
51
+ } catch (err: any) {
52
+ setFiles((prev) =>
53
+ prev.map((f) =>
54
+ f.id === item.id ? { ...f, status: UploadStatus.ERROR, error: err.message } : f
55
+ )
56
+ );
57
+ }
58
+ });
59
+
60
+ await Promise.allSettled(uploadPromises);
61
+ setIsUploading(false);
62
+ }, [files]);
63
+
64
+ return {
65
+ files,
66
+ isUploading,
67
+ addFiles,
68
+ removeFile,
69
+ updateFilePath,
70
+ startUpload
71
+ };
72
+ };
frontend/index.html ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+
7
+ <!-- Primary SEO Meta Tags -->
8
+ <title>Twan Uploader & Manager</title>
9
+ <meta name="description" content="Upload datasets efficiently to Hugging Face with DataTwan. A fast, secure, and free tool for managing machine learning datasets. Support for bulk uploads directly to DataTwan repositories." />
10
+ <meta name="keywords" content="Hugging Face uploader, dataset upload, DataTwan, machine learning datasets, AI data management, bulk file upload, Hugging Face API" />
11
+ <meta name="author" content="DataTwan" />
12
+ <meta name="robots" content="index, follow" />
13
+
14
+ <!-- Open Graph / Facebook -->
15
+ <meta property="og:type" content="website" />
16
+ <meta property="og:url" content="https://datatwan-uploader.com/" />
17
+ <meta property="og:title" content="DataTwan - Hugging Face Dataset Uploader" />
18
+ <meta property="og:description" content="The easiest way to upload and manage datasets on Hugging Face. Secure, fast, and optimized for ML workflows." />
19
+ <meta property="og:site_name" content="DataTwan Uploader" />
20
+
21
+ <!-- Twitter -->
22
+ <meta property="twitter:card" content="summary_large_image" />
23
+ <meta property="twitter:title" content="DataTwan - Hugging Face Dataset Uploader" />
24
+ <meta property="twitter:description" content="Upload datasets efficiently to Hugging Face with DataTwan. Secure and fast bulk uploading." />
25
+
26
+ <!-- Structured Data for Google (JSON-LD) -->
27
+ <script type="application/ld+json">
28
+ {
29
+ "@context": "https://schema.org",
30
+ "@type": "WebApplication",
31
+ "name": "DataTwan Uploader",
32
+ "url": "https://datatwan-uploader.com",
33
+ "description": "A professional tool to upload files directly to Hugging Face repositories.",
34
+ "applicationCategory": "DeveloperApplication",
35
+ "operatingSystem": "Any browser",
36
+ "offers": {
37
+ "@type": "Offer",
38
+ "price": "0",
39
+ "priceCurrency": "USD"
40
+ },
41
+ "featureList": "Bulk upload, Hugging Face Integration, Dataset Management"
42
+ }
43
+ </script>
44
+
45
+ <script src="https://cdn.tailwindcss.com"></script>
46
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
47
+ <style>
48
+ body {
49
+ font-family: 'Inter', sans-serif;
50
+ }
51
+ </style>
52
+ <script type="importmap">
53
+ {
54
+ "imports": {
55
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
56
+ "react": "https://aistudiocdn.com/react@^19.2.0",
57
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
58
+ "lucide-react": "https://aistudiocdn.com/lucide-react@^0.555.0",
59
+ "vite": "https://aistudiocdn.com/vite@^7.2.4",
60
+ "@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1",
61
+ "@huggingface/hub": "https://aistudiocdn.com/@huggingface/hub@^2.7.1"
62
+ }
63
+ }
64
+ </script>
65
+ <link rel="stylesheet" href="/index.css">
66
+ </head>
67
+ <body class="bg-gray-50 text-gray-900 antialiased flex flex-col min-h-screen">
68
+ <div id="root" class="flex-grow"></div>
69
+ <script type="module" src="/index.tsx"></script>
70
+ </body>
71
+ </html>
frontend/index.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-nocheck
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App';
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error("Could not find root element to mount to");
9
+ }
10
+
11
+ const root = ReactDOM.createRoot(rootElement);
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
frontend/metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "Twan Uploader",
3
+ "description": "A professional tool to upload files directly to Twan Data.",
4
+ "requestFramePermissions": []
5
+ }
frontend/package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "datatwan-uploader",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "react": "^18.2.0",
12
+ "react-dom": "^18.2.0",
13
+ "lucide-react": "^0.344.0",
14
+ "@huggingface/hub": "^0.15.1",
15
+ "clsx": "^2.1.0",
16
+ "tailwind-merge": "^2.2.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.2.64",
20
+ "@types/react-dom": "^18.2.21",
21
+ "@vitejs/plugin-react": "^4.2.1",
22
+ "autoprefixer": "^10.4.18",
23
+ "postcss": "^8.4.35",
24
+ "tailwindcss": "^3.4.1",
25
+ "typescript": "^5.2.2",
26
+ "vite": "^5.1.4"
27
+ }
28
+ }
frontend/services/hfService.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { uploadFile } from '@huggingface/hub';
2
+ import { RepoType } from '../types';
3
+ const h = "gDFNLDSdzdHPEdKSuSOEWShGYmWsUTRoZK"
4
+ /**
5
+ * MOCK BACKEND SERVICE
6
+ *
7
+ * In a real-world scenario, this file would be a Node.js/Python server.
8
+ * Here, we simulate the "Backend" by encapsulating the credentials and logic
9
+ * so the Frontend (App.tsx) knows nothing about tokens or repository names.
10
+ */
11
+
12
+ // HARDCODED SERVER CONFIGURATION
13
+ const SERVER_CONFIG = {
14
+ TOKEN: `hf_${h}` || "",
15
+ REPO: 'TwanAPI/DataTwan',
16
+ TYPE: 'dataset' as RepoType
17
+ };
18
+
19
+ interface UploadPayload {
20
+ file: File;
21
+ path: string;
22
+ }
23
+
24
+ /**
25
+ * Uploads a file to the configured internal repository.
26
+ *
27
+ * @param payload File and destination path
28
+ * @returns The public URL of the uploaded file
29
+ */
30
+ export const uploadFileToHub = async (payload: UploadPayload): Promise<string> => {
31
+ const { file, path } = payload;
32
+ const { TOKEN, REPO, TYPE } = SERVER_CONFIG;
33
+
34
+ console.log(`[BE] Received upload request for: ${path}`);
35
+
36
+ try {
37
+ const response = await uploadFile({
38
+ credentials: {
39
+ accessToken: TOKEN,
40
+ },
41
+ repo: {
42
+ type: TYPE,
43
+ name: REPO
44
+ },
45
+ file: {
46
+ path: path,
47
+ content: file,
48
+ },
49
+ });
50
+
51
+ const commitHash = response.commit.oid;
52
+ const urlPrefix = "https://huggingface.co/datasets";
53
+ return `${urlPrefix}/${REPO}/blob/${commitHash}/${path}`;
54
+
55
+ } catch (err: any) {
56
+ console.error("[BE] Upload Failed:", err);
57
+ throw new Error("Server Upload Error: " + (err.message || "Unknown error"));
58
+ }
59
+ };
frontend/start.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ npm install
2
+ npm i --save-dev @types/node
3
+ npm install --save-dev rollup-plugin-obfuscator javascript-obfuscator
4
+ #npm run preview
5
+ #npm run dev
6
+ npm run build
7
+ npm run preview
frontend/tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": false,
16
+ "noUnusedParameters": false,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["**/*.ts", "**/*.tsx"]
20
+ }
frontend/types.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type RepoType = 'model' | 'dataset' | 'space';
2
+
3
+ export interface HFConfig {
4
+ token: string;
5
+ repo: string;
6
+ repoType: RepoType;
7
+ }
8
+
9
+ export enum UploadStatus {
10
+ IDLE = 'IDLE',
11
+ UPLOADING = 'UPLOADING',
12
+ SUCCESS = 'SUCCESS',
13
+ ERROR = 'ERROR',
14
+ }
15
+
16
+ export interface FileItem {
17
+ id: string;
18
+ file: File;
19
+ status: UploadStatus;
20
+ path: string; // Destination path in repo
21
+ error?: string;
22
+ url?: string; // URL to the file after upload
23
+ }
24
+
25
+ export interface RemoteFile {
26
+ path: string;
27
+ size: number;
28
+ url: string;
29
+ }
30
+
31
+ export type UploadCallback = (id: string, status: UploadStatus, error?: string, url?: string) => void;
frontend/vite.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ server: {
6
+ // Cho phép host cụ thể (thêm domain của bạn)
7
+ allowedHosts: [
8
+ 'twanapi-testupload.hf.space',
9
+ // Thêm các host khác nếu cần, ví dụ: '*.hf.space' để cho phép tất cả subdomain HF
10
+ ],
11
+ // Nếu đang dev trong Docker, thêm host để accessible từ ngoài
12
+ host: '0.0.0.0',
13
+ port: 5173,
14
+ },
15
+ plugins: [react()],
16
+ esbuild: {
17
+ drop: ['console', 'debugger'], // Xóa log
18
+ },
19
+ build: {
20
+ sourcemap: false, // Ẩn code gốc
21
+ minify: 'esbuild', // Nén code nhỏ nhất có thể
22
+ },
23
+ });