feat: Implement AI Assistance Portal with types and components for student requests and insights
This commit is contained in:
721
src/components/AIAssistancePortal.tsx
Normal file
721
src/components/AIAssistancePortal.tsx
Normal 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
|
||||
Reference in New Issue
Block a user