Spaces:
Running
Running
| import { displayDirectoryStructure, getSelectedFiles, formatRepoContents } from './utils.js'; | |
| let currentRepoInfo = {}; | |
| // Load saved token on page load | |
| document.addEventListener('DOMContentLoaded', function() { | |
| lucide.createIcons(); | |
| setupShowMoreInfoButton(); | |
| loadSavedToken(); | |
| }); | |
| // Load saved token from local storage | |
| function loadSavedToken() { | |
| const savedToken = localStorage.getItem('githubAccessToken'); | |
| if (savedToken) { | |
| document.getElementById('accessToken').value = savedToken; | |
| } | |
| } | |
| // Save token to local storage | |
| function saveToken(token) { | |
| if (token) { | |
| localStorage.setItem('githubAccessToken', token); | |
| } else { | |
| localStorage.removeItem('githubAccessToken'); | |
| } | |
| } | |
| // Event listener for form submission | |
| document.getElementById('repoForm').addEventListener('submit', async function (e) { | |
| e.preventDefault(); | |
| const repoUrl = document.getElementById('repoUrl').value; | |
| const accessToken = document.getElementById('accessToken').value; | |
| saveToken(accessToken); | |
| const outputText = document.getElementById('outputText'); | |
| outputText.value = 'Fetching repository structure...'; | |
| document.getElementById('directoryStructure').innerHTML = ''; | |
| document.getElementById('generateTextButton').style.display = 'none'; | |
| document.getElementById('downloadZipButton').style.display = 'none'; | |
| document.getElementById('copyButton').style.display = 'none'; | |
| document.getElementById('downloadButton').style.display = 'none'; | |
| try { | |
| const repoInfo = parseRepoUrl(repoUrl); | |
| currentRepoInfo = { ...repoInfo, accessToken }; // Store for later use | |
| let tree; | |
| if (repoInfo.source === 'github') { | |
| const { owner, repo, lastString } = repoInfo; | |
| let refFromUrl = ''; | |
| let pathFromUrl = ''; | |
| if (lastString) { | |
| const references = await getGitHubReferences(owner, repo, accessToken); | |
| const allRefs = [...references.branches, ...references.tags]; | |
| const matchingRef = allRefs.find(ref => lastString.startsWith(ref)); | |
| if (matchingRef) { | |
| refFromUrl = matchingRef; | |
| pathFromUrl = lastString.slice(matchingRef.length + 1); | |
| } else { | |
| refFromUrl = lastString; | |
| } | |
| } | |
| const sha = await fetchRepoSha(owner, repo, refFromUrl, pathFromUrl, accessToken); | |
| tree = await fetchGitHubRepoTree(owner, repo, sha, accessToken); | |
| } else if (repoInfo.source === 'huggingface') { | |
| const { owner, repo, repo_type, lastString } = repoInfo; | |
| let refFromUrl = 'main'; // Default branch | |
| let pathFromUrl = ''; | |
| if (lastString) { | |
| const refs = await getHuggingFaceReferences(owner, repo, repo_type, accessToken); | |
| const matchingRef = refs.find(ref => lastString.startsWith(ref + '/')); | |
| if (matchingRef) { | |
| refFromUrl = matchingRef; | |
| pathFromUrl = lastString.slice(matchingRef.length + 1); | |
| } else { | |
| refFromUrl = lastString.split('/')[0]; | |
| pathFromUrl = lastString.substring(refFromUrl.length + 1); | |
| } | |
| } | |
| currentRepoInfo.ref = refFromUrl; | |
| tree = await fetchHuggingFaceTree(owner, repo, repo_type, refFromUrl, pathFromUrl, accessToken); | |
| } | |
| displayDirectoryStructure(tree); | |
| document.getElementById('generateTextButton').style.display = 'flex'; | |
| document.getElementById('downloadZipButton').style.display = 'flex'; | |
| outputText.value = 'Select files and click "Generate Text File" or "Download Zip".'; | |
| } catch (error) { | |
| outputText.value = `Error fetching repository contents: ${error.message}\n\n` + | |
| "Please ensure:\n" + | |
| "1. The repository URL is correct and accessible.\n" + | |
| "2. You have the necessary permissions to access the repository.\n" + | |
| "3. If it's a private repository, you've provided a valid access token.\n" + | |
| "4. The specified branch/tag and path (if any) exist in the repository."; | |
| } | |
| }); | |
| // Event listener for generating text file | |
| document.getElementById('generateTextButton').addEventListener('click', async function () { | |
| const accessToken = document.getElementById('accessToken').value; | |
| const outputText = document.getElementById('outputText'); | |
| outputText.value = 'Generating text file...'; | |
| saveToken(accessToken); | |
| try { | |
| const selectedFiles = getSelectedFiles(); | |
| if (selectedFiles.length === 0) { | |
| throw new Error('No files selected'); | |
| } | |
| const fileContents = await fetchFileContents(selectedFiles, accessToken, currentRepoInfo.source); | |
| const formattedText = formatRepoContents(fileContents); | |
| outputText.value = formattedText; | |
| document.getElementById('copyButton').style.display = 'flex'; | |
| document.getElementById('downloadButton').style.display = 'flex'; | |
| } catch (error) { | |
| outputText.value = `Error generating text file: ${error.message}\n\n` + | |
| "Please ensure:\n" + | |
| "1. You have selected at least one file from the directory structure.\n" + | |
| "2. Your access token (if provided) is valid and has the necessary permissions.\n" + | |
| "3. You have a stable internet connection."; | |
| } | |
| }); | |
| // Event listener for downloading zip file | |
| document.getElementById('downloadZipButton').addEventListener('click', async function () { | |
| const accessToken = document.getElementById('accessToken').value; | |
| try { | |
| const selectedFiles = getSelectedFiles(); | |
| if (selectedFiles.length === 0) { | |
| throw new Error('No files selected'); | |
| } | |
| const fileContents = await fetchFileContents(selectedFiles, accessToken, currentRepoInfo.source); | |
| await createAndDownloadZip(fileContents); | |
| } catch (error) { | |
| const outputText = document.getElementById('outputText'); | |
| outputText.value = `Error generating zip file: ${error.message}\n\n` + | |
| "Please ensure:\n" + | |
| "1. You have selected at least one file from the directory structure.\n" + | |
| "2. Your access token (if provided) is valid and has the necessary permissions."; | |
| } | |
| }); | |
| // Event listener for copying text to clipboard | |
| document.getElementById('copyButton').addEventListener('click', function () { | |
| const outputText = document.getElementById('outputText'); | |
| outputText.select(); | |
| navigator.clipboard.writeText(outputText.value) | |
| .then(() => console.log('Text copied to clipboard')) | |
| .catch(err => console.error('Failed to copy text: ', err)); | |
| }); | |
| // Event listener for downloading text file | |
| document.getElementById('downloadButton').addEventListener('click', function () { | |
| const outputText = document.getElementById('outputText').value; | |
| if (!outputText.trim()) { | |
| document.getElementById('outputText').value = 'Error: No content to download. Please generate the text file first.'; | |
| return; | |
| } | |
| const blob = new Blob([outputText], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'prompt.txt'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| // Parse GitHub or Hugging Face repository URL | |
| function parseRepoUrl(url) { | |
| url = url.replace(/\/$/, ''); | |
| const githubPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)(?:\/tree\/(.+))?$/; | |
| const hfPattern = /^https:\/\/huggingface\.co\/(?:(datasets|spaces)\/)?([^\/]+)\/([^\/]+)(?:\/tree\/(.+))?$/; | |
| let match = url.match(githubPattern); | |
| if (match) { | |
| return { | |
| source: 'github', | |
| owner: match[1], | |
| repo: match[2], | |
| lastString: match[4] || '' | |
| }; | |
| } | |
| match = url.match(hfPattern); | |
| if (match) { | |
| let repo_type = 'model'; | |
| if (match[1] === 'datasets') repo_type = 'dataset'; | |
| if (match[1] === 'spaces') repo_type = 'space'; | |
| return { | |
| source: 'huggingface', | |
| repo_type: repo_type, | |
| owner: match[2], | |
| repo: match[3], | |
| lastString: match[4] || '' | |
| }; | |
| } | |
| throw new Error('Invalid GitHub or Hugging Face repository URL.'); | |
| } | |
| // Fetch GitHub repository references | |
| async function getGitHubReferences(owner, repo, token) { | |
| const headers = { 'Accept': 'application/vnd.github+json' }; | |
| if (token) headers['Authorization'] = `token ${token}`; | |
| const [branchesResponse, tagsResponse] = await Promise.all([ | |
| fetch(`https://api.github.com/repos/${owner}/${repo}/branches`, { headers }), | |
| fetch(`https://api.github.com/repos/${owner}/${repo}/tags`, { headers }) | |
| ]); | |
| if (!branchesResponse.ok || !tagsResponse.ok) handleFetchError(branchesResponse, 'github'); | |
| const branches = await branchesResponse.json(); | |
| const tags = await tagsResponse.json(); | |
| return { | |
| branches: branches.map(b => b.name), | |
| tags: tags.map(t => t.name) | |
| }; | |
| } | |
| // Fetch Hugging Face repository references | |
| async function getHuggingFaceReferences(owner, repo, repo_type, token) { | |
| const repoId = `${owner}/${repo}`; | |
| const typePath = repo_type === 'model' ? 'models' : repo_type === 'dataset' ? 'datasets' : 'spaces'; | |
| const url = `https://huggingface.co/api/${typePath}/${repoId}/refs`; | |
| const headers = {}; | |
| if (token) headers['Authorization'] = `Bearer ${token}`; | |
| const response = await fetch(url, { headers }); | |
| if (!response.ok) handleFetchError(response, 'huggingface'); | |
| const data = await response.json(); | |
| const branches = data.branches ? data.branches.map(b => b.name) : []; | |
| const tags = data.tags ? data.tags.map(t => t.name) : []; | |
| return [...branches, ...tags]; | |
| } | |
| // Fetch repository SHA | |
| async function fetchRepoSha(owner, repo, ref, path, token) { | |
| const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path ? `${path}` : ''}${ref ? `?ref=${ref}` : ''}`; | |
| const headers = { 'Accept': 'application/vnd.github.object+json' }; | |
| if (token) headers['Authorization'] = `token ${token}`; | |
| const response = await fetch(url, { headers }); | |
| if (!response.ok) handleFetchError(response, 'github'); | |
| const data = await response.json(); | |
| return data.sha; | |
| } | |
| // Fetch GitHub repository tree | |
| async function fetchGitHubRepoTree(owner, repo, sha, token) { | |
| const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}?recursive=1`; | |
| const headers = { 'Accept': 'application/vnd.github+json' }; | |
| if (token) headers['Authorization'] = `token ${token}`; | |
| const response = await fetch(url, { headers }); | |
| if (!response.ok) handleFetchError(response, 'github'); | |
| const data = await response.json(); | |
| return data.tree; | |
| } | |
| // Fetch Hugging Face repository tree | |
| async function fetchHuggingFaceTree(owner, repo, repo_type, ref, path, token) { | |
| const typePath = repo_type === 'model' ? 'models' : repo_type === 'dataset' ? 'datasets' : 'spaces'; | |
| const url = `https://huggingface.co/api/${typePath}/${owner}/${repo}/tree/${ref}?recursive=true`; | |
| const headers = {}; | |
| if (token) headers['Authorization'] = `Bearer ${token}`; | |
| const response = await fetch(url, { headers }); | |
| if (!response.ok) handleFetchError(response, 'huggingface'); | |
| let tree = await response.json(); | |
| if (path) { | |
| tree = tree.filter(item => item.path.startsWith(path + '/') || item.path === path); | |
| } | |
| return tree.map(item => { | |
| let repoIdForUrl; | |
| switch (repo_type) { | |
| case 'dataset': | |
| repoIdForUrl = `datasets/${owner}/${repo}`; | |
| break; | |
| case 'space': | |
| repoIdForUrl = `spaces/${owner}/${repo}`; | |
| break; | |
| default: // model | |
| repoIdForUrl = `${owner}/${repo}`; | |
| } | |
| return { | |
| path: item.path, | |
| type: (item.type === 'file' || item.type === 'lfs') ? 'blob' : 'tree', | |
| urlType: 'hf', | |
| url: `https://huggingface.co/${repoIdForUrl}/raw/${ref}/${item.path}` | |
| }; | |
| }); | |
| } | |
| // Handle fetch errors | |
| function handleFetchError(response, source = 'github') { | |
| if (response.status === 403 && source === 'github' && response.headers.get('X-RateLimit-Remaining') === '0') { | |
| throw new Error('GitHub API rate limit exceeded. Please try again later or provide a valid access token to increase your rate limit.'); | |
| } | |
| if (response.status === 401) { | |
| throw new Error(`Authentication error. Please check if your access token is valid and has the required permissions.`); | |
| } | |
| if (response.status === 404) { | |
| throw new Error(`Repository, branch, or path not found. Please check that the URL, branch/tag, and path are correct and accessible.`); | |
| } | |
| throw new Error(`Failed to fetch repository data. Status: ${response.status}. Please check your input and try again.`); | |
| } | |
| // Fetch contents of selected files | |
| async function fetchFileContents(files, token, source) { | |
| const contents = await Promise.all(files.map(async file => { | |
| let headers = {}; | |
| if (token) { | |
| headers['Authorization'] = source === 'github' ? `token ${token}` : `Bearer ${token}`; | |
| } | |
| if (source === 'github') { | |
| headers['Accept'] = 'application/vnd.github.v3.raw'; | |
| } | |
| const response = await fetch(file.url, { headers }); | |
| if (!response.ok) handleFetchError(response, source); | |
| const text = await response.text(); | |
| return { url: file.url, path: file.path, text }; | |
| })); | |
| return contents; | |
| } | |
| function setupShowMoreInfoButton() { | |
| const showMoreInfoButton = document.getElementById('showMoreInfo'); | |
| const tokenInfo = document.getElementById('tokenInfo'); | |
| showMoreInfoButton.addEventListener('click', function() { | |
| tokenInfo.classList.toggle('hidden'); | |
| updateInfoIcon(this, tokenInfo); | |
| }); | |
| } | |
| function updateInfoIcon(button, tokenInfo) { | |
| const icon = button.querySelector('[data-lucide]'); | |
| if (icon) { | |
| icon.setAttribute('data-lucide', tokenInfo.classList.contains('hidden') ? 'info' : 'x'); | |
| lucide.createIcons(); | |
| } | |
| } | |
| // Create and download zip file | |
| async function createAndDownloadZip(fileContents) { | |
| const zip = new JSZip(); | |
| fileContents.forEach(file => { | |
| const filePath = file.path.startsWith('/') ? file.path.slice(1) : file.path; | |
| zip.file(filePath, file.text); | |
| }); | |
| const content = await zip.generateAsync({type: "blob"}); | |
| const url = URL.createObjectURL(content); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'repo_contents.zip'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } |