SUMMARY: Fixed interactive diagram click handlers not working. The SVG was loaded via <object> tag, which creates an isolated document that requires special access via contentDocument. ISSUE: - Clicks on service nodes had no effect - JavaScript was looking for SVG in main document - SVG loaded via <object> creates separate document context - document.getElementById() couldn't access elements inside object FIX: 1. Updated setup() to access object.contentDocument 2. Wait for object load event before initializing 3. Store SVG reference (this.svg) for later use 4. Updated all methods to use this.svg instead of document.getElementById() Methods updated: - setup(): Access SVG via objectElement.contentDocument - highlightService(): Use this.svg reference - unhighlightService(): Use this.svg reference - showServiceDetails(): Use this.svg reference - closePanel(): Use this.svg reference IMPACT: Interactive diagram now fully functional: ✓ Click any service node → detail panel appears ✓ Hover → connection lines highlight ✓ Close button → panel closes with animation ✓ Keyboard navigation works (Tab, Enter, Space) 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
376 lines
13 KiB
JavaScript
376 lines
13 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.serviceData = {
|
|
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.'
|
|
}
|
|
};
|
|
|
|
this.activeService = null;
|
|
this.init();
|
|
}
|
|
|
|
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
|
|
const initializeSVG = () => {
|
|
const svgDoc = objectElement.contentDocument;
|
|
if (!svgDoc) {
|
|
console.warn('[InteractiveDiagram] Could not access SVG contentDocument');
|
|
return;
|
|
}
|
|
|
|
const svg = svgDoc.getElementById('interactive-arch-diagram');
|
|
if (!svg) {
|
|
console.warn('[InteractiveDiagram] SVG diagram not found in contentDocument');
|
|
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`);
|
|
|
|
nodes.forEach(node => {
|
|
const serviceId = node.getAttribute('data-service');
|
|
|
|
node.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.showServiceDetails(serviceId);
|
|
});
|
|
|
|
node.addEventListener('mouseenter', () => {
|
|
this.highlightService(serviceId);
|
|
});
|
|
|
|
node.addEventListener('mouseleave', () => {
|
|
this.unhighlightService(serviceId);
|
|
});
|
|
});
|
|
|
|
this.addKeyboardNavigation(nodes);
|
|
};
|
|
|
|
// If object already loaded, initialize immediately
|
|
if (objectElement.contentDocument) {
|
|
initializeSVG();
|
|
} else {
|
|
// Otherwise wait for load event
|
|
objectElement.addEventListener('load', initializeSVG);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
let panel = document.getElementById('service-detail-panel');
|
|
|
|
if (!panel) {
|
|
panel = document.createElement('div');
|
|
panel.id = 'service-detail-panel';
|
|
panel.className = 'bg-white rounded-xl shadow-2xl p-6 border-2';
|
|
panel.style.borderColor = service.color;
|
|
|
|
const container = document.querySelector('#diagram-container');
|
|
if (container) {
|
|
container.appendChild(panel);
|
|
} else {
|
|
console.error('[InteractiveDiagram] Diagram container not found');
|
|
return;
|
|
}
|
|
} else {
|
|
panel.style.borderColor = service.color;
|
|
}
|
|
|
|
const html = `
|
|
<div class="flex items-start justify-between 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>
|
|
<button id="close-panel-btn" class="text-gray-400 hover:text-gray-600 transition" aria-label="Close">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</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;
|
|
}
|
|
|
|
// Add close button event listener (CSP-compliant)
|
|
const closeBtn = panel.querySelector('#close-panel-btn');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', () => this.closePanel());
|
|
}
|
|
|
|
panel.style.opacity = '0';
|
|
panel.style.transform = 'translateY(20px)';
|
|
panel.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
|
|
setTimeout(() => {
|
|
panel.style.opacity = '1';
|
|
panel.style.transform = 'translateY(0)';
|
|
}, 10);
|
|
}
|
|
|
|
closePanel() {
|
|
const panel = document.getElementById('service-detail-panel');
|
|
if (panel) {
|
|
panel.style.opacity = '0';
|
|
panel.style.transform = 'translateY(20px)';
|
|
|
|
setTimeout(() => {
|
|
panel.remove();
|
|
}, 300);
|
|
}
|
|
|
|
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'));
|
|
}
|
|
|
|
this.activeService = null;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = InteractiveDiagram;
|
|
}
|