// ============================================================ // MY RESOURCES // A person's own generated teaching material. // // Separate from the Learning Hub on purpose: that is moderator-owned content // published into categories for everyone, this is private and needs no role // beyond being signed in. The server enforces that independently — every query // there filters on the owner — so this only has to be an honest interface to it. // ============================================================ (function () { var inited = false; document.addEventListener('tabChanged', function (e) { if (!e.detail || e.detail.tab !== 'myresources') return; if (!inited) { init(); inited = true; } loadLibrary(); }); function init() { var kind = document.getElementById('mr-kind'); if (kind) kind.addEventListener('change', syncFormatFields); syncFormatFields(); var generate = document.getElementById('btn-mr-generate'); if (generate) generate.addEventListener('click', runGenerate); var refresh = document.getElementById('btn-mr-refresh'); if (refresh) refresh.addEventListener('click', loadLibrary); // One delegated handler: rows are rebuilt on every refresh, so binding per // row would leak listeners and miss anything added later. var list = document.getElementById('mr-list'); if (list) list.addEventListener('click', onRowClick); } function syncFormatFields() { var isArticle = (document.getElementById('mr-kind') || {}).value === 'article'; var slides = document.getElementById('mr-slide-count'); var words = document.getElementById('mr-word-wrap'); if (slides && slides.parentElement) slides.parentElement.hidden = isArticle; if (words) words.hidden = !isArticle; } function status(text, tone) { var el = document.getElementById('mr-status'); if (!el) return; el.textContent = text || ''; el.style.color = tone === 'bad' ? 'var(--red)' : tone === 'good' ? 'var(--green)' : 'var(--g600)'; } function runGenerate() { var topic = (document.getElementById('mr-topic') || {}).value || ''; if (!topic.trim()) { status('Enter a topic first.', 'bad'); return; } var btn = document.getElementById('btn-mr-generate'); if (btn) { btn.disabled = true; btn.innerHTML = ' Generating...'; } status('Searching the library and writing. This takes a moment.'); var corpusBox = document.getElementById('mr-use-corpus'); fetch('/api/my-resources/generate', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ topic: topic.trim(), kind: (document.getElementById('mr-kind') || {}).value || 'presentation', slideCount: (document.getElementById('mr-slide-count') || {}).value, wordCount: (document.getElementById('mr-word-count') || {}).value, refinement: (document.getElementById('mr-refinement') || {}).value || '', useCorpus: corpusBox && corpusBox.checked === false ? 'false' : 'true' }) }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.success) throw new Error(data.error || 'Generation failed'); // Say what it was written from. Ungrounded material presented as // grounded is the failure worth preventing. var g = data.grounding || {}; status(g.used ? 'Saved. Written from ' + g.count + ' library excerpt' + (g.count === 1 ? '' : 's') + '.' : 'Saved. Not grounded' + (g.reason ? ' — ' + g.reason : '') + '; written from the model alone.', g.used ? 'good' : null); loadLibrary(); }) .catch(function (err) { status(err.message, 'bad'); }) .finally(function () { if (btn) { btn.disabled = false; btn.innerHTML = ' Generate'; } }); } function loadLibrary() { var list = document.getElementById('mr-list'); if (!list) return; fetch('/api/my-resources', { headers: getAuthHeaders() }) .then(function (r) { return r.json(); }) .then(function (data) { list.textContent = ''; var rows = (data && data.resources) || []; if (!rows.length) { var empty = document.createElement('p'); empty.style.cssText = 'margin:0;font-size:13px;color:var(--g400);'; empty.textContent = 'Nothing yet. Generate something above and it will appear here.'; list.appendChild(empty); return; } rows.forEach(function (row) { list.appendChild(renderRow(row)); }); }) .catch(function () { list.textContent = ''; var failed = document.createElement('p'); failed.style.cssText = 'margin:0;font-size:13px;color:var(--red);'; failed.textContent = 'Could not load your resources.'; list.appendChild(failed); }); } // Built as elements rather than innerHTML: a title comes from a model, and // this is the one place it reaches the page. function renderRow(row) { var wrap = document.createElement('div'); wrap.className = 'saved-enc-item'; wrap.style.cssText = 'padding:8px 12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;'; var body = document.createElement('div'); body.style.flex = '1'; body.style.minWidth = '180px'; var title = document.createElement('div'); title.style.cssText = 'font-weight:600;font-size:13px;'; title.textContent = row.title || 'Untitled'; var meta = document.createElement('div'); meta.style.cssText = 'font-size:11px;color:var(--g500);'; meta.textContent = (row.kind === 'article' ? 'Article' : 'Presentation') + ' · ' + new Date(row.created_at).toLocaleString() + (row.grounded_count ? ' · ' + row.grounded_count + ' library excerpts' : ' · not grounded'); body.appendChild(title); body.appendChild(meta); wrap.appendChild(body); ['pptx', 'docx', 'pdf'].forEach(function (format) { var btn = document.createElement('button'); btn.className = 'btn-sm btn-ghost'; btn.type = 'button'; btn.dataset.download = String(row.id); btn.dataset.format = format; btn.textContent = format.toUpperCase(); btn.title = 'Download as ' + format.toUpperCase(); wrap.appendChild(btn); }); var del = document.createElement('button'); del.className = 'btn-sm btn-ghost'; del.type = 'button'; del.dataset.remove = String(row.id); del.style.color = 'var(--red)'; del.title = 'Delete'; var icon = document.createElement('i'); icon.className = 'fas fa-trash'; del.appendChild(icon); wrap.appendChild(del); return wrap; } function onRowClick(event) { var download = event.target.closest && event.target.closest('[data-download]'); if (download) return downloadResource(download.dataset.download, download.dataset.format, download); var remove = event.target.closest && event.target.closest('[data-remove]'); if (remove) { showConfirm('Delete this resource? This cannot be undone.', function () { fetch('/api/my-resources/' + encodeURIComponent(remove.dataset.remove), { method: 'DELETE', headers: getAuthHeaders() }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.success) throw new Error(d.error || 'Could not delete'); loadLibrary(); }) .catch(function (err) { showToast(err.message, 'error'); }); }, { danger: true, confirmText: 'Delete' }); } } // Fetched rather than linked, because the download needs the auth header and // an cannot carry one. function downloadResource(id, format, btn) { var original = btn.textContent; btn.disabled = true; btn.textContent = '...'; fetch('/api/my-resources/' + encodeURIComponent(id) + '/export?format=' + encodeURIComponent(format), { headers: getAuthHeaders() }) .then(function (r) { if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Download failed'); }); var name = 'resource.' + format; var disposition = r.headers.get('Content-Disposition') || ''; var match = disposition.match(/filename="([^"]+)"/); if (match) name = match[1]; return r.blob().then(function (blob) { saveBlob(blob, name); }); }) .catch(function (err) { showToast(err.message, 'error'); }) .finally(function () { btn.disabled = false; btn.textContent = original; }); } function saveBlob(blob, name) { var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = name; document.body.appendChild(link); link.click(); link.remove(); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); } }());