tractatus/public/js/components/feedback.js
TheFlow 925b28498d feat: Complete Phase 2 - Agent Lightning integration and Discord community launch
## Website Updates
- **Homepage** (index.html):
  - Updated hero subtitle to mention Agent Lightning integration
  - Added " Now with AL" badges to all pathway cards
  - Removed Audit Logs from hero (moved to researcher page)
  - Added comprehensive community section with both Discord servers

- **Researcher Page** (researcher.html:619-786):
  - Added Agent Lightning integration section
  - 5 open research questions
  - Demo 2 validation status with limitations
  - Both Discord community links

- **Implementer Page** (implementer.html:1324-1341):
  - Added Discord invite buttons to AL CTA section

- **Leader Page** (leader.html:424-441):
  - Added Discord invite buttons to AL CTA section

- **New Integration Page** (integrations/agent-lightning.html):
  - Standalone AL integration guide
  - Overview and community links

## Feedback System (Governed AI Communication)
- Backend: Feedback model, controller, routes, governance service
- Frontend: FAB, modal UI, navbar integration
- Three governance pathways: Autonomous, Deliberation, Human Mandatory

## Discord Communities
- Tractatus Discord: https://discord.gg/Dkke2ADu4E
- Agent Lightning Discord: https://discord.gg/bVZtkceKsS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 12:52:26 +13:00

605 lines
25 KiB
JavaScript

