Fixed non-responsive service node clicks on architecture.html interactive diagram. Root cause: SVG loaded via <object> tag had contentDocument timing issues - event listeners attached before SVG fully accessible. Solution: - Added retry mechanism for contentDocument access (100ms intervals) - Implemented multiple loading strategies (immediate, load event, timeout) - Enabled event capturing phase (addEventListener 3rd param: true) - Enhanced logging for debugging click event flow - Auto-display overview panel on initialization - CSP-compliant cursor styling via JavaScript Deployed to production: https://agenticgovernance.digital/architecture.html 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
386 lines
14 KiB
JavaScript
386 lines
14 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 = {
|
|
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.'
|
|
}
|
|
};
|
|
|
|
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 with retry mechanism
|
|
const initializeSVG = () => {
|
|
const svgDoc = objectElement.contentDocument;
|
|
if (!svgDoc) {
|
|
console.warn('[InteractiveDiagram] Could not access SVG contentDocument, retrying...');
|
|
// Retry after a short delay
|
|
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);
|
|
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');
|
|
console.log(`[InteractiveDiagram] Attaching listeners to service: ${serviceId}`);
|
|
|
|
// Use event capturing to ensure events are caught
|
|
node.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(`[InteractiveDiagram] Clicked service: ${serviceId}`);
|
|
this.showServiceDetails(serviceId);
|
|
}, true);
|
|
|
|
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');
|
|
};
|
|
|
|
// Try multiple approaches to ensure SVG loads
|
|
if (objectElement.contentDocument && objectElement.contentDocument.readyState === 'complete') {
|
|
// Object already loaded and ready
|
|
console.log('[InteractiveDiagram] Object already loaded, initializing immediately');
|
|
initializeSVG();
|
|
} else {
|
|
// Wait for load event
|
|
console.log('[InteractiveDiagram] Waiting for object to load...');
|
|
objectElement.addEventListener('load', () => {
|
|
console.log('[InteractiveDiagram] Object loaded event fired');
|
|
initializeSVG();
|
|
});
|
|
|
|
// Also try after a short delay as fallback
|
|
setTimeout(initializeSVG, 500);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = InteractiveDiagram;
|
|
}
|