/**
* Blog Curation Admin UI
* Tractatus Framework - AI-assisted content generation with human oversight
*/
// Get auth token from localStorage
function getAuthToken() {
return localStorage.getItem('admin_token');
}
// Check authentication
function checkAuth() {
const token = getAuthToken();
if (!token) {
window.location.href = '/admin/login.html';
return false;
}
return true;
}
// API call helper
async function apiCall(endpoint, options = {}) {
const token = getAuthToken();
const defaultOptions = {
cache: 'no-store', // Force fresh requests - prevent cached 500 errors
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
};
const response = await fetch(endpoint, { ...defaultOptions, ...options });
if (response.status === 401) {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
window.location.href = '/admin/login.html';
throw new Error('Unauthorized');
}
return response;
}
// Navigation
function initNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
// If no nav-link elements found, navigation is handled elsewhere (blog-validation.js)
if (navLinks.length === 0) {
console.log('[blog-curation] Navigation handled by blog-validation.js');
return;
}
const sections = {
'draft': document.getElementById('draft-section'),
'queue': document.getElementById('queue-section'),
'guidelines': document.getElementById('guidelines-section')
};
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = link.getAttribute('href').substring(1);
// Update active link
navLinks.forEach(l => l.classList.remove('active', 'bg-gray-100', 'text-blue-600'));
link.classList.add('active', 'bg-gray-100', 'text-blue-600');
// Show target section
Object.values(sections).forEach(section => section.classList.add('hidden'));
if (sections[target]) {
sections[target].classList.remove('hidden');
// Load data for specific sections
if (target === 'queue') {
loadDraftQueue();
} else if (target === 'guidelines') {
loadEditorialGuidelines();
}
}
});
});
// Set first link as active
if (navLinks.length > 0) {
navLinks[0].classList.add('active', 'bg-gray-100', 'text-blue-600');
}
}
// Load statistics
async function loadStatistics() {
// Load pending drafts
try {
const queueResponse = await apiCall('/api/admin/moderation?type=BLOG_POST_DRAFT');
if (queueResponse.ok) {
const queueData = await queueResponse.json();
document.getElementById('stat-pending-drafts').textContent = queueData.items?.length || 0;
}
} catch (error) {
console.error('Failed to load pending drafts stat:', error);
document.getElementById('stat-pending-drafts').textContent = '-';
}
// Load published posts
try {
const postsResponse = await apiCall('/api/blog/admin/posts?status=published&limit=1000');
if (postsResponse.ok) {
const postsData = await postsResponse.json();
document.getElementById('stat-published-posts').textContent = postsData.pagination?.total || 0;
}
} catch (error) {
console.error('Failed to load published posts stat:', error);
document.getElementById('stat-published-posts').textContent = '-';
}
}
// Draft form submission
function initDraftForm() {
const form = document.getElementById('draft-form');
const btn = document.getElementById('draft-btn');
const status = document.getElementById('draft-status');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = {
topic: formData.get('topic'),
audience: formData.get('audience'),
length: formData.get('length') || 'medium',
focus: formData.get('focus') || undefined
};
// UI feedback
btn.disabled = true;
btn.textContent = 'Generating...';
status.textContent = 'Calling Claude API...';
status.className = 'text-sm text-blue-600';
try {
const response = await apiCall('/api/blog/draft-post', {
method: 'POST',
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
// Success - show draft in modal
status.textContent = '✓ Draft generated! Opening preview...';
status.className = 'text-sm text-green-600';
setTimeout(() => {
showDraftModal(result);
form.reset();
status.textContent = '';
}, 1000);
} else {
// Error
status.textContent = `✗ Error: ${result.message}`;
status.className = 'text-sm text-red-600';
}
} catch (error) {
status.textContent = `✗ Error: ${error.message}`;
status.className = 'text-sm text-red-600';
} finally {
btn.disabled = false;
btn.textContent = 'Generate Draft';
}
});
}
// Show draft modal
function showDraftModal(result) {
const { draft, validation, governance, queue_id } = result;
const violationsHtml = validation.violations.length > 0
? `
⚠️ Tractatus Violations Detected
${validation.violations.map(v => `
${v.type}: ${v.message}
Instruction: ${v.instruction}
`).join('')}
`
: '';
const warningsHtml = validation.warnings.length > 0
? `
⚠ Warnings
${validation.warnings.map(w => `
${w.message}
`).join('')}
`
: '';
const modal = `
${violationsHtml}
${warningsHtml}
Title
${draft.title || 'Untitled'}
Subtitle
${draft.subtitle || 'No subtitle'}
Excerpt
${draft.excerpt || 'No excerpt'}
Content Preview
${draft.content ? marked(draft.content.substring(0, 1000)) + '...' : 'No content'}
Tags
${(draft.tags || []).map(tag => `
${tag}
`).join('')}
Word Count
${draft.word_count || 'Unknown'}
Tractatus Angle
${draft.tractatus_angle || 'Not specified'}
Sources
${(draft.sources || ['No sources provided']).map(source => `${source} `).join('')}
🤖 Governance Notice
Policy: ${governance.policy}
Validation: ${validation.recommendation}
Queue ID: ${queue_id}
This draft has been queued for human review and approval before publication.
Close
View in Queue →
`;
const container = document.getElementById('modal-container');
container.innerHTML = modal;
// Close modal handlers
container.querySelectorAll('.close-modal').forEach(btn => {
btn.addEventListener('click', () => {
container.innerHTML = '';
});
});
// View queue handler
container.querySelector('.view-queue').addEventListener('click', () => {
container.innerHTML = '';
document.querySelector('a[href="#queue"]').click();
});
}
// Load draft queue
async function loadDraftQueue() {
const queueDiv = document.getElementById('draft-queue');
queueDiv.innerHTML = 'Loading queue...
';
try {
const response = await apiCall('/api/admin/moderation?type=BLOG_POST_DRAFT');
if (response.ok) {
const data = await response.json();
const queue = data.items || [];
if (queue.length === 0) {
queueDiv.innerHTML = 'No pending drafts
';
return;
}
queueDiv.innerHTML = queue.map(item => `
${item.data.draft?.title || item.data.topic}
${item.data.draft?.subtitle || ''}
Audience: ${item.data.audience}
Length: ${item.data.length}
Created: ${new Date(item.created_at).toLocaleDateString()}
${item.data.validation?.violations.length > 0 ? `
${item.data.validation.violations.length} violation(s)
` : ''}
${item.priority}
Review
`).join('');
// Add review handlers
queueDiv.querySelectorAll('.review-draft').forEach(btn => {
btn.addEventListener('click', () => {
const queueId = btn.dataset.queueId;
const item = queue.find(q => q._id === queueId);
if (item) {
showReviewModal(item);
}
});
});
} else {
queueDiv.innerHTML = 'Failed to load queue
';
}
} catch (error) {
console.error('Failed to load draft queue:', error);
queueDiv.innerHTML = 'Error loading queue
';
}
}
// Show review modal
function showReviewModal(item) {
const { draft, validation } = item.data;
const modal = `
${draft.title}
${draft.subtitle}
${marked(draft.content || '')}
Close
Reject
Approve & Create Post
`;
const container = document.getElementById('modal-container');
container.innerHTML = modal;
// Close modal handler
container.querySelectorAll('.close-modal').forEach(btn => {
btn.addEventListener('click', () => {
container.innerHTML = '';
});
});
// Approve handler
container.querySelector('.approve-draft')?.addEventListener('click', async () => {
const queueId = item._id;
const approveBtn = container.querySelector('.approve-draft');
const rejectBtn = container.querySelector('.reject-draft');
if (!confirm('Approve this draft and publish the blog post?')) {
return;
}
// Disable buttons
approveBtn.disabled = true;
rejectBtn.disabled = true;
approveBtn.textContent = 'Publishing...';
try {
const response = await apiCall(`/api/admin/moderation/${queueId}/review`, {
method: 'POST',
body: JSON.stringify({
action: 'approve',
notes: 'Approved via blog curation interface'
})
});
const result = await response.json();
if (response.ok) {
// Success - show success message and reload queue
alert(`✓ Blog post published successfully!\n\nTitle: ${result.blog_post?.title}\nSlug: ${result.blog_post?.slug}\n\nView at: ${result.blog_post?.url}`);
// Close modal and reload queue
container.innerHTML = '';
loadDraftQueue();
loadStatistics();
} else {
alert(`✗ Error: ${result.message || 'Failed to approve draft'}`);
approveBtn.disabled = false;
rejectBtn.disabled = false;
approveBtn.textContent = 'Approve & Create Post';
}
} catch (error) {
console.error('Approve error:', error);
alert(`✗ Error: ${error.message}`);
approveBtn.disabled = false;
rejectBtn.disabled = false;
approveBtn.textContent = 'Approve & Create Post';
}
});
// Reject handler
container.querySelector('.reject-draft')?.addEventListener('click', async () => {
const queueId = item._id;
const approveBtn = container.querySelector('.approve-draft');
const rejectBtn = container.querySelector('.reject-draft');
const reason = prompt('Reason for rejection (optional):');
if (reason === null) {
// User cancelled
return;
}
// Disable buttons
approveBtn.disabled = true;
rejectBtn.disabled = true;
rejectBtn.textContent = 'Rejecting...';
try {
const response = await apiCall(`/api/admin/moderation/${queueId}/review`, {
method: 'POST',
body: JSON.stringify({
action: 'reject',
notes: reason || 'Rejected via blog curation interface'
})
});
const result = await response.json();
if (response.ok) {
alert('✓ Draft rejected successfully');
// Close modal and reload queue
container.innerHTML = '';
loadDraftQueue();
loadStatistics();
} else {
alert(`✗ Error: ${result.message || 'Failed to reject draft'}`);
approveBtn.disabled = false;
rejectBtn.disabled = false;
rejectBtn.textContent = 'Reject';
}
} catch (error) {
console.error('Reject error:', error);
alert(`✗ Error: ${error.message}`);
approveBtn.disabled = false;
rejectBtn.disabled = false;
rejectBtn.textContent = 'Reject';
}
});
}
// Load editorial guidelines
async function loadEditorialGuidelines() {
try {
const response = await apiCall('/api/blog/editorial-guidelines');
if (response.ok) {
const data = await response.json();
const guidelines = data.guidelines;
// Standards
const standardsDiv = document.getElementById('editorial-standards');
standardsDiv.innerHTML = `
Tone ${guidelines.tone}
Voice ${guidelines.voice}
Style ${guidelines.style}
`;
// Forbidden patterns
const patternsDiv = document.getElementById('forbidden-patterns');
patternsDiv.innerHTML = guidelines.forbiddenPatterns.map(p => `
✗
${p}
`).join('');
// Principles
const principlesDiv = document.getElementById('core-principles');
principlesDiv.innerHTML = guidelines.principles.map(p => `
✓
${p}
`).join('');
}
} catch (error) {
console.error('Failed to load guidelines:', error);
}
}
// Logout
function initLogout() {
const logoutBtn = document.getElementById('logout-btn');
// If no logout button found, it's handled by navbar
if (!logoutBtn) {
console.log('[blog-curation] Logout handled by navbar');
return;
}
logoutBtn.addEventListener('click', () => {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
window.location.href = '/admin/login.html';
});
}
// Refresh queue button
function initRefresh() {
document.getElementById('refresh-queue-btn')?.addEventListener('click', () => {
loadDraftQueue();
});
}
// Suggest Topics button
function initSuggestTopics() {
const btn = document.getElementById('suggest-topics-btn');
if (!btn) return;
btn.addEventListener('click', async () => {
// Show modal with audience selector
const modal = `
Suggest Blog Topics
Topics will be generated based on existing documents and content on agenticgovernance.digital
Primary Audience
Research (Academic, AI safety specialists, data scientists)
Implementer (Engineers, architects, technical leads)
Leader (Executives, policy makers, decision makers)
General (Mixed backgrounds, introductory)
Tone & Approach
Standard (Professional, neutral)
Academic (Rigorous, citations-focused)
Practical (Action-oriented, hands-on)
Strategic (High-level, business-focused)
Conversational (Accessible, engaging)
Cultural & Linguistic Context
Universal (Culture-neutral, global perspective)
Indigenous Perspectives (Māori, First Nations values)
Global South (Emerging economies, localized concerns)
Asia-Pacific (Regional governance traditions)
European (GDPR, EU AI Act context)
North American (Tech industry, startup culture)
Language
English
Te Reo Māori
Español (Spanish)
Français (French)
Deutsch (German)
中文 (Chinese)
日本語 (Japanese)
Close
Generate Topics
`;
const container = document.getElementById('modal-container');
container.innerHTML = modal;
// Close handler
container.querySelector('.close-suggest-modal').addEventListener('click', () => {
container.innerHTML = '';
});
// Generate handler
container.querySelector('#generate-topics-btn').addEventListener('click', async () => {
const audience = document.getElementById('suggest-audience').value;
const tone = document.getElementById('suggest-tone').value;
const culture = document.getElementById('suggest-culture').value;
const language = document.getElementById('suggest-language').value;
const statusDiv = document.getElementById('topic-suggestions-status');
const listDiv = document.getElementById('topic-suggestions-list');
const generateBtn = document.getElementById('generate-topics-btn');
generateBtn.disabled = true;
generateBtn.textContent = 'Generating...';
statusDiv.textContent = 'Analyzing existing documents and generating culturally-aware topic suggestions...';
statusDiv.className = 'mt-4 text-sm text-blue-600';
try {
const response = await apiCall(`/api/blog/suggest-topics`, {
method: 'POST',
body: JSON.stringify({
audience,
tone,
culture,
language
})
});
const result = await response.json();
if (response.ok && result.suggestions) {
statusDiv.textContent = `✓ Generated ${result.suggestions.length} topic suggestions`;
statusDiv.className = 'mt-4 text-sm text-green-600';
listDiv.innerHTML = `
${result.suggestions.map((topic, i) => `
${topic.title || topic}
${topic.rationale ? `
${topic.rationale}
` : ''}
`).join('')}
`;
} else {
statusDiv.textContent = `✗ Error: ${result.message || 'Failed to generate topics'}`;
statusDiv.className = 'mt-4 text-sm text-red-600';
}
} catch (error) {
statusDiv.textContent = `✗ Error: ${error.message}`;
statusDiv.className = 'mt-4 text-sm text-red-600';
} finally {
generateBtn.disabled = false;
generateBtn.textContent = 'Generate Topics';
}
});
});
}
// Analyze Content button
function initAnalyzeContent() {
const btn = document.getElementById('analyze-content-btn');
if (!btn) return;
btn.addEventListener('click', () => {
const modal = `
Analyze Content for Tractatus Compliance
Check existing blog content for compliance with Tractatus principles (inst_016, inst_017, inst_018)
Close
Analyze
`;
const container = document.getElementById('modal-container');
container.innerHTML = modal;
// Close handler
container.querySelector('.close-analyze-modal').addEventListener('click', () => {
container.innerHTML = '';
});
// Analyze handler
container.querySelector('#run-analysis-btn').addEventListener('click', async () => {
const title = document.getElementById('analyze-title').value.trim();
const body = document.getElementById('analyze-body').value.trim();
const statusDiv = document.getElementById('analyze-status');
const resultsDiv = document.getElementById('analyze-results');
const analyzeBtn = document.getElementById('run-analysis-btn');
if (!title || !body) {
statusDiv.textContent = '⚠ Please enter both title and content';
statusDiv.className = 'mt-4 text-sm text-yellow-600';
return;
}
analyzeBtn.disabled = true;
analyzeBtn.textContent = 'Analyzing...';
statusDiv.textContent = 'Analyzing content for Tractatus compliance...';
statusDiv.className = 'mt-4 text-sm text-blue-600';
resultsDiv.innerHTML = '';
try {
const response = await apiCall('/api/blog/analyze-content', {
method: 'POST',
body: JSON.stringify({ title, body })
});
const result = await response.json();
if (response.ok && result.analysis) {
const analysis = result.analysis;
statusDiv.textContent = '✓ Analysis complete';
statusDiv.className = 'mt-4 text-sm text-green-600';
const recommendationClass = {
'PUBLISH': 'bg-green-100 text-green-800',
'EDIT_REQUIRED': 'bg-yellow-100 text-yellow-800',
'REJECT': 'bg-red-100 text-red-800'
}[analysis.recommendation] || 'bg-gray-100 text-gray-800';
resultsDiv.innerHTML = `
Compliance Score: ${analysis.overall_score}/100
${analysis.recommendation}
${analysis.violations && analysis.violations.length > 0 ? `
❌ Violations (${analysis.violations.length})
${analysis.violations.map(v => `
${v.type} - ${v.severity}
"${v.excerpt}"
Reason: ${v.reasoning}
${v.suggested_fix ? `
Fix: ${v.suggested_fix}
` : ''}
`).join('')}
` : ''}
${analysis.warnings && analysis.warnings.length > 0 ? `
⚠ Warnings (${analysis.warnings.length})
${analysis.warnings.map(w => `${w} `).join('')}
` : ''}
${analysis.strengths && analysis.strengths.length > 0 ? `
✓ Strengths (${analysis.strengths.length})
${analysis.strengths.map(s => `${s} `).join('')}
` : ''}
`;
} else {
statusDiv.textContent = `✗ Error: ${result.message || 'Analysis failed'}`;
statusDiv.className = 'mt-4 text-sm text-red-600';
}
} catch (error) {
statusDiv.textContent = `✗ Error: ${error.message}`;
statusDiv.className = 'mt-4 text-sm text-red-600';
} finally {
analyzeBtn.disabled = false;
analyzeBtn.textContent = 'Analyze';
}
});
});
}
// Marked.js simple implementation (fallback)
function marked(text) {
// Simple markdown to HTML conversion
return text
.replace(/### (.*)/g, '$1 ')
.replace(/## (.*)/g, '$1 ')
.replace(/# (.*)/g, '$1 ')
.replace(/\*\*(.*?)\*\*/g, '$1 ')
.replace(/\*(.*?)\*/g, '$1 ')
.replace(/\n\n/g, '')
.replace(/\n/g, ' ');
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
if (!checkAuth()) return;
initNavigation();
initDraftForm();
initLogout();
initRefresh();
initSuggestTopics();
initAnalyzeContent();
loadStatistics();
});