Spaces:
Running
Running
| document.getElementById('repoForm').addEventListener('submit', async function (e) { | |
| e.preventDefault(); | |
| const repoUrl = document.getElementById('repoUrl').value; | |
| const ref = document.getElementById('ref').value || ''; | |
| const path = document.getElementById('path').value || ''; | |
| const accessToken = document.getElementById('accessToken').value; | |
| const outputText = document.getElementById('outputText'); | |
| outputText.value = ''; | |
| try { | |
| const { owner, repo, refFromUrl, pathFromUrl } = parseRepoUrl(repoUrl); | |
| const finalRef = ref || refFromUrl; | |
| const finalPath = path || pathFromUrl; | |
| const sha = await fetchRepoSha(owner, repo, finalRef, finalPath, accessToken); | |
| const tree = await fetchRepoTree(owner, repo, sha, accessToken); | |
| displayDirectoryStructure(tree); | |
| document.getElementById('generateTextButton').style.display = 'flex'; | |
| } catch (error) { | |
| outputText.value = `Error fetching repository contents: ${error.message}\n\nPlease ensure:\n1. The repository URL is correct and accessible.\n2. You have the necessary permissions to access the repository.\n3. If it's a private repository, you've provided a valid access token.\n4. The specified branch/tag and path (if any) exist in the repository.`; | |
| } | |
| }); | |
| document.getElementById('generateTextButton').addEventListener('click', async function () { | |
| const accessToken = document.getElementById('accessToken').value; | |
| const outputText = document.getElementById('outputText'); | |
| outputText.value = ''; | |
| try { | |
| const selectedFiles = getSelectedFiles(); | |
| if (selectedFiles.length === 0) { | |
| throw new Error('No files selected'); | |
| } | |
| const fileContents = await fetchFileContents(selectedFiles, accessToken); | |
| 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\nPlease ensure:\n1. You have selected at least one file from the directory structure.\n2. Your access token (if provided) is valid and has the necessary permissions.\n3. You have a stable internet connection.\n4. The GitHub API is accessible and functioning normally.`; | |
| } | |
| }); | |
| 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); | |
| }); | |
| }); | |
| 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 = 'file.txt'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| function parseRepoUrl(url) { | |
| url = url.replace(/\/$/, ''); | |
| const urlPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)(\/tree\/([^\/]+)(\/(.+))?)?$/; | |
| const match = url.match(urlPattern); | |
| if (!match) { | |
| throw new Error('Invalid GitHub repository URL. Please ensure the URL is in the correct format: https://github.com/owner/repo or https://github.com/owner/repo/tree/branch/path'); | |
| } | |
| return { | |
| owner: match[1], | |
| repo: match[2], | |
| refFromUrl: match[4], | |
| pathFromUrl: match[6] | |
| }; | |
| } | |
| 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) { | |
| if (response.status === 403 && 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 === 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 SHA. Status: ${response.status}. Please check your input and try again.`); | |
| } | |
| const data = await response.json(); | |
| return data.sha; | |
| } | |
| async function fetchRepoTree(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) { | |
| if (response.status === 403 && 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.'); | |
| } | |
| throw new Error(`Failed to fetch repository tree. Status: ${response.status}. Please check your input and try again.`); | |
| } | |
| const data = await response.json(); | |
| return data.tree; | |
| } | |
| function displayDirectoryStructure(tree) { | |
| tree = tree.filter(item => item.type === 'blob'); | |
| tree = sortContents(tree); | |
| const container = document.getElementById('directoryStructure'); | |
| container.innerHTML = ''; | |
| const rootUl = document.createElement('ul'); | |
| container.appendChild(rootUl); | |
| const directoryStructure = {}; | |
| tree.forEach(item => { | |
| item.path = item.path.startsWith('/') ? item.path : '/' + item.path; | |
| const pathParts = item.path.split('/'); | |
| let currentLevel = directoryStructure; | |
| pathParts.forEach((part, index) => { | |
| if (part === '') { | |
| part = './'; | |
| } | |
| if (!currentLevel[part]) { | |
| currentLevel[part] = index === pathParts.length - 1 ? item : {}; | |
| } | |
| currentLevel = currentLevel[part]; | |
| }); | |
| }); | |
| function createTreeNode(name, item, parentUl) { | |
| const li = document.createElement('li'); | |
| const checkbox = document.createElement('input'); | |
| checkbox.type = 'checkbox'; | |
| const commonExtensions = ['.js', '.py', '.java', '.cpp', '.html', '.css', '.ts', '.jsx', '.tsx']; | |
| const fileName = name.toLowerCase(); | |
| const isCommonFile = commonExtensions.some(ext => fileName.endsWith(ext)); | |
| checkbox.checked = isCommonFile; | |
| checkbox.className = 'mr-2'; | |
| if (typeof item === 'object' && (!item.type || typeof item.type !== 'string')) { | |
| // Directory | |
| checkbox.classList.add('directory-checkbox'); | |
| li.appendChild(checkbox); | |
| // Add collapse/expand button | |
| const collapseButton = document.createElement('button'); | |
| collapseButton.innerHTML = '<i data-lucide="chevron-down" class="w-4 h-4"></i>'; | |
| collapseButton.className = 'mr-1 focus:outline-none'; | |
| li.appendChild(collapseButton); | |
| const folderIcon = document.createElement('i'); | |
| folderIcon.setAttribute('data-lucide', 'folder'); | |
| folderIcon.className = 'inline-block w-4 h-4 mr-1'; | |
| li.appendChild(folderIcon); | |
| li.appendChild(document.createTextNode(name)); | |
| const ul = document.createElement('ul'); | |
| ul.className = 'ml-6 mt-2'; | |
| li.appendChild(ul); | |
| for (const [childName, childItem] of Object.entries(item)) { | |
| createTreeNode(childName, childItem, ul); | |
| } | |
| checkbox.addEventListener('change', function() { | |
| const childCheckboxes = li.querySelectorAll('input[type="checkbox"]'); | |
| childCheckboxes.forEach(childBox => { | |
| childBox.checked = this.checked; | |
| childBox.indeterminate = false; | |
| }); | |
| }); | |
| // Add collapse/expand functionality | |
| collapseButton.addEventListener('click', function() { | |
| ul.classList.toggle('hidden'); | |
| const icon = this.querySelector('[data-lucide]'); | |
| if (ul.classList.contains('hidden')) { | |
| icon.setAttribute('data-lucide', 'chevron-right'); | |
| } else { | |
| icon.setAttribute('data-lucide', 'chevron-down'); | |
| } | |
| lucide.createIcons(); | |
| }); | |
| } else { | |
| // File | |
| checkbox.value = JSON.stringify({ url: item.url, path: item.path }); | |
| li.appendChild(checkbox); | |
| const fileIcon = document.createElement('i'); | |
| fileIcon.setAttribute('data-lucide', 'file'); | |
| fileIcon.className = 'inline-block w-4 h-4 mr-1'; | |
| li.appendChild(fileIcon); | |
| li.appendChild(document.createTextNode(name)); | |
| } | |
| li.className = 'my-2'; | |
| parentUl.appendChild(li); | |
| updateParentCheckbox(checkbox); | |
| } | |
| for (const [name, item] of Object.entries(directoryStructure)) { | |
| createTreeNode(name, item, rootUl); | |
| } | |
| // Add event listener to container for checkbox changes | |
| container.addEventListener('change', function(event) { | |
| if (event.target.type === 'checkbox') { | |
| updateParentCheckbox(event.target); | |
| } | |
| }); | |
| function updateParentCheckbox(checkbox) { | |
| if (!checkbox) return; | |
| const li = checkbox.closest('li'); | |
| if (!li) return; | |
| if (!li.parentElement) return; | |
| const parentLi = li.parentElement.closest('li'); | |
| if (!parentLi) return; | |
| const parentCheckbox = parentLi.querySelector(':scope > input[type="checkbox"]'); | |
| const siblingCheckboxes = parentLi.querySelectorAll(':scope > ul > li > input[type="checkbox"]'); | |
| const checkedCount = Array.from(siblingCheckboxes).filter(cb => cb.checked).length; | |
| const indeterminateCount = Array.from(siblingCheckboxes).filter(cb => cb.indeterminate).length; | |
| if (indeterminateCount !== 0) { | |
| parentCheckbox.checked = false; | |
| parentCheckbox.indeterminate = true; | |
| } else if (checkedCount === 0) { | |
| parentCheckbox.checked = false; | |
| parentCheckbox.indeterminate = false; | |
| } else if (checkedCount === siblingCheckboxes.length) { | |
| parentCheckbox.checked = true; | |
| parentCheckbox.indeterminate = false; | |
| } else { | |
| parentCheckbox.checked = false; | |
| parentCheckbox.indeterminate = true; | |
| } | |
| // Recursively update parent checkboxes | |
| updateParentCheckbox(parentCheckbox); | |
| } | |
| lucide.createIcons(); | |
| } | |
| function getSelectedFiles() { | |
| const checkboxes = document.querySelectorAll('#directoryStructure input[type="checkbox"]:checked:not(.directory-checkbox)'); | |
| return Array.from(checkboxes).map(checkbox => JSON.parse(checkbox.value)); | |
| } | |
| async function fetchFileContents(files, token) { | |
| const headers = { | |
| 'Accept': 'application/vnd.github.v3.raw' | |
| }; | |
| if (token) { | |
| headers['Authorization'] = `token ${token}`; | |
| } | |
| const contents = await Promise.all(files.map(async file => { | |
| const response = await fetch(file.url, { headers }); | |
| if (!response.ok) { | |
| if (response.status === 403 && response.headers.get('X-RateLimit-Remaining') === '0') { | |
| throw new Error(`GitHub API rate limit exceeded while fetching ${file.path}. Please try again later or provide a valid access token to increase your rate limit.`); | |
| } | |
| throw new Error(`Failed to fetch content for ${file.path}. Status: ${response.status}. Please check your permissions and try again.`); | |
| } | |
| const text = await response.text(); | |
| return { url: file.url, path: file.path, text }; | |
| })); | |
| return contents; | |
| } | |
| function formatRepoContents(contents) { | |
| let text = ''; | |
| let index = ''; | |
| contents = sortContents(contents); | |
| // Create a directory tree structure | |
| const tree = {}; | |
| contents.forEach(item => { | |
| const parts = item.path.split('/'); | |
| let currentLevel = tree; | |
| parts.forEach((part, i) => { | |
| if (!currentLevel[part]) { | |
| currentLevel[part] = i === parts.length - 1 ? null : {}; | |
| } | |
| currentLevel = currentLevel[part]; | |
| }); | |
| }); | |
| // Function to recursively build the index | |
| function buildIndex(node, prefix = '') { | |
| let result = ''; | |
| const entries = Object.entries(node); | |
| entries.forEach(([name, subNode], index) => { | |
| const isLastItem = index === entries.length - 1; | |
| const linePrefix = isLastItem ? 'βββ ' : 'βββ '; | |
| const childPrefix = isLastItem ? ' ' : 'β '; | |
| if (name === '') { | |
| name = './'; | |
| } | |
| result += `${prefix}${linePrefix}${name}\n`; | |
| if (subNode) { | |
| result += buildIndex(subNode, `${prefix}${childPrefix}`); | |
| } | |
| }); | |
| return result; | |
| } | |
| index = buildIndex(tree); | |
| contents.forEach((item) => { | |
| text += `\n\n---\nFile: ${item.path}\n---\n\n${item.text}\n`; | |
| }); | |
| return `Directory Structure:\n\n${index}\n${text}`; | |
| } | |
| function sortContents(contents) { | |
| contents.sort((a, b) => { | |
| const aPath = a.path.split('/'); | |
| const bPath = b.path.split('/'); | |
| const minLength = Math.min(aPath.length, bPath.length); | |
| for (let i = 0; i < minLength; i++) { | |
| if (aPath[i] !== bPath[i]) { | |
| if (i === aPath.length - 1 && i < bPath.length - 1) return 1; // a is a directory, b is a file or subdirectory | |
| if (i === bPath.length - 1 && i < aPath.length - 1) return -1; // b is a directory, a is a file or subdirectory | |
| return aPath[i].localeCompare(bPath[i]); | |
| } | |
| } | |
| return aPath.length - bPath.length; | |
| }); | |
| return contents; | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| lucide.createIcons(); | |
| // Add event listener for the showMoreInfo button | |
| const showMoreInfoButton = document.getElementById('showMoreInfo'); | |
| const tokenInfo = document.getElementById('tokenInfo'); | |
| showMoreInfoButton.addEventListener('click', function() { | |
| tokenInfo.classList.toggle('hidden'); | |
| // Change the icon based on the visibility state | |
| const icon = this.querySelector('[data-lucide]'); | |
| if (icon) { | |
| if (tokenInfo.classList.contains('hidden')) { | |
| icon.setAttribute('data-lucide', 'info'); | |
| } else { | |
| icon.setAttribute('data-lucide', 'x'); | |
| } | |
| lucide.createIcons(); | |
| } | |
| }); | |
| }); |