CoolFace
Apppublic

sbidesignlab/uploader

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
index.html889 linesDownload Raw Back to root
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>