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:
1 parent
de598496bc
commit
b048f51d46
3 files changed
+33
-40
No files matched your search
+5
-4
@@ -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
@@ -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 = '';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user