- 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>
307 lines
13 KiB
JavaScript
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;
|
|
}
|