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:
@@ -15,6 +15,13 @@ COPY src ./src
|
|||||||
COPY lib ./lib
|
COPY lib ./lib
|
||||||
RUN mkdir -p src/static/bundled-lib && cp -f lib/*.py src/static/bundled-lib/
|
RUN mkdir -p src/static/bundled-lib && cp -f lib/*.py src/static/bundled-lib/
|
||||||
COPY workspace ./workspace
|
COPY workspace ./workspace
|
||||||
|
# Mirror canonical demo files into the static bundle so the editor's
|
||||||
|
# "Reset demos" button works from a static-only host too.
|
||||||
|
RUN mkdir -p src/static/bundled-demos && \
|
||||||
|
for f in pattern_rainbow_demo.py pattern_twinkle_demo.py pattern_chase_demo.py \
|
||||||
|
adc_slider_demo.py pin_demo.py serial_demo.py; do \
|
||||||
|
cp -f "workspace/code/$f" "src/static/bundled-demos/$f"; \
|
||||||
|
done
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
55
src/static/bundled-demos/adc_slider_demo.py
Normal file
55
src/static/bundled-demos/adc_slider_demo.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""ADC slider demo — drag the sliders that appear under the editor.
|
||||||
|
|
||||||
|
Two simulated ADCs:
|
||||||
|
* pin 34 — sets the base hue of a rainbow
|
||||||
|
* pin 35 — sets overall brightness
|
||||||
|
|
||||||
|
The strip lights up while the script runs; the values update live (no need to
|
||||||
|
restart the script when you move the slider).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import ADC, Pin
|
||||||
|
from neopixel import NeoPixel
|
||||||
|
|
||||||
|
|
||||||
|
NUM_LEDS = 16
|
||||||
|
strip = NeoPixel(Pin(5, Pin.OUT), NUM_LEDS)
|
||||||
|
|
||||||
|
hue_pot = ADC(Pin(34))
|
||||||
|
bri_pot = ADC(Pin(35))
|
||||||
|
|
||||||
|
|
||||||
|
def hsv_to_rgb(h, s, v):
|
||||||
|
h = h - int(h)
|
||||||
|
i = int(h * 6)
|
||||||
|
f = h * 6 - i
|
||||||
|
p = v * (1 - s)
|
||||||
|
q = v * (1 - f * s)
|
||||||
|
t = v * (1 - (1 - f) * s)
|
||||||
|
if i == 0:
|
||||||
|
r, g, b = v, t, p
|
||||||
|
elif i == 1:
|
||||||
|
r, g, b = q, v, p
|
||||||
|
elif i == 2:
|
||||||
|
r, g, b = p, v, t
|
||||||
|
elif i == 3:
|
||||||
|
r, g, b = p, q, v
|
||||||
|
elif i == 4:
|
||||||
|
r, g, b = t, p, v
|
||||||
|
else:
|
||||||
|
r, g, b = v, p, q
|
||||||
|
return int(r * 255), int(g * 255), int(b * 255)
|
||||||
|
|
||||||
|
|
||||||
|
print("Move the ADC sliders below the editor while this runs.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
base_hue = hue_pot.read_u16() / 65535
|
||||||
|
brightness = bri_pot.read_u16() / 65535
|
||||||
|
for i in range(NUM_LEDS):
|
||||||
|
h = (base_hue + i / NUM_LEDS) % 1.0
|
||||||
|
strip[i] = hsv_to_rgb(h, 1.0, brightness)
|
||||||
|
strip.write()
|
||||||
|
time.sleep(0.04)
|
||||||
10
src/static/bundled-demos/manifest.json
Normal file
10
src/static/bundled-demos/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"pattern_rainbow_demo.py",
|
||||||
|
"pattern_twinkle_demo.py",
|
||||||
|
"pattern_chase_demo.py",
|
||||||
|
"adc_slider_demo.py",
|
||||||
|
"pin_demo.py",
|
||||||
|
"serial_demo.py"
|
||||||
|
]
|
||||||
|
}
|
||||||
83
src/static/bundled-demos/pattern_chase_demo.py
Normal file
83
src/static/bundled-demos/pattern_chase_demo.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""Knight Rider–style bouncing scanner — self-contained (stdlib + simulated hardware only)."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import Pin
|
||||||
|
import neopixel
|
||||||
|
|
||||||
|
# --- helpers
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp(channel: int) -> int:
|
||||||
|
return max(0, min(255, int(channel)))
|
||||||
|
|
||||||
|
|
||||||
|
def _bounce_head_index(led_count: int, frame: int) -> int:
|
||||||
|
if led_count <= 1:
|
||||||
|
return 0
|
||||||
|
span = led_count - 1
|
||||||
|
cycle = span * 2
|
||||||
|
if cycle <= 0:
|
||||||
|
return 0
|
||||||
|
t = frame % cycle
|
||||||
|
return t if t <= span else 2 * span - t
|
||||||
|
|
||||||
|
|
||||||
|
def _bounce_phase_tail_direction(led_count: int, frame: int) -> int:
|
||||||
|
if led_count <= 1:
|
||||||
|
return -1
|
||||||
|
span = led_count - 1
|
||||||
|
cycle = span * 2
|
||||||
|
if cycle <= 0:
|
||||||
|
return -1
|
||||||
|
t = frame % cycle
|
||||||
|
if t <= span:
|
||||||
|
return -1
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def knight_rider_scanner_frame(
|
||||||
|
led_count: int,
|
||||||
|
frame: int,
|
||||||
|
head_color=(220, 0, 28),
|
||||||
|
tail_len: int = 8,
|
||||||
|
falloff_gamma: float = 2.6,
|
||||||
|
):
|
||||||
|
if led_count <= 0:
|
||||||
|
return []
|
||||||
|
out = [(0, 0, 0) for _ in range(led_count)]
|
||||||
|
tl = max(1, tail_len)
|
||||||
|
head = _bounce_head_index(led_count, frame)
|
||||||
|
direc = _bounce_phase_tail_direction(led_count, frame)
|
||||||
|
gamma = max(1.05, falloff_gamma)
|
||||||
|
for rk in reversed(range(tl)):
|
||||||
|
idx = head + direc * rk
|
||||||
|
if idx < 0 or idx >= led_count:
|
||||||
|
continue
|
||||||
|
w = max(0.0, float(tl - rk) / float(tl))
|
||||||
|
strength = w**gamma
|
||||||
|
out[idx] = tuple(_clamp(int(head_color[ch] * strength)) for ch in range(3))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --- demo
|
||||||
|
|
||||||
|
NUM_LEDS = 16
|
||||||
|
|
||||||
|
np = neopixel.NeoPixel(Pin(4), NUM_LEDS)
|
||||||
|
|
||||||
|
for frame in range(200):
|
||||||
|
frame_colors = knight_rider_scanner_frame(
|
||||||
|
len(np),
|
||||||
|
frame,
|
||||||
|
head_color=(220, 0, 36),
|
||||||
|
tail_len=10,
|
||||||
|
falloff_gamma=2.85,
|
||||||
|
)
|
||||||
|
for i, color in enumerate(frame_colors):
|
||||||
|
np[i] = color
|
||||||
|
np.write()
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
np.fill((0, 0, 0))
|
||||||
|
np.write()
|
||||||
47
src/static/bundled-demos/pattern_rainbow_demo.py
Normal file
47
src/static/bundled-demos/pattern_rainbow_demo.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Rainbow NeoPixel sweep — self-contained (stdlib + simulated hardware only)."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import Pin
|
||||||
|
import neopixel
|
||||||
|
|
||||||
|
# --- helpers (same logic as bundled led_patterns.py, inlined here)
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp(channel: int) -> int:
|
||||||
|
return max(0, min(255, int(channel)))
|
||||||
|
|
||||||
|
|
||||||
|
def wheel(pos: int):
|
||||||
|
"""Return rainbow RGB at position 0–255."""
|
||||||
|
pos = 255 - (pos & 255)
|
||||||
|
if pos < 85:
|
||||||
|
return (_clamp(255 - pos * 3), 0, _clamp(pos * 3))
|
||||||
|
if pos < 170:
|
||||||
|
pos -= 85
|
||||||
|
return (0, _clamp(pos * 3), _clamp(255 - pos * 3))
|
||||||
|
pos -= 170
|
||||||
|
return (_clamp(pos * 3), _clamp(255 - pos * 3), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def rainbow_frame(led_count: int, frame: int, step: int = 4):
|
||||||
|
if led_count <= 0:
|
||||||
|
return []
|
||||||
|
return [wheel((i * 256 // led_count + frame * step) & 255) for i in range(led_count)]
|
||||||
|
|
||||||
|
|
||||||
|
# --- demo
|
||||||
|
|
||||||
|
NUM_LEDS = 16
|
||||||
|
|
||||||
|
np = neopixel.NeoPixel(Pin(4), NUM_LEDS)
|
||||||
|
|
||||||
|
for frame in range(120):
|
||||||
|
frame_colors = rainbow_frame(len(np), frame, step=5)
|
||||||
|
for i, color in enumerate(frame_colors):
|
||||||
|
np[i] = color
|
||||||
|
np.write()
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
np.fill((0, 0, 0))
|
||||||
|
np.write()
|
||||||
54
src/static/bundled-demos/pattern_twinkle_demo.py
Normal file
54
src/static/bundled-demos/pattern_twinkle_demo.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""Twinkle NeoPixel demo — self-contained (stdlib + simulated hardware only)."""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import Pin
|
||||||
|
import neopixel
|
||||||
|
|
||||||
|
# --- helpers
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp(channel: int) -> int:
|
||||||
|
return max(0, min(255, int(channel)))
|
||||||
|
|
||||||
|
|
||||||
|
def twinkle_frame(
|
||||||
|
led_count: int,
|
||||||
|
frame: int,
|
||||||
|
base=(0, 0, 8),
|
||||||
|
sparkle=(255, 255, 180),
|
||||||
|
sparkles: int = 3,
|
||||||
|
seed: int = 1337,
|
||||||
|
):
|
||||||
|
if led_count <= 0:
|
||||||
|
return []
|
||||||
|
out = [tuple(_clamp(v) for v in base) for _ in range(led_count)]
|
||||||
|
rng = random.Random(seed + frame)
|
||||||
|
for _ in range(min(max(0, sparkles), led_count)):
|
||||||
|
idx = rng.randrange(led_count)
|
||||||
|
out[idx] = tuple(_clamp(v) for v in sparkle)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --- demo
|
||||||
|
|
||||||
|
NUM_LEDS = 16
|
||||||
|
|
||||||
|
np = neopixel.NeoPixel(Pin(4), NUM_LEDS)
|
||||||
|
|
||||||
|
for frame in range(120):
|
||||||
|
frame_colors = twinkle_frame(
|
||||||
|
len(np),
|
||||||
|
frame,
|
||||||
|
base=(0, 0, 6),
|
||||||
|
sparkle=(255, 210, 130),
|
||||||
|
sparkles=3,
|
||||||
|
)
|
||||||
|
for i, color in enumerate(frame_colors):
|
||||||
|
np[i] = color
|
||||||
|
np.write()
|
||||||
|
time.sleep(0.08)
|
||||||
|
|
||||||
|
np.fill((0, 0, 0))
|
||||||
|
np.write()
|
||||||
54
src/static/bundled-demos/pin_demo.py
Normal file
54
src/static/bundled-demos/pin_demo.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""Pin features demo.
|
||||||
|
|
||||||
|
A "Pins" panel appears below the editor while this script runs:
|
||||||
|
|
||||||
|
* Pin 2 (OUT) — blinks every 200 ms; the indicator follows along.
|
||||||
|
* Pin 4 (OUT) — chases through .on() / .off() / .toggle().
|
||||||
|
* Pin 0 (IN) — click the toggle button in the panel to flip its value.
|
||||||
|
When it goes 0 -> 1 we register an IRQ that toggles pin 2.
|
||||||
|
* Pin 13 (PWM) — duty sweeps up and down; the bar shows the live duty cycle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import Pin, PWM
|
||||||
|
|
||||||
|
|
||||||
|
led_a = Pin(2, Pin.OUT)
|
||||||
|
led_b = Pin(4, Pin.OUT)
|
||||||
|
button = Pin(0, Pin.IN, Pin.PULL_UP)
|
||||||
|
fader = PWM(Pin(13), freq=1000, duty_u16=0)
|
||||||
|
|
||||||
|
|
||||||
|
def on_button(pin):
|
||||||
|
print("[irq] button rising edge -> toggling pin 2")
|
||||||
|
led_a.toggle()
|
||||||
|
|
||||||
|
|
||||||
|
button.irq(handler=on_button, trigger=Pin.IRQ_RISING)
|
||||||
|
|
||||||
|
|
||||||
|
tick = 0
|
||||||
|
duty = 0
|
||||||
|
direction = 1024
|
||||||
|
|
||||||
|
while True:
|
||||||
|
led_a.value(tick % 2)
|
||||||
|
if tick % 4 == 0:
|
||||||
|
led_b.on()
|
||||||
|
elif tick % 4 == 2:
|
||||||
|
led_b.off()
|
||||||
|
|
||||||
|
duty += direction
|
||||||
|
if duty >= 65535:
|
||||||
|
duty = 65535
|
||||||
|
direction = -1024
|
||||||
|
elif duty <= 0:
|
||||||
|
duty = 0
|
||||||
|
direction = 1024
|
||||||
|
fader.duty_u16(duty)
|
||||||
|
|
||||||
|
button.value()
|
||||||
|
|
||||||
|
tick += 1
|
||||||
|
time.sleep(0.1)
|
||||||
90
src/static/bundled-demos/serial_demo.py
Normal file
90
src/static/bundled-demos/serial_demo.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""Serial in/out demo.
|
||||||
|
|
||||||
|
When this script runs, a "Serial monitor" pane appears below the editor.
|
||||||
|
|
||||||
|
Try this:
|
||||||
|
* type hello and press Enter -> Python echoes "echo: hello"
|
||||||
|
* type color red -> the strip turns red
|
||||||
|
* try color 0,128,255 -> any (r,g,b) tuple works
|
||||||
|
* type off -> strip blanks
|
||||||
|
* type bye -> script exits cleanly
|
||||||
|
|
||||||
|
Anything Python `write()`s to the UART shows up in green; what you type back
|
||||||
|
is shown in white.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import Pin, UART
|
||||||
|
from neopixel import NeoPixel
|
||||||
|
|
||||||
|
|
||||||
|
NUM_LEDS = 16
|
||||||
|
strip = NeoPixel(Pin(5, Pin.OUT), NUM_LEDS)
|
||||||
|
uart = UART(0, baudrate=115200)
|
||||||
|
|
||||||
|
PALETTE = {
|
||||||
|
"red": (255, 0, 0),
|
||||||
|
"green": (0, 255, 0),
|
||||||
|
"blue": (0, 0, 255),
|
||||||
|
"white": (200, 200, 200),
|
||||||
|
"purple": (160, 0, 200),
|
||||||
|
"orange": (255, 110, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fill(color):
|
||||||
|
strip.fill(color)
|
||||||
|
strip.write()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_color(arg):
|
||||||
|
arg = arg.strip().lower()
|
||||||
|
if arg in PALETTE:
|
||||||
|
return PALETTE[arg]
|
||||||
|
parts = [p for p in arg.replace(",", " ").split() if p]
|
||||||
|
if len(parts) == 3:
|
||||||
|
try:
|
||||||
|
return tuple(max(0, min(255, int(p))) for p in parts)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
uart.write("ready. commands: color <name|r,g,b> | off | bye\n")
|
||||||
|
fill((0, 0, 0))
|
||||||
|
|
||||||
|
running = True
|
||||||
|
while running:
|
||||||
|
line = uart.readline()
|
||||||
|
if line is None:
|
||||||
|
time.sleep(0.05)
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = line.decode("utf-8", errors="replace").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if text == "bye":
|
||||||
|
uart.write("goodbye!\n")
|
||||||
|
running = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if text == "off":
|
||||||
|
fill((0, 0, 0))
|
||||||
|
uart.write("strip off\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if text.startswith("color"):
|
||||||
|
rest = text[len("color"):].strip()
|
||||||
|
color = parse_color(rest) if rest else None
|
||||||
|
if color is None:
|
||||||
|
uart.write("usage: color <name> | color r,g,b\n")
|
||||||
|
else:
|
||||||
|
fill(color)
|
||||||
|
uart.write(f"strip = {color}\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
uart.write(f"echo: {text}\n")
|
||||||
|
|
||||||
|
fill((0, 0, 0))
|
||||||
@@ -118,6 +118,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/static/script.js?v=56"></script>
|
<script type="module" src="/static/script.js?v=57"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -192,6 +192,14 @@ class TextEditor {
|
|||||||
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
||||||
badge.appendChild(importBtn);
|
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');
|
const exit = document.createElement('button');
|
||||||
exit.type = 'button';
|
exit.type = 'button';
|
||||||
exit.className = 'workspace-badge-exit';
|
exit.className = 'workspace-badge-exit';
|
||||||
@@ -259,9 +267,107 @@ class TextEditor {
|
|||||||
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
||||||
badge.appendChild(importBtn);
|
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');
|
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() {
|
async pickLocalFolder() {
|
||||||
if (!this.localWorkspace) return;
|
if (!this.localWorkspace) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user