${buildPublicCitationText(item)}
${meta ? `` : ""} ${links ? `${links}
` : ""} ${renderCitationAbstractBlock(item.abstract_text || fields.abstract || "", false)}function getAppBase() {
const { pathname } = window.location;
if (pathname === "/" || pathname === "/index.html") {
return "";
}
if (pathname.endsWith("/index.html")) {
return pathname.slice(0, -"/index.html".length);
}
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
}
function getInitialSpeciesSlug() {
const hash = window.location.hash.replace(/^#/, "").trim();
return hash || "";
}
const apiBase = getAppBase();
const speciesList = document.querySelector("#species-list");
const searchInput = document.querySelector("#search");
const archiveFilterGroup = document.querySelector("#archive-filter-group");
const detailEmpty = document.querySelector("#detail-empty");
const detail = document.querySelector("#detail");
const detailCode = document.querySelector("#detail-code");
const detailCommonName = document.querySelector("#detail-common-name");
const detailArchiveBadge = document.querySelector("#detail-archive-badge");
const detailArchiveNote = document.querySelector("#detail-archive-note");
const detailScientificName = document.querySelector("#detail-scientific-name");
const detailSummary = document.querySelector("#detail-summary");
const detailSections = document.querySelector("#detail-sections");
const legacyPanel = document.querySelector("#legacy-panel");
const legacySourceMeta = document.querySelector("#legacy-source-meta");
const legacySourceText = document.querySelector("#legacy-source-text");
const speciesCount = document.querySelector("#species-count");
const sectionCount = document.querySelector("#section-count");
const authTokenInput = document.querySelector("#auth-token");
const authSaveButton = document.querySelector("#auth-save");
const authClearButton = document.querySelector("#auth-clear");
const authStatus = document.querySelector("#auth-status");
const contributorEmailInput = document.querySelector("#contributor-email");
const contributorAgeGate = document.querySelector("#contributor-age-gate");
const contributorAgeLabel = document.querySelector("#contributor-age-label");
const contributorRegisterButton = document.querySelector("#contributor-register");
const contributorStatus = document.querySelector("#contributor-status");
const contributorCreateButton = document.querySelector("#contributor-create");
const accessPanel = document.querySelector("#access-panel");
const editorPanel = document.querySelector("#editor-panel");
const editorPublicationStatus = document.querySelector("#editor-publication-status");
const editorNotes = document.querySelector("#editor-notes");
const editorIsArchived = document.querySelector("#editor-is-archived");
const editorSaveButton = document.querySelector("#editor-save");
const editorStatus = document.querySelector("#editor-status");
const documentPanel = document.querySelector("#document-panel");
const documentMarkdown = document.querySelector("#document-markdown");
const documentPreview = document.querySelector("#document-preview");
const documentSaveButton = document.querySelector("#document-save");
const documentStatus = document.querySelector("#document-status");
const citationPanel = document.querySelector("#citation-panel");
const citationStatus = document.querySelector("#citation-status");
const citationList = document.querySelector("#citation-list");
const citationBackfillSpeciesButton = document.querySelector("#citation-backfill-species");
const citationEnrichAllButton = document.querySelector("#citation-enrich-all");
const citationMatchDialog = document.querySelector("#citation-match-dialog");
const citationMatchSeed = document.querySelector("#citation-match-seed");
const citationMatchCandidates = document.querySelector("#citation-match-candidates");
const citationMatchStatus = document.querySelector("#citation-match-status");
const citationMatchCloseButton = document.querySelector("#citation-match-close");
const auditPanel = document.querySelector("#audit-panel");
const auditList = document.querySelector("#audit-list");
const collapsibleToggles = document.querySelectorAll(".collapsible-toggle");
let currentItems = [];
let currentSlug = null;
let currentSession = null;
let currentArchiveFilter = "active";
let currentCitationMatch = null;
let currentSpeciesCitations = [];
let workflowPanelState = {
"legacy-panel": false,
"access-panel": false,
"editor-panel": false,
"document-panel": false,
"citation-panel": false,
"audit-panel": false,
};
function setCollapsibleState(panel, expanded) {
if (!panel) {
return;
}
panel.classList.toggle("collapsed", !expanded);
const toggle = panel.querySelector(".collapsible-toggle");
if (!toggle) {
return;
}
const label = toggle.dataset.label || panel.dataset.label || "Section";
toggle.textContent = `${expanded ? "Hide" : "Show"} ${label}`;
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
workflowPanelState[panel.id] = expanded;
}
function collapseWorkflowPanels() {
[legacyPanel, accessPanel, editorPanel, documentPanel, citationPanel, auditPanel].forEach((panel) => {
setCollapsibleState(panel, false);
});
}
function expandCitationPanel() {
setCollapsibleState(citationPanel, true);
}
function restoreWorkflowPanels() {
[legacyPanel, accessPanel, editorPanel, documentPanel, citationPanel, auditPanel].forEach((panel) => {
if (!panel || panel.classList.contains("hidden")) {
return;
}
setCollapsibleState(panel, Boolean(workflowPanelState[panel.id]));
});
}
function renderStructuredBody(body) {
const trimmed = String(body || "").trim();
if (!trimmed) {
return "";
}
const paragraphs = trimmed
.split(/\n\s*\n/)
.map((paragraph) => paragraph.trim())
.filter(Boolean);
return paragraphs
.map((paragraph) => {
const html = escapeHtml(paragraph).replace(/\n/g, "
");
return `
${html}
`; }) .join(""); } function isCitationHeading(title) { const normalized = String(title || "").trim().replace(/:$/, "").toLowerCase(); return [ "reference numbers", "references", "reference", "citations", "citation", "bibliography", "related references", "related citations", ].includes(normalized); } function parseBibtexFields(draftBibtex) { const fields = {}; const text = String(draftBibtex || ""); const pattern = /([a-zA-Z_]+)\s*=\s*\{([^}]*)\}/g; let match = pattern.exec(text); while (match) { fields[match[1].toLowerCase()] = match[2].trim(); match = pattern.exec(text); } return fields; } function collectBibtexRecords(items) { const seen = new Set(); const records = []; for (const item of items || []) { const draftBibtex = String(item && item.draft_bibtex ? item.draft_bibtex : "").trim(); if (!draftBibtex || seen.has(draftBibtex)) { continue; } seen.add(draftBibtex); records.push(draftBibtex); } return records; } function sanitizeFilenamePart(value, fallback = "records") { const cleaned = String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return cleaned || fallback; } function downloadBibtexRecords(items, filenameStem) { const records = collectBibtexRecords(items); if (!records.length) { return false; } const blob = new Blob([`${records.join("\n\n")}\n`], { type: "application/x-bibtex;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${sanitizeFilenamePart(filenameStem)}.bib`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.setTimeout(() => URL.revokeObjectURL(url), 0); return true; } function buildPublicCitationText(item) { const fields = parseBibtexFields(item.draft_bibtex || ""); if (item.normalized_text) { return escapeHtml(String(item.normalized_text)); } const author = fields.author || ""; const year = fields.year || ""; const title = fields.title || ""; const venue = fields.journal || fields.booktitle || fields.publisher || ""; const volume = fields.volume || ""; const issue = fields.number || ""; const pages = fields.pages || ""; const parts = []; const lead = [author, year ? `(${year})` : ""].filter(Boolean).join(" "); if (lead) { parts.push(lead); } if (title) { parts.push(title); } const venueBits = [venue, volume ? `${volume}${issue ? `(${issue})` : ""}` : issue ? `(${issue})` : "", pages] .filter(Boolean) .join(", "); if (venueBits) { parts.push(venueBits); } return escapeHtml(parts.join(". ").trim() || String(item.raw_text || "")); } function renderPublicCitationEntry(item) { const fields = parseBibtexFields(item.draft_bibtex || ""); const meta = [ item.legacy_reference_number ? `Imported reference ${escapeHtml(item.legacy_reference_number)}` : "", item.source_type === "editor_added_candidate" ? "Added citation" : "", item.source_type === "editor_selected_candidate" ? "Reviewed citation" : "", ] .filter(Boolean) .join(" • "); const links = [ item.doi ? `DOI` : "", item.source_url ? `Source` : "", item.openalex_id ? `OpenAlex` : "", ] .filter(Boolean) .join(" · "); return `${buildPublicCitationText(item)}
${meta ? `` : ""} ${links ? `${links}
` : ""} ${renderCitationAbstractBlock(item.abstract_text || fields.abstract || "", false)}${records.length ? `${records.length} BibTeX record${records.length === 1 ? "" : "s"} available for download.` : "No BibTeX records are available for download yet."}
No extracted bibliography entries are available yet.
`; } function renderStructuredNodes(nodes, container, citations, renderState = { renderedBibliography: false }) { for (const node of nodes || []) { const rawTitle = String(node.title || "").trim() || "Untitled section"; const isCitationSection = isCitationHeading(rawTitle); if (isCitationSection && renderState.renderedBibliography) { continue; } const sectionEl = document.createElement("section"); sectionEl.className = "detail-section structured-node"; const depth = Number(node.depth || 2); const headingLevel = Math.min(6, Math.max(3, depth + 1)); const title = escapeHtml(isCitationHeading(rawTitle) ? "Bibliography" : rawTitle); const body = String(node.body || "").trim(); const children = Array.isArray(node.children) ? node.children : []; const citationMarkup = isCitationSection ? buildPublicBibliographyMarkup(citations, `${currentSlug || "ecospecies"}-bibliography`) : ""; sectionEl.innerHTML = `${escapeHtml(section.content)}
`;
detailSections.appendChild(sectionEl);
}
}
function renderLegacySource(data) {
const legacySource = data.legacy_source;
const hasLegacySource = Boolean(legacySource && String(legacySource.text || "").trim());
legacyPanel.classList.toggle("hidden", !hasLegacySource);
if (!hasLegacySource) {
legacySourceMeta.textContent = "";
legacySourceText.textContent = "";
return;
}
legacySourceMeta.textContent = legacySource.source_file
? `Original imported file: ${legacySource.source_file}`
: "Original imported legacy material";
legacySourceText.textContent = String(legacySource.text || "");
}
function getAuthToken() {
return window.localStorage.getItem("ecospecies_auth_token") || "";
}
function getAuthHeaders() {
const token = getAuthToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll('"', """)
.replaceAll("<", "<")
.replaceAll(">", ">");
}
function normalizeAbstractForDisplay(value) {
const raw = String(value || "").trim();
if (!raw) {
return "";
}
const temp = document.createElement("div");
temp.innerHTML = raw;
return temp.textContent
.replace(/^abstract\s*[:.\-]?\s*/i, "")
.replace(/\s+/g, " ")
.trim();
}
function parseMarkdownFrontMatter(markdown) {
const stripped = markdown.trimStart();
if (!stripped.startsWith("---\n")) {
return { metadata: {}, body: markdown };
}
const remainder = stripped.slice(4);
const separatorIndex = remainder.indexOf("\n---\n");
if (separatorIndex === -1) {
return { metadata: {}, body: markdown };
}
const metadataBlock = remainder.slice(0, separatorIndex);
const body = remainder.slice(separatorIndex + 5);
const metadata = {};
for (const line of metadataBlock.split("\n")) {
const separator = line.indexOf(":");
if (separator === -1) {
continue;
}
const key = line.slice(0, separator).trim();
const value = line.slice(separator + 1).trim();
if (key) {
metadata[key] = value;
}
}
return { metadata, body };
}
function renderDocumentPreview(markdown) {
const { metadata, body } = parseMarkdownFrontMatter(markdown);
const headings = body
.split("\n")
.map((line) => {
const match = line.match(/^(#{2,6})\s+(.+?)\s*$/);
if (!match) {
return null;
}
return {
depth: match[1].length,
title: match[2].trim(),
};
})
.filter(Boolean);
if (!headings.length && !Object.keys(metadata).length) {
documentPreview.innerHTML = `No headings detected yet.
`; return; } const metadataItems = Object.entries(metadata) .map(([key, value]) => `No headings detected yet.
'} `; } function renderCitationList(items, editable) { citationList.innerHTML = ""; if (!items.length) { citationList.innerHTML = `No citations have been extracted yet.
`; return; } for (const item of items) { const article = document.createElement("article"); article.className = "citation-entry"; const readOnlyMeta = [ item.section_heading ? `Section: ${escapeHtml(item.section_heading)}` : "", item.legacy_reference_number ? `Legacy reference: ${escapeHtml(item.legacy_reference_number)}` : "", item.source_type ? `Source: ${escapeHtml(item.source_type)}` : "", item.enrichment_status ? `Enrichment: ${escapeHtml(item.enrichment_status)}` : "", ] .filter(Boolean) .join(" • "); if (!editable) { article.innerHTML = `${escapeHtml(item.raw_text || "")}
${item.doi ? `` : ""} ${item.openalex_id ? `` : ""} ${item.resolver_source_label ? `` : ""} ${renderCitationAbstractBlock(item.abstract_text || "", false)} ${item.enrichment_error ? `` : ""} ${renderCitationBibtexBlock(item.draft_bibtex || "", false)} `; attachCitationToggleControls(article); citationList.appendChild(article); continue; } article.innerHTML = `${escapeHtml(item.raw_text || "")}
${renderCitationAbstractBlock(item.abstract_text || "", true)} ${renderCitationBibtexBlock(item.draft_bibtex || "", true)} ${item.enrichment_error ? `` : ""}No structured metadata extracted yet.
`; } function renderFieldMatches(fieldMatches) { return Object.entries(fieldMatches || {}) .map(([field, detail]) => { const status = String(detail.status || "unknown"); return `${escapeHtml(data.error || "Unable to load candidates.")}
`; return; } citationMatchSeed.innerHTML = `${escapeHtml(data.citation.raw_text || "")}
${renderMetadataTable((data.seed && data.seed.fields) || {})} ${renderCitationAbstractBlock((data.seed && (data.seed.abstract_text || (data.seed.fields && data.seed.fields.abstract))) || "", false)} ${data.seed && data.seed.normalized_text ? `${escapeHtml(data.seed.normalized_text)}
` : ""} `; attachCitationToggleControls(citationMatchSeed); const candidates = Array.isArray(data.candidates) ? data.candidates : []; citationMatchStatus.textContent = `${candidates.length} candidate${candidates.length === 1 ? "" : "s"} found`; if (!candidates.length) { citationMatchCandidates.innerHTML = `No close candidates were returned for this citation.
`; return; } citationMatchCandidates.innerHTML = ""; for (const candidate of candidates) { const alreadyExists = candidateAlreadyExists(candidate); const card = document.createElement("article"); card.className = "match-candidate-card"; card.innerHTML = `${escapeHtml(candidate.normalized_text)}
` : ""}${escapeHtml(data.error || "Unable to load document.")}
`; documentStatus.textContent = data.error || "Document load failed"; return; } documentMarkdown.value = data.markdown || ""; renderDocumentPreview(documentMarkdown.value); documentStatus.textContent = data.updated_by ? `Document last updated by ${data.updated_by}` : "Document loaded"; } async function loadSpeciesCitations(slug, fallbackData = null) { citationPanel.classList.remove("hidden"); citationBackfillSpeciesButton.classList.toggle("hidden", !isEditorSession()); citationEnrichAllButton.classList.toggle("hidden", !isEditorSession()); if (!isEditorSession() && !isContributorSession()) { const items = Array.isArray(fallbackData && fallbackData.citations) ? fallbackData.citations : []; currentSpeciesCitations = items; renderCitationList(items, false); citationStatus.textContent = `${items.length} citation${items.length === 1 ? "" : "s"}`; return; } citationStatus.textContent = "Loading citations..."; const path = isEditorSession() ? `/api/editor/species/${slug}/citations` : `/api/contributor/species/${slug}/citations`; const { response, data } = await requestJson(path); if (!response.ok) { citationList.innerHTML = `${escapeHtml(data.error || "Unable to load citations.")}
`; citationStatus.textContent = data.error || "Citation load failed"; return; } currentSpeciesCitations = Array.isArray(data.citations) ? data.citations : []; renderCitationList(currentSpeciesCitations, isEditorSession()); citationStatus.textContent = `${data.citation_count || 0} citation${data.citation_count === 1 ? "" : "s"} extracted`; } async function requestJson(path, options = {}) { const headers = new Headers(options.headers || {}); const authHeaders = getAuthHeaders(); for (const [key, value] of Object.entries(authHeaders)) { headers.set(key, value); } if (options.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(`${apiBase}${path}`, { ...options, headers }); const data = await response.json(); return { response, data }; } function isEditorSession() { return Boolean(currentSession && currentSession.user && ["editor", "admin"].includes(currentSession.user.role)); } function isContributorSession() { return Boolean(currentSession && currentSession.user && currentSession.user.role === "contributor"); } function getVisibleItems(items) { if (!isEditorSession()) { return items; } if (currentArchiveFilter === "archived") { return items.filter((item) => item.is_archived); } if (currentArchiveFilter === "all") { return items; } return items.filter((item) => !item.is_archived); } function syncArchiveFilterUi() { archiveFilterGroup.classList.toggle("hidden", !isEditorSession()); contributorCreateButton.classList.toggle("hidden", !isContributorSession()); for (const button of archiveFilterGroup.querySelectorAll("[data-archive-filter]")) { button.classList.toggle("is-active", button.dataset.archiveFilter === currentArchiveFilter); } } async function loadSession() { const { data } = await requestJson("/api/auth/session"); currentSession = data; if (!isEditorSession()) { currentArchiveFilter = "active"; } contributorAgeLabel.textContent = String(data.minimum_contributor_age || 13); authTokenInput.value = getAuthToken(); if (data.authenticated) { authStatus.textContent = `${data.user.username} (${data.user.role})`; contributorStatus.textContent = isContributorSession() ? "Contributor token stored in this browser." : ""; } else if (data.auth_configured) { authStatus.textContent = "Auth configured, public session"; } else { authStatus.textContent = "Public access"; } syncArchiveFilterUi(); } async function loadSummary() { const { data } = await requestJson("/api/insights/summary"); speciesCount.textContent = data.species_count; sectionCount.textContent = data.section_count; } function renderSpecies(items) { speciesList.innerHTML = ""; const visibleItems = getVisibleItems(items); if (!visibleItems.length) { speciesList.innerHTML = `${isEditorSession() ? "No species match the current archive filter." : "No species match the current search."}
`; return; } for (const item of visibleItems) { const button = document.createElement("button"); button.className = item.is_archived ? "species-card species-card-archived" : "species-card"; button.type = "button"; const archivedMeta = item.is_archived ? `Archived` : ""; const commonName = item.common_name || item.title; const scientificName = item.scientific_name || "Scientific name missing"; button.innerHTML = ` ${escapeHtml(commonName)} ${escapeHtml((item.summary || "No summary extracted yet.").slice(0, 180))} `; button.addEventListener("click", () => { window.location.hash = item.slug; loadSpecies(item.slug); }); speciesList.appendChild(button); } } function formatIdentifierBanner(item) { if (item.primary_taxon_identifier && item.primary_taxon_authority) { return `${String(item.primary_taxon_authority).toUpperCase()} ${item.primary_taxon_identifier.identifier || ""}`.trim(); } const legacyIdentifier = Array.isArray(item.legacy_identifiers) ? item.legacy_identifiers[0] : null; if (legacyIdentifier && legacyIdentifier.identifier) { const label = legacyIdentifier.label || "Legacy identifier"; return `${label} ${legacyIdentifier.identifier}`; } return "No external taxon identifier assigned"; } async function loadSpeciesList(search = "") { const query = search ? `?search=${encodeURIComponent(search)}` : ""; const path = isEditorSession() ? `/api/editor/species${query}` : isContributorSession() ? `/api/contributor/species${query}` : `/api/species${query}`; const { data } = await requestJson(path); currentItems = data.items; syncArchiveFilterUi(); renderSpecies(currentItems); } async function loadSpecies(slug) { const previousSlug = currentSlug; currentSlug = slug; closeCitationMatchDialog(); const path = isEditorSession() ? `/api/editor/species/${slug}` : isContributorSession() ? `/api/contributor/species/${slug}` : `/api/species/${slug}`; const { response, data } = await requestJson(path); if (!response.ok) { detailEmpty.classList.remove("hidden"); detail.classList.add("hidden"); speciesList.innerHTML = `${escapeHtml(data.error || "Unable to load species.")}
`; return; } detailEmpty.classList.add("hidden"); detail.classList.remove("hidden"); if (previousSlug !== slug) { collapseWorkflowPanels(); } detailCode.textContent = formatIdentifierBanner(data); detailCommonName.textContent = data.common_name || data.title; detailArchiveBadge.classList.toggle("hidden", !data.is_archived); detailArchiveNote.classList.toggle("hidden", !data.is_archived); detailScientificName.textContent = data.scientific_name || "Scientific name missing in source"; detailSummary.textContent = data.summary || "No summary extracted from the current source file."; editorPanel.classList.toggle("hidden", !isEditorSession()); auditPanel.classList.toggle("hidden", !isEditorSession()); if (isEditorSession()) { editorPublicationStatus.value = data.publication_status || "published"; editorNotes.value = data.editor_notes || ""; editorIsArchived.checked = Boolean(data.is_archived); editorStatus.textContent = data.last_modified_by ? `Last modified by ${data.last_modified_by}` : "Editor session active"; await Promise.all([loadAudit(slug), loadSpeciesDocument(slug), loadSpeciesCitations(slug)]); } else if (isContributorSession()) { editorStatus.textContent = ""; await Promise.all([loadSpeciesDocument(slug), loadSpeciesCitations(slug)]); } else { documentPanel.classList.add("hidden"); await loadSpeciesCitations(slug, data); } renderLegacySource(data); restoreWorkflowPanels(); renderPrimaryContent(data); } function renderAudit(items) { auditList.innerHTML = ""; if (!items.length) { auditList.innerHTML = `No audit entries yet.
`; return; } for (const item of items) { const entry = document.createElement("article"); entry.className = "audit-entry"; const detailRows = Object.entries(item.details) .map(([field, values]) => { if (values && typeof values === "object" && "from" in values && "to" in values) { return `${escapeHtml(data.error || "Unable to load audit history.")}
`; return; } renderAudit(data.items); } async function saveEditorialChanges() { if (!currentSlug || !isEditorSession()) { return; } editorStatus.textContent = "Saving..."; const { response, data } = await requestJson(`/api/editor/species/${currentSlug}/editorial`, { method: "POST", body: JSON.stringify({ publication_status: editorPublicationStatus.value, editor_notes: editorNotes.value, is_archived: editorIsArchived.checked, }), }); if (!response.ok) { editorStatus.textContent = data.error || "Save failed"; return; } editorStatus.textContent = `Saved by ${data.last_modified_by}`; await Promise.all([loadSummary(), loadSpeciesList(searchInput.value), loadSpecies(currentSlug)]); } async function saveDocumentMarkdown() { if (!currentSlug || (!isEditorSession() && !isContributorSession())) { return; } documentStatus.textContent = "Saving document..."; const path = isEditorSession() ? `/api/editor/species/${currentSlug}/document` : `/api/contributor/species/${currentSlug}/document`; const { response, data } = await requestJson(path, { method: "POST", body: JSON.stringify({ markdown: documentMarkdown.value }), }); if (!response.ok) { documentStatus.textContent = data.error || "Document save failed"; return; } renderDocumentPreview(documentMarkdown.value); documentStatus.textContent = `Document saved by ${data.updated_by}`; await Promise.all([loadSummary(), loadSpeciesList(searchInput.value), loadSpecies(currentSlug)]); } async function registerContributor() { contributorStatus.textContent = "Registering contributor..."; const { response, data } = await requestJson("/api/contributor/register", { method: "POST", body: JSON.stringify({ email: contributorEmailInput.value.trim(), age_gate_confirmed: contributorAgeGate.checked, }), }); if (!response.ok) { contributorStatus.textContent = data.error || "Contributor registration failed"; return; } window.localStorage.setItem("ecospecies_auth_token", data.token); authTokenInput.value = data.token; contributorStatus.textContent = data.warning; await loadSession(); await loadSpeciesList(searchInput.value); } async function createContributorDraft() { if (!isContributorSession()) { return; } contributorStatus.textContent = "Creating new contributor draft..."; const { response, data } = await requestJson("/api/contributor/species", { method: "POST", body: JSON.stringify({}), }); if (!response.ok) { contributorStatus.textContent = data.error || "Draft creation failed"; return; } contributorStatus.textContent = "Draft created. Store your token carefully."; await loadSpeciesList(searchInput.value); await loadSpecies(data.slug); } searchInput.addEventListener("input", async (event) => { await loadSpeciesList(event.target.value); }); for (const button of archiveFilterGroup.querySelectorAll("[data-archive-filter]")) { button.addEventListener("click", () => { currentArchiveFilter = button.dataset.archiveFilter || "active"; syncArchiveFilterUi(); renderSpecies(currentItems); }); } for (const button of collapsibleToggles) { button.addEventListener("click", () => { const panel = document.getElementById(button.dataset.target || ""); if (!panel || panel.classList.contains("hidden")) { return; } const expanded = panel.classList.contains("collapsed"); setCollapsibleState(panel, expanded); }); } authSaveButton.addEventListener("click", async () => { const token = authTokenInput.value.trim(); if (token) { window.localStorage.setItem("ecospecies_auth_token", token); } await loadSession(); await loadSpeciesList(searchInput.value); if (currentSlug) { await loadSpecies(currentSlug); } }); authClearButton.addEventListener("click", async () => { window.localStorage.removeItem("ecospecies_auth_token"); authTokenInput.value = ""; contributorStatus.textContent = ""; await loadSession(); await loadSpeciesList(searchInput.value); if (currentSlug) { await loadSpecies(currentSlug); } }); editorSaveButton.addEventListener("click", saveEditorialChanges); documentSaveButton.addEventListener("click", saveDocumentMarkdown); contributorRegisterButton.addEventListener("click", registerContributor); contributorCreateButton.addEventListener("click", createContributorDraft); citationMatchCloseButton.addEventListener("click", closeCitationMatchDialog); citationMatchDialog.querySelector(".match-dialog-backdrop").addEventListener("click", closeCitationMatchDialog); citationBackfillSpeciesButton.addEventListener("click", async () => { if (!currentSlug || !isEditorSession()) { return; } expandCitationPanel(); citationStatus.textContent = "Running citation backfill for this species..."; const { response, data } = await requestJson(`/api/editor/species/${currentSlug}/citations/backfill`, { method: "POST", body: JSON.stringify({}), }); if (!response.ok) { citationStatus.textContent = data.error || "Species citation backfill failed"; return; } citationStatus.textContent = `Species backfill complete: ${data.backfilled_count || 0} checked, ${data.changed_count || 0} changed, ${data.resolved_count || 0} resolved, ${data.unresolved_count || 0} unresolved, ${data.error_count || 0} errors`; await Promise.all([loadSpecies(currentSlug), loadSpeciesCitations(currentSlug)]); }); citationEnrichAllButton.addEventListener("click", async () => { if (!currentSlug || !isEditorSession()) { return; } expandCitationPanel(); citationStatus.textContent = "Running enrichment for all citations..."; const { response, data } = await requestJson(`/api/editor/species/${currentSlug}/citations/enrich`, { method: "POST", body: JSON.stringify({}), }); if (!response.ok) { citationStatus.textContent = data.error || "Batch citation enrichment failed"; return; } citationStatus.textContent = `Batch enrichment complete: ${data.resolved_count || 0} resolved, ${data.unresolved_count || 0} unresolved, ${data.error_count || 0} errors`; await Promise.all([loadSpecies(currentSlug), loadSpeciesCitations(currentSlug)]); }); documentMarkdown.addEventListener("input", () => { renderDocumentPreview(documentMarkdown.value); }); async function bootstrap() { await loadSession(); await Promise.all([loadSummary(), loadSpeciesList()]); const initialSlug = getInitialSpeciesSlug(); if (initialSlug) { await loadSpecies(initialSlug); } } bootstrap().catch((error) => { speciesList.innerHTML = `Failed to load data: ${escapeHtml(String(error))}
`; }); window.addEventListener("hashchange", async () => { const slug = getInitialSpeciesSlug(); if (slug && slug !== currentSlug) { await loadSpecies(slug); } });