tractatus/public/js/admin/blog-pre-publication.js
TheFlow 20a108402e feat(content): add framework-guided blog pre-publication and comment analysis
Blog Pre-Publication Workflow:
- New admin interface (blog-pre-publication.html) for framework-guided content review
- Analysis provides: sensitivity check, compliance validation, audience analysis
- Publication guidance: timing, monitoring, action recommendations
- Response templates for anticipated reader feedback
- Overall recommendation: APPROVE/REVIEW/REJECT decision
- CSP-compliant implementation (no inline scripts/styles)

Comment & Feedback Analysis Workflow:
- New admin interface (comment-analysis.html) for social media/article feedback
- Sentiment analysis (positive/negative/neutral/mixed with confidence)
- Values alignment check (aligned values, concerns, misunderstandings)
- Risk assessment (low/medium/high with factors)
- Recommended responses (prioritized with rationale)
- Framework guidance on whether/how to respond

Backend Implementation:
- New controller: framework-content-analysis.controller.js
- Services invoked: PluralisticDeliberationOrchestrator, BoundaryEnforcer
- API routes: /api/admin/blog/analyze, /api/admin/feedback/analyze
- Integration with existing auth and validation middleware

Framework Validation:
During implementation, framework caught and blocked TWO CSP violations:
1. Inline onclick attribute - forced addEventListener pattern
2. Inline style attribute - forced data attributes + JavaScript
This demonstrates framework is actively preventing violations in real-time.

Transforms blog curation from passive reporter to active agency manager.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 19:45:43 +13:00

331 lines
11 KiB
JavaScript

