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>
331 lines
11 KiB
JavaScript
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);
|
|
});
|