tractatus/public/js/components/interactive-diagram.js
TheFlow 06ce4744e9 fix(interactive-diagram): add fallback data to prevent race condition with i18n
- Added loadFallbackData() method with English defaults
- Check if i18n translations loaded before using them
- Falls back to English if window.i18nTranslations not ready yet
- Fixes 'Failed to load SVG after 20 retries' error caused by empty serviceData
2025-10-26 13:24:04 +13:00

512 lines
18 KiB
JavaScript

/**
* Interactive Architecture Diagram Component
* Tractatus Framework - Phase 3: Interactive Architecture Diagram
*
* Handles click/hover interactions on the hexagonal service diagram
* Shows service details in a side panel
*/
class InteractiveDiagram {
constructor() {
this.activeService = null;
this.loadTranslations();
this.init();
}
loadTranslations() {
// Get translations from i18n system
const i18n = window.i18nTranslations || {};
const diagram = i18n.diagram_services || {};
// If no translations available, use English fallback
const hasTranslations = diagram && Object.keys(diagram).length > 0;
if (!hasTranslations) {
console.warn('[InteractiveDiagram] No translations loaded, using English fallback');
this.loadFallbackData();
return;
}
// Define static properties (icons and colors)
const staticProps = {
overview: { color: '#0ea5e9', icon: '⚙️' },
boundary: { color: '#10b981', icon: '🔒' },
instruction: { color: '#6366f1', icon: '📋' },
validator: { color: '#8b5cf6', icon: '✓' },
pressure: { color: '#f59e0b', icon: '⚡' },
metacognitive: { color: '#ec4899', icon: '💡' },
deliberation: { color: '#14b8a6', icon: '👥' }
};
// Build serviceData from translations
this.serviceData = {};
Object.keys(staticProps).forEach(serviceId => {
const trans = diagram[serviceId] || {};
const details = [];
// Collect detail1-detail6 (some services have 4, overview has 6)
for (let i = 1; i <= 6; i++) {
if (trans[`detail${i}`]) {
details.push(trans[`detail${i}`]);
}
}
this.serviceData[serviceId] = {
name: trans.name || serviceId,
shortName: trans.shortName || serviceId,
color: staticProps[serviceId].color,
icon: staticProps[serviceId].icon,
description: trans.description || '',
details: details,
promise: trans.promise || ''
};
});
console.log('[InteractiveDiagram] Loaded translations for', Object.keys(this.serviceData).length, 'services');
}
loadFallbackData() {
// English fallback data when i18n not ready
this.serviceData = {
overview: {
name: 'Tractatus Governance Layer',
shortName: 'Overview',
color: '#0ea5e9',
icon: '⚙️',
description: 'Six external governance services working together to enforce AI safety boundaries outside the AI runtime.',
details: [
'All services operate externally to the AI—making manipulation harder',
'Instruction storage and validation work together to prevent directive fade',
'Boundary enforcement and deliberation coordinate on values decisions',
'Pressure monitoring adjusts verification requirements dynamically',
'Metacognitive gates ensure AI pauses before high-risk operations',
'Each service addresses a different failure mode in AI safety'
],
promise: 'External architectural enforcement that is structurally more difficult to bypass than behavioral training alone.'
},
boundary: {
name: 'BoundaryEnforcer',
shortName: 'Boundary',
color: '#10b981',
icon: '🔒',
description: 'Blocks AI from making values decisions (privacy, ethics, strategic direction). Requires human approval.',
details: [
'Enforces Tractatus 12.1-12.7 boundaries',
'Values decisions architecturally require humans',
'Prevents AI autonomous decision-making on ethical questions',
'External enforcement - harder to bypass via prompting'
],
promise: 'Values boundaries enforced externally—harder to manipulate through prompting.'
},
instruction: {
name: 'InstructionPersistenceClassifier',
shortName: 'Instruction',
color: '#6366f1',
icon: '📋',
description: 'Stores instructions externally with persistence levels (HIGH/MEDIUM/LOW). Aims to reduce directive fade.',
details: [
'Quadrant-based classification (STR/OPS/TAC/SYS/STO)',
'Time-persistence metadata tagging',
'Temporal horizon modeling (STRATEGIC, OPERATIONAL, TACTICAL)',
'External storage independent of AI runtime'
],
promise: 'Instructions stored outside AI—more resistant to context manipulation.'
},
validator: {
name: 'CrossReferenceValidator',
shortName: 'Validator',
color: '#8b5cf6',
icon: '✓',
description: 'Validates AI actions against instruction history. Aims to prevent pattern bias overriding explicit directives.',
details: [
'Cross-references AI claims with external instruction history',
'Detects pattern-based overrides of explicit user directives',
'Independent verification layer',
'Helps prevent instruction drift'
],
promise: 'Independent verification—AI claims checked against external source.'
},
pressure: {
name: 'ContextPressureMonitor',
shortName: 'Pressure',
color: '#f59e0b',
icon: '⚡',
description: 'Monitors AI performance degradation. Escalates when context pressure threatens quality.',
details: [
'Tracks token usage, complexity, error rates',
'Detects degraded operating conditions',
'Adjusts verification requirements under pressure',
'Objective metrics for quality monitoring'
],
promise: 'Objective metrics may detect manipulation attempts early.'
},
metacognitive: {
name: 'MetacognitiveVerifier',
shortName: 'Metacognitive',
color: '#ec4899',
icon: '💡',
description: 'Requires AI to pause and verify complex operations before execution. Structural safety check.',
details: [
'AI self-checks alignment, coherence, safety before execution',
'Structural pause-and-verify gates',
'Selective verification (not constant)',
'Architectural enforcement of reflection steps'
],
promise: 'Architectural gates aim to enforce verification steps.'
},
deliberation: {
name: 'PluralisticDeliberationOrchestrator',
shortName: 'Deliberation',
color: '#14b8a6',
icon: '👥',
description: 'Facilitates multi-stakeholder deliberation for values conflicts where no single "correct" answer exists.',
details: [
'Non-hierarchical coordination for values conflicts',
'Stakeholder perspective representation',
'Consensus-building for ethical trade-offs',
'Addresses values pluralism in AI safety'
],
promise: 'Facilitates deliberation across stakeholder perspectives for values conflicts.'
}
};
console.log('[InteractiveDiagram] Loaded English fallback data for', Object.keys(this.serviceData).length, 'services');
}
handleLanguageChange() {
console.log('[InteractiveDiagram] Language changed, reloading translations');
this.loadTranslations();
// Re-render current service if one is active
if (this.activeService) {
const service = this.serviceData[this.activeService];
if (service) {
this.renderServicePanel(service);
}
}
}
init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
console.log('[InteractiveDiagram] Initialized');
}
setup() {
// SVG is loaded via <object> tag, need to access its contentDocument
const objectElement = document.getElementById('interactive-svg-object');
if (!objectElement) {
console.warn('[InteractiveDiagram] SVG object element not found');
return;
}
// Wait for object to load with retry mechanism
const initializeSVG = () => {
const svgDoc = objectElement.contentDocument;
if (!svgDoc) {
console.warn('[InteractiveDiagram] Could not access SVG contentDocument, retrying...');
setTimeout(initializeSVG, 100);
return;
}
// The SVG is the document element itself, or we can query for it
let svg = svgDoc.getElementById('interactive-arch-diagram');
if (!svg) {
// Try getting the root SVG element
svg = svgDoc.documentElement;
console.log('[InteractiveDiagram] Using documentElement as SVG');
}
if (!svg) {
console.warn('[InteractiveDiagram] SVG diagram not found in contentDocument');
return;
}
// Verify it's actually an SVG element (case-insensitive check)
const tagName = svg.tagName ? svg.tagName.toLowerCase() : '';
if (tagName !== 'svg') {
console.warn('[InteractiveDiagram] Element found but not SVG, tagName:', tagName, '- retrying...');
// This is the race condition - contentDocument is HTML, not SVG yet
setTimeout(initializeSVG, 100);
return;
}
// Store reference to SVG document for later use
this.svgDoc = svgDoc;
this.svg = svg;
const nodes = svg.querySelectorAll('.service-node');
console.log(`[InteractiveDiagram] Found ${nodes.length} service nodes`);
if (nodes.length === 0) {
console.warn('[InteractiveDiagram] No service nodes found in SVG');
return;
}
nodes.forEach(node => {
const serviceId = node.getAttribute('data-service');
// Click handler
node.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.showServiceDetails(serviceId);
}, true);
// Touch support for mobile devices
node.addEventListener('touchstart', (e) => {
e.preventDefault();
const serviceId = node.getAttribute('data-service');
this.showServiceDetails(serviceId);
}, { passive: false });
// Hover effects
node.addEventListener('mouseenter', () => {
this.highlightService(serviceId);
});
node.addEventListener('mouseleave', () => {
this.unhighlightService(serviceId);
});
// Add pointer cursor via JavaScript (CSP-compliant)
node.style.cursor = 'pointer';
});
this.addKeyboardNavigation(nodes);
// Show initial state (overview)
this.showServiceDetails('overview');
console.log('[InteractiveDiagram] Setup complete, showing overview');
};
// FIXED: Better load detection with SVG verification
const checkAndInit = () => {
const svgDoc = objectElement.contentDocument;
// Check if contentDocument exists and is actually SVG
if (svgDoc && svgDoc.documentElement) {
const rootTagName = svgDoc.documentElement.tagName ? svgDoc.documentElement.tagName.toLowerCase() : '';
if (rootTagName === 'svg') {
// It's an SVG - safe to initialize
console.log('[InteractiveDiagram] SVG detected in contentDocument, initializing');
initializeSVG();
return true;
} else {
console.log('[InteractiveDiagram] contentDocument exists but root is:', rootTagName, '- not ready yet');
return false;
}
}
return false;
};
// Try immediate initialization if already loaded
if (!checkAndInit()) {
// Not ready yet - wait for load event
console.log('[InteractiveDiagram] Waiting for object to load...');
objectElement.addEventListener('load', () => {
console.log('[InteractiveDiagram] Object load event fired');
// Small delay to ensure contentDocument is fully parsed
setTimeout(() => {
if (!checkAndInit()) {
// Still not ready - start retry mechanism
initializeSVG();
}
}, 50);
});
// Also try periodic checks as fallback
let retryCount = 0;
const maxRetries = 20;
const retryInterval = setInterval(() => {
retryCount++;
if (checkAndInit() || retryCount >= maxRetries) {
clearInterval(retryInterval);
if (retryCount >= maxRetries) {
console.error('[InteractiveDiagram] Failed to load SVG after', maxRetries, 'retries');
}
}
}, 100);
}
}
highlightService(serviceId) {
if (!this.svg) return;
const connectionLine = this.svg.querySelector(`#conn-${serviceId}`);
if (connectionLine) {
connectionLine.classList.add('active');
}
const node = this.svg.querySelector(`#node-${serviceId}`);
if (node) {
node.classList.add('hover');
}
}
unhighlightService(serviceId) {
if (!this.svg) return;
if (this.activeService === serviceId) return;
const connectionLine = this.svg.querySelector(`#conn-${serviceId}`);
if (connectionLine) {
connectionLine.classList.remove('active');
}
const node = this.svg.querySelector(`#node-${serviceId}`);
if (node) {
node.classList.remove('hover');
}
}
showServiceDetails(serviceId) {
const service = this.serviceData[serviceId];
if (!service) {
console.error('[InteractiveDiagram] Service not found:', serviceId);
return;
}
this.activeService = serviceId;
if (this.svg) {
this.svg.querySelectorAll('.service-node').forEach(n => n.classList.remove('active'));
this.svg.querySelectorAll('.connection-line').forEach(l => l.classList.remove('active'));
const node = this.svg.querySelector(`#node-${serviceId}`);
if (node) {
node.classList.add('active');
}
const connectionLine = this.svg.querySelector(`#conn-${serviceId}`);
if (connectionLine) {
connectionLine.classList.add('active');
}
}
this.renderServicePanel(service);
console.log('[InteractiveDiagram] Showing details for:', service.name);
}
renderServicePanel(service) {
const panel = document.getElementById('service-detail-panel');
if (!panel) {
console.error('[InteractiveDiagram] Service detail panel not found in DOM');
return;
}
// Update border color to match selected service
panel.style.borderColor = service.color;
panel.style.borderWidth = '2px';
const html = `
<div class="flex items-start mb-4">
<div class="flex items-center space-x-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl service-icon-box" data-color="${service.color}">
${service.icon}
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">${service.name}</h3>
<span class="text-xs font-medium text-gray-600 uppercase">${service.shortName}</span>
</div>
</div>
</div>
<p class="text-gray-700 mb-4 leading-relaxed">${service.description}</p>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-900 mb-2 uppercase">Key Features</h4>
<ul class="space-y-2" id="service-features-list">
${service.details.map(detail => `
<li class="flex items-start text-sm text-gray-700">
<svg class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0 service-check-icon" data-color="${service.color}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
</svg>
<span>${detail}</span>
</li>
`).join('')}
</ul>
</div>
<div class="text-xs rounded px-3 py-2 bg-opacity-20 service-promise-badge" data-color="${service.color}">
<strong class="service-promise-text" data-color="${service.color}">Early Promise:</strong>
<span class="text-gray-800">${service.promise}</span>
</div>
`;
panel.innerHTML = html;
// Apply styles via JavaScript (CSP-compliant)
const iconBox = panel.querySelector('.service-icon-box');
if (iconBox) {
const color = iconBox.getAttribute('data-color');
iconBox.style.background = `linear-gradient(135deg, ${color} 0%, ${color}dd 100%)`;
}
// Style all check icons
const checkIcons = panel.querySelectorAll('.service-check-icon');
checkIcons.forEach(icon => {
const color = icon.getAttribute('data-color');
icon.style.color = color;
});
// Style promise badge
const promiseBadge = panel.querySelector('.service-promise-badge');
if (promiseBadge) {
const color = promiseBadge.getAttribute('data-color');
promiseBadge.style.backgroundColor = color;
}
// Style promise text
const promiseText = panel.querySelector('.service-promise-text');
if (promiseText) {
const color = promiseText.getAttribute('data-color');
promiseText.style.color = color;
}
}
addKeyboardNavigation(nodes) {
nodes.forEach((node, index) => {
node.setAttribute('tabindex', '0');
node.setAttribute('role', 'button');
node.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const serviceId = node.getAttribute('data-service');
this.showServiceDetails(serviceId);
}
});
});
}
}
if (typeof window !== 'undefined') {
window.interactiveDiagram = new InteractiveDiagram();
// Listen for i18n initialization
window.addEventListener('i18nInitialized', () => {
if (window.interactiveDiagram) {
window.interactiveDiagram.handleLanguageChange();
}
});
// Listen for language changes
window.addEventListener('languageChanged', () => {
if (window.interactiveDiagram) {
window.interactiveDiagram.handleLanguageChange();
}
});
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = InteractiveDiagram;
}