Remove page scanning / Ollama AI vision feature

Removes the scan-a-page workflow (photo → lesson/quiz content), all
Ollama/cloud provider code, the scan settings panel, and the related
DB seed values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-04 13:52:55 -04:00
1 parent 61f9288aa5
commit a10239ead9
4 files changed
+1 -371

No files matched your search

-48
View File
@@ -202,18 +202,6 @@
</label>
</div>
<!-- Scan a page: visible for lesson + quiz types -->
<div id="ie-scan-section" class="scan-card" hidden>
<strong>📷 Scan a book page</strong>
<input type="file" id="scan-file-input" accept="image/*" capture="environment" hidden>
<div class="scan-controls">
<button type="button" id="scan-pick-btn" class="secondary">Choose photo</button>
<button type="button" id="scan-go-btn" disabled>🔍 Scan this page</button>
</div>
<img id="scan-preview" alt="Selected page photo" hidden>
<p id="scan-status" class="hint"></p>
</div>
<!-- Import from .docx file: visible for lesson, assignment, quiz types -->
<div id="ie-import-section" class="scan-card" hidden>
<strong>📂 Import from .docx</strong>
@@ -515,42 +503,6 @@
<button type="submit">Change PIN</button>
</form>
<h2>Page scanning (AI vision)</h2>
<p class="hint">Used by "Scan a page" in the lesson/quiz editor. Configure one or both providers — the primary is tried first and the other is used as automatic fallback.</p>
<form id="scan-config-form">
<fieldset class="scan-provider-box">
<legend>Primary provider</legend>
<label class="radio-label">
<input type="radio" name="scan_primary" value="local"> Local (Ollama) — runs on your machine, completely free &amp; private
</label>
<label class="radio-label">
<input type="radio" name="scan_primary" value="ollama_cloud"> Ollama Cloud — free hosted tier, requires API key
</label>
</fieldset>
<fieldset class="scan-provider-box">
<legend>Local — Ollama</legend>
<div class="inline-form">
<input id="ollama-host" placeholder="http://localhost:11434" aria-label="Ollama host">
<input id="ollama-model" placeholder="llava" aria-label="Vision model">
<button type="button" id="ollama-test-btn" class="secondary">Test</button>
</div>
<p id="ollama-test-result" class="hint"></p>
<p class="hint">Install a vision model: <code>ollama pull llava</code> or <code>ollama pull llava-llama3</code></p>
</fieldset>
<fieldset class="scan-provider-box">
<legend>Ollama Cloud (free tier)</legend>
<div class="inline-form">
<input id="ollama-cloud-model" placeholder="gemma4" aria-label="Ollama Cloud model">
<input id="ollama-cloud-api-key" type="password" placeholder="Ollama Cloud API key" aria-label="Ollama Cloud API key">
<span id="ollama-cloud-key-status" class="hint"></span>
</div>
<p class="hint">Runs through your local Ollama but offloads compute to Ollama's cloud. Get a free API key at <strong>ollama.com</strong> → Account → API Keys, then pull the model: <code>ollama pull gemma4:cloud</code>. Enter the base name only (e.g. <code>gemma4</code>) — <code>:cloud</code> is added automatically.</p>
</fieldset>
<button type="submit">Save scan settings</button>
</form>
</section>
</main>
+1 -129
View File
@@ -95,7 +95,7 @@ function showPanel(name) {
const loaders = {
kids: loadKids, courses: loadCourses, planner: loadPlannerPanel,
grading: loadGrading, gradebook: loadGradebookPanel, spelling: loadSpelling, decks: loadDecks,
settings: loadOllamaConfig,
settings: () => {},
};
if (loaders[name]) loaders[name]();
if (name === 'course-detail') openCourseDetail(currentCourseId);
@@ -580,7 +580,6 @@ async function openItemEditor(unitId, itemId) {
$('#ie-delete').hidden = true;
}
updateItemFieldVisibility();
resetScanSection();
resetImportSection();
showPanel('item-editor');
}
@@ -595,7 +594,6 @@ function updateItemFieldVisibility() {
$('#ie-field-spelling').hidden = type !== 'spelling_practice' && type !== 'spelling_test';
$('#ie-field-flashcards').hidden = type !== 'flashcards';
$('#ie-field-worksheet').hidden = type !== 'worksheet';
$('#ie-scan-section').hidden = type !== 'lesson' && type !== 'quiz';
$('#ie-import-section').hidden = type === 'spelling_practice' || type === 'spelling_test'
|| type === 'flashcards' || type === 'worksheet';
}
@@ -659,10 +657,6 @@ document.querySelectorAll('.rte-youtube-btn').forEach((btn) => {
});
});
// ---------- scan a page (photo -> lesson/quiz content via local Ollama) ----------
let scanImageBase64 = null;
function resetImportSection() {
$('#import-local-input').value = '';
$('#import-local-name').textContent = '';
@@ -670,87 +664,6 @@ function resetImportSection() {
$('#import-status').textContent = '';
}
function resetScanSection() {
scanImageBase64 = null;
$('#scan-file-input').value = '';
$('#scan-preview').hidden = true;
$('#scan-preview').src = '';
$('#scan-go-btn').disabled = true;
$('#scan-status').textContent = '';
}
function fileToScanImage(file, maxDim = 1600, quality = 0.85) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error('Could not read that file'));
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error('Could not read that image'));
img.onload = () => {
let { width, height } = img;
if (width > maxDim || height > maxDim) {
const scale = maxDim / Math.max(width, height);
width = Math.round(width * scale);
height = Math.round(height * scale);
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
resolve(canvas.toDataURL('image/jpeg', quality));
};
img.src = reader.result;
};
reader.readAsDataURL(file);
});
}
$('#scan-pick-btn').addEventListener('click', () => $('#scan-file-input').click());
$('#scan-file-input').addEventListener('change', async () => {
const file = $('#scan-file-input').files[0];
if (!file) return;
try {
const dataUrl = await fileToScanImage(file);
scanImageBase64 = dataUrl.split(',')[1];
$('#scan-preview').src = dataUrl;
$('#scan-preview').hidden = false;
$('#scan-go-btn').disabled = false;
$('#scan-status').textContent = '';
} catch (err) {
$('#scan-status').textContent = `❌ ${err.message}`;
}
});
$('#scan-go-btn').addEventListener('click', async () => {
if (!scanImageBase64) return;
const mode = $('#ie-type').value === 'quiz' ? 'quiz' : 'lesson';
$('#scan-go-btn').disabled = true;
$('#scan-status').textContent = '🔍 Reading the page… this can take a minute or two on local hardware.';
try {
const result = await api('/api/admin/scan', { method: 'POST', body: { image: scanImageBase64, mode } });
if (result.title) $('#ie-title').value = result.title;
const providerLabel = result._provider === 'cloud' ? 'cloud API' : 'local Ollama';
const fallbackNote = result._fallback ? ` (primary failed — used ${providerLabel} as fallback: ${result._primaryError})` : ` via ${providerLabel}`;
if (mode === 'lesson') {
$('#ie-body-lesson').innerHTML = result.body || '';
$('#scan-status').textContent = `✅ Scanned${fallbackNote}. Review the text below before saving.`;
} else {
$('#ie-questions').innerHTML = '';
questionCount = 0;
// addQuestionRow expects correct_answer (snake_case); scan returns correctAnswer (camelCase) — normalize here.
(result.questions || []).forEach((q) => addQuestionRow({ ...q, correct_answer: q.correctAnswer }));
const blanks = (result.questions || []).filter((q) => !q.correctAnswer).length;
$('#scan-status').textContent = `✅ Found ${result.questions?.length || 0} question(s)${fallbackNote}` +
(blanks ? ` — ${blanks} need${blanks === 1 ? 's' : ''} a correct answer` : '') + '. Review before saving.';
}
} catch (err) {
$('#scan-status').textContent = `❌ ${err.message}`;
} finally {
$('#scan-go-btn').disabled = false;
}
});
// ---------- import from local .docx file ----------
$('#import-browse-btn').addEventListener('click', () => $('#import-local-input').click());
@@ -2421,44 +2334,3 @@ $('#pin-change-form').addEventListener('submit', async (e) => {
}
});
// ---------- Scan config (settings panel) ----------
async function loadOllamaConfig() {
const cfg = await api('/api/admin/scan-config');
document.querySelector(`input[name="scan_primary"][value="${cfg.primary || 'local'}"]`).checked = true;
$('#ollama-host').value = cfg.ollama_host || '';
$('#ollama-model').value = cfg.ollama_model || '';
$('#ollama-test-result').textContent = '';
$('#ollama-cloud-model').value = cfg.ollama_cloud_model || '';
$('#ollama-cloud-api-key').value = '';
$('#ollama-cloud-key-status').textContent = cfg.ollama_cloud_has_key ? '🔑 Key saved' : 'No key saved';
}
$('#scan-config-form').addEventListener('submit', async (e) => {
e.preventDefault();
const primary = document.querySelector('input[name="scan_primary"]:checked')?.value || 'local';
const body = {
primary,
ollama_host: $('#ollama-host').value.trim(),
ollama_model: $('#ollama-model').value.trim(),
ollama_cloud_model: $('#ollama-cloud-model').value.trim(),
};
const ollamaCloudKey = $('#ollama-cloud-api-key').value.trim();
if (ollamaCloudKey) body.ollama_cloud_api_key = ollamaCloudKey;
await api('/api/admin/scan-config', { method: 'POST', body });
if (ollamaCloudKey) { $('#ollama-cloud-api-key').value = ''; $('#ollama-cloud-key-status').textContent = '🔑 Key saved'; }
msg('Scan settings saved.');
});
$('#ollama-test-btn').addEventListener('click', async () => {
$('#ollama-test-result').textContent = 'Testing…';
const r = await api('/api/admin/ollama-test', { method: 'POST' });
if (r.reachable && r.hasModel) {
$('#ollama-test-result').textContent = `✅ Connected — "${esc($('#ollama-model').value)}" is available.`;
} else if (r.reachable) {
$('#ollama-test-result').textContent =
`⚠️ Connected but "${esc($('#ollama-model').value)}" isn't pulled. Available: ${esc(r.availableModels.join(', ') || '(none)')}. Run: ollama pull ${esc($('#ollama-model').value)}`;
} else {
$('#ollama-test-result').textContent = `❌ ${esc(r.error)}`;
}
});