diff --git a/README.md b/README.md index 5270c4108..e0174e3c1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ Our goals include: - User-friendly navigation and search - Accessibility features for users with disabilities - Regular updates and maintenance +- **AI Assistant** (New): + - Duplicate detection for idea submissions to reduce redundancy + - Emergency announcements integration for critical alerts + - Smart guidance for new contributors ## Join Us as a Volunteer diff --git a/src/components/AIAssistant/DuplicateDetectionModal.tsx b/src/components/AIAssistant/DuplicateDetectionModal.tsx new file mode 100644 index 000000000..f83eb0e17 --- /dev/null +++ b/src/components/AIAssistant/DuplicateDetectionModal.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { AlertTriangle, X } from 'lucide-react'; +import { + SimilarityResult, + getSimilarityLevel, + formatSimilarityPercentage, +} from '../../services/aiAssistant/duplicateDetection'; + +interface DuplicateDetectionModalProps { + isOpen: boolean; + onClose: () => void; + similarIdeas: SimilarityResult[]; + onProceed: () => void; + newIdeaTitle: string; +} + +export const DuplicateDetectionModal: React.FC< + DuplicateDetectionModalProps +> = ({ isOpen, onClose, similarIdeas, onProceed, newIdeaTitle }) => { + if (!isOpen) return null; + + const getSimilarityColor = (score: number): string => { + if (score >= 0.8) return 'text-red-600 bg-red-50'; + if (score >= 0.6) return 'text-orange-600 bg-orange-50'; + if (score >= 0.4) return 'text-yellow-600 bg-yellow-50'; + return 'text-blue-600 bg-blue-50'; + }; + + return ( +
+
+
+
+
+
+ +
+
+

+ Similar Ideas Detected +

+

+ AI Assistant found existing ideas similar to yours +

+
+
+ +
+
+ +
+
+

+ Your idea:{' '} + “{newIdeaTitle}” +

+

+ We found {similarIdeas.length} similar{' '} + {similarIdeas.length === 1 ? 'idea' : 'ideas'}. Consider reviewing + or contributing to existing ideas before creating a new one. +

+
+ +
+ {similarIdeas.map(idea => ( +
+
+
+

+ {idea.title} +

+ {idea.category && ( + + {idea.category} + + )} +
+
+
+ {formatSimilarityPercentage(idea.similarity)} +
+
+ {getSimilarityLevel(idea.similarity)} Match +
+
+
+
+ ))} +
+
+ +
+
+

+ Help reduce duplicates and strengthen existing ideas +

