/** * 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 = `

Blog Draft Preview

${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.

`; 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}
`).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 = `

Review Draft

${draft.title}

${draft.subtitle}

${marked(draft.content || '')}
`; 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

    `; 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)

    `; 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.strengths && analysis.strengths.length > 0 ? `
    ✓ Strengths (${analysis.strengths.length})
    ` : ''}
    `; } 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(); });