Add 'Reset demos' button to refresh canonical demo files
Existing accounts (including admin) seeded before new demos shipped had no easy way to pull in the latest copies — the registration-time seeder is intentionally non-destructive. The new badge action fetches src/static/bundled-demos/manifest.json, confirms the overwrite, and re-copies each canonical demo into code/. Open tabs of those files are refreshed in place so the user sees the new content immediately. src/static/bundled-demos/ ships the six canonical files plus the manifest so this works in local mode and on a static-only host. The Dockerfile now mirrors workspace/code/<demo>.py into bundled-demos/ during the image build, keeping the two locations in sync. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -192,6 +192,14 @@ class TextEditor {
|
||||
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
||||
badge.appendChild(importBtn);
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.type = 'button';
|
||||
resetBtn.className = 'workspace-badge-action';
|
||||
resetBtn.textContent = 'Reset demos';
|
||||
resetBtn.title = 'Re-copy the bundled demos into code/ (overwrites your edits to those files)';
|
||||
resetBtn.addEventListener('click', () => this.resetDemoFiles());
|
||||
badge.appendChild(resetBtn);
|
||||
|
||||
const exit = document.createElement('button');
|
||||
exit.type = 'button';
|
||||
exit.className = 'workspace-badge-exit';
|
||||
@@ -259,9 +267,107 @@ class TextEditor {
|
||||
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
||||
badge.appendChild(importBtn);
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.type = 'button';
|
||||
resetBtn.className = 'workspace-badge-action';
|
||||
resetBtn.textContent = 'Reset demos';
|
||||
resetBtn.title = 'Re-copy the bundled demos into code/ (overwrites your edits to those files)';
|
||||
resetBtn.addEventListener('click', () => this.resetDemoFiles());
|
||||
badge.appendChild(resetBtn);
|
||||
|
||||
badge.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async resetDemoFiles() {
|
||||
let manifest;
|
||||
try {
|
||||
const r = await fetch('/static/bundled-demos/manifest.json', { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error(`manifest fetch ${r.status}`);
|
||||
manifest = await r.json();
|
||||
} catch (err) {
|
||||
this.showError(
|
||||
`Could not load demo manifest: ${err && err.message ? err.message : err}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const names = Array.isArray(manifest && manifest.files) ? manifest.files.slice() : [];
|
||||
if (!names.length) {
|
||||
this.showError('No demos in bundle');
|
||||
return;
|
||||
}
|
||||
if (!confirm(
|
||||
`Reset ${names.length} demo file${names.length === 1 ? '' : 's'}?\n\n` +
|
||||
names.map((n) => ` • code/${n}`).join('\n') +
|
||||
'\n\nAny edits you made to these files will be overwritten. Other ' +
|
||||
'files (main.py, your own scripts) are not touched.'
|
||||
)) return;
|
||||
|
||||
let written = 0;
|
||||
let failed = 0;
|
||||
for (const name of names) {
|
||||
try {
|
||||
const r = await fetch(`/static/bundled-demos/${encodeURIComponent(name)}`, {
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!r.ok) {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
const content = await r.text();
|
||||
const w = await this.apiFetch(`/api/file/code/${encodeURIComponent(name)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
if (w && w.ok) {
|
||||
written += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
} catch (_err) {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
this.workspaceSourcesCache = null;
|
||||
this.directoryCache.clear();
|
||||
/* If a stale demo is currently open in a tab, refresh the editor
|
||||
contents from disk so the user sees the new version. */
|
||||
for (const name of names) {
|
||||
const path = `code/${name}`;
|
||||
if (this.findTab(path)) {
|
||||
try {
|
||||
const fr = await this.apiFetch(`/api/file/${encodeURIComponent(path)}`);
|
||||
if (fr && fr.ok) {
|
||||
const fd = await fr.json();
|
||||
const tab = this.findTab(path);
|
||||
if (tab) {
|
||||
tab.content = typeof fd.content === 'string' ? fd.content : '';
|
||||
tab.savedContent = tab.content;
|
||||
if (this.activeTabPath === path) {
|
||||
this.ignoreNextChange = true;
|
||||
this.editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.editor.state.doc.length,
|
||||
insert: tab.content,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
// Skip refresh failure; user can re-open manually.
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.loadInitialDirectoryState();
|
||||
if (failed) {
|
||||
this.showError(`Reset ${written} demo${written === 1 ? '' : 's'} (${failed} failed)`);
|
||||
} else {
|
||||
this.showSuccess(`Reset ${written} demo${written === 1 ? '' : 's'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async pickLocalFolder() {
|
||||
if (!this.localWorkspace) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user