- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
11 KiB
JavaScript
308 lines
11 KiB
JavaScript
/**
|
|
* Context Pressure Visualization
|
|
* Tractatus Framework - Phase 3: Data Visualization
|
|
*
|
|
* Visual representation of Context Pressure Monitor metrics
|
|
* Uses amber color scheme matching the ContextPressureMonitor service
|
|
*/
|
|
|
|
class PressureChart {
|
|
constructor(containerId, gaugeContainerId = 'pressure-gauge') {
|
|
this.container = document.getElementById(containerId);
|
|
this.gaugeContainer = document.getElementById(gaugeContainerId);
|
|
|
|
if (!this.container) {
|
|
console.error(`[PressureChart] Container #${containerId} not found`);
|
|
return;
|
|
}
|
|
|
|
this.currentLevel = 0; // 0-100
|
|
this.targetLevel = 0;
|
|
this.animating = false;
|
|
|
|
this.colors = {
|
|
low: '#10b981', // Green - NORMAL
|
|
moderate: '#f59e0b', // Amber - ELEVATED
|
|
high: '#ef4444', // Red - HIGH
|
|
critical: '#991b1b' // Dark Red - CRITICAL
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.render();
|
|
this.attachEventListeners();
|
|
console.log('[PressureChart] Initialized');
|
|
}
|
|
|
|
render() {
|
|
console.log('[PressureChart] render() called, container:', this.container);
|
|
|
|
this.container.innerHTML = `
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h2 class="text-lg font-semibold text-gray-900">Context Pressure Monitor</h2>
|
|
<div id="pressure-status" class="px-4 py-2 rounded-full text-sm font-bold uppercase bg-green-100 text-green-700 transition-all duration-500">
|
|
NORMAL
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
|
<button id="pressure-simulate-btn" class="flex-1 bg-amber-800 hover:bg-amber-900 text-white px-4 py-3 rounded-lg font-bold text-sm transition-colors">
|
|
Simulate Pressure
|
|
</button>
|
|
<button id="pressure-reset-btn" class="flex-1 bg-gray-900 hover:bg-black text-white px-4 py-3 rounded-lg font-bold text-sm transition-colors">
|
|
Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-3 bg-blue-50 border border-blue-200 rounded-lg mb-6">
|
|
<p class="text-sm text-gray-700 leading-relaxed mb-2">
|
|
<strong>Interactive Demo:</strong> Click "Simulate Pressure" to watch how context pressure builds. As <strong>token usage increases</strong>, tasks become more <strong>complex</strong>, and <strong>error rates rise</strong>. The framework monitors this relationship to detect when AI performance may degrade.
|
|
</p>
|
|
<p class="text-xs text-gray-600">
|
|
The timeline on the right shows how six governance components coordinate to validate each request and maintain safe operation.
|
|
</p>
|
|
</div>
|
|
|
|
<svg class="w-full mb-6" viewBox="0 0 300 150" preserveAspectRatio="xMidYMid meet">
|
|
<path id="gauge-bg" d="M 54 120 A 96 96 0 0 1 246 120"
|
|
stroke="#e5e7eb" stroke-width="16" fill="none" stroke-linecap="round"/>
|
|
<path id="gauge-fill" d="M 54 120 A 96 96 0 0 1 54 120"
|
|
stroke="#f59e0b" stroke-width="16" fill="none" stroke-linecap="round"
|
|
class="gauge-fill-path"/>
|
|
<text x="150" y="105" text-anchor="middle" font-size="28" font-weight="bold" fill="#1f2937" id="gauge-value">0%</text>
|
|
<text x="150" y="125" text-anchor="middle" font-size="12" fill="#6b7280">Pressure Level</text>
|
|
</svg>
|
|
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-gray-900" id="metric-tokens">0</div>
|
|
<div class="text-xs text-gray-600">Tokens Used</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-gray-900" id="metric-complexity">Low</div>
|
|
<div class="text-xs text-gray-600">Complexity</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-gray-900" id="metric-errors">0</div>
|
|
<div class="text-xs text-gray-600">Error Rate</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Clear gauge container if it exists (no longer needed)
|
|
if (this.gaugeContainer) {
|
|
this.gaugeContainer.innerHTML = '';
|
|
}
|
|
|
|
// Store references
|
|
this.elements = {
|
|
gaugeFill: document.getElementById('gauge-fill'),
|
|
gaugeValue: document.getElementById('gauge-value'),
|
|
status: document.getElementById('pressure-status'),
|
|
tokens: document.getElementById('metric-tokens'),
|
|
complexity: document.getElementById('metric-complexity'),
|
|
errors: document.getElementById('metric-errors'),
|
|
simulateBtn: document.getElementById('pressure-simulate-btn'),
|
|
resetBtn: document.getElementById('pressure-reset-btn')
|
|
};
|
|
|
|
// Verify innerHTML was set
|
|
console.log('[PressureChart] innerHTML length:', this.container.innerHTML.length);
|
|
console.log('[PressureChart] First 100 chars:', this.container.innerHTML.substring(0, 100));
|
|
|
|
// Verify elements were found
|
|
console.log('[PressureChart] Elements found:', {
|
|
gaugeFill: !!this.elements.gaugeFill,
|
|
gaugeValue: !!this.elements.gaugeValue,
|
|
status: !!this.elements.status,
|
|
simulateBtn: !!this.elements.simulateBtn,
|
|
resetBtn: !!this.elements.resetBtn
|
|
});
|
|
}
|
|
|
|
attachEventListeners() {
|
|
if (!this.elements.simulateBtn || !this.elements.resetBtn) {
|
|
console.error('[PressureChart] Cannot attach event listeners - buttons not found');
|
|
return;
|
|
}
|
|
console.log('[PressureChart] Attaching event listeners to buttons');
|
|
this.elements.simulateBtn.addEventListener('click', () => this.simulate());
|
|
this.elements.resetBtn.addEventListener('click', () => this.reset());
|
|
console.log('[PressureChart] Event listeners attached successfully');
|
|
}
|
|
|
|
setLevel(level) {
|
|
this.targetLevel = Math.max(0, Math.min(100, level));
|
|
this.animateToTarget();
|
|
}
|
|
|
|
animateToTarget() {
|
|
if (this.animating) return;
|
|
this.animating = true;
|
|
|
|
const animate = () => {
|
|
const diff = this.targetLevel - this.currentLevel;
|
|
if (Math.abs(diff) < 0.5) {
|
|
this.currentLevel = this.targetLevel;
|
|
this.animating = false;
|
|
this.updateGauge();
|
|
return;
|
|
}
|
|
|
|
this.currentLevel += diff * 0.1;
|
|
this.updateGauge();
|
|
requestAnimationFrame(animate);
|
|
};
|
|
|
|
animate();
|
|
}
|
|
|
|
updateGauge() {
|
|
const level = this.currentLevel;
|
|
const angle = (level / 100) * 180; // 0-180 degrees
|
|
const radians = (angle * Math.PI) / 180;
|
|
|
|
// Calculate arc endpoint (20% smaller gauge: radius 96 instead of 120)
|
|
const centerX = 150;
|
|
const centerY = 120;
|
|
const radius = 96;
|
|
const startX = 54;
|
|
const startY = 120;
|
|
const endX = centerX + radius * Math.cos(Math.PI - radians);
|
|
const endY = centerY - radius * Math.sin(Math.PI - radians);
|
|
|
|
const largeArcFlag = angle > 180 ? 1 : 0;
|
|
const path = `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`;
|
|
|
|
this.elements.gaugeFill.setAttribute('d', path);
|
|
this.elements.gaugeValue.textContent = `${Math.round(level)}%`;
|
|
|
|
// Update color based on level
|
|
let color, status;
|
|
if (level < 25) {
|
|
color = this.colors.low;
|
|
status = 'NORMAL';
|
|
} else if (level < 50) {
|
|
color = this.colors.moderate;
|
|
status = 'ELEVATED';
|
|
} else if (level < 75) {
|
|
color = this.colors.high;
|
|
status = 'HIGH';
|
|
} else {
|
|
color = this.colors.critical;
|
|
status = 'CRITICAL';
|
|
}
|
|
|
|
this.elements.gaugeFill.setAttribute('stroke', color);
|
|
|
|
// Update status badge with animation
|
|
const previousStatus = this.elements.status.textContent;
|
|
this.elements.status.textContent = status;
|
|
|
|
// Badge styling based on level
|
|
const baseClasses = 'px-4 py-2 rounded-full text-sm font-bold uppercase transition-all duration-500';
|
|
let bgClass, textClass;
|
|
|
|
if (level < 25) {
|
|
bgClass = 'bg-green-100';
|
|
textClass = 'text-green-700';
|
|
} else if (level < 50) {
|
|
bgClass = 'bg-amber-100';
|
|
textClass = 'text-amber-700';
|
|
} else if (level < 75) {
|
|
bgClass = 'bg-red-100';
|
|
textClass = 'text-red-700';
|
|
} else {
|
|
bgClass = 'bg-red-200';
|
|
textClass = 'text-red-900';
|
|
}
|
|
|
|
// Add pulse animation when status changes
|
|
const pulseClass = previousStatus !== status ? 'animate-pulse' : '';
|
|
this.elements.status.className = `${baseClasses} ${bgClass} ${textClass} ${pulseClass}`;
|
|
|
|
// Remove pulse after animation
|
|
if (pulseClass) {
|
|
setTimeout(() => {
|
|
this.elements.status.className = `${baseClasses} ${bgClass} ${textClass}`;
|
|
}, 1000);
|
|
}
|
|
|
|
// Update metrics based on pressure level
|
|
const tokens = Math.round(level * 2000); // 0-200k tokens
|
|
const complexityLevels = ['Low', 'Moderate', 'High', 'Extreme'];
|
|
const complexityIndex = Math.min(3, Math.floor(level / 25));
|
|
const errorRate = Math.round(level / 5); // 0-20%
|
|
|
|
this.elements.tokens.textContent = tokens.toLocaleString();
|
|
this.elements.complexity.textContent = complexityLevels[complexityIndex];
|
|
this.elements.errors.textContent = `${errorRate}%`;
|
|
}
|
|
|
|
simulate() {
|
|
console.log('[PressureChart] Simulate button clicked - starting pressure simulation');
|
|
|
|
// Trigger timeline simulation if available
|
|
if (window.activityTimeline) {
|
|
console.log('[PressureChart] Triggering governance flow timeline');
|
|
window.activityTimeline.simulateFlow();
|
|
}
|
|
|
|
// Simulate pressure increasing from current to 85%
|
|
const targetLevels = [30, 50, 70, 85];
|
|
let index = 0;
|
|
|
|
const step = () => {
|
|
if (index >= targetLevels.length) return;
|
|
console.log('[PressureChart] Setting pressure level to', targetLevels[index]);
|
|
this.setLevel(targetLevels[index]);
|
|
index++;
|
|
setTimeout(step, 1500);
|
|
};
|
|
|
|
step();
|
|
}
|
|
|
|
reset() {
|
|
console.log('[PressureChart] Reset button clicked');
|
|
|
|
// Reset timeline if available
|
|
if (window.activityTimeline) {
|
|
console.log('[PressureChart] Resetting governance flow timeline');
|
|
window.activityTimeline.reset();
|
|
}
|
|
|
|
this.setLevel(0);
|
|
}
|
|
}
|
|
|
|
// Auto-initialize if container exists
|
|
if (typeof window !== 'undefined') {
|
|
function initPressureChart() {
|
|
console.log('[PressureChart] Attempting to initialize, readyState:', document.readyState);
|
|
const container = document.getElementById('pressure-chart');
|
|
if (container) {
|
|
console.log('[PressureChart] Container found, creating instance');
|
|
window.pressureChart = new PressureChart('pressure-chart');
|
|
} else {
|
|
console.error('[PressureChart] Container #pressure-chart not found in DOM');
|
|
}
|
|
}
|
|
|
|
// Initialize immediately if DOM is already loaded, otherwise wait for DOMContentLoaded
|
|
console.log('[PressureChart] Script loaded, readyState:', document.readyState);
|
|
if (document.readyState === 'loading') {
|
|
console.log('[PressureChart] Waiting for DOMContentLoaded');
|
|
document.addEventListener('DOMContentLoaded', initPressureChart);
|
|
} else {
|
|
console.log('[PressureChart] DOM already loaded, initializing immediately');
|
|
initPressureChart();
|
|
}
|
|
}
|
|
|
|
// Export for module systems
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = PressureChart;
|
|
}
|