sbidesignlab/uploader
0
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 <title>Pro Project Manager (Sandbox Safe)</title>7 <style>8 :root {9 --bg-canvas: #0d1117;10 --bg-primary: #161b22;11 --bg-secondary: #21262d;12 --border-primary: #30363d;13 --text-primary: #e6edf3;14 --text-secondary: #8b949e;15 --accent-blue: #58a6ff;16 --accent-green: #3fb950;17 --accent-red: #f85149;18 --accent-orange: #d29922;19 }20 21 * { box-sizing: border-box; margin: 0; padding: 0; }22 23 body {24 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;25 background: var(--bg-canvas);26 color: var(--text-primary);27 min-height: 100vh;28 line-height: 1.5;29 }30 31 .container { max-width: 1200px; margin: 0 auto; padding: 20px; }32 33 .header {34 text-align: center;35 padding: 40px 0;36 border-bottom: 1px solid var(--border-primary);37 margin-bottom: 30px;38 }39 .header h1 { font-size: 32px; margin-bottom: 8px; background: linear-gradient(90deg, #58a6ff, #a371f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }40 .header p { color: var(--text-secondary); font-size: 15px; }41 42 .platform-selector {43 display: flex;44 gap: 12px;45 justify-content: center;46 margin-bottom: 30px;47 }48 .platform-btn {49 display: flex;50 align-items: center;51 gap: 10px;52 padding: 14px 32px;53 border: 1px solid var(--border-primary);54 border-radius: 8px;55 background: var(--bg-primary);56 color: var(--text-secondary);57 font-size: 15px;58 font-weight: 600;59 cursor: pointer;60 transition: all 0.2s;61 }62 .platform-btn:hover { border-color: var(--accent-blue); color: var(--text-primary); }63 .platform-btn.active { 64 border-color: var(--accent-blue); 65 background: rgba(88, 166, 255, 0.1); 66 color: var(--accent-blue); 67 }68 .platform-btn svg { width: 22px; height: 22px; }69 70 .grid-layout {71 display: grid;72 grid-template-columns: 350px 1fr;73 gap: 24px;74 }75 @media (max-width: 900px) {76 .grid-layout { grid-template-columns: 1fr; }77 }78 79 .panel {80 background: var(--bg-primary);81 border: 1px solid var(--border-primary);82 border-radius: 12px;83 overflow: hidden;84 display: flex;85 flex-direction: column;86 }87 .panel-header {88 padding: 16px 20px;89 background: var(--bg-secondary);90 border-bottom: 1px solid var(--border-primary);91 display: flex;92 justify-content: space-between;93 align-items: center;94 font-weight: 600;95 }96 .panel-body {97 padding: 16px;98 flex: 1;99 overflow-y: auto;100 }101 102 /* Auth Section */103 .auth-card {104 background: linear-gradient(145deg, #161b22, #1c2128);105 border: 1px solid var(--border-primary);106 border-radius: 12px;107 padding: 24px;108 margin-bottom: 24px;109 display: flex;110 justify-content: space-between;111 align-items: center;112 flex-wrap: wrap;113 gap: 16px;114 }115 .auth-info { display: flex; align-items: center; gap: 12px; }116 .avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--bg-secondary); display: flex; align-items: center; justify-content: center; color: var(--accent-blue); font-weight: bold; border: 1px solid var(--border-primary); }117 .status-badge { padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; background: rgba(88, 166, 255, 0.1); color: var(--accent-blue); }118 .status-badge.connected { background: rgba(63, 185, 80, 0.1); color: var(--accent-green); }119 120 .list-item {121 padding: 12px;122 border-radius: 8px;123 cursor: pointer;124 transition: background 0.15s;125 border: 1px solid transparent;126 margin-bottom: 6px;127 }128 .list-item:hover { background: var(--bg-secondary); }129 .list-item.selected { 130 background: rgba(88, 166, 255, 0.1); 131 border-color: var(--accent-blue); 132 }133 .item-row { display: flex; justify-content: space-between; align-items: center; }134 .item-meta { font-size: 12px; color: var(--text-secondary); margin-top: 4px; display: flex; gap: 8px; }135 136 input, select {137 flex: 1;138 padding: 10px 12px;139 background: var(--bg-canvas);140 border: 1px solid var(--border-primary);141 border-radius: 6px;142 color: var(--text-primary);143 font-size: 14px;144 }145 input:focus, select:focus { outline: none; border-color: var(--accent-blue); }146 147 .btn {148 display: inline-flex;149 align-items: center;150 justify-content: center;151 gap: 8px;152 padding: 10px 18px;153 border: none;154 border-radius: 6px;155 font-size: 14px;156 font-weight: 500;157 cursor: pointer;158 transition: all 0.2s;159 white-space: nowrap;160 }161 .btn:disabled { opacity: 0.5; cursor: not-allowed; }162 .btn-primary { background: #238636; color: white; }163 .btn-primary:hover { background: #2ea043; }164 .btn-secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-primary); }165 .btn-secondary:hover { background: #30363d; }166 .btn-danger { background: #da3633; color: white; }167 .btn-danger:hover { background: #f85149; }168 .btn-sm { padding: 6px 12px; font-size: 12px; }169 170 .drop-zone {171 border: 2px dashed var(--border-primary);172 border-radius: 8px;173 padding: 40px 20px;174 text-align: center;175 transition: all 0.3s;176 cursor: pointer;177 background: rgba(0,0,0,0.1);178 }179 .drop-zone.active { border-color: var(--accent-green); background: rgba(63, 185, 80, 0.05); }180 181 .file-queue { margin-top: 16px; max-height: 300px; overflow-y: auto; }182 .file-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg-secondary); padding: 8px 12px; border-radius: 6px; margin-bottom: 4px; font-size: 13px; }183 184 #log-console {185 background: #010409;186 border: 1px solid var(--border-primary);187 border-radius: 8px;188 padding: 16px;189 margin-top: 24px;190 font-family: 'SF Mono', 'Consolas', monospace;191 font-size: 12px;192 height: 150px;193 overflow-y: auto;194 color: var(--text-secondary);195 }196 .log-success { color: var(--accent-green); }197 .log-error { color: var(--accent-red); }198 .log-info { color: var(--accent-blue); }199 200 .spinner {201 border: 2px solid rgba(255,255,255,0.1);202 border-top-color: #fff;203 border-radius: 50%;204 width: 16px;205 height: 16px;206 animation: spin 0.8s linear infinite;207 display: inline-block;208 }209 @keyframes spin { to { transform: rotate(360deg); } }210 211 .hidden { display: none !important; }212 </style>213</head>214<body>215 <div class="container">216 <div class="header">217 <h1>Pro Project Manager</h1>218 <p>Enterprise-grade management for GitHub & Hugging Face repositories</p>219 </div>220 221 <div class="platform-selector">222 <button class="platform-btn active" id="btn-github" onclick="switchPlatform('github')">223 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>224 GitHub225 </button>226 <button class="platform-btn" id="btn-huggingface" onclick="switchPlatform('huggingface')">227 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.866-.013-1.7-2.782.604-3.369-1.341-3.369-1.341-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>228 Hugging Face229 </button>230 </div>231 232 <!-- Auth Card -->233 <div class="auth-card">234 <div class="auth-info">235 <div class="avatar" id="user-avatar">236 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/></svg>237 </div>238 <div>239 <div id="user-name" style="font-weight: 600; font-size: 16px;">Welcome, Developer</div>240 <div id="auth-status-text" style="font-size: 13px; color: var(--text-secondary);">241 <span id="status-label">Not Connected</span>242 <span id="rate-limit" class="hidden" style="margin-left: 8px; opacity: 0.7;"></span>243 </div>244 </div>245 </div>246 247 <!-- Step 1: Not Connected State -->248 <div id="auth-step-1" style="display: flex; gap: 10px; align-items: center;">249 <button class="btn btn-primary" onclick="guideLogin()">250 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>251 Login with GitHub252 </button>253 <button class="btn btn-secondary" onclick="switchPlatform('huggingface')">Switch to HF</button>254 </div>255 256 <!-- Step 2: Token Input State -->257 <div id="auth-step-2" class="hidden" style="width: 100%;">258 <p style="font-size: 13px; margin-bottom: 8px; color: var(--accent-green);">259 1. Token page opened in new tab.<br>260 2. Click <strong>Generate new token (classic)</strong>.<br>261 3. Paste the token below:262 </p>263 <div style="display: flex; gap: 10px;">264 <input type="password" id="token-input" placeholder="Paste your Personal Access Token here..." style="flex: 1;" autofocus>265 <button class="btn btn-primary" onclick="connectToken()">Verify & Connect</button>266 <button class="btn btn-secondary" onclick="resetLogin()">Cancel</button>267 </div>268 </div>269 270 <!-- Step 3: Connected State -->271 <div id="auth-step-3" class="hidden" style="display: flex; gap: 10px; align-items: center;">272 <button class="btn btn-secondary" onclick="disconnect()">273 <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/></svg>274 Logout275 </button>276 </div>277 </div>278 279 <div class="grid-layout">280 <div class="panel">281 <div class="panel-header">282 <span>Repositories</span>283 <button class="btn btn-secondary btn-sm" onclick="refreshRepos()">Refresh</button>284 </div>285 <div class="panel-body">286 <div id="repo-list-container">287 <div class="list-item" style="pointer-events: none; opacity: 0.5;">288 <div class="item-row">Connect to view repositories</div>289 </div>290 </div>291 <div style="margin-top: 12px; display: flex; gap: 8px;">292 <input type="text" id="new-repo-input" placeholder="New repository name">293 <button class="btn btn-primary btn-sm" onclick="createRepo()">Create</button>294 </div>295 </div>296 </div>297 298 <div class="panel" style="min-height: 500px;">299 <div class="panel-header">300 <span>Workspace: <span id="workspace-title" style="color: var(--accent-blue);">Select a Repository</span></span>301 <div id="branch-selector" class="hidden">302 <select id="branch-select" onchange="changeBranch()"></select>303 </div>304 </div>305 <div class="panel-body">306 <div id="upload-section">307 <div class="drop-zone" id="drop-zone">308 <svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/></svg>309 <p class="drop-zone-text" style="margin-top:10px;"><strong>Drag & drop</strong> files or folders here<br><span style="font-size:12px; opacity:0.7;">Supports .zip, .tar.gz, and raw files</span></p>310 </div>311 312 <div id="file-queue-container" class="hidden" style="margin-top: 20px;">313 <div style="display:flex; justify-content:space-between; margin-bottom:10px;">314 <h4>Queue <span id="queue-count" style="color:var(--text-secondary); font-weight:normal;"></span></h4>315 <button class="btn btn-sm btn-secondary" onclick="clearQueue()">Clear</button>316 </div>317 <div id="file-queue" class="file-queue"></div>318 </div>319 320 <div style="margin-top: 20px; background: var(--bg-secondary); padding: 15px; border-radius: 8px; border: 1px solid var(--border-primary);">321 <div style="margin-bottom: 12px;">322 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">323 <input type="checkbox" id="opt-clean-repo"> <strong>Sync (Delete remote files not in queue)</strong>324 </label>325 <p style="font-size: 12px; color: var(--accent-orange); margin-left: 24px;">Warning: This will delete all other files in the repository.</p>326 </div>327 <div style="display: flex; gap: 10px; margin-top: 15px;">328 <button class="btn btn-primary" style="flex:1" id="btn-upload" onclick="uploadFiles()" disabled>Upload Files</button>329 <button class="btn btn-secondary" id="btn-download" onclick="downloadRepo()" disabled>Download ZIP</button>330 <button class="btn btn-danger" id="btn-delete-repo" onclick="confirmDeleteRepo()" disabled>Delete Repo</button>331 </div>332 </div>333 </div>334 </div>335 </div>336 </div>337 338 <div id="log-console">339 <div class="log-info">System ready. Connect a token to begin.</div>340 </div>341 </div>342 343 <!-- Libraries -->344 <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>345 <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>346 347 <script>348 // --- SANDBOX SAFE STORAGE ---349 // We use a simple object instead of localStorage to avoid sandbox errors.350 const memoryStorage = {351 _data: {},352 setItem: function(id, val) { this._data[id] = String(val); },353 getItem: function(id) { return this._data[id] || null; },354 removeItem: function(id) { delete this._data[id]; }355 };356 357 // --- Configuration ---358 const CONFIG = {359 github: {360 name: 'GitHub',361 apiBase: 'https://api.github.com',362 tokenUrl: 'https://github.com/settings/tokens/new?description=Pro%20Manager&scopes=repo,delete_repo',363 authHeader: (t) => `token ${t}`,364 defaultBranch: 'main'365 },366 huggingface: {367 name: 'Hugging Face',368 apiBase: 'https://huggingface.co/api',369 tokenUrl: 'https://huggingface.co/settings/tokens',370 authHeader: (t) => `Bearer ${t}`,371 defaultBranch: 'main'372 }373 };374 375 let state = {376 platform: 'github',377 token: null,378 user: null,379 repos: [],380 currentRepo: null,381 currentBranch: 'main',382 files: []383 };384 385 // --- Initialization ---386 function init() {387 // Try to load token from memory388 const savedPlatform = memoryStorage.getItem('pm_platform') || 'github';389 state.token = memoryStorage.getItem(`pm_token_${savedPlatform}`);390 391 switchPlatform(savedPlatform, true);392 393 // UI Setup394 const dropZone = document.getElementById('drop-zone');395 ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {396 dropZone.addEventListener(event, (e) => {397 e.preventDefault();398 e.stopPropagation();399 });400 });401 dropZone.addEventListener('dragover', () => dropZone.classList.add('active'));402 dropZone.addEventListener('dragleave', () => dropZone.classList.remove('active'));403 dropZone.addEventListener('drop', handleDrop);404 dropZone.addEventListener('click', () => {405 const input = document.createElement('input');406 input.type = 'file';407 input.multiple = true;408 input.webkitdirectory = true;409 input.onchange = (e) => handleFiles(e.target.files);410 input.click();411 });412 413 if (state.token) connectToken(true);414 }415 416 // --- Auth Logic ---417 function switchPlatform(platform, silent = false) {418 state.platform = platform;419 state.repos = [];420 state.currentRepo = null;421 memoryStorage.setItem('pm_platform', platform);422 423 document.getElementById('btn-github').classList.toggle('active', platform === 'github');424 document.getElementById('btn-huggingface').classList.toggle('active', platform === 'huggingface');425 426 if (!silent) {427 state.token = null;428 memoryStorage.removeItem(`pm_token_${platform}`);429 updateAuthUI(false);430 renderRepos();431 }432 }433 434 function guideLogin() {435 const cfg = CONFIG[state.platform];436 window.open(cfg.tokenUrl, '_blank');437 438 document.getElementById('auth-step-1').classList.add('hidden');439 document.getElementById('auth-step-2').classList.remove('hidden');440 document.getElementById('token-input').focus();441 log('Please generate a token and paste it above.', 'info');442 }443 444 function resetLogin() {445 document.getElementById('auth-step-1').classList.remove('hidden');446 document.getElementById('auth-step-2').classList.add('hidden');447 document.getElementById('auth-step-3').classList.add('hidden');448 }449 450 async function connectToken(silent = false) {451 const input = document.getElementById('token-input');452 const token = input.value || state.token;453 if (!token) {454 if (!silent) log('Please enter a token', 'error');455 return;456 }457 458 state.token = token;459 log(`Connecting to ${CONFIG[state.platform].name}...`, 'info');460 461 try {462 const user = await fetchUser();463 state.user = user;464 memoryStorage.setItem(`pm_token_${state.platform}`, token);465 updateAuthUI(true);466 log(`Logged in as ${user.name}`, 'success');467 refreshRepos();468 } catch (err) {469 log(`Connection failed: ${err.message}`, 'error');470 updateAuthUI(false);471 }472 }473 474 async function fetchUser() {475 const cfg = CONFIG[state.platform];476 let url;477 478 if (state.platform === 'github') {479 url = `${cfg.apiBase}/user`;480 const res = await fetch(url, { headers: { Authorization: cfg.authHeader(state.token) }});481 if (!res.ok) throw new Error('Invalid Token');482 const json = await res.json();483 return { name: json.login, id: json.id };484 } else {485 url = `${cfg.apiBase}/whoami-v2`;486 const res = await fetch(url, { headers: { Authorization: cfg.authHeader(state.token) }});487 if (!res.ok) throw new Error('Invalid Token');488 const json = await res.json();489 return { name: json.name, id: json.name };490 }491 }492 493 function updateAuthUI(connected) {494 const avatar = document.getElementById('user-avatar');495 496 document.getElementById('auth-step-1').classList.toggle('hidden', connected);497 document.getElementById('auth-step-2').classList.add('hidden');498 document.getElementById('auth-step-3').classList.toggle('hidden', !connected);499 500 if (connected && state.user) {501 avatar.innerHTML = state.user.name[0].toUpperCase();502 document.getElementById('user-name').textContent = state.user.name;503 document.getElementById('status-label').textContent = 'Connected';504 document.getElementById('status-label').style.color = 'var(--accent-green)';505 fetchRateLimit();506 } else {507 avatar.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/></svg>`;508 document.getElementById('user-name').textContent = 'Welcome, Developer';509 document.getElementById('status-label').textContent = 'Not Connected';510 document.getElementById('status-label').style.color = 'var(--text-secondary)';511 }512 }513 514 async function fetchRateLimit() {515 if (state.platform !== 'github') return;516 try {517 const res = await fetch('https://api.github.com/rate_limit', {518 headers: { Authorization: `token ${state.token}` }519 });520 const data = await res.json();521 const remaining = data.resources.core.remaining;522 const limit = data.resources.core.limit;523 const reset = new Date(data.resources.core.reset * 1000).toLocaleTimeString();524 525 document.getElementById('rate-limit').textContent = `API: ${remaining}/${limit} (Resets ${reset})`;526 document.getElementById('rate-limit').classList.remove('hidden');527 } catch(e) {528 console.error("Rate limit check failed");529 }530 }531 532 function disconnect() {533 memoryStorage.removeItem(`pm_token_${state.platform}`);534 state.token = null;535 state.user = null;536 state.repos = [];537 state.currentRepo = null;538 updateAuthUI(false);539 renderRepos();540 log('Disconnected', 'info');541 }542 543 // --- Repo Logic ---544 async function refreshRepos() {545 if (!state.token) return;546 log('Fetching repositories...', 'info');547 const cfg = CONFIG[state.platform];548 549 try {550 let data;551 if (state.platform === 'github') {552 const res = await fetch(`${cfg.apiBase}/user/repos?sort=updated&per_page=100`, { headers: { Authorization: cfg.authHeader(state.token) }});553 data = await res.json();554 state.repos = data.map(r => ({555 id: r.id, name: r.name, fullName: r.full_name, desc: r.description, private: r.private,556 updated: new Date(r.updated_at).toLocaleDateString(), url: r.html_url, defaultBranch: r.default_branch || 'main'557 }));558 } else {559 const [models, spaces] = await Promise.all([560 fetch(`${cfg.apiBase}/models?author=${state.user.name}&limit=100`, { headers: { Authorization: cfg.authHeader(state.token) }}).then(r => r.json()),561 fetch(`${cfg.apiBase}/spaces?author=${state.user.name}&limit=100`, { headers: { Authorization: cfg.authHeader(state.token) }}).then(r => r.json())562 ]);563 data = [...models, ...spaces];564 state.repos = data.map(r => ({565 id: r.id, name: r.id.split('/')[1] || r.id, fullName: r.id, desc: r.pipeline_tag, private: r.private,566 updated: 'N/A', url: `https://huggingface.co/${r.id}`, defaultBranch: 'main'567 }));568 }569 renderRepos();570 log(`Found ${state.repos.length} repositories`, 'success');571 } catch (err) {572 log('Failed to fetch repos: ' + err.message, 'error');573 }574 }575 576 function renderRepos() {577 const container = document.getElementById('repo-list-container');578 if (state.repos.length === 0) {579 container.innerHTML = `<div class="list-item" style="opacity:0.5">No repositories found</div>`;580 return;581 }582 container.innerHTML = state.repos.map((r, i) => `583 <div class="list-item ${state.currentRepo?.id === r.id ? 'selected' : ''}" onclick="selectRepo(${i})">584 <div class="item-row">585 <span style="font-weight:500">${r.name}</span>586 <span style="font-size:11px; padding:2px 6px; border-radius:4px; background:${r.private ? '#d29922' : '#238636'}; color:${r.private ? '#000' : '#fff'};">${r.private ? 'Private' : 'Public'}</span>587 </div>588 <div class="item-meta">589 <span>${r.desc || 'No description'}</span>590 <span>• ${r.updated}</span>591 </div>592 </div>593 `).join('');594 }595 596 async function createRepo() {597 const name = document.getElementById('new-repo-input').value.trim();598 if (!name || !state.token) return;599 log(`Creating repository '${name}'...`, 'info');600 601 const cfg = CONFIG[state.platform];602 try {603 const endpoint = state.platform === 'github' 604 ? `${cfg.apiBase}/user/repos` 605 : `${cfg.apiBase}/repos/create`;606 607 const body = state.platform === 'github'608 ? JSON.stringify({ name, private: false, auto_init: true })609 : JSON.stringify({ name, type: 'model', private: false });610 611 const res = await fetch(endpoint, {612 method: 'POST',613 headers: { Authorization: cfg.authHeader(state.token), 'Content-Type': 'application/json' },614 body615 });616 617 if (!res.ok) {618 const err = await res.json();619 throw new Error(err.message || 'Creation failed');620 }621 622 log('Repository created!', 'success');623 document.getElementById('new-repo-input').value = '';624 refreshRepos();625 } catch (err) {626 log('Error: ' + err.message, 'error');627 }628 }629 630 function selectRepo(index) {631 state.currentRepo = state.repos[index];632 document.getElementById('workspace-title').textContent = state.currentRepo.fullName;633 document.getElementById('btn-upload').disabled = false;634 document.getElementById('btn-download').disabled = false;635 document.getElementById('btn-delete-repo').disabled = false;636 637 if (state.platform === 'github') {638 state.currentBranch = state.currentRepo.defaultBranch;639 fetchBranches();640 }641 renderRepos();642 log(`Selected repository: ${state.currentRepo.fullName}`, 'info');643 }644 645 async function fetchBranches() {646 if (state.platform !== 'github' || !state.currentRepo) return;647 const cfg = CONFIG[state.platform];648 try {649 const res = await fetch(`${cfg.apiBase}/repos/${state.currentRepo.fullName}/branches`, {650 headers: { Authorization: cfg.authHeader(state.token) }651 });652 const branches = await res.json();653 654 const select = document.getElementById('branch-select');655 select.innerHTML = branches.map(b => `<option value="${b.name}" ${b.name === state.currentBranch ? 'selected' : ''}>${b.name}</option>`).join('');656 document.getElementById('branch-selector').classList.remove('hidden');657 } catch(e) {658 console.error("Could not fetch branches");659 }660 }661 662 function changeBranch() {663 state.currentBranch = document.getElementById('branch-select').value;664 log(`Switched to branch: ${state.currentBranch}`, 'info');665 }666 667 // --- File Handling ---668 function handleDrop(e) {669 handleFiles(e.dataTransfer.files);670 }671 672 async function handleFiles(fileList) {673 document.getElementById('drop-zone').classList.remove('active');674 const files = Array.from(fileList);675 state.files = [];676 677 log(`Processing ${files.length} items...`, 'info');678 679 for (const file of files) {680 if (file.name.endsWith('.zip')) {681 try {682 const content = await JSZip.loadAsync(file);683 for (const [path, zipEntry] of Object.entries(content.files)) {684 if (!zipEntry.dir && !path.startsWith('.git/')) {685 const blob = await zipEntry.async('blob');686 state.files.push({ path, blob, name: path.split('/').pop() });687 }688 }689 } catch(e) { log("Error unzipping file", "error"); }690 } else {691 let path = file.webkitRelativePath || file.name;692 state.files.push({ path, blob: file, name: file.name });693 }694 }695 renderFileQueue();696 }697 698 function renderFileQueue() {699 const container = document.getElementById('file-queue-container');700 const list = document.getElementById('file-queue');701 702 if (state.files.length === 0) {703 container.classList.add('hidden');704 return;705 }706 707 container.classList.remove('hidden');708 document.getElementById('queue-count').textContent = `(${state.files.length} files)`;709 710 list.innerHTML = state.files.slice(0, 10).map(f => `711 <div class="file-item">712 <span>${f.path}</span>713 <span>${formatBytes(f.blob.size)}</span>714 </div>715 `).join('') + (state.files.length > 10 ? `<div class="file-item" style="justify-content:center; opacity:0.5">...and ${state.files.length - 10} more</div>` : '');716 }717 718 function clearQueue() {719 state.files = [];720 renderFileQueue();721 }722 723 async function uploadFiles() {724 if (!state.currentRepo || state.files.length === 0) return;725 726 const cleanMode = document.getElementById('opt-clean-repo').checked;727 log(`Starting upload to ${state.currentRepo.fullName} (Clean: ${cleanMode})`, 'info');728 729 const uploadBtn = document.getElementById('btn-upload');730 uploadBtn.innerHTML = '<span class="spinner"></span> Uploading...';731 uploadBtn.disabled = true;732 733 try {734 if (cleanMode) await cleanAndUpload();735 else await incrementalUpload();736 log('Upload complete!', 'success');737 } catch (err) {738 log('Upload failed: ' + err.message, 'error');739 } finally {740 uploadBtn.innerHTML = 'Upload Files';741 uploadBtn.disabled = false;742 }743 }744 745 async function incrementalUpload() {746 const cfg = CONFIG[state.platform];747 748 for (const file of state.files) {749 if (file.path.startsWith('.') || file.path.includes('node_modules')) continue;750 751 try {752 if (state.platform === 'github') {753 let sha = null;754 const checkRes = await fetch(`https://api.github.com/repos/${state.currentRepo.fullName}/contents/${file.path}?ref=${state.currentBranch}`, {755 headers: { Authorization: cfg.authHeader(state.token) }756 });757 if (checkRes.ok) sha = (await checkRes.json()).sha;758 759 const content = await toBase64(file.blob);760 await fetch(`https://api.github.com/repos/${state.currentRepo.fullName}/contents/${file.path}`, {761 method: 'PUT',762 headers: { Authorization: cfg.authHeader(state.token), 'Content-Type': 'application/json' },763 body: JSON.stringify({764 message: `Update ${file.path}`,765 content: content,766 branch: state.currentBranch,767 sha: sha768 })769 });770 } else {771 const formData = new FormData();772 formData.append('file', file.blob, file.path);773 await fetch(`https://huggingface.co/api/models/${state.currentRepo.fullName}/upload/main/${file.path}`, {774 method: 'POST',775 headers: { Authorization: cfg.authHeader(state.token) },776 body: formData777 });778 }779 } catch (e) {780 console.error(`Failed to upload ${file.path}`, e);781 }782 }783 }784 785 async function cleanAndUpload() {786 log('Syncing: Removing old files...', 'info');787 if (state.platform === 'github') {788 const treeRes = await fetch(`https://api.github.com/repos/${state.currentRepo.fullName}/git/trees/${state.currentBranch}?recursive=1`, {789 headers: { Authorization: `token ${state.token}` }790 });791 const tree = await treeRes.json();792 793 for (const item of tree.tree.filter(i => i.type === 'blob')) {794 const fileRes = await fetch(`https://api.github.com/repos/${state.currentRepo.fullName}/contents/${item.path}?ref=${state.currentBranch}`, {795 headers: { Authorization: `token ${state.token}` }796 });797 if (fileRes.ok) {798 const fileData = await fileRes.json();799 await fetch(`https://api.github.com/repos/${state.currentRepo.fullName}/contents/${item.path}`, {800 method: 'DELETE',801 headers: { Authorization: `token ${state.token}`, 'Content-Type': 'application/json' },802 body: JSON.stringify({803 message: `Clean delete ${item.path}`,804 sha: fileData.sha,805 branch: state.currentBranch806 })807 });808 }809 }810 }811 await incrementalUpload();812 }813 814 // --- Utilities ---815 function toBase64(blob) {816 return new Promise((resolve, reject) => {817 const reader = new FileReader();818 reader.onload = () => resolve(btoa(reader.result));819 reader.onerror = reject;820 reader.readAsBinaryString(blob);821 });822 }823 824 function formatBytes(bytes, decimals = 2) {825 if (!bytes) return '0 Bytes';826 const k = 1024;827 const dm = decimals < 0 ? 0 : decimals;828 const sizes = ['Bytes', 'KB', 'MB', 'GB'];829 const i = Math.floor(Math.log(bytes) / Math.log(k));830 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];831 }832 833 function downloadRepo() {834 if (!state.currentRepo) return;835 let url;836 if (state.platform === 'github') {837 url = `https://api.github.com/repos/${state.currentRepo.fullName}/zipball/${state.currentBranch}`;838 } else {839 url = `https://huggingface.co/${state.currentRepo.fullName}/resolve/main?download=true`;840 }841 window.open(url, '_blank');842 }843 844 function confirmDeleteRepo() {845 if (!state.currentRepo) return;846 if (confirm(`Are you sure you want to delete ${state.currentRepo.fullName}? THIS CANNOT BE UNDONE.`)) {847 deleteRepo();848 }849 }850 851 async function deleteRepo() {852 const cfg = CONFIG[state.platform];853 log(`Deleting ${state.currentRepo.fullName}...`, 'info');854 855 try {856 let endpoint = state.platform === 'github' 857 ? `${cfg.apiBase}/repos/${state.currentRepo.fullName}`858 : `${cfg.apiBase}/repos/delete`; 859 860 const res = await fetch(endpoint, {861 method: 'DELETE',862 headers: { Authorization: cfg.authHeader(state.token) }863 });864 865 if (!res.ok) throw new Error('Deletion failed');866 867 log('Repository deleted successfully', 'success');868 state.currentRepo = null;869 refreshRepos();870 } catch (err) {871 log('Error: ' + err.message, 'error');872 }873 }874 875 function log(msg, type = '') {876 const consoleEl = document.getElementById('log-console');877 const line = document.createElement('div');878 line.className = type ? 'log-' + type : '';879 const time = new Date().toLocaleTimeString();880 line.textContent = `[${time}] ${msg}`;881 consoleEl.appendChild(line);882 consoleEl.scrollTop = consoleEl.scrollHeight;883 }884 885 // Start886 init();887 </script>888</body>889</html>