/**
* Blog Pre-Publication Analysis
* Framework-guided content review before publishing
*/
// Get auth token
function getAuthToken() {
return localStorage.getItem('admin_token');
}
// Analyze blog post
async function analyzePost() {
const title = document.getElementById('post-title').value.trim();
const content = document.getElementById('post-content').value.trim();
const category = document.getElementById('post-category').value;
const tags = document.getElementById('post-tags').value;
if (!title || !content) {
alert('Please enter both title and content');
return;
}
// Show loading state
const analyzeBtn = document.getElementById('analyze-btn');
analyzeBtn.disabled = true;
analyzeBtn.innerHTML = '<svg class="animate-spin w-5 h-5 inline-block mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Analyzing...';
try {
const response = await fetch('/api/admin/blog/analyze', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, content, category, tags })
});
const data = await response.json();
if (data.success) {
displayResults(data.analysis);
} else {
alert('Analysis failed: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Analysis error:', error);
alert('Failed to analyze post. Please try again.');
} finally {
analyzeBtn.disabled = false;
analyzeBtn.innerHTML = '<svg class="w-5 h-5 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>Analyze with Framework';
}
}
// Display analysis results
function displayResults(analysis) {
const resultsSection = document.getElementById('results-section');
resultsSection.classList.remove('hidden');
// Overall recommendation
const overallEl = document.getElementById('overall-recommendation');
const recommendationClass =
analysis.overall.decision === 'APPROVE' ? 'bg-green-50 border-green-300' :
analysis.overall.decision === 'REVIEW' ? 'bg-yellow-50 border-yellow-300' :
'bg-red-50 border-red-300';
const recommendationIcon =
analysis.overall.decision === 'APPROVE' ? '✅' :
analysis.overall.decision === 'REVIEW' ? '⚠️' : '🚫';
overallEl.className = `rounded-lg p-6 border-2 ${recommendationClass}`;
overallEl.innerHTML = `
<div class="flex items-start">
<div class="text-4xl mr-4">${recommendationIcon}</div>
<div class="flex-1">
<h3 class="text-xl font-bold text-gray-900 mb-2">${analysis.overall.title}</h3>
<p class="text-gray-700 mb-4">${analysis.overall.message}</p>
${analysis.overall.action ? `<p class="text-sm font-medium text-gray-800"><strong>Recommended Action:</strong> ${analysis.overall.action}</p>` : ''}
</div>
</div>
`;
// Sensitivity check
const sensitivityEl = document.getElementById('sensitivity-result');
sensitivityEl.innerHTML = renderCheckResult(analysis.sensitivity);
// Compliance check
const complianceEl = document.getElementById('compliance-result');
complianceEl.innerHTML = renderCheckResult(analysis.compliance);
// Audience analysis
const audienceEl = document.getElementById('audience-result');
audienceEl.innerHTML = renderAudienceAnalysis(analysis.audience);
// Publication guidance
const publicationEl = document.getElementById('publication-result');
publicationEl.innerHTML = renderPublicationGuidance(analysis.publication);
// Response templates
const templatesEl = document.getElementById('response-templates');
templatesEl.innerHTML = renderResponseTemplates(analysis.responseTemplates);
// Apply dynamic widths using data attributes (CSP-compliant)
applyDynamicStyles();
// Setup template copy handlers
setupTemplateCopyHandlers();
// Scroll to results
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Apply dynamic styles using data attributes (CSP-compliant)
function applyDynamicStyles() {
document.querySelectorAll('[data-width]').forEach(el => {
const width = el.getAttribute('data-width');
el.style.width = width + '%';
});
}
// Render check result
function renderCheckResult(check) {
const statusIcon = check.status === 'PASS' ? '✅' : check.status === 'WARN' ? '⚠️' : '❌';
const statusColor = check.status === 'PASS' ? 'text-green-600' : check.status === 'WARN' ? 'text-yellow-600' : 'text-red-600';
let html = `
<div class="mb-3">
<span class="${statusColor} font-semibold">${statusIcon} ${check.summary}</span>
</div>
`;
if (check.details && check.details.length > 0) {
html += '<ul class="list-disc list-inside space-y-1 text-gray-600">';
check.details.forEach(detail => {
html += `<li>${detail}</li>`;
});
html += '</ul>';
}
if (check.recommendation) {
html += `<div class="mt-3 p-2 bg-gray-50 rounded border border-gray-200 text-xs">
<strong>Recommendation:</strong> ${check.recommendation}
</div>`;
}
return html;
}
// Render audience analysis
function renderAudienceAnalysis(audience) {
let html = `<div class="space-y-3">`;
if (audience.engagement) {
html += `
<div>
<div class="text-xs font-medium text-gray-500 mb-1">Expected Engagement</div>
<div class="flex items-center">
<div class="flex-1 h-2 bg-gray-200 rounded-full">
<div class="h-2 bg-blue-600 rounded-full" data-width="${audience.engagement.level}"></div>
</div>
<span class="ml-3 text-sm font-semibold">${audience.engagement.level}%</span>
</div>
<div class="text-xs text-gray-600 mt-1">${audience.engagement.description}</div>
</div>
`;
}
if (audience.similarPosts && audience.similarPosts.length > 0) {
html += `
<div>
<div class="text-xs font-medium text-gray-500 mb-1">Similar Posts</div>
<div class="text-xs text-gray-600">
${audience.similarPosts.map(post => `${post.title} (${post.views} views)`).join('<br>')}
</div>
</div>
`;
}
html += `</div>`;
return html;
}
// Render publication guidance
function renderPublicationGuidance(guidance) {
let html = `<div class="space-y-3">`;
if (guidance.timing) {
html += `
<div>
<div class="text-xs font-medium text-gray-500 mb-1">Recommended Timing</div>
<div class="text-sm text-gray-700">${guidance.timing}</div>
</div>
`;
}
if (guidance.monitoring) {
html += `
<div>
<div class="text-xs font-medium text-gray-500 mb-1">Post-Publication Monitoring</div>
<div class="text-sm text-gray-700">${guidance.monitoring}</div>
</div>
`;
}
if (guidance.actions && guidance.actions.length > 0) {
html += `
<div>
<div class="text-xs font-medium text-gray-500 mb-1">Recommended Actions</div>
<ul class="list-disc list-inside text-sm text-gray-700 space-y-1">
${guidance.actions.map(action => `<li>${action}</li>`).join('')}
</ul>
</div>
`;
}
html += `</div>`;
return html;
}
// Render response templates
function renderResponseTemplates(templates) {
if (!templates || templates.length === 0) {
return '<p class="text-sm text-gray-500 italic">No response templates generated</p>';
}
return templates.map((template, index) => `
<div class="border border-gray-200 rounded-lg p-4 hover:border-blue-300 transition cursor-pointer template-card" data-template-index="${index}">
<div class="flex items-start justify-between mb-2">
<div class="font-medium text-gray-900">${template.scenario}</div>
<button class="copy-template-btn text-xs text-blue-600 hover:text-blue-800">Copy</button>
</div>
<div class="text-sm text-gray-700 italic template-text">"${template.response}"</div>
</div>
`).join('');
}
// Copy template to clipboard
function setupTemplateCopyHandlers() {
document.querySelectorAll('.copy-template-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const card = this.closest('.template-card');
const text = card.querySelector('.template-text').textContent.replace(/"/g, '');
navigator.clipboard.writeText(text).then(() => {
const originalText = this.textContent;
this.textContent = 'Copied!';
this.classList.add('text-green-600');
setTimeout(() => {
this.textContent = originalText;
this.classList.remove('text-green-600');
}, 2000);
});
});
});
}
// Save as draft
async function saveDraft() {
const title = document.getElementById('post-title').value.trim();
const content = document.getElementById('post-content').value.trim();
const category = document.getElementById('post-category').value;
const tags = document.getElementById('post-tags').value;
if (!title || !content) {
alert('Please enter both title and content');
return;
}
try {
const response = await fetch('/api/admin/blog/draft', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, content, category, tags, status: 'draft' })
});
const data = await response.json();
if (data.success) {
alert('Draft saved successfully!');
} else {
alert('Failed to save draft: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Save draft error:', error);
alert('Failed to save draft. Please try again.');
}
}
// Publish post
async function publishPost() {
if (!confirm('Are you sure you want to publish this post?')) {
return;
}
const title = document.getElementById('post-title').value.trim();
const content = document.getElementById('post-content').value.trim();
const category = document.getElementById('post-category').value;
const tags = document.getElementById('post-tags').value;
try {
const response = await fetch('/api/admin/blog/publish', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, content, category, tags, status: 'published' })
});
const data = await response.json();
if (data.success) {
alert('Post published successfully!');
window.location.href = '/admin/blog-posts.html';
} else {
alert('Failed to publish: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Publish error:', error);
alert('Failed to publish. Please try again.');
}
}
// Event listeners
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('analyze-btn').addEventListener('click', analyzePost);
document.getElementById('save-draft-btn')?.addEventListener('click', saveDraft);
document.getElementById('publish-btn')?.addEventListener('click', publishPost);
});