/**
* 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 = `
<button id="feedback-fab"
class="fixed bottom-6 right-6 z-40 bg-gradient-to-r from-blue-600 to-blue-700 text-white p-4 rounded-full shadow-lg hover:shadow-xl hover:scale-110 transition-all duration-200 group flex items-center gap-2"
aria-label="Give Feedback"
title="Give Feedback">
<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="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
<span class="max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 whitespace-nowrap font-semibold">
Feedback
</span>
</button>
`;
document.body.insertAdjacentHTML('beforeend', fabHTML);
}
/**
* Render Feedback Modal/Bottom Sheet
* Adapts to mobile (bottom sheet) vs desktop (modal)
*/
renderModal() {
const modalHTML = `
<!-- Feedback Modal/Bottom Sheet -->
<div id="feedback-modal" class="hidden fixed inset-0 z-50" role="dialog" aria-modal="true" aria-labelledby="feedback-modal-title">
<!-- Backdrop -->
<div id="feedback-backdrop" class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity"></div>
<!-- Modal/Sheet Container -->
<div id="feedback-container" class="absolute md:inset-0 md:flex md:items-center md:justify-center">
<!-- Modal Content -->
<div id="feedback-panel"
class="bg-white md:rounded-xl shadow-2xl w-full md:max-w-2xl md:mx-4
fixed bottom-0 left-0 right-0 md:relative
max-h-[85vh] md:max-h-[90vh] overflow-hidden
transform transition-transform duration-300 ease-out translate-y-full md:translate-y-0">
<!-- Header -->
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-blue-100">
<div>
<h2 id="feedback-modal-title" class="text-xl font-bold text-gray-900">Governed Feedback System</h2>
<p class="text-sm text-gray-600 mt-0.5">Powered by Tractatus + Agent Lightning</p>
</div>
<button id="feedback-close-btn" class="text-gray-500 hover:text-gray-700 p-2 rounded-lg hover:bg-white/50 transition" aria-label="Close">
<svg class="w-5 h-5" 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>
<!-- Content (scrollable) -->
<div class="overflow-y-auto max-h-[calc(85vh-120px)] md:max-h-[calc(90vh-180px)]">
<!-- Step 1: Type Selection -->
<div id="feedback-step-1" class="p-6">
<label class="block text-sm font-semibold text-gray-900 mb-3">What type of feedback do you have?</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button type="button" data-type="technical_question" class="feedback-type-btn p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left group">
<div class="flex items-start gap-3">
<span class="text-2xl">🔧</span>
<div>
<div class="font-semibold text-gray-900 group-hover:text-blue-700">Technical Question</div>
<div class="text-xs text-gray-500 mt-1">Installation, usage, bugs</div>
<div class="text-xs text-blue-600 mt-1.5 font-medium">→ AI responds autonomously</div>
</div>
</div>
</button>
<button type="button" data-type="feature" class="feedback-type-btn p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left group">
<div class="flex items-start gap-3">
<span class="text-2xl">💡</span>
<div>
<div class="font-semibold text-gray-900 group-hover:text-blue-700">Feature Request</div>
<div class="text-xs text-gray-500 mt-1">Suggest new capabilities</div>
<div class="text-xs text-amber-600 mt-1.5 font-medium">→ Requires deliberation</div>
</div>
</div>
</button>
<button type="button" data-type="research" class="feedback-type-btn p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left group">
<div class="flex items-start gap-3">
<span class="text-2xl">🔬</span>
<div>
<div class="font-semibold text-gray-900 group-hover:text-blue-700">Research Collaboration</div>
<div class="text-xs text-gray-500 mt-1">Co-authorship, joint research</div>
<div class="text-xs text-amber-600 mt-1.5 font-medium">→ Requires deliberation</div>
</div>
</div>
</button>
<button type="button" data-type="commercial" class="feedback-type-btn p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left group">
<div class="flex items-start gap-3">
<span class="text-2xl">💼</span>
<div>
<div class="font-semibold text-gray-900 group-hover:text-blue-700">Commercial Inquiry</div>
<div class="text-xs text-gray-500 mt-1">Licensing, consulting, partnerships</div>
<div class="text-xs text-red-600 mt-1.5 font-medium">→ Human review required</div>
</div>
</div>
</button>
<button type="button" data-type="bug" class="feedback-type-btn p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left group">
<div class="flex items-start gap-3">
<span class="text-2xl">🐛</span>
<div>
<div class="font-semibold text-gray-900 group-hover:text-blue-700">Bug Report</div>
<div class="text-xs text-gray-500 mt-1">Something isn't working</div>
<div class="text-xs text-blue-600 mt-1.5 font-medium">→ AI responds autonomously</div>
</div>
</div>
</button>
<button type="button" data-type="general" class="feedback-type-btn p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left group">
<div class="flex items-start gap-3">
<span class="text-2xl">💬</span>
<div>
<div class="font-semibold text-gray-900 group-hover:text-blue-700">General Comment</div>
<div class="text-xs text-gray-500 mt-1">Other feedback</div>
<div class="text-xs text-blue-600 mt-1.5 font-medium">→ AI responds autonomously</div>
</div>
</div>
</button>
</div>
<!-- Governance Info -->
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-sm">
<p class="font-semibold text-blue-900">How this works</p>
<p class="text-blue-700 mt-1">Your feedback is automatically classified by our <strong>BoundaryEnforcer</strong> to determine the appropriate response pathway. This ensures you get the right type of response while maintaining governance.</p>
</div>
</div>
</div>
</div>
<!-- Step 2: Feedback Form -->
<div id="feedback-step-2" class="hidden p-6">
<form id="feedback-form">
<!-- Pathway Indicator -->
<div id="pathway-indicator" class="mb-6 p-4 rounded-lg border-2"></div>
<!-- Content -->
<div class="mb-4">
<label for="feedback-content" class="block text-sm font-semibold text-gray-900 mb-2">Your Feedback</label>
<textarea id="feedback-content"
name="content"
rows="6"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="Describe your feedback in detail..."
required></textarea>
<p class="text-xs text-gray-500 mt-1">Be specific to help us provide the best response.</p>
</div>
<!-- Optional Contact Info -->
<div class="mb-4">
<label class="block text-sm font-semibold text-gray-900 mb-2">Contact Information (Optional)</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<input type="text"
id="feedback-name"
name="name"
class="px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="Your name">
<input type="email"
id="feedback-email"
name="email"
class="px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="your@email.com">
</div>
<p class="text-xs text-gray-500 mt-2">Provide your email if you'd like a response. We won't share your information.</p>
</div>
<!-- Governance Constraints Display -->
<div id="constraints-display" class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg">
<p class="text-xs font-semibold text-gray-700 mb-2">Governance Constraints:</p>
<ul id="constraints-list" class="text-xs text-gray-600 space-y-1"></ul>
</div>
<!-- Actions -->
<div class="flex gap-3">
<button type="button" id="feedback-back-btn" class="px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition font-medium">
← Back
</button>
<button type="submit" id="feedback-submit-btn" class="flex-1 px-6 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:shadow-lg transition font-semibold">
Submit Feedback
</button>
</div>
</form>
</div>
<!-- Step 3: Confirmation -->
<div id="feedback-step-3" class="hidden p-6">
<div class="text-center py-8">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-2">Feedback Received!</h3>
<p id="confirmation-message" class="text-gray-600 mb-4"></p>
<!-- Tracking Info -->
<div class="inline-block bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 mb-6">
<p class="text-xs text-gray-500 mb-1">Tracking ID:</p>
<code id="tracking-id" class="text-sm font-mono text-blue-600"></code>
</div>
<!-- Governance Summary -->
<div id="governance-summary" class="bg-blue-50 border border-blue-200 rounded-lg p-4 text-left mb-6"></div>
<button id="feedback-done-btn" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold">
Done
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="flex items-start gap-3">
<span class="text-2xl">${info.icon}</span>
<div>
<p class="font-semibold text-${info.color}-900">Pathway: ${info.pathway}</p>
<p class="text-sm text-${info.color}-700 mt-1">${info.description}</p>
</div>
</div>
`;
// Display constraints
constraintsList.innerHTML = info.constraints.map(c => `
<li class="flex items-center gap-2">
<svg class="w-3 h-3 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
${this.formatConstraint(c)}
</li>
`).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 = `
<div class="text-sm">
<p class="font-semibold text-blue-900 mb-3">Governance Summary</p>
<div class="space-y-2 text-blue-700">
<div class="flex items-center gap-2">
<span class="text-lg">${pathwayIcons[data.pathway]}</span>
<span><strong>Pathway:</strong> ${data.pathway.replace('_', ' ')}</span>
</div>
<div><strong>Classification:</strong> BoundaryEnforcer approved</div>
<div><strong>Tracking:</strong> <code class="text-xs bg-white px-2 py-0.5 rounded">${data.feedbackId}</code></div>
${data.trackingUrl ? `<div class="mt-3 pt-3 border-t border-blue-200"><a href="${data.trackingUrl}" class="text-blue-600 hover:text-blue-800 underline text-xs">Track status →</a></div>` : ''}
</div>
</div>
`;
}
/**
* 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();
}