Download app/src/local-folder-ui.mjs from Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP: direct link, hf CLI and curl.
- Browser
- Download file 8.06 kB
-
https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/src/local-folder-ui.mjs
- Command line
-
hf download hf://spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/app/src/local-folder-ui.mjs
-
curl -L -o local-folder-ui.mjs https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/src/local-folder-ui.mjs
8.06 kB
| import { createTwoFilesPatch } from 'diff'; | |
| const $ = selector => document.querySelector(selector); | |
| const text = value => { | |
| if (value === null) return ''; | |
| const decoded = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(atob(value), c => c.charCodeAt(0))); | |
| if (decoded.includes('\0')) throw Error('Binary file'); | |
| return decoded; | |
| }; | |
| export function createLocalFolderUI({ rpc, notice, canSwitch, onSwitch }) { | |
| let handle, saved, current, busy = false, opening = false, reviewPatches = []; | |
| const dialog = $('#local-review'); | |
| const embedded = window.self !== window.top; | |
| const supported = typeof window.showDirectoryPicker === 'function'; | |
| function controls() { | |
| $('#open-folder').hidden = Boolean(current); | |
| $('#reconnect-folder').hidden = Boolean(current) || !saved; | |
| $('#local-folder-bar').hidden = !current; | |
| for (const id of ['open-folder', 'reconnect-folder', 'review-changes', 'refresh-folder', 'disconnect-folder']) $('#' + id).disabled = busy || opening; | |
| $('#review-changes').disabled ||= !current?.count; | |
| $('#refresh-folder').disabled ||= Boolean(current?.count); | |
| $('#review-apply').disabled = busy || !reviewPatches.length; | |
| $('#review-discard').disabled = busy || !reviewPatches.length; | |
| $('#review-close').disabled = busy; | |
| if (current) { | |
| $('#local-folder-name').textContent = current.name; | |
| $('#local-folder-hint').textContent = current.count ? `${current.count} staged change${current.count === 1 ? '' : 's'} · original files unchanged until Apply` : 'Local folder · edits are staged until you apply them'; | |
| $('#review-changes').textContent = current.count ? `Review & apply (${current.count})` : 'Review & apply'; | |
| } | |
| $('#export').textContent = current ? 'Export changes' : 'Export'; | |
| $('#save').textContent = current ? 'Stage edit' : 'Save'; | |
| } | |
| function available() { | |
| if (embedded) { | |
| $('#open-tab').href = new URL('?local-folder', location.href).href; $('#open-tab').hidden = false; | |
| notice('Open the workspace in a new tab, then choose Link a folder. Browsers cannot grant folder access inside this embedded Space.'); | |
| return false; | |
| } | |
| if (!supported) { | |
| notice('Direct local-folder access needs a supported browser, such as Chrome or Edge. In this browser, use Import and Export to work with copies of your files.'); | |
| return false; | |
| } | |
| return true; | |
| } | |
| async function connect(restore) { | |
| if (!available() || busy || opening) return; | |
| try { | |
| canSwitch(); opening = true; controls(); | |
| // Invoke the picker/permission request directly from the click, before | |
| // waiting for the worker: these APIs require transient user activation. | |
| let next; | |
| if (restore) { | |
| next = saved.handle; | |
| if (await next.requestPermission({ mode: 'read' }) !== 'granted') throw Error('Folder access was not granted.'); | |
| } else { | |
| if (saved?.count && !window.confirm('Opening another folder replaces the saved staged edits for the previous folder. Reconnect to review them first, or continue?')) return; | |
| next = await window.showDirectoryPicker({ id: 'pi-workspace', mode: 'read' }); | |
| } | |
| const state = await rpc('local_open', { handle: next, restore }); | |
| handle = next; saved = { handle: next, count: state.localFolder.count }; | |
| onSwitch(state); notice(`Opened ${next.name}. Review & apply writes staged changes to disk. Files load on demand; Refresh picks up external edits.`); | |
| } catch (error) { if (error.name !== 'AbortError') notice(error.message); } | |
| finally { opening = false; controls(); } | |
| } | |
| $('#open-folder').onclick = () => connect(false); | |
| $('#reconnect-folder').onclick = () => connect(true); | |
| $('#disconnect-folder').onclick = async () => { | |
| try { | |
| canSwitch(); saved = { handle, count: current.count }; | |
| const state = await rpc('local_close'); onSwitch(state); | |
| notice('Demo workspace restored. Reconnect to continue with your local folder and staged edits.'); | |
| } catch (error) { notice(error.message); } | |
| }; | |
| $('#refresh-folder').onclick = async () => { | |
| try { canSwitch(); onSwitch(await rpc('local_refresh')); notice('Folder refreshed from disk. Started a new chat with the current files.'); } | |
| catch (error) { notice(error.message); } | |
| }; | |
| function renderReview(patches) { | |
| reviewPatches = patches; | |
| $('#review-files').replaceChildren(); $('#review-error').textContent = ''; | |
| $('#review-description').textContent = `Review changes to ${current.name}. Applying writes them to your local folder.`; | |
| for (const patch of patches) { | |
| const details = document.createElement('details'), title = document.createElement('summary'), pre = document.createElement('pre'); | |
| const kind = patch.kind === 'directory' ? 'New folder' : patch.after === null ? 'Delete' : patch.before === null ? 'Create' : 'Edit'; | |
| title.textContent = `${kind} · ${patch.path.slice(1)}`; | |
| details.append(title); details.open = patches.length < 5; | |
| if (patch.kind !== 'directory') { | |
| try { | |
| const before = text(patch.before), after = text(patch.after); | |
| if (before.length + after.length > 160_000) pre.textContent = 'Large text change. Inspect the file in the editor before applying.'; | |
| else { | |
| const diff = createTwoFilesPatch(patch.path, patch.path, before, after, 'on disk when read', 'staged', { context: 3 }); | |
| for (const line of diff.split('\n')) { | |
| const span = document.createElement('span'); span.textContent = line + '\n'; | |
| if (line.startsWith('+')) span.className = 'diff-add'; | |
| if (line.startsWith('-')) span.className = 'diff-remove'; | |
| pre.append(span); | |
| } | |
| } | |
| } catch { pre.textContent = `Binary file · ${patch.before === null ? 0 : atob(patch.before).length} → ${patch.after === null ? 0 : atob(patch.after).length} bytes`; } | |
| details.append(pre); | |
| } | |
| $('#review-files').append(details); | |
| } | |
| controls(); | |
| } | |
| $('#review-changes').onclick = async () => { | |
| try { canSwitch(); renderReview(await rpc('local_changes')); dialog.showModal(); } | |
| catch (error) { notice(error.message); } | |
| }; | |
| $('#review-close').onclick = () => dialog.close(); | |
| dialog.addEventListener('cancel', event => { if (busy) event.preventDefault(); }); | |
| $('#review-apply').onclick = async () => { | |
| try { | |
| // Only this user gesture requests write access. The worker never prompts. | |
| if (await handle.requestPermission({ mode: 'readwrite' }) !== 'granted') throw Error('Write access was not granted. Changes are still staged.'); | |
| const result = await rpc('local_apply'); | |
| dialog.close(); notice(`Applied ${result.applied} change${result.applied === 1 ? '' : 's'} to ${handle.name}.`); | |
| } catch (error) { | |
| // Application can partially succeed; refresh the review to show only the remaining edits. | |
| try { renderReview(await rpc('local_changes')); } catch {} | |
| $('#review-error').textContent = error.message; | |
| } | |
| }; | |
| $('#review-discard').onclick = async () => { | |
| if (!window.confirm('Discard all staged changes and reload files from disk? Your local files will not be changed.')) return; | |
| try { onSwitch(await rpc('local_discard')); dialog.close(); notice('Staged edits discarded. Folder refreshed and chat reset.'); } | |
| catch (error) { $('#review-error').textContent = error.message; } | |
| }; | |
| window.addEventListener('beforeunload', event => { | |
| if (current?.count) { event.preventDefault(); event.returnValue = ''; } | |
| }); | |
| if (new URLSearchParams(location.search).has('local-folder')) notice('Choose Link a folder to select the project you want Pi to work on.'); | |
| return { | |
| setBusy(value) { busy = value; controls(); }, | |
| setState(value) { current = value; if (saved && value) saved.count = value.count; controls(); }, | |
| restore(value) { saved = value; controls(); }, | |
| get folderName() { return current?.name; }, | |
| get active() { return Boolean(current); }, | |
| }; | |
| } | |