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:
1 parent
61f9288aa5
commit
a10239ead9
4 files changed
+1
-371
No files matched your search
@@ -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 & 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
@@ -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)}`;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user