/** * Tractatus Framework - Governed Feedback Component * Demonstrates Agent Lightning + Tractatus governance in action * * Features: * - Floating Action Button (FAB) for quick access * - Modal dialog for feedback submission * - Mobile-optimized bottom sheet * - Real-time governance pathway classification * - Integration with BoundaryEnforcer, PluralisticDeliberator, CrossReferenceValidator */ class TractausFeedback { constructor() { this.isOpen = false; this.isMobile = window.matchMedia('(max-width: 768px)').matches; this.selectedType = null; this.csrfToken = null; this.init(); } async init() { // Get CSRF token await this.fetchCsrfToken(); // Render components this.renderFAB(); this.renderModal(); // Attach event listeners this.attachEventListeners(); // Listen for window resize window.addEventListener('resize', () => { this.isMobile = window.matchMedia('(max-width: 768px)').matches; }); // Listen for external open feedback requests (from navbar, etc.) window.addEventListener('openFeedbackModal', () => { this.openModal(); }); } async fetchCsrfToken() { try { const response = await fetch('/api/csrf-token'); const data = await response.json(); this.csrfToken = data.csrfToken; } catch (error) { console.error('[Feedback] Failed to fetch CSRF token:', error); } } /** * Render Floating Action Button (FAB) * Omnipresent on all pages for quick feedback access */ renderFAB() { const fabHTML = ` `; document.body.insertAdjacentHTML('beforeend', fabHTML); } /** * Render Feedback Modal/Bottom Sheet * Adapts to mobile (bottom sheet) vs desktop (modal) */ renderModal() { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); } /** * Attach all event listeners */ attachEventListeners() { // FAB click - open modal const fab = document.getElementById('feedback-fab'); if (fab) { fab.addEventListener('click', () => this.openModal()); } // Close buttons const closeBtn = document.getElementById('feedback-close-btn'); const backdrop = document.getElementById('feedback-backdrop'); if (closeBtn) closeBtn.addEventListener('click', () => this.closeModal()); if (backdrop) backdrop.addEventListener('click', () => this.closeModal()); // Type selection buttons const typeButtons = document.querySelectorAll('.feedback-type-btn'); typeButtons.forEach(btn => { btn.addEventListener('click', () => { this.selectedType = btn.getAttribute('data-type'); this.showStep2(); }); }); // Back button const backBtn = document.getElementById('feedback-back-btn'); if (backBtn) { backBtn.addEventListener('click', () => this.showStep1()); } // Form submission const form = document.getElementById('feedback-form'); if (form) { form.addEventListener('submit', (e) => this.handleSubmit(e)); } // Done button const doneBtn = document.getElementById('feedback-done-btn'); if (doneBtn) { doneBtn.addEventListener('click', () => this.closeModal()); } } /** * Open feedback modal */ openModal() { this.isOpen = true; const modal = document.getElementById('feedback-modal'); const panel = document.getElementById('feedback-panel'); modal.classList.remove('hidden'); // Animate in setTimeout(() => { panel.classList.remove('translate-y-full'); panel.classList.add('translate-y-0'); }, 10); // Prevent body scroll document.body.style.overflow = 'hidden'; // Reset to step 1 this.showStep1(); } /** * Close feedback modal */ closeModal() { this.isOpen = false; const modal = document.getElementById('feedback-modal'); const panel = document.getElementById('feedback-panel'); // Animate out panel.classList.remove('translate-y-0'); panel.classList.add('translate-y-full'); setTimeout(() => { modal.classList.add('hidden'); }, 300); // Restore body scroll document.body.style.overflow = ''; // Reset form this.resetForm(); } /** * Show step 1 (type selection) */ showStep1() { document.getElementById('feedback-step-1').classList.remove('hidden'); document.getElementById('feedback-step-2').classList.add('hidden'); document.getElementById('feedback-step-3').classList.add('hidden'); } /** * Show step 2 (feedback form) */ showStep2() { document.getElementById('feedback-step-1').classList.add('hidden'); document.getElementById('feedback-step-2').classList.remove('hidden'); document.getElementById('feedback-step-3').classList.add('hidden'); // Update pathway indicator based on type this.updatePathwayIndicator(); } /** * Show step 3 (confirmation) */ showStep3(data) { document.getElementById('feedback-step-1').classList.add('hidden'); document.getElementById('feedback-step-2').classList.add('hidden'); document.getElementById('feedback-step-3').classList.remove('hidden'); // Display confirmation message const confirmationMessage = document.getElementById('confirmation-message'); confirmationMessage.textContent = data.message; // Display tracking ID const trackingId = document.getElementById('tracking-id'); trackingId.textContent = data.feedbackId; // Display governance summary this.displayGovernanceSummary(data); } /** * Update pathway indicator based on selected type */ updatePathwayIndicator() { const indicator = document.getElementById('pathway-indicator'); const constraintsList = document.getElementById('constraints-list'); const pathwayInfo = { technical_question: { pathway: 'Autonomous', color: 'blue', icon: '🤖', description: 'AI will respond autonomously with technical information', constraints: ['cite_documentation', 'no_financial_commitments', 'no_legal_advice', 'accurate_only'] }, bug: { pathway: 'Autonomous', color: 'blue', icon: '🤖', description: 'AI will respond autonomously with troubleshooting guidance', constraints: ['cite_documentation', 'no_financial_commitments', 'accurate_only'] }, general: { pathway: 'Autonomous', color: 'blue', icon: '🤖', description: 'AI will respond autonomously with general information', constraints: ['cite_documentation', 'no_financial_commitments', 'no_legal_advice', 'stay_on_topic'] }, feature: { pathway: 'Deliberation', color: 'amber', icon: '⚖️', description: 'Requires multi-stakeholder deliberation before response', constraints: ['align_with_roadmap', 'assess_scope', 'community_benefit'] }, research: { pathway: 'Deliberation', color: 'amber', icon: '⚖️', description: 'Requires stakeholder consultation (maintainer, research lead, community)', constraints: ['check_availability', 'align_with_research_gaps', 'assess_mutual_benefit'] }, commercial: { pathway: 'Human Mandatory', color: 'red', icon: '👤', description: 'Requires personal human review and response', constraints: ['no_financial_commitments', 'no_pricing_discussion', 'refer_to_human'] } }; const info = pathwayInfo[this.selectedType]; indicator.className = `mb-6 p-4 rounded-lg border-2 border-${info.color}-200 bg-${info.color}-50`; indicator.innerHTML = `
${info.icon}

Pathway: ${info.pathway}

${info.description}

`; // Display constraints constraintsList.innerHTML = info.constraints.map(c => `
  • ${this.formatConstraint(c)}
  • `).join(''); } /** * Format constraint for display */ formatConstraint(constraint) { const labels = { cite_documentation: 'Must cite documentation', no_financial_commitments: 'No financial commitments', no_legal_advice: 'No legal advice', accurate_only: 'Factually accurate only', helpful_tone: 'Helpful, respectful tone', stay_on_topic: 'Stay on topic', align_with_roadmap: 'Align with roadmap', assess_scope: 'Assess feasibility', community_benefit: 'Community benefit assessment', check_availability: 'Check maintainer availability', align_with_research_gaps: 'Align with research priorities', assess_mutual_benefit: 'Mutual benefit assessment', no_pricing_discussion: 'No pricing discussion', refer_to_human: 'Escalate to human' }; return labels[constraint] || constraint; } /** * Handle form submission */ async handleSubmit(e) { e.preventDefault(); const submitBtn = document.getElementById('feedback-submit-btn'); submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; try { const formData = { type: this.selectedType, content: document.getElementById('feedback-content').value, name: document.getElementById('feedback-name').value || null, email: document.getElementById('feedback-email').value || null }; const response = await fetch('/api/feedback/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, credentials: 'include', body: JSON.stringify(formData) }); const data = await response.json(); if (data.success) { this.showStep3(data); } else { alert('Error: ' + (data.message || 'Failed to submit feedback')); submitBtn.disabled = false; submitBtn.textContent = 'Submit Feedback'; } } catch (error) { console.error('[Feedback] Submission error:', error); alert('An error occurred. Please try again.'); submitBtn.disabled = false; submitBtn.textContent = 'Submit Feedback'; } } /** * Display governance summary after submission */ displayGovernanceSummary(data) { const summary = document.getElementById('governance-summary'); const pathwayIcons = { autonomous: '🤖', deliberation: '⚖️', human_mandatory: '👤' }; summary.innerHTML = `

    Governance Summary

    ${pathwayIcons[data.pathway]} Pathway: ${data.pathway.replace('_', ' ')}
    Classification: BoundaryEnforcer approved
    Tracking: ${data.feedbackId}
    ${data.trackingUrl ? `` : ''}
    `; } /** * Reset form to initial state */ resetForm() { this.selectedType = null; document.getElementById('feedback-form').reset(); this.showStep1(); } } // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new TractausFeedback()); } else { new TractausFeedback(); }