Replace server-folder docx import with direct file picker

The old import scanned a hardcoded Seafile CloudStorage path that only
worked on one specific machine. Replaced with a standard <input type=file>
browser picker that reads the .docx as base64 and uploads it directly, so
the feature works everywhere. Server endpoint now accepts either a base64
payload or the legacy path. Also fixed a crash where resetImportSection()
still referenced the removed select element, which was breaking the item
editor for all item types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-29 17:15:23 -04:00
1 parent de598496bc
commit b048f51d46
3 files changed
+33 -40

No files matched your search

+5 -4
View File
@@ -169,12 +169,13 @@
<p id="scan-status" class="hint"></p> <p id="scan-status" class="hint"></p>
</div> </div>
<!-- Import from Homeschool folder: visible for lesson, assignment, quiz types --> <!-- Import from .docx file: visible for lesson, assignment, quiz types -->
<div id="ie-import-section" class="scan-card" hidden> <div id="ie-import-section" class="scan-card" hidden>
<strong>📂 Import from Homeschool folder</strong> <strong>📂 Import from .docx</strong>
<input type="file" id="import-local-input" accept=".docx" hidden>
<div class="scan-controls"> <div class="scan-controls">
<select id="import-file-select" style="flex:1"><option value="">— choose a file —</option></select> <button type="button" id="import-browse-btn" class="secondary">📂 Choose file…</button>
<button type="button" id="import-load-btn" class="secondary">↓ Load files</button> <span id="import-local-name" class="hint" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
<button type="button" id="import-go-btn" class="secondary" disabled>📥 Import</button> <button type="button" id="import-go-btn" class="secondary" disabled>📥 Import</button>
</div> </div>
<p id="import-status" class="hint"></p> <p id="import-status" class="hint"></p>
+15 -27
View File
@@ -504,7 +504,8 @@ $('#rte-youtube-btn').addEventListener('click', () => {
let scanImageBase64 = null; let scanImageBase64 = null;
function resetImportSection() { function resetImportSection() {
$('#import-file-select').innerHTML = '<option value="">— choose a file —</option>'; $('#import-local-input').value = '';
$('#import-local-name').textContent = '';
$('#import-go-btn').disabled = true; $('#import-go-btn').disabled = true;
$('#import-status').textContent = ''; $('#import-status').textContent = '';
} }
@@ -590,41 +591,28 @@ $('#scan-go-btn').addEventListener('click', async () => {
} }
}); });
// ---------- import from Homeschool folder ---------- // ---------- import from local .docx file ----------
$('#import-load-btn').addEventListener('click', async () => { $('#import-browse-btn').addEventListener('click', () => $('#import-local-input').click());
$('#import-status').textContent = 'Loading files…';
$('#import-file-select').innerHTML = '<option value="">— loading… —</option>';
try {
const { files } = await api('/api/admin/homeschool-files');
if (!files.length) {
$('#import-file-select').innerHTML = '<option value="">No .docx files found</option>';
$('#import-status').textContent = '';
return;
}
$('#import-file-select').innerHTML =
'<option value="">— choose a file —</option>' +
files.map((f) => `<option value="${esc(f.path)}">${esc(f.path)}</option>`).join('');
$('#import-go-btn').disabled = false;
$('#import-status').textContent = `${files.length} file(s) found.`;
} catch (err) {
$('#import-status').textContent = `❌ ${err.message}`;
}
});
$('#import-file-select').addEventListener('change', () => { $('#import-local-input').addEventListener('change', () => {
$('#import-go-btn').disabled = !$('#import-file-select').value; const file = $('#import-local-input').files[0];
$('#import-local-name').textContent = file ? file.name : '';
$('#import-go-btn').disabled = !file;
$('#import-status').textContent = '';
}); });
$('#import-go-btn').addEventListener('click', async () => { $('#import-go-btn').addEventListener('click', async () => {
const path = $('#import-file-select').value; const file = $('#import-local-input').files[0];
if (!path) return; if (!file) return;
const type = $('#ie-type').value; const type = $('#ie-type').value;
const mode = type === 'quiz' ? 'quiz' : type === 'lesson' ? 'lesson' : 'lesson'; const mode = type === 'quiz' ? 'quiz' : 'lesson';
$('#import-go-btn').disabled = true; $('#import-go-btn').disabled = true;
$('#import-status').textContent = '🔍 Extracting content… this may take a minute.'; $('#import-status').textContent = '🔍 Extracting content… this may take a minute.';
try { try {
const result = await api('/api/admin/import-docx', { method: 'POST', body: { path, mode } }); const arrayBuffer = await file.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const result = await api('/api/admin/import-docx', { method: 'POST', body: { base64, mode } });
if (!$('#ie-title').value && result.title) $('#ie-title').value = result.title; if (!$('#ie-title').value && result.title) $('#ie-title').value = result.title;
if (mode === 'quiz' && result.questions) { if (mode === 'quiz' && result.questions) {
$('#ie-questions').innerHTML = ''; $('#ie-questions').innerHTML = '';
+13 -9
View File
@@ -1538,18 +1538,22 @@ Respond with ONLY a JSON object in exactly this shape, nothing else, no markdown
}; };
app.post('/api/admin/import-docx', requirePin, async (req, res) => { app.post('/api/admin/import-docx', requirePin, async (req, res) => {
const { path: relPath, mode } = req.body; const { path: relPath, base64: b64, mode } = req.body;
if (!relPath || !TEXT_SCAN_PROMPTS[mode]) { if (!TEXT_SCAN_PROMPTS[mode] || (!relPath && !b64)) {
return res.status(400).json({ error: 'path and mode (lesson or quiz) required' }); return res.status(400).json({ error: 'mode and either path or base64 required' });
}
const fullPath = join(HOMESCHOOL_DIR, relPath);
if (!fullPath.startsWith(HOMESCHOOL_DIR)) {
return res.status(403).json({ error: 'Access denied' });
} }
let buffer; let buffer;
try { buffer = await readFile(fullPath); } if (b64) {
catch (err) { return res.status(404).json({ error: `Could not read file: ${err.message}` }); } buffer = Buffer.from(b64, 'base64');
} else {
const fullPath = join(HOMESCHOOL_DIR, relPath);
if (!fullPath.startsWith(HOMESCHOOL_DIR)) {
return res.status(403).json({ error: 'Access denied' });
}
try { buffer = await readFile(fullPath); }
catch (err) { return res.status(404).json({ error: `Could not read file: ${err.message}` }); }
}
let docText; let docText;
try { try {