+
+ + +
+
+
+
+
+ ); +}; + +export default DuplicateDetectionModal; diff --git a/src/components/AIAssistant/EmergencyAnnouncements.tsx b/src/components/AIAssistant/EmergencyAnnouncements.tsx new file mode 100644 index 000000000..020da8657 --- /dev/null +++ b/src/components/AIAssistant/EmergencyAnnouncements.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from 'react'; +import { AlertTriangle, Cloud, Activity, MapPin, Clock, X } from 'lucide-react'; + +export interface EmergencyAnnouncement { + id: string; + type: 'typhoon' | 'earthquake' | 'flood' | 'volcanic' | 'advisory'; + severity: 'low' | 'moderate' | 'high' | 'critical'; + title: string; + description: string; + location?: string; + timestamp: Date; + source: string; + expiresAt?: Date; +} + +interface EmergencyAnnouncementsProps { + announcements?: EmergencyAnnouncement[]; +} + +export const EmergencyAnnouncements: React.FC = ({ + announcements = [], +}) => { + const [dismissedIds, setDismissedIds] = useState>(new Set()); + const [activeAnnouncements, setActiveAnnouncements] = useState< + EmergencyAnnouncement[] + >([]); + + useEffect(() => { + // Filter out dismissed and expired announcements + const now = new Date(); + const active = announcements.filter( + ann => + !dismissedIds.has(ann.id) && (!ann.expiresAt || ann.expiresAt > now) + ); + setActiveAnnouncements(active); + }, [announcements, dismissedIds]); + + const handleDismiss = (id: string) => { + setDismissedIds(prev => new Set([...prev, id])); + }; + + const getSeverityStyles = (severity: string) => { + switch (severity) { + case 'critical': + return 'bg-red-50 border-red-500 text-red-900'; + case 'high': + return 'bg-orange-50 border-orange-500 text-orange-900'; + case 'moderate': + return 'bg-yellow-50 border-yellow-500 text-yellow-900'; + default: + return 'bg-blue-50 border-blue-500 text-blue-900'; + } + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case 'typhoon': + return ; + case 'earthquake': + return ; + case 'volcanic': + return ; + default: + return ; + } + }; + + const formatTimestamp = (date: Date) => { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`; + if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`; + if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; + return 'Just now'; + }; + + if (activeAnnouncements.length === 0) return null; + + return ( +
+ {activeAnnouncements.map(announcement => ( +
+
+
+
+ {getTypeIcon(announcement.type)} +
+
+
+

+ {announcement.title} +

+ + {announcement.severity.toUpperCase()} + +
+

{announcement.description}

+
+ {announcement.location && ( +
+ + {announcement.location} +
+ )} +
+ + {formatTimestamp(announcement.timestamp)} +
+
Source: {announcement.source}
+
+
+
+ +
+
+ ))} +
+ ); +}; + +export default EmergencyAnnouncements; diff --git a/src/components/AIAssistant/IdeaSubmissionForm.tsx b/src/components/AIAssistant/IdeaSubmissionForm.tsx new file mode 100644 index 000000000..b61e28138 --- /dev/null +++ b/src/components/AIAssistant/IdeaSubmissionForm.tsx @@ -0,0 +1,209 @@ +import React, { useState } from 'react'; +import { Bot, Send, AlertCircle } from 'lucide-react'; +import { + findSimilarIdeas, + SimilarityResult, +} from '../../services/aiAssistant/duplicateDetection'; +import DuplicateDetectionModal from './DuplicateDetectionModal'; + +interface IdeaSubmissionFormProps { + existingIdeas: Array<{ + id: string; + title: string; + description: string; + category?: string; + }>; + onSubmit: (title: string, description: string, category: string) => void; + onClose: () => void; +} + +export const IdeaSubmissionForm: React.FC = ({ + existingIdeas, + onSubmit, + onClose, +}) => { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [category, setCategory] = useState(''); + const [showDuplicateModal, setShowDuplicateModal] = useState(false); + const [similarIdeas, setSimilarIdeas] = useState([]); + const [aiChecking, setAiChecking] = useState(false); + + const categories = [ + 'Transparency & Accountability', + 'Public Feedback', + 'Platform Development', + 'Political Accountability', + 'Government Services', + 'Emergency Response', + 'Data & Analytics', + 'Citizen Engagement', + ]; + + const handleCheckDuplicates = async () => { + if (!title || !description) { + alert('Please provide both title and description'); + return; + } + + setAiChecking(true); + + // Simulate AI processing time + await new Promise(resolve => setTimeout(resolve, 800)); + + const similar = findSimilarIdeas(title, description, existingIdeas, 0.3); + + setAiChecking(false); + + if (similar.length > 0) { + setSimilarIdeas(similar); + setShowDuplicateModal(true); + } else { + handleFinalSubmit(); + } + }; + + const handleFinalSubmit = () => { + onSubmit(title, description, category); + setTitle(''); + setDescription(''); + setCategory(''); + setShowDuplicateModal(false); + }; + + return ( + <> +
+
+
+
+
+
+ +
+
+

+ Submit New Idea with AI Assistant +

+

+ AI will check for duplicates before submission +

+
+
+ +
+
+ +
+
+
+ + setTitle(e.target.value)} + placeholder='Enter your idea title' + className='w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500' + /> +
+ +
+ +