Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
128 changes: 128 additions & 0 deletions src/components/AIAssistant/DuplicateDetectionModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-hidden'>
<div className='p-6 border-b border-gray-200'>
<div className='flex items-start justify-between'>
<div className='flex items-center'>
<div className='p-2 bg-yellow-100 rounded-lg mr-3'>
<AlertTriangle className='h-6 w-6 text-yellow-600' />
</div>
<div>
<h3 className='text-xl font-semibold text-gray-900'>
Similar Ideas Detected
</h3>
<p className='text-sm text-gray-600 mt-1'>
AI Assistant found existing ideas similar to yours
</p>
</div>
</div>
<button
onClick={onClose}
className='text-gray-400 hover:text-gray-600 transition-colors'
>
<X className='h-6 w-6' />
</button>
</div>
</div>

<div className='p-6 overflow-y-auto max-h-[50vh]'>
<div className='mb-4'>
<p className='text-gray-700'>
Your idea:{' '}
<span className='font-medium'>&ldquo;{newIdeaTitle}&rdquo;</span>
</p>
<p className='text-sm text-gray-600 mt-2'>
We found {similarIdeas.length} similar{' '}
{similarIdeas.length === 1 ? 'idea' : 'ideas'}. Consider reviewing
or contributing to existing ideas before creating a new one.
</p>
</div>

<div className='space-y-3'>
{similarIdeas.map(idea => (
<div
key={idea.id}
className='border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors'
>
<div className='flex items-start justify-between'>
<div className='flex-1'>
<h4 className='font-medium text-gray-900 mb-1'>
{idea.title}
</h4>
{idea.category && (
<span className='inline-block px-2 py-1 text-xs font-medium rounded bg-gray-100 text-gray-700'>
{idea.category}
</span>
)}
</div>
<div className='ml-4 text-right'>
<div
className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${getSimilarityColor(idea.similarity)}`}
>
{formatSimilarityPercentage(idea.similarity)}
</div>
<div className='text-xs text-gray-500 mt-1'>
{getSimilarityLevel(idea.similarity)} Match
</div>
</div>
</div>
</div>
))}
</div>
</div>

<div className='p-6 border-t border-gray-200 bg-gray-50'>
<div className='flex items-center justify-between'>
<p className='text-sm text-gray-600'>
Help reduce duplicates and strengthen existing ideas
</p>
<div className='flex gap-3'>
<button
onClick={onClose}
className='px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors'
>
Review Existing
</button>
<button
onClick={onProceed}
className='px-4 py-2 text-white bg-primary-600 rounded-md hover:bg-primary-700 transition-colors'
>
Continue Anyway
</button>
</div>
</div>
</div>
</div>
</div>
);
};

export default DuplicateDetectionModal;
144 changes: 144 additions & 0 deletions src/components/AIAssistant/EmergencyAnnouncements.tsx
Original file line number Diff line number Diff line change
@@ -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<EmergencyAnnouncementsProps> = ({
announcements = [],
}) => {
const [dismissedIds, setDismissedIds] = useState<Set<string>>(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 <Cloud className='h-5 w-5' />;
case 'earthquake':
return <Activity className='h-5 w-5' />;
case 'volcanic':
return <AlertTriangle className='h-5 w-5' />;
default:
return <AlertTriangle className='h-5 w-5' />;
}
};

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 (
<div className='space-y-3 mb-6'>
{activeAnnouncements.map(announcement => (
<div
key={announcement.id}
className={`border-l-4 rounded-lg p-4 ${getSeverityStyles(announcement.severity)}`}
>
<div className='flex items-start justify-between'>
<div className='flex items-start flex-1'>
<div className='mr-3 mt-0.5'>
{getTypeIcon(announcement.type)}
</div>
<div className='flex-1'>
<div className='flex items-center gap-2 mb-1'>
<h4 className='font-semibold text-sm'>
{announcement.title}
</h4>
<span
className={`px-2 py-0.5 text-xs font-medium rounded-full ${
announcement.severity === 'critical'
? 'bg-red-200 text-red-800'
: announcement.severity === 'high'
? 'bg-orange-200 text-orange-800'
: announcement.severity === 'moderate'
? 'bg-yellow-200 text-yellow-800'
: 'bg-blue-200 text-blue-800'
}`}
>
{announcement.severity.toUpperCase()}
</span>
</div>
<p className='text-sm mb-2'>{announcement.description}</p>
<div className='flex items-center gap-4 text-xs opacity-75'>
{announcement.location && (
<div className='flex items-center gap-1'>
<MapPin className='h-3 w-3' />
{announcement.location}
</div>
)}
<div className='flex items-center gap-1'>
<Clock className='h-3 w-3' />
{formatTimestamp(announcement.timestamp)}
</div>
<div>Source: {announcement.source}</div>
</div>
</div>
</div>
<button
onClick={() => handleDismiss(announcement.id)}
className='ml-4 text-gray-500 hover:text-gray-700'
aria-label='Dismiss announcement'
>
<X className='h-4 w-4' />
</button>
</div>
</div>
))}
</div>
);
};

export default EmergencyAnnouncements;
Loading
Loading