tractatus/public/js/components/activity-timeline.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

307 lines
13 KiB
JavaScript

/**
* Framework Activity Timeline
* Tractatus Framework - Phase 3: Data Visualization
*
* Visual timeline showing framework component interactions
* Color-coded by service
*/
class ActivityTimeline {
constructor(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error(`[ActivityTimeline] Container #${containerId} not found`);
return;
}
this.currentPath = 'fast'; // Default to fast path
// Define three execution paths with realistic timings
this.pathProfiles = {
fast: {
name: 'Fast Path',
description: 'Simple request, all checks pass',
totalTime: '65ms',
events: [
{ time: '0ms', timeMs: 0, service: 'instruction', name: 'InstructionPersistence', action: 'Load cached instructions', color: '#4338ca' },
{ time: '5ms', timeMs: 5, service: 'validator', name: 'CrossReferenceValidator', action: 'Quick validation check', color: '#6d28d9' },
{ time: '15ms', timeMs: 15, service: 'boundary', name: 'BoundaryEnforcer', action: 'Auto-approved operation', color: '#047857' },
{ time: '25ms', timeMs: 25, service: 'pressure', name: 'ContextPressureMonitor', action: 'Normal pressure detected', color: '#b45309' },
{ time: '50ms', timeMs: 50, service: 'validator', name: 'CrossReferenceValidator', action: 'Final validation', color: '#6d28d9' },
{ time: '65ms', timeMs: 65, service: 'pressure', name: 'ContextPressureMonitor', action: 'Update metrics', color: '#b45309' }
]
},
standard: {
name: 'Standard Path',
description: 'Needs validation and verification',
totalTime: '135ms',
events: [
{ time: '0ms', timeMs: 0, service: 'instruction', name: 'InstructionPersistence', action: 'Load HIGH persistence instructions', color: '#4338ca' },
{ time: '8ms', timeMs: 8, service: 'validator', name: 'CrossReferenceValidator', action: 'Verify against instruction history', color: '#6d28d9' },
{ time: '30ms', timeMs: 30, service: 'boundary', name: 'BoundaryEnforcer', action: 'Check approval requirements', color: '#047857' },
{ time: '55ms', timeMs: 55, service: 'pressure', name: 'ContextPressureMonitor', action: 'Calculate pressure level', color: '#b45309' },
{ time: '95ms', timeMs: 95, service: 'metacognitive', name: 'MetacognitiveVerifier', action: 'Verify operation alignment', color: '#be185d' },
{ time: '120ms', timeMs: 120, service: 'validator', name: 'CrossReferenceValidator', action: 'Final validation check', color: '#6d28d9' },
{ time: '135ms', timeMs: 135, service: 'pressure', name: 'ContextPressureMonitor', action: 'Update pressure metrics', color: '#b45309' }
]
},
complex: {
name: 'Complex Path',
description: 'Requires deliberation and consensus',
totalTime: '285ms',
events: [
{ time: '0ms', timeMs: 0, service: 'instruction', name: 'InstructionPersistence', action: 'Load HIGH persistence instructions', color: '#4338ca' },
{ time: '8ms', timeMs: 8, service: 'validator', name: 'CrossReferenceValidator', action: 'Verify request against instruction history', color: '#6d28d9' },
{ time: '35ms', timeMs: 35, service: 'boundary', name: 'BoundaryEnforcer', action: 'Check if request requires human approval', color: '#047857' },
{ time: '60ms', timeMs: 60, service: 'pressure', name: 'ContextPressureMonitor', action: 'Calculate current pressure level', color: '#b45309' },
{ time: '105ms', timeMs: 105, service: 'metacognitive', name: 'MetacognitiveVerifier', action: 'Verify operation alignment', color: '#be185d' },
{ time: '160ms', timeMs: 160, service: 'deliberation', name: 'PluralisticDeliberation', action: 'Coordinate stakeholder perspectives', color: '#0f766e' },
{ time: '255ms', timeMs: 255, service: 'validator', name: 'CrossReferenceValidator', action: 'Final validation check', color: '#6d28d9' },
{ time: '285ms', timeMs: 285, service: 'pressure', name: 'ContextPressureMonitor', action: 'Update pressure metrics', color: '#b45309' }
]
}
};
// Initialize with fast path by default
this.events = this.pathProfiles[this.currentPath].events;
this.init();
}
init() {
this.render();
this.isSimulating = false;
console.log('[ActivityTimeline] Initialized');
}
render() {
const eventsHTML = this.events.map((event, index) => `
<div class="timeline-event flex items-start space-x-4 p-4 bg-white rounded-lg border-2 border-gray-200 hover:shadow-md cursor-pointer transition-all duration-300"
data-service="${event.service}"
data-event-index="${index}">
<div class="flex-shrink-0 w-16 text-right">
<span class="text-sm font-semibold text-gray-600 event-time">${event.time}</span>
</div>
<div class="flex-shrink-0">
<div class="w-3 h-3 rounded-full service-dot transition-all duration-300" data-color="${event.color}"></div>
</div>
<div class="flex-1">
<div class="text-sm font-semibold text-gray-900 service-name transition-colors duration-300" data-color="${event.color}">
${event.name}
</div>
<div class="text-xs text-gray-600 mt-1 event-action">${event.action}</div>
</div>
</div>
`).join('');
const currentProfile = this.pathProfiles[this.currentPath];
this.container.innerHTML = `
<div class="activity-timeline-container">
<div class="mb-4">
<h2 class="text-lg font-semibold text-gray-900">Governance Flow</h2>
<p class="text-xs text-gray-500 mt-1 italic">Estimated timing based on current performance data</p>
</div>
<!-- Path Selection -->
<div class="mb-4 p-3 bg-gray-50 border border-gray-300 rounded-lg">
<div class="text-xs font-semibold text-gray-700 mb-2">Execution Path:</div>
<div class="flex flex-col sm:flex-row gap-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="path" value="fast" ${this.currentPath === 'fast' ? 'checked' : ''} class="path-radio">
<span class="text-sm font-medium text-gray-900">Fast</span>
<span class="text-xs text-gray-600">(${this.pathProfiles.fast.totalTime})</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="path" value="standard" ${this.currentPath === 'standard' ? 'checked' : ''} class="path-radio">
<span class="text-sm font-medium text-gray-900">Standard</span>
<span class="text-xs text-gray-600">(${this.pathProfiles.standard.totalTime})</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="path" value="complex" ${this.currentPath === 'complex' ? 'checked' : ''} class="path-radio">
<span class="text-sm font-medium text-gray-900">Complex</span>
<span class="text-xs text-gray-600">(${this.pathProfiles.complex.totalTime})</span>
</label>
</div>
<div class="text-xs text-gray-600 mt-2">${currentProfile.description}</div>
</div>
<!-- Timeline Explanation -->
<div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p class="text-sm text-gray-700 leading-relaxed mb-2">
This shows the framework's governance components working together to validate and process each request. Each component has a specific role in ensuring safe, values-aligned AI operation.
</p>
<p class="text-xs text-gray-600 italic">
Note: Timing values are estimates based on current performance statistics and may vary in production.
</p>
</div>
<div class="space-y-3">
${eventsHTML}
</div>
<div class="mt-6 text-xs text-gray-500 text-center">
Total processing time: ${currentProfile.totalTime} | All services coordinated
</div>
</div>
`;
// Apply colors via JavaScript (CSP-compliant)
this.applyColors();
// Attach event listeners to path radio buttons
this.attachPathListeners();
}
attachPathListeners() {
const radios = this.container.querySelectorAll('.path-radio');
radios.forEach(radio => {
radio.addEventListener('change', (e) => {
this.setPath(e.target.value);
});
});
}
setPath(pathName) {
if (!this.pathProfiles[pathName]) {
console.error(`[ActivityTimeline] Unknown path: ${pathName}`);
return;
}
console.log(`[ActivityTimeline] Switching to ${pathName} path`);
this.currentPath = pathName;
this.events = this.pathProfiles[pathName].events;
this.render();
}
applyColors() {
document.querySelectorAll('.service-dot').forEach(dot => {
const color = dot.getAttribute('data-color');
dot.style.backgroundColor = color;
});
document.querySelectorAll('.service-name').forEach(name => {
const color = name.getAttribute('data-color');
name.style.color = color;
});
}
activateEvent(index) {
const eventElement = this.container.querySelector(`[data-event-index="${index}"]`);
if (!eventElement) return;
const event = this.events[index];
// Highlight the event card
eventElement.style.borderColor = event.color;
eventElement.style.backgroundColor = `${event.color}10`; // 10% opacity
eventElement.style.boxShadow = `0 4px 12px ${event.color}40`;
// Enlarge and pulse the service dot
const dot = eventElement.querySelector('.service-dot');
if (dot) {
dot.style.width = '12px';
dot.style.height = '12px';
dot.style.boxShadow = `0 0 8px ${event.color}`;
}
console.log(`[ActivityTimeline] Activated event ${index}: ${event.name}`);
}
deactivateEvent(index) {
const eventElement = this.container.querySelector(`[data-event-index="${index}"]`);
if (!eventElement) return;
// Reset to default styling
eventElement.style.borderColor = '#e5e7eb';
eventElement.style.backgroundColor = '#ffffff';
eventElement.style.boxShadow = '';
// Reset service dot
const dot = eventElement.querySelector('.service-dot');
if (dot) {
dot.style.width = '12px';
dot.style.height = '12px';
dot.style.boxShadow = '';
}
}
async simulateFlow() {
if (this.isSimulating) {
console.log('[ActivityTimeline] Already simulating, ignoring request');
return;
}
this.isSimulating = true;
console.log('[ActivityTimeline] Starting governance flow simulation');
// Reset all events first
for (let i = 0; i < this.events.length; i++) {
this.deactivateEvent(i);
}
// Simulate each event activation with realistic timing
for (let i = 0; i < this.events.length; i++) {
const event = this.events[i];
const prevEvent = i > 0 ? this.events[i - 1] : null;
// Calculate actual delay based on event timing (scaled 2x for visibility)
const delay = prevEvent ? (event.timeMs - prevEvent.timeMs) * 2 : 0;
await new Promise(resolve => setTimeout(resolve, delay));
// Deactivate previous event
if (i > 0) {
this.deactivateEvent(i - 1);
}
// Activate current event
this.activateEvent(i);
console.log(`[ActivityTimeline] Event ${i} activated at ${event.time} (delay: ${delay}ms)`);
}
// Keep the last event active for a moment, then deactivate
await new Promise(resolve => setTimeout(resolve, 800));
this.deactivateEvent(this.events.length - 1);
this.isSimulating = false;
console.log('[ActivityTimeline] Governance flow simulation complete');
}
reset() {
console.log('[ActivityTimeline] Resetting timeline');
for (let i = 0; i < this.events.length; i++) {
this.deactivateEvent(i);
}
this.isSimulating = false;
}
}
// Auto-initialize if container exists
if (typeof window !== 'undefined') {
function initActivityTimeline() {
console.log('[ActivityTimeline] Attempting to initialize, readyState:', document.readyState);
const container = document.getElementById('activity-timeline');
if (container) {
console.log('[ActivityTimeline] Container found, creating instance');
window.activityTimeline = new ActivityTimeline('activity-timeline');
} else {
console.error('[ActivityTimeline] Container #activity-timeline not found in DOM');
}
}
// Initialize immediately if DOM is already loaded, otherwise wait for DOMContentLoaded
console.log('[ActivityTimeline] Script loaded, readyState:', document.readyState);
if (document.readyState === 'loading') {
console.log('[ActivityTimeline] Waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', initActivityTimeline);
} else {
console.log('[ActivityTimeline] DOM already loaded, initializing immediately');
initActivityTimeline();
}
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = ActivityTimeline;
}