tractatus/public/js/components/pressure-chart.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- 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>
2025-10-24 08:47:42 +13:00

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;
}