Spaces:
Sleeping
Sleeping
Upload 20 files
Browse files- frontend/.env +2 -0
- frontend/.gitattributes +35 -0
- frontend/App.tsx +140 -0
- frontend/Dockerfile +36 -0
- frontend/README.md +11 -0
- frontend/components/ConfigPanel.tsx +131 -0
- frontend/components/FilePreview.tsx +96 -0
- frontend/components/FileUploader.tsx +126 -0
- frontend/components/RemoteFileList.tsx +78 -0
- frontend/components/UploadList.tsx +103 -0
- frontend/hooks/useFileUpload.ts +72 -0
- frontend/index.html +71 -0
- frontend/index.tsx +16 -0
- frontend/metadata.json +5 -0
- frontend/package.json +28 -0
- frontend/services/hfService.ts +59 -0
- frontend/start.sh +7 -0
- frontend/tsconfig.json +20 -0
- frontend/types.ts +31 -0
- frontend/vite.config.ts +23 -0
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 |
+
});
|