// ============================================================
// 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);
}
}());