feat: Implement AI Assistance Portal with types and components for student requests and insights

This commit is contained in:
defiQUG
2025-10-05 05:14:58 -07:00
parent 914c4180b5
commit 47d913cf34
9 changed files with 6761 additions and 101 deletions

View File

@@ -0,0 +1,721 @@
// Phase 3: AI Assistance Portal React Components
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { formatDistanceToNow } from 'date-fns'
import type {
StudentRequest,
MatchResult,
AIInsight,
AIMetrics,
AIUpdate,
UrgencyLevel,
AssistanceCategory
} from '../ai/types'
import { processingPipeline } from '../ai/ProcessingPipeline'
// Icons (using the existing icon system)
const Brain = ({ className = "w-5 h-5" }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 20 20">
<path d="M17.28 9.28a.75.75 0 00-1.06-1.06l-7.5 7.5a.75.75 0 101.06 1.06l7.5-7.5z" />
</svg>
)
const Cpu = ({ className = "w-4 h-4" }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="4" y="4" width="16" height="16" rx="2" />
<rect x="9" y="9" width="6" height="6" />
<path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3" />
</svg>
)
const Activity = ({ className = "w-4 h-4" }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<polyline points="22,12 18,12 15,21 9,3 6,12 2,12" />
</svg>
)
const TrendingUp = ({ className = "w-8 h-8" }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
<polyline points="17 6 23 6 23 12" />
</svg>
)
const Users = ({ className = "w-8 h-8" }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
)
// Mock data for demonstration
const mockRequests: StudentRequest[] = [
{
id: 'req-1',
studentId: 'std-1',
studentName: 'Maria Rodriguez',
description: 'Need winter coat and boots for my daughter. Size 8 shoes and medium coat. Getting cold and she only has summer clothes.',
category: 'clothing',
urgency: 'high',
location: { city: 'Austin', state: 'TX', zipCode: '78701' },
constraints: { timeframe: 'within-week', deliveryMethod: 'school-delivery', privacyLevel: 'semi-anonymous' },
submittedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
},
{
id: 'req-2',
studentId: 'std-2',
studentName: 'James Thompson',
description: 'My son needs school supplies - notebooks, pencils, calculator for math class. Starting new semester next week.',
category: 'school-supplies',
urgency: 'medium',
location: { city: 'Round Rock', state: 'TX', zipCode: '78664' },
constraints: { timeframe: 'within-week', deliveryMethod: 'pickup', privacyLevel: 'open' },
submittedAt: new Date(Date.now() - 45 * 60 * 1000), // 45 minutes ago
},
{
id: 'req-3',
studentId: 'std-3',
studentName: 'Sarah Kim',
description: 'Emergency - no food at home for kids this weekend. Need groceries or meal assistance ASAP.',
category: 'food-assistance',
urgency: 'emergency',
location: { city: 'Cedar Park', state: 'TX', zipCode: '78613' },
constraints: { timeframe: 'immediate', deliveryMethod: 'delivery', privacyLevel: 'anonymous' },
submittedAt: new Date(Date.now() - 20 * 60 * 1000), // 20 minutes ago
}
]
interface AIAssistancePortalProps {
userRole: 'student' | 'coordinator' | 'admin'
}
export function AIAssistancePortal({ userRole }: AIAssistancePortalProps) {
const [requests, setRequests] = useState<StudentRequest[]>(mockRequests)
const [aiInsights, setAIInsights] = useState<AIInsight[]>([])
const [processing, setProcessing] = useState(false)
const [selectedRequest, setSelectedRequest] = useState<string | null>(null)
useEffect(() => {
// Subscribe to real-time AI updates
const unsubscribe = processingPipeline.subscribe(handleRealTimeUpdate)
// Generate initial insights
generateInsights()
return unsubscribe
}, [])
const handleRealTimeUpdate = (update: AIUpdate) => {
console.log('🔄 Real-time update received:', update)
switch (update.type) {
case 'request-processed':
setRequests(prev => prev.map(r =>
r.id === update.requestId
? { ...r, status: update.status, aiRecommendations: update.recommendations }
: r
))
break
case 'new-insight':
if (update.insight) {
setAIInsights(prev => [update.insight!, ...prev.slice(0, 4)])
}
break
case 'auto-approval':
// Show success notification
console.log('✅ Auto-approval notification:', update.message)
break
}
}
const generateInsights = async () => {
try {
const insights = await processingPipeline.generateInsights(requests)
setAIInsights(insights)
} catch (error) {
console.error('Error generating insights:', error)
}
}
const handleApproval = async (requestId: string, recommendation: MatchResult) => {
setProcessing(true)
try {
console.log(`✅ Approving request ${requestId} with recommendation:`, recommendation)
// In production: make API call to approve
setRequests(prev => prev.map(r =>
r.id === requestId ? { ...r, status: 'approved' } : r
))
} catch (error) {
console.error('Error approving request:', error)
} finally {
setProcessing(false)
}
}
const handleModification = async (requestId: string) => {
console.log(`✏️ Modifying request ${requestId}`)
setSelectedRequest(requestId)
}
const submitNewRequest = async (request: Omit<StudentRequest, 'id' | 'submittedAt'>) => {
setProcessing(true)
try {
const newRequest: StudentRequest = {
...request,
id: `req-${Date.now()}`,
submittedAt: new Date()
}
// Submit to AI processing pipeline
await processingPipeline.submitRequest(newRequest)
setRequests(prev => [newRequest, ...prev])
} catch (error) {
console.error('Error submitting request:', error)
} finally {
setProcessing(false)
}
}
return (
<div className="ai-assistance-portal grid grid-cols-1 lg:grid-cols-3 gap-8 p-6">
{/* AI Insights Panel */}
<motion.div
className="insights-panel bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 p-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-gray-900 dark:text-gray-100">
<Brain className="w-5 h-5 text-purple-500" />
AI Insights
</h3>
<AnimatePresence mode="popLayout">
{aiInsights.map((insight) => (
<motion.div
key={insight.id}
className="insight-card p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-3 border border-purple-100 dark:border-purple-800"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
whileHover={{ scale: 1.02 }}
>
<div className="flex items-start gap-3">
<div className={`w-2 h-2 rounded-full mt-2 ${getInsightColor(insight.type)}`} />
<div className="flex-1">
<p className="font-medium text-sm text-gray-900 dark:text-gray-100">{insight.title}</p>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
{insight.description}
</p>
{insight.confidence && (
<div className="mt-2 flex items-center gap-2">
<div className="w-16 bg-gray-200 dark:bg-gray-600 rounded-full h-1">
<div
className="bg-purple-500 h-1 rounded-full transition-all duration-300"
style={{ width: `${insight.confidence * 100}%` }}
/>
</div>
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
{Math.round(insight.confidence * 100)}%
</span>
</div>
)}
</div>
</div>
</motion.div>
))}
</AnimatePresence>
{aiInsights.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<Brain className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">No insights available yet</p>
</div>
)}
</motion.div>
{/* Request Processing Interface */}
<div className="request-processing lg:col-span-2">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Smart Request Processing
</h3>
<motion.button
onClick={generateInsights}
className="btn-secondary flex items-center gap-2 text-sm"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
disabled={processing}
>
<Activity className="w-4 h-4" />
{processing ? 'Processing...' : 'Refresh'}
</motion.button>
</div>
<div className="space-y-4">
{requests.map((request) => (
<RequestCard
key={request.id}
request={request}
onApprove={handleApproval}
onModify={handleModification}
showAIRecommendations={userRole !== 'student'}
isSelected={selectedRequest === request.id}
/>
))}
</div>
{userRole === 'student' && (
<NewRequestForm onSubmit={submitNewRequest} />
)}
</div>
{/* Performance Metrics */}
<div className="lg:col-span-3">
<AIPerformanceMetrics />
</div>
</div>
)
}
interface RequestCardProps {
request: StudentRequest & { status?: string; aiRecommendations?: MatchResult[] }
onApprove: (requestId: string, recommendation: MatchResult) => void
onModify: (requestId: string) => void
showAIRecommendations: boolean
isSelected: boolean
}
function RequestCard({ request, onApprove, onModify, showAIRecommendations, isSelected }: RequestCardProps) {
return (
<motion.div
className={`request-card p-6 border-2 rounded-xl mb-4 transition-all ${
isSelected
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800'
}`}
whileHover={{ y: -2, boxShadow: "0 8px 25px rgba(0,0,0,0.1)" }}
layout
>
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{request.studentName}
</h4>
<p className="text-gray-700 dark:text-gray-300 text-sm mb-2 line-clamp-3">
{request.description}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Submitted {formatDistanceToNow(request.submittedAt)} ago
</p>
</div>
<div className="flex flex-col items-end gap-2 ml-4">
<UrgencyBadge urgency={request.urgency} />
<CategoryBadge category={request.category} />
{request.status && <StatusBadge status={request.status} />}
</div>
</div>
{showAIRecommendations && request.aiRecommendations && (
<motion.div
className="ai-recommendations bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 p-4 rounded-xl mb-4 border border-blue-200 dark:border-blue-800"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.3 }}
>
<div className="flex items-center gap-3 mb-3">
<Cpu className="w-5 h-5 text-blue-500" />
<span className="text-sm font-semibold text-blue-700 dark:text-blue-300">
AI Recommendation
</span>
<ConfidenceIndicator confidence={request.aiRecommendations[0].confidenceScore} />
</div>
<div className="space-y-3">
{request.aiRecommendations.slice(0, 2).map((rec, index) => (
<div key={index} className="flex justify-between items-center">
<div className="flex-1">
<p className="font-medium text-sm text-gray-900 dark:text-gray-100">
{rec.resourceName}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
{rec.reasoningFactors[0]}
</p>
</div>
<div className="flex items-center gap-4 text-sm">
<span className="text-green-600 dark:text-green-400 font-medium">
${rec.estimatedCost}
</span>
<span className="text-blue-600 dark:text-blue-400">
{rec.fulfillmentTimeline}
</span>
</div>
</div>
))}
</div>
<div className="mt-4 flex gap-2">
<motion.button
onClick={() => onApprove(request.id, request.aiRecommendations![0])}
className="btn-primary text-xs px-4 py-2 flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Approve AI Recommendation
</motion.button>
<button
onClick={() => onModify(request.id)}
className="btn-secondary text-xs px-4 py-2"
>
Modify
</button>
</div>
</motion.div>
)}
<div className="flex justify-between items-center">
<div className="flex gap-2">
<LocationBadge location={request.location.city} />
</div>
<div className="flex gap-2">
{!showAIRecommendations && (
<span className="text-xs text-gray-500 dark:text-gray-400">
Processing...
</span>
)}
</div>
</div>
</motion.div>
)
}
function UrgencyBadge({ urgency }: { urgency: UrgencyLevel }) {
const colors = {
emergency: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
low: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
}
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colors[urgency]}`}>
{urgency.toUpperCase()}
</span>
)
}
function CategoryBadge({ category }: { category: AssistanceCategory }) {
return (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{category.replace('-', ' ').toUpperCase()}
</span>
)
}
function StatusBadge({ status }: { status: string }) {
const colors = {
'approved': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
'auto-approved': 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
'under-review': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
'pending': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
}
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colors[status as keyof typeof colors] || colors.pending}`}>
{status.replace('-', ' ').toUpperCase()}
</span>
)
}
function LocationBadge({ location }: { location: string }) {
return (
<span className="px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
📍 {location}
</span>
)
}
function ConfidenceIndicator({ confidence }: { confidence: number }) {
const getColor = (conf: number) => {
if (conf >= 0.8) return 'text-green-500'
if (conf >= 0.6) return 'text-yellow-500'
return 'text-red-500'
}
return (
<div className="flex items-center gap-2">
<div className="w-20 bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${confidence * 100}%` }}
/>
</div>
<span className={`text-xs font-bold ${getColor(confidence)}`}>
{Math.round(confidence * 100)}%
</span>
</div>
)
}
function AIPerformanceMetrics() {
const [metrics, setMetrics] = useState<AIMetrics>({
accuracyRate: 0.87,
accuracyTrend: 2.3,
avgProcessingTime: 1.8,
speedTrend: -0.5,
autoApprovalRate: 0.68,
automationTrend: 5.2,
impactPredictionAccuracy: 0.82,
impactTrend: 1.8,
totalRequestsProcessed: 247,
successfulMatches: 213
})
return (
<div className="ai-performance-metrics mt-8">
<h4 className="text-lg font-semibold mb-6 text-gray-900 dark:text-gray-100 flex items-center gap-2">
<Activity className="w-5 h-5 text-blue-500" />
AI Performance Dashboard
</h4>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
title="Accuracy Rate"
value={`${(metrics.accuracyRate * 100).toFixed(1)}%`}
trend={metrics.accuracyTrend}
color="green"
icon={<TrendingUp className="w-6 h-6" />}
/>
<MetricCard
title="Avg Processing Time"
value={`${metrics.avgProcessingTime}s`}
trend={metrics.speedTrend}
color="blue"
icon={<Activity className="w-6 h-6" />}
/>
<MetricCard
title="Auto-Approval Rate"
value={`${(metrics.autoApprovalRate * 100).toFixed(1)}%`}
trend={metrics.automationTrend}
color="purple"
icon={<Cpu className="w-6 h-6" />}
/>
<MetricCard
title="Impact Accuracy"
value={`${(metrics.impactPredictionAccuracy * 100).toFixed(1)}%`}
trend={metrics.impactTrend}
color="orange"
icon={<Users className="w-6 h-6" />}
/>
</div>
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-600 dark:text-gray-400">Total Requests Processed</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{metrics.totalRequestsProcessed}</span>
</div>
<div className="flex justify-between items-center text-sm mt-2">
<span className="text-gray-600 dark:text-gray-400">Successful Matches</span>
<span className="font-semibold text-green-600 dark:text-green-400">{metrics.successfulMatches}</span>
</div>
</div>
</div>
)
}
interface MetricCardProps {
title: string
value: string
trend: number
color: 'green' | 'blue' | 'purple' | 'orange'
icon: React.ReactNode
}
function MetricCard({ title, value, trend, color, icon }: MetricCardProps) {
const colorClasses = {
green: 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800',
blue: 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800',
purple: 'bg-purple-50 border-purple-200 dark:bg-purple-900/20 dark:border-purple-800',
orange: 'bg-orange-50 border-orange-200 dark:bg-orange-900/20 dark:border-orange-800'
}
const iconColors = {
green: 'text-green-500',
blue: 'text-blue-500',
purple: 'text-purple-500',
orange: 'text-orange-500'
}
return (
<motion.div
className={`metric-card p-4 rounded-xl border ${colorClasses[color]}`}
whileHover={{ scale: 1.02, y: -2 }}
>
<div className="flex items-center justify-between mb-3">
<div className={iconColors[color]}>{icon}</div>
<div className={`text-xs px-2 py-1 rounded-full ${
trend > 0
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
}`}>
{trend > 0 ? '↗' : '↘'} {Math.abs(trend)}%
</div>
</div>
<div>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-1">{value}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{title}</p>
</div>
</motion.div>
)
}
function NewRequestForm({ onSubmit }: { onSubmit: (request: Omit<StudentRequest, 'id' | 'submittedAt'>) => void }) {
const [formData, setFormData] = useState({
studentName: '',
description: '',
category: 'clothing' as AssistanceCategory,
urgency: 'medium' as UrgencyLevel,
city: '',
state: 'TX',
zipCode: ''
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit({
studentId: `std-${Date.now()}`,
studentName: formData.studentName,
description: formData.description,
category: formData.category,
urgency: formData.urgency,
location: {
city: formData.city,
state: formData.state,
zipCode: formData.zipCode
},
constraints: {
timeframe: 'within-week',
deliveryMethod: 'any',
privacyLevel: 'semi-anonymous'
}
})
// Reset form
setFormData({
studentName: '',
description: '',
category: 'clothing',
urgency: 'medium',
city: '',
state: 'TX',
zipCode: ''
})
}
return (
<motion.div
className="new-request-form mt-8 p-6 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h4 className="text-md font-semibold mb-4 text-gray-900 dark:text-gray-100">Submit New Request</h4>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
placeholder="Student Name"
value={formData.studentName}
onChange={(e) => setFormData(prev => ({ ...prev, studentName: e.target.value }))}
className="input-field"
required
/>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as AssistanceCategory }))}
className="input-field"
>
<option value="clothing">Clothing</option>
<option value="school-supplies">School Supplies</option>
<option value="food-assistance">Food Assistance</option>
<option value="transportation">Transportation</option>
<option value="emergency-housing">Emergency Housing</option>
<option value="medical-needs">Medical Needs</option>
<option value="technology">Technology</option>
<option value="other">Other</option>
</select>
</div>
<textarea
placeholder="Describe the assistance needed..."
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
className="input-field h-24 resize-none"
required
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<select
value={formData.urgency}
onChange={(e) => setFormData(prev => ({ ...prev, urgency: e.target.value as UrgencyLevel }))}
className="input-field"
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
<option value="emergency">Emergency</option>
</select>
<input
type="text"
placeholder="City"
value={formData.city}
onChange={(e) => setFormData(prev => ({ ...prev, city: e.target.value }))}
className="input-field"
required
/>
<input
type="text"
placeholder="ZIP Code"
value={formData.zipCode}
onChange={(e) => setFormData(prev => ({ ...prev, zipCode: e.target.value }))}
className="input-field"
required
/>
</div>
<motion.button
type="submit"
className="btn-primary w-full"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
Submit Request for AI Analysis
</motion.button>
</form>
</motion.div>
)
}
function getInsightColor(type: string): string {
switch (type) {
case 'anomaly': return 'bg-red-400'
case 'optimization': return 'bg-green-400'
case 'trend': return 'bg-blue-400'
case 'prediction': return 'bg-purple-400'
case 'recommendation': return 'bg-yellow-400'
default: return 'bg-gray-400'
}
}
export default AIAssistancePortal