feat: Implement AI Assistance Portal with types and components for student requests and insights
This commit is contained in:
1185
src/App.tsx
1185
src/App.tsx
File diff suppressed because it is too large
Load Diff
418
src/ai/ProcessingPipeline.ts
Normal file
418
src/ai/ProcessingPipeline.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
// Phase 3: Real-time Processing Pipeline for AI Assistance Matching
|
||||
import type {
|
||||
StudentRequest,
|
||||
MatchResult,
|
||||
AIUpdate,
|
||||
AIInsight,
|
||||
ProcessingPipelineConfig
|
||||
} from './types'
|
||||
import { aiEngine } from './StudentAssistanceAI'
|
||||
|
||||
// Mock Queue Implementation (in production, use Redis + Bull)
|
||||
class MockQueue<T> {
|
||||
private jobs: Array<{ id: string; data: T; priority: number }> = []
|
||||
private processors: Map<string, (job: { id: string; data: T }) => Promise<void>> = new Map()
|
||||
|
||||
async add(jobType: string, data: T, options: { priority: number; attempts?: number; backoff?: string } = { priority: 0 }): Promise<{ id: string }> {
|
||||
const job = {
|
||||
id: `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
data,
|
||||
priority: options.priority
|
||||
}
|
||||
|
||||
this.jobs.push(job)
|
||||
this.jobs.sort((a, b) => b.priority - a.priority) // Higher priority first
|
||||
|
||||
// Process job immediately for demo
|
||||
setTimeout(() => this.processNextJob(jobType), 100)
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
process(jobType: string, concurrency: number, processor: (job: { id: string; data: T }) => Promise<void>): void {
|
||||
this.processors.set(jobType, processor)
|
||||
}
|
||||
|
||||
private async processNextJob(jobType: string): Promise<void> {
|
||||
const processor = this.processors.get(jobType)
|
||||
if (!processor || this.jobs.length === 0) return
|
||||
|
||||
const job = this.jobs.shift()!
|
||||
|
||||
try {
|
||||
await processor(job)
|
||||
} catch (error) {
|
||||
console.error(`Error processing job ${job.id}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification Service for real-time updates
|
||||
class NotificationService {
|
||||
private subscribers: Set<(update: AIUpdate) => void> = new Set()
|
||||
|
||||
subscribe(callback: (update: AIUpdate) => void): () => void {
|
||||
this.subscribers.add(callback)
|
||||
return () => this.subscribers.delete(callback)
|
||||
}
|
||||
|
||||
notify(update: AIUpdate): void {
|
||||
this.subscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update)
|
||||
} catch (error) {
|
||||
console.error('Error in notification callback:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async notifyStudent(studentId: string, assignment: any): Promise<void> {
|
||||
console.log(`📧 Notifying student ${studentId} about assignment`)
|
||||
// In production: send email, SMS, or push notification
|
||||
}
|
||||
|
||||
async notifyVolunteer(volunteerId: string, assignment: any): Promise<void> {
|
||||
console.log(`📧 Notifying volunteer ${volunteerId} about new assignment`)
|
||||
// In production: send volunteer notification
|
||||
}
|
||||
|
||||
async notifyCoordinators(assignment: any): Promise<void> {
|
||||
console.log(`📧 Notifying coordinators about new assignment`)
|
||||
// In production: alert coordination team
|
||||
}
|
||||
|
||||
async updateDonors(estimatedCost: number): Promise<void> {
|
||||
console.log(`💰 Updating donors about $${estimatedCost} impact opportunity`)
|
||||
// In production: trigger donor engagement campaign
|
||||
}
|
||||
|
||||
async notifyReviewer(reviewer: any, reviewTask: any, aiInsights: any): Promise<void> {
|
||||
console.log(`👥 Notifying reviewer ${reviewer.id} about review task with AI confidence: ${aiInsights.aiConfidence}`)
|
||||
// In production: send detailed review notification with AI recommendations
|
||||
}
|
||||
}
|
||||
|
||||
// Assignment and Review Management
|
||||
class AssignmentManager {
|
||||
private static assignments: Map<string, any> = new Map()
|
||||
|
||||
static async createAssignment(assignmentData: any): Promise<any> {
|
||||
const assignment = {
|
||||
id: `assign-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
...assignmentData,
|
||||
createdAt: new Date(),
|
||||
status: 'pending'
|
||||
}
|
||||
|
||||
this.assignments.set(assignment.id, assignment)
|
||||
console.log(`✅ Created assignment ${assignment.id}`)
|
||||
return assignment
|
||||
}
|
||||
|
||||
static async getById(id: string): Promise<any | null> {
|
||||
return this.assignments.get(id) || null
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewManager {
|
||||
private static reviewTasks: Map<string, any> = new Map()
|
||||
|
||||
static async createReviewTask(taskData: any): Promise<any> {
|
||||
const task = {
|
||||
id: `review-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
...taskData,
|
||||
createdAt: new Date(),
|
||||
status: 'pending'
|
||||
}
|
||||
|
||||
this.reviewTasks.set(task.id, task)
|
||||
console.log(`📋 Created review task ${task.id}`)
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
// Main Processing Pipeline
|
||||
export class RealTimeProcessingPipeline {
|
||||
private queue: MockQueue<StudentRequest>
|
||||
private notificationService: NotificationService
|
||||
private config: ProcessingPipelineConfig
|
||||
|
||||
constructor(config: Partial<ProcessingPipelineConfig> = {}) {
|
||||
this.queue = new MockQueue<StudentRequest>()
|
||||
this.notificationService = new NotificationService()
|
||||
this.config = {
|
||||
autoApprovalThreshold: 0.85,
|
||||
urgencyWeights: {
|
||||
'emergency': 1.0,
|
||||
'high': 0.8,
|
||||
'medium': 0.5,
|
||||
'low': 0.2
|
||||
},
|
||||
categoryWeights: {
|
||||
'emergency-housing': 1.0,
|
||||
'food-assistance': 0.9,
|
||||
'medical-needs': 0.85,
|
||||
'clothing': 0.7,
|
||||
'school-supplies': 0.6,
|
||||
'transportation': 0.5,
|
||||
'technology': 0.4,
|
||||
'extracurricular': 0.3,
|
||||
'other': 0.4
|
||||
},
|
||||
maxProcessingTime: 5000, // 5 seconds
|
||||
retryAttempts: 3,
|
||||
notificationEnabled: true,
|
||||
...config
|
||||
}
|
||||
|
||||
this.setupQueueProcessors()
|
||||
}
|
||||
|
||||
private setupQueueProcessors(): void {
|
||||
this.queue.process('analyze-request', 5, async (job) => {
|
||||
const request = job.data
|
||||
|
||||
try {
|
||||
console.log(`🔄 Processing request ${request.id} for ${request.studentName}`)
|
||||
|
||||
// AI analysis and matching
|
||||
const matches = await aiEngine.processRequest(request)
|
||||
|
||||
// Auto-approval for high-confidence matches
|
||||
if (matches.length > 0 && matches[0].confidenceScore >= this.config.autoApprovalThreshold) {
|
||||
await this.autoApproveRequest(request, matches[0])
|
||||
} else {
|
||||
await this.routeForHumanReview(request, matches)
|
||||
}
|
||||
|
||||
// Update real-time dashboard
|
||||
await this.updateDashboard(request.id, matches)
|
||||
|
||||
// Notify subscribers of processing completion
|
||||
this.notificationService.notify({
|
||||
type: 'request-processed',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
status: matches.length > 0 && matches[0].confidenceScore >= this.config.autoApprovalThreshold ? 'auto-approved' : 'under-review',
|
||||
recommendations: matches,
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
await this.handleProcessingError(request, error as Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async submitRequest(request: StudentRequest): Promise<string> {
|
||||
console.log(`📥 Submitting request from ${request.studentName}: ${request.category}`)
|
||||
|
||||
// Add to processing queue with priority based on urgency
|
||||
const priority = this.calculatePriority(request.urgency)
|
||||
const job = await this.queue.add('analyze-request', request, {
|
||||
priority,
|
||||
attempts: this.config.retryAttempts,
|
||||
backoff: 'exponential'
|
||||
})
|
||||
|
||||
// Immediate acknowledgment
|
||||
await this.sendAcknowledgment(request)
|
||||
|
||||
return job.id
|
||||
}
|
||||
|
||||
private calculatePriority(urgency: string): number {
|
||||
return this.config.urgencyWeights[urgency as keyof typeof this.config.urgencyWeights] || 0.5
|
||||
}
|
||||
|
||||
private async sendAcknowledgment(request: StudentRequest): Promise<void> {
|
||||
console.log(`✉️ Sending acknowledgment to ${request.studentName}`)
|
||||
// In production: send immediate confirmation email/SMS
|
||||
}
|
||||
|
||||
private async autoApproveRequest(request: StudentRequest, match: MatchResult): Promise<void> {
|
||||
console.log(`🤖 Auto-approving request ${request.id} with ${(match.confidenceScore * 100).toFixed(1)}% confidence`)
|
||||
|
||||
// Create assistance assignment
|
||||
const assignment = await AssignmentManager.createAssignment({
|
||||
requestId: request.id,
|
||||
studentId: request.studentId,
|
||||
studentName: request.studentName,
|
||||
resourceId: match.resourceId,
|
||||
resourceName: match.resourceName,
|
||||
volunteerId: match.volunteerMatch?.id,
|
||||
volunteerName: match.volunteerMatch?.volunteerName,
|
||||
scheduledDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Tomorrow
|
||||
estimatedCost: match.estimatedCost,
|
||||
approvalStatus: 'auto-approved',
|
||||
confidence: match.confidenceScore,
|
||||
aiRecommendation: true,
|
||||
urgency: request.urgency,
|
||||
category: request.category
|
||||
})
|
||||
|
||||
// Notify all stakeholders
|
||||
if (this.config.notificationEnabled) {
|
||||
await Promise.all([
|
||||
this.notificationService.notifyStudent(request.studentId, assignment),
|
||||
match.volunteerMatch ? this.notificationService.notifyVolunteer(assignment.volunteerId, assignment) : Promise.resolve(),
|
||||
this.notificationService.notifyCoordinators(assignment),
|
||||
this.notificationService.updateDonors(assignment.estimatedCost)
|
||||
])
|
||||
}
|
||||
|
||||
// Notify real-time subscribers
|
||||
this.notificationService.notify({
|
||||
type: 'auto-approval',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
message: `Request automatically approved with ${(match.confidenceScore * 100).toFixed(1)}% confidence`,
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
// Track decision for learning
|
||||
await this.trackDecision(request, match, 'auto-approved')
|
||||
}
|
||||
|
||||
private async routeForHumanReview(request: StudentRequest, matches: MatchResult[]): Promise<void> {
|
||||
console.log(`👤 Routing request ${request.id} for human review`)
|
||||
|
||||
// Determine best reviewer based on request type and matches
|
||||
const reviewer = await this.selectOptimalReviewer(request, matches)
|
||||
|
||||
// Create review assignment
|
||||
const reviewTask = await ReviewManager.createReviewTask({
|
||||
requestId: request.id,
|
||||
assignedTo: reviewer.id,
|
||||
assignedToName: reviewer.name,
|
||||
aiRecommendations: matches,
|
||||
priority: this.calculateReviewPriority(request, matches),
|
||||
deadline: this.calculateReviewDeadline(request.urgency),
|
||||
studentName: request.studentName,
|
||||
category: request.category,
|
||||
urgency: request.urgency
|
||||
})
|
||||
|
||||
// Notify reviewer with AI insights
|
||||
if (this.config.notificationEnabled) {
|
||||
await this.notificationService.notifyReviewer(reviewer, reviewTask, {
|
||||
aiConfidence: matches[0]?.confidenceScore || 0,
|
||||
recommendedAction: this.generateRecommendation(matches),
|
||||
riskFactors: matches[0]?.riskFactors || []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async selectOptimalReviewer(request: StudentRequest, matches: MatchResult[]) {
|
||||
// Mock reviewer selection - in production, this would use actual staff data
|
||||
const reviewers = [
|
||||
{ id: 'rev1', name: 'Sarah Martinez', specialties: ['clothing', 'emergency-housing'], workload: 5 },
|
||||
{ id: 'rev2', name: 'John Davis', specialties: ['food-assistance', 'transportation'], workload: 3 },
|
||||
{ id: 'rev3', name: 'Lisa Chen', specialties: ['school-supplies', 'technology'], workload: 7 },
|
||||
{ id: 'rev4', name: 'Mike Johnson', specialties: ['medical-needs', 'other'], workload: 4 }
|
||||
]
|
||||
|
||||
// Select reviewer based on specialty and workload
|
||||
const categoryReviewers = reviewers.filter(r =>
|
||||
r.specialties.includes(request.category) || r.specialties.includes('other')
|
||||
)
|
||||
|
||||
// Return reviewer with lowest workload
|
||||
return categoryReviewers.sort((a, b) => a.workload - b.workload)[0] || reviewers[0]
|
||||
}
|
||||
|
||||
private calculateReviewPriority(request: StudentRequest, matches: MatchResult[]): number {
|
||||
let priority = this.config.urgencyWeights[request.urgency as keyof typeof this.config.urgencyWeights] || 0.5
|
||||
|
||||
// Boost priority for high AI confidence but below threshold
|
||||
if (matches.length > 0) {
|
||||
const topMatch = matches[0]
|
||||
if (topMatch.confidenceScore > 0.7 && topMatch.confidenceScore < this.config.autoApprovalThreshold) {
|
||||
priority += 0.2
|
||||
}
|
||||
}
|
||||
|
||||
// Boost priority for critical categories
|
||||
priority += this.config.categoryWeights[request.category as keyof typeof this.config.categoryWeights] || 0
|
||||
|
||||
return Math.min(priority, 1.0)
|
||||
}
|
||||
|
||||
private calculateReviewDeadline(urgency: string): Date {
|
||||
const now = new Date()
|
||||
switch (urgency) {
|
||||
case 'emergency':
|
||||
return new Date(now.getTime() + 30 * 60 * 1000) // 30 minutes
|
||||
case 'high':
|
||||
return new Date(now.getTime() + 2 * 60 * 60 * 1000) // 2 hours
|
||||
case 'medium':
|
||||
return new Date(now.getTime() + 8 * 60 * 60 * 1000) // 8 hours
|
||||
case 'low':
|
||||
default:
|
||||
return new Date(now.getTime() + 24 * 60 * 60 * 1000) // 24 hours
|
||||
}
|
||||
}
|
||||
|
||||
private generateRecommendation(matches: MatchResult[]): string {
|
||||
if (matches.length === 0) return 'No suitable matches found - manual resource allocation needed'
|
||||
|
||||
const topMatch = matches[0]
|
||||
if (topMatch.confidenceScore > 0.8) {
|
||||
return `Strong AI recommendation: ${topMatch.resourceName} (${(topMatch.confidenceScore * 100).toFixed(1)}% confidence)`
|
||||
} else if (topMatch.confidenceScore > 0.6) {
|
||||
return `Moderate AI recommendation: ${topMatch.resourceName} - review for accuracy`
|
||||
} else {
|
||||
return `Low confidence match: manual evaluation recommended`
|
||||
}
|
||||
}
|
||||
|
||||
private async updateDashboard(requestId: string, matches: MatchResult[]): Promise<void> {
|
||||
console.log(`📊 Updating dashboard for request ${requestId}`)
|
||||
// In production: update real-time analytics dashboard
|
||||
}
|
||||
|
||||
private async handleProcessingError(request: StudentRequest, error: Error): Promise<void> {
|
||||
console.error(`❌ Error processing request ${request.id}:`, error.message)
|
||||
|
||||
// Notify administrators of processing error
|
||||
this.notificationService.notify({
|
||||
type: 'alert',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
message: `Processing error: ${error.message}`,
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
// Route to manual processing
|
||||
await this.routeForHumanReview(request, [])
|
||||
}
|
||||
|
||||
private async trackDecision(request: StudentRequest, match: MatchResult, decision: string): Promise<void> {
|
||||
console.log(`📈 Tracking decision: ${decision} for request ${request.id}`)
|
||||
// In production: log decision for ML model training
|
||||
}
|
||||
|
||||
// Public methods for integration
|
||||
subscribe(callback: (update: AIUpdate) => void): () => void {
|
||||
return this.notificationService.subscribe(callback)
|
||||
}
|
||||
|
||||
async generateInsights(requests: StudentRequest[]): Promise<AIInsight[]> {
|
||||
return await aiEngine.generateInsights(requests)
|
||||
}
|
||||
|
||||
getConfig(): ProcessingPipelineConfig {
|
||||
return { ...this.config }
|
||||
}
|
||||
|
||||
updateConfig(newConfig: Partial<ProcessingPipelineConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig }
|
||||
console.log('🔧 Pipeline configuration updated')
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const processingPipeline = new RealTimeProcessingPipeline()
|
||||
|
||||
// Export classes for testing and advanced usage
|
||||
export { NotificationService, AssignmentManager, ReviewManager }
|
||||
803
src/ai/StudentAssistanceAI.ts
Normal file
803
src/ai/StudentAssistanceAI.ts
Normal file
@@ -0,0 +1,803 @@
|
||||
// Phase 3: AI-Powered Student Assistance Matching Engine
|
||||
import * as tf from '@tensorflow/tfjs'
|
||||
import type {
|
||||
StudentRequest,
|
||||
MatchResult,
|
||||
RequestAnalysis,
|
||||
AIInsight,
|
||||
LearningFeedback,
|
||||
NeedCategory,
|
||||
ResourceRequirement
|
||||
} from './types'
|
||||
|
||||
// Text Vectorization for NLP Analysis
|
||||
class TextVectorizer {
|
||||
private vocabulary: Map<string, number> = new Map()
|
||||
private vectorSize: number = 100
|
||||
|
||||
constructor() {
|
||||
this.initializeVocabulary()
|
||||
}
|
||||
|
||||
private initializeVocabulary() {
|
||||
// Common assistance-related vocabulary
|
||||
const assistanceVocabulary = [
|
||||
// Clothing
|
||||
'clothes', 'clothing', 'shirt', 'pants', 'shoes', 'coat', 'jacket', 'uniform', 'dress',
|
||||
'socks', 'underwear', 'boots', 'sneakers', 'winter', 'warm', 'size', 'outgrown',
|
||||
|
||||
// School supplies
|
||||
'supplies', 'backpack', 'notebook', 'pencil', 'pen', 'binder', 'calculator', 'books',
|
||||
'textbook', 'paper', 'folders', 'highlighter', 'ruler', 'glue', 'scissors',
|
||||
|
||||
// Food assistance
|
||||
'food', 'hungry', 'lunch', 'breakfast', 'dinner', 'meal', 'snack', 'groceries',
|
||||
'nutrition', 'eat', 'starving', 'appetite', 'diet', 'allergic',
|
||||
|
||||
// Transportation
|
||||
'transport', 'bus', 'ride', 'car', 'walk', 'distance', 'far', 'pickup', 'drop-off',
|
||||
'gas', 'vehicle', 'bicycle', 'train', 'subway',
|
||||
|
||||
// Emergency/housing
|
||||
'emergency', 'urgent', 'homeless', 'shelter', 'roof', 'home', 'housing', 'evicted',
|
||||
'temporary', 'crisis', 'help', 'desperate',
|
||||
|
||||
// Medical
|
||||
'medical', 'doctor', 'medicine', 'sick', 'health', 'prescription', 'hospital',
|
||||
'glasses', 'dental', 'therapy', 'insurance',
|
||||
|
||||
// Technology
|
||||
'computer', 'laptop', 'tablet', 'phone', 'internet', 'wifi', 'online', 'digital',
|
||||
'device', 'charger', 'software', 'access',
|
||||
|
||||
// Emotional/contextual
|
||||
'need', 'help', 'family', 'parent', 'mother', 'father', 'guardian', 'sibling',
|
||||
'student', 'school', 'grade', 'class', 'teacher', 'principal', 'counselor',
|
||||
'poor', 'afford', 'money', 'financial', 'struggle', 'difficult', 'hardship'
|
||||
]
|
||||
|
||||
assistanceVocabulary.forEach((word, index) => {
|
||||
this.vocabulary.set(word.toLowerCase(), index)
|
||||
})
|
||||
}
|
||||
|
||||
async encode(text: string): Promise<number[]> {
|
||||
const words = this.preprocessText(text)
|
||||
const vector = new Array(this.vectorSize).fill(0)
|
||||
|
||||
// Simple bag-of-words with TF-IDF weighting
|
||||
const wordCounts = new Map<string, number>()
|
||||
words.forEach(word => {
|
||||
wordCounts.set(word, (wordCounts.get(word) || 0) + 1)
|
||||
})
|
||||
|
||||
// Create weighted vector
|
||||
wordCounts.forEach((count, word) => {
|
||||
const vocabIndex = this.vocabulary.get(word)
|
||||
if (vocabIndex !== undefined && vocabIndex < this.vectorSize) {
|
||||
// Simple TF-IDF approximation
|
||||
const tf = count / words.length
|
||||
const idf = Math.log(this.vocabulary.size / (count + 1))
|
||||
vector[vocabIndex] = tf * idf
|
||||
}
|
||||
})
|
||||
|
||||
// Simple semantic analysis without external NLP library
|
||||
// Extract entities and sentiment patterns
|
||||
|
||||
// Boost urgency indicators
|
||||
const urgencyWords = ['urgent', 'emergency', 'immediate', 'asap', 'desperate', 'critical']
|
||||
const urgencyScore = urgencyWords.reduce((score, word) => {
|
||||
return score + (text.toLowerCase().includes(word) ? 1 : 0)
|
||||
}, 0)
|
||||
|
||||
// Add urgency as a feature
|
||||
if (vector.length > this.vectorSize - 10) {
|
||||
vector[this.vectorSize - 1] = urgencyScore / urgencyWords.length
|
||||
}
|
||||
|
||||
return vector
|
||||
}
|
||||
|
||||
private preprocessText(text: string): string[] {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 2)
|
||||
}
|
||||
}
|
||||
|
||||
// Core AI Matching Engine
|
||||
export class StudentAssistanceAI {
|
||||
private vectorizer: TextVectorizer
|
||||
private matchingModel: tf.LayersModel | null = null
|
||||
private impactModel: tf.LayersModel | null = null
|
||||
private isInitialized = false
|
||||
|
||||
// Mock data for demonstration - in production, this would come from databases
|
||||
private mockResources = [
|
||||
{ id: 'r1', name: 'Emergency Clothing Fund', type: 'clothing', capacity: 100, avgCost: 75 },
|
||||
{ id: 'r2', name: 'School Supply Backpack Program', type: 'supplies', capacity: 50, avgCost: 45 },
|
||||
{ id: 'r3', name: 'Weekend Food Program', type: 'food', capacity: 200, avgCost: 25 },
|
||||
{ id: 'r4', name: 'Transportation Assistance', type: 'transport', capacity: 30, avgCost: 40 },
|
||||
{ id: 'r5', name: 'Technology Access Fund', type: 'technology', capacity: 25, avgCost: 200 }
|
||||
]
|
||||
|
||||
private mockVolunteers = [
|
||||
{ id: 'v1', name: 'Sarah Johnson', skills: ['clothing', 'shopping'], rating: 4.8, location: { city: 'Austin', state: 'TX' } },
|
||||
{ id: 'v2', name: 'Mike Chen', skills: ['supplies', 'delivery'], rating: 4.9, location: { city: 'Austin', state: 'TX' } },
|
||||
{ id: 'v3', name: 'Emily Rodriguez', skills: ['food', 'cooking'], rating: 4.7, location: { city: 'Round Rock', state: 'TX' } },
|
||||
{ id: 'v4', name: 'David Thompson', skills: ['transport', 'technology'], rating: 4.6, location: { city: 'Cedar Park', state: 'TX' } }
|
||||
]
|
||||
|
||||
constructor() {
|
||||
this.vectorizer = new TextVectorizer()
|
||||
this.initializeModels()
|
||||
}
|
||||
|
||||
private async initializeModels() {
|
||||
try {
|
||||
// Create simple neural network models for demonstration
|
||||
// In production, these would be pre-trained models loaded from files
|
||||
|
||||
// Matching Model: Input features -> Match probability
|
||||
this.matchingModel = tf.sequential({
|
||||
layers: [
|
||||
tf.layers.dense({ inputShape: [110], units: 64, activation: 'relu' }),
|
||||
tf.layers.dropout({ rate: 0.2 }),
|
||||
tf.layers.dense({ units: 32, activation: 'relu' }),
|
||||
tf.layers.dense({ units: 16, activation: 'relu' }),
|
||||
tf.layers.dense({ units: 1, activation: 'sigmoid' })
|
||||
]
|
||||
})
|
||||
|
||||
// Impact Prediction Model: Match features -> Impact score
|
||||
this.impactModel = tf.sequential({
|
||||
layers: [
|
||||
tf.layers.dense({ inputShape: [20], units: 32, activation: 'relu' }),
|
||||
tf.layers.dense({ units: 16, activation: 'relu' }),
|
||||
tf.layers.dense({ units: 4, activation: 'linear' }) // [beneficiaries, success_prob, time, sustainability]
|
||||
]
|
||||
})
|
||||
|
||||
// Compile models
|
||||
this.matchingModel.compile({
|
||||
optimizer: tf.train.adam(0.001),
|
||||
loss: 'binaryCrossentropy',
|
||||
metrics: ['accuracy']
|
||||
})
|
||||
|
||||
this.impactModel.compile({
|
||||
optimizer: tf.train.adam(0.001),
|
||||
loss: 'meanSquaredError',
|
||||
metrics: ['mae']
|
||||
})
|
||||
|
||||
this.isInitialized = true
|
||||
console.log('🤖 AI Models initialized successfully')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize AI models:', error)
|
||||
// Fallback to rule-based system
|
||||
this.isInitialized = false
|
||||
}
|
||||
}
|
||||
|
||||
async processRequest(request: StudentRequest): Promise<MatchResult[]> {
|
||||
console.log(`🧠 Processing request for ${request.studentName}: ${request.description}`)
|
||||
|
||||
try {
|
||||
// 1. Analyze request with NLP
|
||||
const analysis = await this.analyzeRequest(request)
|
||||
|
||||
// 2. Find candidate resources
|
||||
const candidates = await this.findCandidateResources(analysis)
|
||||
|
||||
// 3. Score matches using AI or rule-based fallback
|
||||
const scoredMatches = this.isInitialized
|
||||
? await this.aiScoreMatches(candidates, analysis, request)
|
||||
: await this.ruleBasedScoring(candidates, analysis, request)
|
||||
|
||||
// 4. Predict impact for top matches
|
||||
const enrichedMatches = await this.enrichWithPredictions(scoredMatches)
|
||||
|
||||
// 5. Sort by confidence and return top 3
|
||||
return enrichedMatches
|
||||
.sort((a, b) => b.confidenceScore - a.confidenceScore)
|
||||
.slice(0, 3)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing request:', error)
|
||||
return this.generateFallbackMatches()
|
||||
}
|
||||
}
|
||||
|
||||
private async analyzeRequest(request: StudentRequest): Promise<RequestAnalysis> {
|
||||
// NLP analysis of request description
|
||||
await this.vectorizer.encode(request.description) // Process text for any side effects
|
||||
|
||||
// Extract needs using rule-based analysis + NLP
|
||||
const primaryNeeds = this.extractNeedCategories(request)
|
||||
const urgencyScore = this.calculateUrgencyScore(request)
|
||||
const complexityEstimate = this.estimateComplexity(request)
|
||||
|
||||
return {
|
||||
primaryNeeds,
|
||||
urgencyScore,
|
||||
complexityEstimate,
|
||||
resourceRequirements: this.estimateResourceRequirements(primaryNeeds),
|
||||
estimatedBudget: this.estimateBudget(primaryNeeds, request.description)
|
||||
}
|
||||
}
|
||||
|
||||
private extractNeedCategories(request: StudentRequest): NeedCategory[] {
|
||||
const categories: NeedCategory[] = []
|
||||
const desc = request.description.toLowerCase()
|
||||
|
||||
// Clothing detection
|
||||
if (desc.includes('clothes') || desc.includes('clothing') || desc.includes('shirt') ||
|
||||
desc.includes('pants') || desc.includes('shoes') || desc.includes('coat')) {
|
||||
categories.push({
|
||||
category: 'clothing',
|
||||
priority: desc.includes('winter') || desc.includes('cold') ? 0.9 : 0.7,
|
||||
specifications: { season: desc.includes('winter') ? 'winter' : 'general' }
|
||||
})
|
||||
}
|
||||
|
||||
// School supplies detection
|
||||
if (desc.includes('supplies') || desc.includes('backpack') || desc.includes('notebook') ||
|
||||
desc.includes('pencil') || desc.includes('books')) {
|
||||
categories.push({
|
||||
category: 'school-supplies',
|
||||
priority: 0.8,
|
||||
specifications: { type: 'general' }
|
||||
})
|
||||
}
|
||||
|
||||
// Food assistance detection
|
||||
if (desc.includes('food') || desc.includes('hungry') || desc.includes('lunch') ||
|
||||
desc.includes('meal') || desc.includes('eat')) {
|
||||
categories.push({
|
||||
category: 'food-assistance',
|
||||
priority: desc.includes('hungry') || desc.includes('starving') ? 0.95 : 0.75,
|
||||
specifications: { urgency: desc.includes('hungry') ? 'high' : 'medium' }
|
||||
})
|
||||
}
|
||||
|
||||
// Transportation detection
|
||||
if (desc.includes('transport') || desc.includes('bus') || desc.includes('ride') ||
|
||||
desc.includes('car') || desc.includes('far')) {
|
||||
categories.push({
|
||||
category: 'transportation',
|
||||
priority: 0.6,
|
||||
specifications: { type: 'general' }
|
||||
})
|
||||
}
|
||||
|
||||
// Emergency/housing detection
|
||||
if (desc.includes('emergency') || desc.includes('urgent') || desc.includes('homeless') ||
|
||||
desc.includes('shelter') || desc.includes('crisis')) {
|
||||
categories.push({
|
||||
category: 'emergency-housing',
|
||||
priority: 0.98,
|
||||
specifications: { urgency: 'critical' }
|
||||
})
|
||||
}
|
||||
|
||||
// Technology detection
|
||||
if (desc.includes('computer') || desc.includes('laptop') || desc.includes('internet') ||
|
||||
desc.includes('online') || desc.includes('device')) {
|
||||
categories.push({
|
||||
category: 'technology',
|
||||
priority: 0.65,
|
||||
specifications: { type: 'educational' }
|
||||
})
|
||||
}
|
||||
|
||||
// Default to the request category if nothing else matches
|
||||
if (categories.length === 0) {
|
||||
categories.push({
|
||||
category: request.category,
|
||||
priority: 0.5,
|
||||
specifications: {}
|
||||
})
|
||||
}
|
||||
|
||||
return categories.sort((a, b) => b.priority - a.priority)
|
||||
}
|
||||
|
||||
private calculateUrgencyScore(request: StudentRequest): number {
|
||||
let score = 0
|
||||
|
||||
// Base score from urgency level
|
||||
switch (request.urgency) {
|
||||
case 'emergency': score += 1.0; break
|
||||
case 'high': score += 0.8; break
|
||||
case 'medium': score += 0.5; break
|
||||
case 'low': score += 0.2; break
|
||||
}
|
||||
|
||||
// Boost for urgency keywords in description
|
||||
const desc = request.description.toLowerCase()
|
||||
const urgencyKeywords = ['urgent', 'emergency', 'immediate', 'asap', 'desperate', 'critical', 'help', 'need now']
|
||||
urgencyKeywords.forEach(keyword => {
|
||||
if (desc.includes(keyword)) score += 0.1
|
||||
})
|
||||
|
||||
// Boost for deadline
|
||||
if (request.deadline) {
|
||||
const daysUntilDeadline = (request.deadline.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
||||
if (daysUntilDeadline < 1) score += 0.3
|
||||
else if (daysUntilDeadline < 3) score += 0.2
|
||||
else if (daysUntilDeadline < 7) score += 0.1
|
||||
}
|
||||
|
||||
return Math.min(score, 1.0) // Cap at 1.0
|
||||
}
|
||||
|
||||
private estimateComplexity(request: StudentRequest): number {
|
||||
let complexity = 0.3 // Base complexity
|
||||
|
||||
// Multiple needs increase complexity
|
||||
const needCount = this.extractNeedCategories(request).length
|
||||
complexity += needCount * 0.15
|
||||
|
||||
// Special constraints increase complexity
|
||||
if (request.constraints.deliveryMethod === 'delivery') complexity += 0.2
|
||||
if (request.constraints.privacyLevel === 'anonymous') complexity += 0.1
|
||||
if (request.constraints.specialRequirements?.length) complexity += 0.2
|
||||
|
||||
// Geographic complexity
|
||||
if (!request.location.city || !request.location.zipCode) complexity += 0.15
|
||||
|
||||
return Math.min(complexity, 1.0)
|
||||
}
|
||||
|
||||
private estimateResourceRequirements(needs: NeedCategory[]): ResourceRequirement[] {
|
||||
return needs.map(need => ({
|
||||
type: need.category,
|
||||
quantity: 1,
|
||||
specifications: need.specifications || {},
|
||||
estimatedCost: this.estimateCategoryBudget(need.category)
|
||||
}))
|
||||
}
|
||||
|
||||
private estimateBudget(needs: NeedCategory[], description: string): number {
|
||||
let totalBudget = 0
|
||||
|
||||
needs.forEach(need => {
|
||||
totalBudget += this.estimateCategoryBudget(need.category) * need.priority
|
||||
})
|
||||
|
||||
// Adjust based on description indicators
|
||||
const desc = description.toLowerCase()
|
||||
if (desc.includes('multiple') || desc.includes('several')) totalBudget *= 1.5
|
||||
if (desc.includes('family') || desc.includes('siblings')) totalBudget *= 2
|
||||
if (desc.includes('brand new') || desc.includes('quality')) totalBudget *= 1.3
|
||||
|
||||
return Math.round(totalBudget)
|
||||
}
|
||||
|
||||
private estimateCategoryBudget(category: string): number {
|
||||
const budgets: Record<string, number> = {
|
||||
'clothing': 75,
|
||||
'school-supplies': 45,
|
||||
'food-assistance': 25,
|
||||
'transportation': 40,
|
||||
'emergency-housing': 200,
|
||||
'medical-needs': 150,
|
||||
'technology': 200,
|
||||
'extracurricular': 60,
|
||||
'other': 50
|
||||
}
|
||||
return budgets[category] || 50
|
||||
}
|
||||
|
||||
private async findCandidateResources(analysis: RequestAnalysis) {
|
||||
// Find resources that match the primary needs
|
||||
const candidates = this.mockResources.filter(resource =>
|
||||
analysis.primaryNeeds.some(need =>
|
||||
resource.type === need.category ||
|
||||
this.isCompatibleResourceType(resource.type, need.category)
|
||||
)
|
||||
)
|
||||
|
||||
// Add volunteer matches
|
||||
const volunteerCandidates = this.mockVolunteers.filter(volunteer =>
|
||||
volunteer.skills.some(skill =>
|
||||
analysis.primaryNeeds.some(need =>
|
||||
skill.includes(need.category) || need.category.includes(skill)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return candidates.map(resource => ({
|
||||
...resource,
|
||||
volunteers: volunteerCandidates.filter(v =>
|
||||
v.skills.some(skill => skill.includes(resource.type))
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
private isCompatibleResourceType(resourceType: string, needCategory: string): boolean {
|
||||
const compatibility: Record<string, string[]> = {
|
||||
'clothing': ['clothing'],
|
||||
'supplies': ['school-supplies'],
|
||||
'food': ['food-assistance'],
|
||||
'transport': ['transportation'],
|
||||
'technology': ['technology'],
|
||||
'emergency': ['emergency-housing', 'medical-needs']
|
||||
}
|
||||
|
||||
return compatibility[resourceType]?.includes(needCategory) || false
|
||||
}
|
||||
|
||||
private async aiScoreMatches(candidates: any[], analysis: RequestAnalysis, request: StudentRequest): Promise<MatchResult[]> {
|
||||
if (!this.matchingModel) return this.ruleBasedScoring(candidates, analysis, request)
|
||||
|
||||
const matches: MatchResult[] = []
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
// Prepare features for ML model
|
||||
const features = await this.prepareFeaturesForML(candidate, analysis, request)
|
||||
const featureTensor = tf.tensor2d([features])
|
||||
|
||||
// Get confidence score from AI model
|
||||
const prediction = this.matchingModel.predict(featureTensor) as tf.Tensor
|
||||
const confidenceData = await prediction.data()
|
||||
const confidenceScore = confidenceData[0]
|
||||
|
||||
// Clean up tensors
|
||||
featureTensor.dispose()
|
||||
prediction.dispose()
|
||||
|
||||
// Select best volunteer
|
||||
const bestVolunteer = candidate.volunteers.length > 0
|
||||
? candidate.volunteers.sort((a: any, b: any) => b.rating - a.rating)[0]
|
||||
: null
|
||||
|
||||
matches.push({
|
||||
resourceId: candidate.id,
|
||||
resourceName: candidate.name,
|
||||
resourceType: candidate.type,
|
||||
confidenceScore,
|
||||
estimatedImpact: this.estimateImpact(analysis, candidate),
|
||||
logisticalComplexity: analysis.complexityEstimate,
|
||||
volunteerMatch: bestVolunteer ? {
|
||||
id: bestVolunteer.id,
|
||||
volunteerId: bestVolunteer.id,
|
||||
volunteerName: bestVolunteer.name,
|
||||
skills: bestVolunteer.skills,
|
||||
availability: [], // Would be populated from real data
|
||||
location: bestVolunteer.location,
|
||||
rating: bestVolunteer.rating,
|
||||
completedAssignments: Math.floor(Math.random() * 50) + 10
|
||||
} : undefined,
|
||||
estimatedCost: candidate.avgCost,
|
||||
fulfillmentTimeline: this.estimateTimeline(analysis.urgencyScore, analysis.complexityEstimate),
|
||||
reasoningFactors: this.generateReasoningFactors(candidate, analysis),
|
||||
riskFactors: this.identifyRiskFactors(candidate, analysis)
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in AI scoring:', error)
|
||||
// Fallback to rule-based for this candidate
|
||||
const ruleBasedScore = this.calculateRuleBasedScore(candidate, analysis, request)
|
||||
matches.push(ruleBasedScore)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
private async prepareFeaturesForML(candidate: any, analysis: RequestAnalysis, request: StudentRequest): Promise<number[]> {
|
||||
// Create feature vector for ML model (110 features to match model input)
|
||||
const features = new Array(110).fill(0)
|
||||
|
||||
// Request analysis features (first 100 from text vectorization)
|
||||
const textVector = await this.vectorizer.encode(request.description)
|
||||
for (let i = 0; i < Math.min(textVector.length, 100); i++) {
|
||||
features[i] = textVector[i]
|
||||
}
|
||||
|
||||
// Resource matching features
|
||||
features[100] = analysis.urgencyScore
|
||||
features[101] = analysis.complexityEstimate
|
||||
features[102] = analysis.estimatedBudget / 1000 // Normalize
|
||||
features[103] = candidate.capacity / 100 // Normalize
|
||||
features[104] = candidate.avgCost / 200 // Normalize
|
||||
features[105] = candidate.volunteers.length / 10 // Normalize
|
||||
features[106] = candidate.volunteers.length > 0 ? Math.max(...candidate.volunteers.map((v: any) => v.rating)) / 5 : 0
|
||||
features[107] = analysis.primaryNeeds.length / 5 // Normalize
|
||||
features[108] = request.constraints.maxBudget ? (candidate.avgCost / request.constraints.maxBudget) : 0.5
|
||||
features[109] = this.getTimeframeScore(request.constraints.timeframe)
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
private getTimeframeScore(timeframe: string): number {
|
||||
switch (timeframe) {
|
||||
case 'immediate': return 1.0
|
||||
case 'within-week': return 0.7
|
||||
case 'within-month': return 0.4
|
||||
case 'flexible': return 0.2
|
||||
default: return 0.5
|
||||
}
|
||||
}
|
||||
|
||||
private async ruleBasedScoring(candidates: any[], analysis: RequestAnalysis, request: StudentRequest): Promise<MatchResult[]> {
|
||||
return candidates.map(candidate => this.calculateRuleBasedScore(candidate, analysis, request))
|
||||
}
|
||||
|
||||
private calculateRuleBasedScore(candidate: any, analysis: RequestAnalysis, request: StudentRequest): MatchResult {
|
||||
let score = 0.5 // Base score
|
||||
|
||||
// Category match bonus
|
||||
const categoryMatch = analysis.primaryNeeds.some(need =>
|
||||
candidate.type === need.category || this.isCompatibleResourceType(candidate.type, need.category)
|
||||
)
|
||||
if (categoryMatch) score += 0.3
|
||||
|
||||
// Capacity and cost considerations
|
||||
if (candidate.capacity > 10) score += 0.1
|
||||
if (candidate.avgCost <= analysis.estimatedBudget) score += 0.2
|
||||
|
||||
// Volunteer availability bonus
|
||||
if (candidate.volunteers?.length > 0) {
|
||||
score += 0.15
|
||||
const avgRating = candidate.volunteers.reduce((sum: number, v: any) => sum + v.rating, 0) / candidate.volunteers.length
|
||||
score += (avgRating / 5) * 0.1
|
||||
}
|
||||
|
||||
// Urgency adjustment
|
||||
score += analysis.urgencyScore * 0.1
|
||||
|
||||
// Complexity penalty
|
||||
score -= analysis.complexityEstimate * 0.05
|
||||
|
||||
const bestVolunteer = candidate.volunteers?.length > 0
|
||||
? candidate.volunteers.sort((a: any, b: any) => b.rating - a.rating)[0]
|
||||
: null
|
||||
|
||||
return {
|
||||
resourceId: candidate.id,
|
||||
resourceName: candidate.name,
|
||||
resourceType: candidate.type,
|
||||
confidenceScore: Math.min(Math.max(score, 0), 1),
|
||||
estimatedImpact: this.estimateImpact(analysis, candidate),
|
||||
logisticalComplexity: analysis.complexityEstimate,
|
||||
volunteerMatch: bestVolunteer ? {
|
||||
id: bestVolunteer.id,
|
||||
volunteerId: bestVolunteer.id,
|
||||
volunteerName: bestVolunteer.name,
|
||||
skills: bestVolunteer.skills,
|
||||
availability: [],
|
||||
location: bestVolunteer.location,
|
||||
rating: bestVolunteer.rating,
|
||||
completedAssignments: Math.floor(Math.random() * 50) + 10
|
||||
} : undefined,
|
||||
estimatedCost: candidate.avgCost,
|
||||
fulfillmentTimeline: this.estimateTimeline(analysis.urgencyScore, analysis.complexityEstimate),
|
||||
reasoningFactors: this.generateReasoningFactors(candidate, analysis),
|
||||
riskFactors: this.identifyRiskFactors(candidate, analysis, request)
|
||||
}
|
||||
}
|
||||
|
||||
private estimateImpact(analysis: RequestAnalysis, candidate: any): number {
|
||||
let impact = 0.6 // Base impact
|
||||
|
||||
// Higher impact for urgent needs
|
||||
impact += analysis.urgencyScore * 0.2
|
||||
|
||||
// Higher impact for critical categories
|
||||
const criticalCategories = ['emergency-housing', 'food-assistance', 'medical-needs']
|
||||
if (analysis.primaryNeeds.some(need => criticalCategories.includes(need.category))) {
|
||||
impact += 0.15
|
||||
}
|
||||
|
||||
// Volunteer quality impact
|
||||
if (candidate.volunteers?.length > 0) {
|
||||
const avgRating = candidate.volunteers.reduce((sum: number, v: any) => sum + v.rating, 0) / candidate.volunteers.length
|
||||
impact += (avgRating / 5) * 0.1
|
||||
}
|
||||
|
||||
return Math.min(impact, 1.0)
|
||||
}
|
||||
|
||||
private estimateTimeline(urgencyScore: number, complexityScore: number): string {
|
||||
const baseHours = 24 + (complexityScore * 48) - (urgencyScore * 12)
|
||||
|
||||
if (baseHours <= 2) return 'Within 2 hours'
|
||||
if (baseHours <= 24) return 'Same day'
|
||||
if (baseHours <= 48) return '1-2 days'
|
||||
if (baseHours <= 168) return '3-7 days'
|
||||
return '1-2 weeks'
|
||||
}
|
||||
|
||||
private generateReasoningFactors(candidate: any, analysis: RequestAnalysis): string[] {
|
||||
const factors: string[] = []
|
||||
|
||||
factors.push(`Resource specializes in ${candidate.type} assistance`)
|
||||
|
||||
if (candidate.capacity > 50) factors.push('High capacity resource available')
|
||||
if (candidate.avgCost <= analysis.estimatedBudget) factors.push('Cost within estimated budget')
|
||||
|
||||
if (candidate.volunteers?.length > 0) {
|
||||
const avgRating = candidate.volunteers.reduce((sum: number, v: any) => sum + v.rating, 0) / candidate.volunteers.length
|
||||
factors.push(`${candidate.volunteers.length} volunteers available (avg rating: ${avgRating.toFixed(1)})`)
|
||||
}
|
||||
|
||||
if (analysis.urgencyScore > 0.7) factors.push('High urgency request prioritized')
|
||||
|
||||
return factors
|
||||
}
|
||||
|
||||
private identifyRiskFactors(candidate: any, analysis: RequestAnalysis): string[] {
|
||||
const risks: string[] = []
|
||||
|
||||
if (candidate.capacity < 5) risks.push('Limited resource capacity')
|
||||
if (candidate.avgCost > analysis.estimatedBudget * 1.2) risks.push('Cost exceeds estimated budget')
|
||||
if (candidate.volunteers?.length === 0) risks.push('No volunteers currently available')
|
||||
if (analysis.complexityEstimate > 0.7) risks.push('High complexity request')
|
||||
|
||||
return risks
|
||||
}
|
||||
|
||||
private async enrichWithPredictions(matches: MatchResult[]): Promise<MatchResult[]> {
|
||||
// For each match, predict impact using AI model if available
|
||||
for (const match of matches) {
|
||||
try {
|
||||
if (this.impactModel && this.isInitialized) {
|
||||
const impactFeatures = this.prepareImpactFeatures(match)
|
||||
const impactTensor = tf.tensor2d([impactFeatures])
|
||||
|
||||
const impactPrediction = this.impactModel.predict(impactTensor) as tf.Tensor
|
||||
const impactData = await impactPrediction.data()
|
||||
|
||||
// Update match with AI predictions
|
||||
match.estimatedImpact = Math.min(Math.max(impactData[1], 0), 1) // success probability
|
||||
|
||||
// Clean up tensors
|
||||
impactTensor.dispose()
|
||||
impactPrediction.dispose()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in impact prediction:', error)
|
||||
// Keep existing impact estimate
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
private prepareImpactFeatures(match: MatchResult): number[] {
|
||||
// Create 20-feature vector for impact prediction
|
||||
return [
|
||||
match.confidenceScore,
|
||||
match.estimatedCost / 200, // Normalize
|
||||
match.logisticalComplexity,
|
||||
match.volunteerMatch ? match.volunteerMatch.rating / 5 : 0,
|
||||
match.volunteerMatch ? match.volunteerMatch.completedAssignments / 50 : 0,
|
||||
match.resourceType === 'emergency' ? 1 : 0,
|
||||
match.resourceType === 'food' ? 1 : 0,
|
||||
match.resourceType === 'clothing' ? 1 : 0,
|
||||
match.resourceType === 'supplies' ? 1 : 0,
|
||||
match.resourceType === 'transport' ? 1 : 0,
|
||||
match.reasoningFactors.length / 5,
|
||||
match.riskFactors.length / 5,
|
||||
Math.random(), // Random noise for variation
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random()
|
||||
]
|
||||
}
|
||||
|
||||
private generateFallbackMatches(): MatchResult[] {
|
||||
// Emergency fallback when AI fails
|
||||
return [{
|
||||
resourceId: 'fallback-1',
|
||||
resourceName: 'General Assistance Fund',
|
||||
resourceType: 'other',
|
||||
confidenceScore: 0.5,
|
||||
estimatedImpact: 0.6,
|
||||
logisticalComplexity: 0.5,
|
||||
estimatedCost: 75,
|
||||
fulfillmentTimeline: '1-2 days',
|
||||
reasoningFactors: ['Fallback resource for emergency situations'],
|
||||
riskFactors: ['System processing temporarily unavailable']
|
||||
}]
|
||||
}
|
||||
|
||||
async generateInsights(requests: StudentRequest[]): Promise<AIInsight[]> {
|
||||
const insights: AIInsight[] = []
|
||||
|
||||
// Pattern analysis
|
||||
const categoryDistribution = this.analyzeCategoryDistribution(requests)
|
||||
const urgencyTrends = this.analyzeUrgencyTrends(requests)
|
||||
|
||||
// Generate insights based on patterns
|
||||
if (categoryDistribution.clothing > 0.4) {
|
||||
insights.push({
|
||||
id: `insight-${Date.now()}-1`,
|
||||
type: 'trend',
|
||||
title: 'High Clothing Demand Detected',
|
||||
description: `${Math.round(categoryDistribution.clothing * 100)}% of recent requests are for clothing assistance. Consider expanding clothing inventory.`,
|
||||
confidence: 0.85,
|
||||
severity: 'medium',
|
||||
timestamp: new Date(),
|
||||
actionItems: ['Increase clothing fund allocation', 'Recruit clothing-focused volunteers', 'Partner with local clothing stores']
|
||||
})
|
||||
}
|
||||
|
||||
if (urgencyTrends.emergencyRate > 0.3) {
|
||||
insights.push({
|
||||
id: `insight-${Date.now()}-2`,
|
||||
type: 'anomaly',
|
||||
title: 'Increased Emergency Requests',
|
||||
description: `${Math.round(urgencyTrends.emergencyRate * 100)}% of requests are marked as emergency. This is above normal baseline.`,
|
||||
confidence: 0.9,
|
||||
severity: 'high',
|
||||
timestamp: new Date(),
|
||||
actionItems: ['Review emergency response protocols', 'Ensure emergency fund availability', 'Alert senior coordinators']
|
||||
})
|
||||
}
|
||||
|
||||
return insights
|
||||
}
|
||||
|
||||
private analyzeCategoryDistribution(requests: StudentRequest[]) {
|
||||
const total = requests.length
|
||||
if (total === 0) return {}
|
||||
|
||||
const counts: Record<string, number> = {}
|
||||
requests.forEach(req => {
|
||||
counts[req.category] = (counts[req.category] || 0) + 1
|
||||
})
|
||||
|
||||
const distribution: Record<string, number> = {}
|
||||
Object.entries(counts).forEach(([category, count]) => {
|
||||
distribution[category] = count / total
|
||||
})
|
||||
|
||||
return distribution
|
||||
}
|
||||
|
||||
private analyzeUrgencyTrends(requests: StudentRequest[]) {
|
||||
const total = requests.length
|
||||
if (total === 0) return { emergencyRate: 0 }
|
||||
|
||||
const emergencyCount = requests.filter(req => req.urgency === 'emergency').length
|
||||
return {
|
||||
emergencyRate: emergencyCount / total
|
||||
}
|
||||
}
|
||||
|
||||
async learnFromFeedback(feedback: LearningFeedback): Promise<void> {
|
||||
console.log(`📚 Learning from feedback for request ${feedback.requestId}`)
|
||||
|
||||
// In production, this would update training data and trigger model retraining
|
||||
// For now, we'll log the feedback for analysis
|
||||
|
||||
const learningData = {
|
||||
requestId: feedback.requestId,
|
||||
outcome: feedback.outcome,
|
||||
accuracyScore: feedback.outcome === 'successful' ? 1 : 0,
|
||||
costAccuracy: Math.abs(feedback.actualCost - (feedback.actualCost)) / feedback.actualCost,
|
||||
timeAccuracy: feedback.actualTimeToComplete,
|
||||
satisfaction: feedback.satisfactionScore,
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
// Store for batch processing and model improvement
|
||||
console.log('Learning data recorded:', learningData)
|
||||
}
|
||||
}
|
||||
|
||||
// Export the AI engine
|
||||
export const aiEngine = new StudentAssistanceAI()
|
||||
191
src/ai/types.ts
Normal file
191
src/ai/types.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
// Phase 3: AI Types and Interfaces
|
||||
|
||||
export interface StudentRequest {
|
||||
id: string
|
||||
studentId: string
|
||||
studentName: string
|
||||
description: string
|
||||
category: AssistanceCategory
|
||||
urgency: UrgencyLevel
|
||||
location: GeographicLocation
|
||||
constraints: RequestConstraints
|
||||
deadline?: Date
|
||||
submittedAt: Date
|
||||
estimatedCost?: number
|
||||
requiredSkills?: string[]
|
||||
}
|
||||
|
||||
export interface MatchResult {
|
||||
resourceId: string
|
||||
resourceName: string
|
||||
resourceType: 'clothing' | 'supplies' | 'food' | 'transport' | 'emergency' | 'other'
|
||||
confidenceScore: number
|
||||
estimatedImpact: number
|
||||
logisticalComplexity: number
|
||||
volunteerMatch?: VolunteerAssignment
|
||||
estimatedCost: number
|
||||
fulfillmentTimeline: string
|
||||
reasoningFactors: string[]
|
||||
riskFactors: string[]
|
||||
}
|
||||
|
||||
export interface RequestAnalysis {
|
||||
primaryNeeds: NeedCategory[]
|
||||
urgencyScore: number
|
||||
complexityEstimate: number
|
||||
resourceRequirements: ResourceRequirement[]
|
||||
locationConstraints?: GeographicConstraint[]
|
||||
timeConstraints?: TemporalConstraint[]
|
||||
requiredSkills?: string[]
|
||||
estimatedBudget: number
|
||||
}
|
||||
|
||||
export interface AIInsight {
|
||||
id: string
|
||||
type: 'anomaly' | 'optimization' | 'trend' | 'prediction' | 'recommendation'
|
||||
title: string
|
||||
description: string
|
||||
confidence: number
|
||||
severity?: 'low' | 'medium' | 'high' | 'critical'
|
||||
timestamp: Date
|
||||
actionItems?: string[]
|
||||
estimatedImpact?: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface AIMetrics {
|
||||
accuracyRate: number
|
||||
accuracyTrend: number
|
||||
avgProcessingTime: number
|
||||
speedTrend: number
|
||||
autoApprovalRate: number
|
||||
automationTrend: number
|
||||
impactPredictionAccuracy: number
|
||||
impactTrend: number
|
||||
totalRequestsProcessed: number
|
||||
successfulMatches: number
|
||||
}
|
||||
|
||||
export interface AIUpdate {
|
||||
type: 'request-processed' | 'new-insight' | 'auto-approval' | 'model-updated' | 'alert'
|
||||
requestId?: string
|
||||
studentName?: string
|
||||
status?: string
|
||||
recommendations?: MatchResult[]
|
||||
insight?: AIInsight
|
||||
message?: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
export type AssistanceCategory =
|
||||
| 'clothing'
|
||||
| 'school-supplies'
|
||||
| 'food-assistance'
|
||||
| 'transportation'
|
||||
| 'emergency-housing'
|
||||
| 'medical-needs'
|
||||
| 'technology'
|
||||
| 'extracurricular'
|
||||
| 'other'
|
||||
|
||||
export type UrgencyLevel = 'low' | 'medium' | 'high' | 'emergency'
|
||||
|
||||
export interface GeographicLocation {
|
||||
latitude?: number
|
||||
longitude?: number
|
||||
address?: string
|
||||
city: string
|
||||
state: string
|
||||
zipCode: string
|
||||
schoolDistrict?: string
|
||||
}
|
||||
|
||||
export interface RequestConstraints {
|
||||
maxBudget?: number
|
||||
timeframe: 'immediate' | 'within-week' | 'within-month' | 'flexible'
|
||||
deliveryMethod: 'pickup' | 'delivery' | 'mail' | 'school-delivery' | 'any'
|
||||
privacyLevel: 'anonymous' | 'semi-anonymous' | 'open'
|
||||
specialRequirements?: string[]
|
||||
}
|
||||
|
||||
export interface VolunteerAssignment {
|
||||
id: string
|
||||
volunteerId: string
|
||||
volunteerName: string
|
||||
skills: string[]
|
||||
availability: Date[]
|
||||
location: GeographicLocation
|
||||
rating: number
|
||||
completedAssignments: number
|
||||
}
|
||||
|
||||
export interface NeedCategory {
|
||||
category: AssistanceCategory
|
||||
subcategory?: string
|
||||
priority: number
|
||||
quantity?: number
|
||||
specifications?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ResourceRequirement {
|
||||
type: string
|
||||
quantity: number
|
||||
specifications: Record<string, any>
|
||||
alternatives?: string[]
|
||||
estimatedCost: number
|
||||
}
|
||||
|
||||
export interface GeographicConstraint {
|
||||
maxDistance: number
|
||||
preferredAreas?: string[]
|
||||
excludedAreas?: string[]
|
||||
}
|
||||
|
||||
export interface TemporalConstraint {
|
||||
earliestStart: Date
|
||||
latestCompletion: Date
|
||||
preferredTimes?: string[]
|
||||
blackoutPeriods?: DateRange[]
|
||||
}
|
||||
|
||||
export interface DateRange {
|
||||
start: Date
|
||||
end: Date
|
||||
}
|
||||
|
||||
export interface ImpactPrediction {
|
||||
estimatedBeneficiaries: number
|
||||
successProbability: number
|
||||
timeToImpact: number
|
||||
sustainabilityScore: number
|
||||
rippleEffects: RippleEffect[]
|
||||
measurableOutcomes: string[]
|
||||
}
|
||||
|
||||
export interface RippleEffect {
|
||||
type: 'family' | 'community' | 'academic' | 'social' | 'economic'
|
||||
description: string
|
||||
estimatedBeneficiaries: number
|
||||
confidenceLevel: number
|
||||
}
|
||||
|
||||
export interface LearningFeedback {
|
||||
requestId: string
|
||||
matchId: string
|
||||
outcome: 'successful' | 'partial' | 'failed'
|
||||
actualCost: number
|
||||
actualTimeToComplete: number
|
||||
satisfactionScore: number
|
||||
issues?: string[]
|
||||
improvements?: string[]
|
||||
measuredImpact?: Record<string, number>
|
||||
}
|
||||
|
||||
export interface ProcessingPipelineConfig {
|
||||
autoApprovalThreshold: number
|
||||
urgencyWeights: Record<UrgencyLevel, number>
|
||||
categoryWeights: Record<AssistanceCategory, number>
|
||||
maxProcessingTime: number
|
||||
retryAttempts: number
|
||||
notificationEnabled: boolean
|
||||
}
|
||||
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