Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef, useCallback } from 'react'; | |
| import { useLocation, useNavigate } from 'react-router-dom'; | |
| import { Input } from '@/src/components/ui/Input'; | |
| import { Button } from '@/src/components/ui/Button'; | |
| import { Badge } from '@/src/components/ui/Badge'; | |
| import { | |
| Send, Bot, User, Sparkles, Loader2, X, MessageCircle, | |
| ScrollText, BookOpen, Scale, FileText, AlertTriangle, | |
| Droplets, MapPin, TrendingUp, Clock, | |
| Shield, Waves, Database, Building2, FileSearch, | |
| Search, ChevronRight, Star, ArrowRight, HelpCircle | |
| } from 'lucide-react'; | |
| import { cn } from '@/src/utils/cn'; | |
| import { getPopularGRs, mockGRs } from '@/src/data/mockGRs'; | |
| type DocumentType = 'GR' | 'Act' | 'Circular' | 'Manual' | 'SOP' | 'Report'; | |
| interface Document { | |
| id: string; | |
| title: string; | |
| type: DocumentType; | |
| date: string; | |
| department: string; | |
| relevance: number; | |
| summary?: string; | |
| } | |
| interface Insight { | |
| title: string; | |
| value: string; | |
| trend?: string; | |
| trendUp?: boolean; | |
| icon: React.ElementType; | |
| color: string; | |
| } | |
| type Message = { | |
| id: string; | |
| role: 'user' | 'assistant'; | |
| content: string; | |
| sources?: Document[]; | |
| insights?: Insight[]; | |
| timestamp?: Date; | |
| }; | |
| // Extended mock document database | |
| const mockDocuments: Document[] = [ | |
| { id: 'GR-2024-WRD-089', title: 'Flood Relief Compensation Guidelines 2024', type: 'GR', date: '2024-07-15', department: 'Relief & Rehabilitation', relevance: 95, summary: 'Compensation rates: ₹5,000/acre for crop loss, ₹95,000 for house damage' }, | |
| { id: 'GR-2024-WRD-056', title: 'Emergency Dam Safety Protocol', type: 'GR', date: '2024-06-20', department: 'Dam Safety', relevance: 92, summary: 'Mandatory 2-hour advance notification for downstream districts' }, | |
| { id: 'ACT-1976-012', title: 'Maharashtra Irrigation Act', type: 'Act', date: '1976-05-15', department: 'Legislative', relevance: 88, summary: 'Governs water distribution, irrigation rights, and canal maintenance' }, | |
| { id: 'CIRC-2024-WRD-023', title: 'Monsoon Preparedness Circular', type: 'Circular', date: '2024-05-01', department: 'Operations', relevance: 85, summary: 'Pre-monsoon checklist for all dam control rooms' }, | |
| { id: 'GR-2023-WRD-124', title: 'Dam Gate Operations SOP', type: 'SOP', date: '2023-08-10', department: 'Operations', relevance: 90, summary: 'Standard procedures for gate opening/closing with safety checks' }, | |
| { id: 'MANUAL-2024-001', title: 'Flood Management Field Manual', type: 'Manual', date: '2024-03-15', department: 'Training', relevance: 82, summary: 'Field guide for emergency response teams' }, | |
| { id: 'GR-2024-WRD-042', title: 'Water Resource Allocation Policy', type: 'GR', date: '2024-04-20', department: 'Planning', relevance: 78, summary: 'Priority: Drinking > Agriculture > Industrial use' }, | |
| { id: 'ACT-2005-WRD-003', title: 'MWRRA Act', type: 'Act', date: '2005-08-12', department: 'Legislative', relevance: 75, summary: 'Regulatory authority for water pricing and distribution' }, | |
| { id: 'CIRC-2024-WRD-015', title: 'Real-time Monitoring Guidelines', type: 'Circular', date: '2024-04-10', department: 'IT & Operations', relevance: 80, summary: 'IoT sensor deployment for live dam monitoring' }, | |
| { id: 'RPT-2024-001', title: 'Koyna Dam Safety Audit', type: 'Report', date: '2024-06-01', department: 'Dam Safety', relevance: 94, summary: 'Structural integrity: GOOD | Seismic risk: MODERATE' }, | |
| ]; | |
| // GR Helper Quick Actions | |
| const grQuickActions = [ | |
| { text: 'Find latest flood relief GR', icon: Search, color: 'blue' }, | |
| { text: 'Explain GR-2024-WRD-089', icon: FileText, color: 'indigo' }, | |
| { text: 'Compare compensation GRs', icon: Scale, color: 'purple' }, | |
| { text: 'Dam safety protocols', icon: Shield, color: 'red' }, | |
| ]; | |
| // Query suggestions with categories - Focused on rainfall regions and GR rules | |
| const queryCategories = [ | |
| { | |
| title: 'Rainfall & Weather', | |
| icon: Droplets, | |
| color: 'blue', | |
| queries: [ | |
| { text: 'Konkan region rainfall data', icon: Droplets }, | |
| { text: 'Vidarbha drought relief GR', icon: FileText }, | |
| ] | |
| }, | |
| { | |
| title: 'GR Rules & Acts', | |
| icon: Scale, | |
| color: 'purple', | |
| queries: [ | |
| { text: 'Water distribution priority rules', icon: Shield }, | |
| { text: 'Canal maintenance responsibility', icon: ScrollText }, | |
| ] | |
| }, | |
| { | |
| title: 'Relief & Compensation', | |
| icon: Waves, | |
| color: 'red', | |
| queries: [ | |
| { text: 'Flood affected area compensation', icon: FileText }, | |
| { text: 'Crop damage claim procedure', icon: BookOpen }, | |
| ] | |
| }, | |
| { | |
| title: 'Administrative', | |
| icon: Building2, | |
| color: 'emerald', | |
| queries: [ | |
| { text: 'Tahsildar water rights powers', icon: Scale }, | |
| { text: 'WUA formation guidelines', icon: MapPin }, | |
| ] | |
| }, | |
| ]; | |
| // Generate insights based on query | |
| const generateInsights = (query: string): Insight[] => { | |
| const q = query.toLowerCase(); | |
| if (q.includes('koyna')) { | |
| return [ | |
| { title: 'Current Level', value: '92.4%', trend: '+2.1%', trendUp: true, icon: Droplets, color: 'text-blue-600' }, | |
| { title: 'Storage', value: '2.8 TMC', trend: 'of 3.0 TMC', icon: Database, color: 'text-cyan-600' }, | |
| { title: 'Inflow', value: '45,000', trend: 'cusecs', icon: Waves, color: 'text-indigo-600' }, | |
| { title: 'Outflow', value: '32,000', trend: 'cusecs', icon: Waves, color: 'text-orange-600' }, | |
| ]; | |
| } | |
| if (q.includes('flood') && q.includes('compensation')) { | |
| return [ | |
| { title: 'Crop Loss', value: '₹5,000', trend: 'per acre', icon: FileText, color: 'text-emerald-600' }, | |
| { title: 'House Damage', value: '₹95,000', trend: 'full damage', icon: Building2, color: 'text-blue-600' }, | |
| { title: 'Processing Time', value: '30', trend: 'days', icon: Clock, color: 'text-orange-600' }, | |
| { title: 'Claims 2024', value: '12,450', trend: '+23%', trendUp: true, icon: TrendingUp, color: 'text-purple-600' }, | |
| ]; | |
| } | |
| if (q.includes('dam') && q.includes('gate')) { | |
| return [ | |
| { title: 'Active Releases', value: '12', trend: '+3 today', trendUp: true, icon: Waves, color: 'text-blue-600' }, | |
| { title: 'Critical Dams', value: '3', trend: 'High alert', icon: AlertTriangle, color: 'text-red-600' }, | |
| { title: 'Total Outflow', value: '1.2M', trend: 'cusecs', icon: Droplets, color: 'text-cyan-600' }, | |
| { title: 'Downstream Alert', value: '18', trend: 'districts', icon: Shield, color: 'text-orange-600' }, | |
| ]; | |
| } | |
| return [ | |
| { title: 'Total Dams', value: '287', trend: 'monitored', icon: Database, color: 'text-blue-600' }, | |
| { title: 'GRs 2024', value: '142', trend: 'indexed', icon: FileText, color: 'text-emerald-600' }, | |
| { title: 'Active Alerts', value: '7', trend: 'critical', icon: AlertTriangle, color: 'text-red-600' }, | |
| { title: 'Avg Storage', value: '68%', trend: '+5%', trendUp: true, icon: Droplets, color: 'text-cyan-600' }, | |
| ]; | |
| }; | |
| // Generate response based on query | |
| const generateResponse = (query: string): { content: string; docs: Document[] } => { | |
| const q = query.toLowerCase(); | |
| if (q.includes('koyna')) { | |
| return { | |
| content: `**Koyna Dam - Current Status (as of ${new Date().toLocaleTimeString('en-IN')})** | |
| • **Storage Level:** 92.4% (2.8 TMC of 3.0 TMC capacity) | |
| • **Inflow:** 45,000 cusecs | **Outflow:** 32,000 cusecs | |
| • **Status:** ⚠️ CRITICAL - Emergency discharge initiated | |
| • **Districts Alerted:** Satara, Sangli, Kolhapur collectors notified | |
| **Recent Actions:** | |
| - 4 radial gates opened at 50% capacity | |
| - Downstream evacuation advisory issued | |
| - Hourly monitoring activated | |
| Refer to **GR-2024-WRD-056** for emergency protocols.`, | |
| docs: mockDocuments.filter(d => d.id.includes('056') || d.id.includes('RPT-2024-001')) | |
| }; | |
| } | |
| if (q.includes('flood') && q.includes('compensation')) { | |
| return { | |
| content: `**Flood Relief Compensation - 2024 Guidelines (GR-2024-WRD-089)** | |
| **Eligibility Criteria:** | |
| • Residence in notified flood-affected area | |
| • Damage between July 1 - October 31, 2024 | |
| • Registration with local Tahsildar office | |
| **Compensation Rates:** | |
| • Crop Loss: ₹5,000 per acre (max 10 acres) | |
| • House Damage: ₹95,000 (full) / ₹50,000 (partial) | |
| • Livestock: ₹50,000 per animal (max 5) | |
| **Application Process:** | |
| 1. Submit Form WRD-FLOOD-01 to Tahsildar | |
| 2. Attach damage assessment report | |
| 3. Processing time: 30 days | |
| 4. Direct bank transfer | |
| Contact: District Relief Office for assistance.`, | |
| docs: mockDocuments.filter(d => d.id.includes('089') || d.id.includes('001')) | |
| }; | |
| } | |
| if (q.includes('gate') || q.includes('sop')) { | |
| return { | |
| content: `**Dam Gate Operations SOP (GR-2023-WRD-124)** | |
| **Pre-Opening Checklist:** | |
| ✓ Seismic sensors calibrated | |
| ✓ Downstream flow sensors active | |
| ✓ Alert systems tested | |
| ✓ Control room staffed (min 2 engineers) | |
| **Opening Procedure:** | |
| 1. **Notification** (T-2 hours): Alert downstream districts | |
| 2. **Gradual Opening**: 10% increments every 15 minutes | |
| 3. **Flow Monitoring**: Continuous discharge measurement | |
| 4. **Public Announcement**: Local media + sirens | |
| **Emergency Override:** | |
| Chief Engineer can authorize immediate release if dam level exceeds 95%. | |
| **Documentation:** All gate operations logged in DAMS system within 1 hour.`, | |
| docs: mockDocuments.filter(d => d.id.includes('124') || d.id.includes('056')) | |
| }; | |
| } | |
| if (q.includes('irrigation') || q.includes('act')) { | |
| return { | |
| content: `**Maharashtra Irrigation Act, 1976 - Key Provisions** | |
| **Section 33 - Water Rights:** | |
| • Priority order: Drinking water > Agriculture > Industry | |
| • Canal water distribution by WRD, not local bodies | |
| • Penalty for unauthorized diversion: Up to 3 years imprisonment | |
| **Section 45 - Maintenance:** | |
| • WRD responsible for major repairs (>₹1 lakh) | |
| • Water User Associations (WUAs) for minor maintenance | |
| • 5% water cess collected for maintenance fund | |
| **Recent Amendment (2024):** | |
| • IoT-based usage monitoring mandated | |
| • Real-time allocation adjustments during scarcity | |
| • Digital grievance portal: wrd.maharashtra.gov.in/grievance`, | |
| docs: mockDocuments.filter(d => d.type === 'Act') | |
| }; | |
| } | |
| const relevantDocs = mockDocuments | |
| .filter(d => q.includes(d.type.toLowerCase()) || | |
| d.title.toLowerCase().includes(q.split(' ')[0])) | |
| .slice(0, 3); | |
| return { | |
| content: `I found **${relevantDocs.length} relevant document(s)** for your query on "${query}". | |
| ${relevantDocs.map(d => `**${d.id}** - ${d.title}\n• ${d.summary || 'No summary available'}`).join('\n\n')} | |
| Would you like me to provide detailed information about any of these documents, or search for something more specific?`, | |
| docs: relevantDocs | |
| }; | |
| }; | |
| export function Chatbot() { | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [isMobileOpen, setIsMobileOpen] = useState(false); | |
| const [showWelcome, setShowWelcome] = useState(true); | |
| const [showSidebar, setShowSidebar] = useState(true); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| const popularGRs = getPopularGRs(); | |
| useEffect(() => { | |
| const searchParams = new URLSearchParams(location.search); | |
| const query = searchParams.get('q'); | |
| if (query) { | |
| handleSend(query); | |
| setShowWelcome(false); | |
| } | |
| }, [location]); | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [messages]); | |
| // Generate unique ID to avoid React key duplicates | |
| const generateId = useCallback(() => { | |
| return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| }, []); | |
| const handleSend = async (text: string) => { | |
| if (!text.trim()) return; | |
| setShowWelcome(false); | |
| const userMsgId = generateId(); | |
| const userMsg: Message = { | |
| id: userMsgId, | |
| role: 'user', | |
| content: text, | |
| timestamp: new Date() | |
| }; | |
| setMessages(prev => [...prev, userMsg]); | |
| setInput(''); | |
| setIsLoading(true); | |
| await new Promise(r => setTimeout(r, 800)); | |
| const { content, docs } = generateResponse(text); | |
| const insights = generateInsights(text); | |
| const aiMsg: Message = { | |
| id: generateId(), | |
| role: 'assistant', | |
| content, | |
| sources: docs, | |
| insights, | |
| timestamp: new Date() | |
| }; | |
| setMessages(prev => [...prev, aiMsg]); | |
| setIsLoading(false); | |
| }; | |
| const getTypeIcon = (type: DocumentType) => { | |
| switch (type) { | |
| case 'GR': return ScrollText; | |
| case 'Act': return Scale; | |
| case 'Circular': return FileText; | |
| case 'Manual': return BookOpen; | |
| case 'SOP': return Shield; | |
| case 'Report': return FileSearch; | |
| } | |
| }; | |
| const formatTime = (date?: Date) => { | |
| if (!date) return ''; | |
| return date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }); | |
| }; | |
| const WelcomeScreen = () => ( | |
| <div className="flex flex-col justify-center gap-4 h-full w-full max-w-2xl mx-auto py-2"> | |
| {/* Top Section - Title */} | |
| <div className="text-center"> | |
| <div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center mx-auto mb-2 shadow-md"> | |
| <Sparkles className="h-6 w-6 text-white" /> | |
| </div> | |
| <h2 className="text-lg font-semibold text-slate-800">GR Helper</h2> | |
| <p className="text-xs text-slate-500 mt-1">AI Assistant for Government Resolutions & Acts</p> | |
| </div> | |
| {/* GR Quick Actions */} | |
| <div className="px-2 mt-2"> | |
| <p className="text-xs font-medium text-slate-500 mb-2 px-1">Quick Actions</p> | |
| <div className="grid grid-cols-2 gap-2"> | |
| {grQuickActions.map((action, idx) => { | |
| const Icon = action.icon; | |
| return ( | |
| <button | |
| key={idx} | |
| onClick={() => handleSend(action.text)} | |
| className="text-left p-3 bg-white rounded-lg border border-slate-200 hover:border-indigo-300 hover:shadow-sm active:bg-slate-50 transition-all" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <div className={cn("p-1 rounded", `bg-${action.color}-100`)}> | |
| <Icon className={cn("h-3.5 w-3.5", `text-${action.color}-600`)} /> | |
| </div> | |
| <span className="text-xs font-medium text-slate-700 line-clamp-1">{action.text}</span> | |
| </div> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Middle Section - Category Cards */} | |
| <div className="px-2 mt-2"> | |
| <p className="text-xs font-medium text-slate-500 mb-2 px-1">Browse by Category</p> | |
| <div className="grid grid-cols-2 gap-2"> | |
| {queryCategories.map((cat, idx) => { | |
| const Icon = cat.icon; | |
| return ( | |
| <button | |
| key={idx} | |
| onClick={() => handleSend(cat.queries[0].text)} | |
| className="text-left p-3 bg-white rounded-lg border border-slate-200 hover:border-blue-300 active:bg-slate-50 transition-all shadow-sm" | |
| > | |
| <div className="flex items-center gap-2 mb-1"> | |
| <div className={cn("p-1 rounded", `bg-${cat.color}-100`)}> | |
| <Icon className={cn("h-3.5 w-3.5", `text-${cat.color}-600`)} /> | |
| </div> | |
| <span className="text-xs font-semibold text-slate-700">{cat.title}</span> | |
| </div> | |
| <p className="text-[10px] text-slate-500 truncate">{cat.queries[0].text}</p> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Bottom Section - Quick Links */} | |
| <div className="flex flex-wrap justify-center gap-1.5 px-2 mt-2"> | |
| {['Konkan rainfall', 'Vidarbha GR', 'Water rights', 'Compensation'].map((s, i) => ( | |
| <button | |
| key={i} | |
| onClick={() => handleSend(s)} | |
| className="text-[10px] px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full hover:bg-slate-200 transition-colors" | |
| > | |
| {s} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| const RecentGRsSidebar = () => ( | |
| <div className={cn( | |
| "w-64 border-l border-slate-200 bg-white flex-col shrink-0 hidden lg:flex", | |
| showSidebar ? "lg:flex" : "lg:hidden" | |
| )}> | |
| <div className="p-3 border-b border-slate-100"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-xs font-semibold text-slate-700">Popular GRs</h3> | |
| <Star className="h-3 w-3 text-yellow-500" /> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-2 space-y-1.5"> | |
| {popularGRs.map(gr => ( | |
| <button | |
| key={gr.id} | |
| onClick={() => handleSend(`Explain ${gr.id}: ${gr.title}`)} | |
| className="w-full text-left p-2 rounded-md hover:bg-slate-50 transition-colors group" | |
| > | |
| <div className="flex items-start justify-between gap-1"> | |
| <p className="text-[10px] font-mono text-indigo-600">{gr.id}</p> | |
| <ChevronRight className="h-3 w-3 text-slate-300 group-hover:text-indigo-500 shrink-0 mt-0.5" /> | |
| </div> | |
| <p className="text-xs text-slate-700 line-clamp-2 mt-0.5">{gr.title}</p> | |
| <div className="flex items-center gap-1 mt-1"> | |
| <Badge className="text-[8px] px-1 py-0 h-auto bg-slate-100 text-slate-600"> | |
| {gr.category} | |
| </Badge> | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| <div className="p-2 border-t border-slate-100"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="w-full text-xs h-8" | |
| onClick={() => navigate('/gr-finder')} | |
| > | |
| <Search className="h-3 w-3 mr-1" /> | |
| Browse All GRs | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| const MessageBubble = ({ msg }: { msg: Message }) => ( | |
| <div className={cn( | |
| "flex w-full mb-4", | |
| msg.role === 'user' ? "justify-end" : "justify-start" | |
| )}> | |
| <div className={cn( | |
| "flex gap-3 max-w-[90%] md:max-w-[80%]", | |
| msg.role === 'user' ? "flex-row-reverse" : "flex-row" | |
| )}> | |
| <div className={cn( | |
| "flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center", | |
| msg.role === 'user' | |
| ? "bg-blue-600 text-white" | |
| : "bg-gradient-to-br from-blue-500 to-indigo-600 text-white" | |
| )}> | |
| {msg.role === 'user' ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4" />} | |
| </div> | |
| <div className="flex flex-col gap-2"> | |
| <div className={cn( | |
| "px-4 py-3 rounded-2xl text-sm leading-relaxed", | |
| msg.role === 'user' | |
| ? "bg-blue-600 text-white rounded-tr-sm" | |
| : "bg-white border border-slate-200 text-slate-700 rounded-tl-sm shadow-sm" | |
| )}> | |
| <div className="whitespace-pre-wrap"> | |
| {msg.content.split('**').map((part, i) => | |
| i % 2 === 0 ? part : <strong key={i} className="font-semibold">{part}</strong> | |
| )} | |
| </div> | |
| </div> | |
| {msg.insights && msg.insights.length > 0 && ( | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-1"> | |
| {msg.insights.map((insight, idx) => { | |
| const Icon = insight.icon; | |
| return ( | |
| <div key={idx} className="bg-white border border-slate-200 rounded-lg p-2.5 shadow-sm"> | |
| <div className="flex items-center gap-1.5 mb-1"> | |
| <Icon className={cn("h-3.5 w-3.5", insight.color)} /> | |
| <span className="text-[10px] text-slate-500 uppercase tracking-wider">{insight.title}</span> | |
| </div> | |
| <div className="flex items-baseline gap-1"> | |
| <span className="text-lg font-bold text-slate-800">{insight.value}</span> | |
| {insight.trend && ( | |
| <span className={cn( | |
| "text-[10px]", | |
| insight.trendUp ? "text-emerald-600" : "text-slate-400" | |
| )}> | |
| {insight.trend} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| {msg.sources && msg.sources.length > 0 && ( | |
| <div className="flex flex-wrap gap-1.5 mt-1"> | |
| {msg.sources.map((doc, idx) => { | |
| const TypeIcon = getTypeIcon(doc.type); | |
| return ( | |
| <div | |
| key={idx} | |
| className="flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 bg-slate-100 text-slate-600 rounded-md border border-slate-200" | |
| > | |
| <TypeIcon className="h-3 w-3" /> | |
| <span className="font-medium">{doc.id}</span> | |
| <span className="text-slate-400">|</span> | |
| <span className="truncate max-w-[120px]">{doc.title}</span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| <span className="text-[10px] text-slate-400 ml-1"> | |
| {formatTime(msg.timestamp)} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| return ( | |
| <> | |
| {/* Desktop */} | |
| <div className="hidden md:flex h-full w-full flex-row bg-slate-50 overflow-hidden rounded-lg min-h-0"> | |
| {/* Main Chat Area */} | |
| <div className="flex-1 flex flex-col min-w-0"> | |
| <div className="bg-white border-b border-slate-200 px-4 py-3 shrink-0"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center shadow-sm"> | |
| <Sparkles className="h-4 w-4 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="text-base font-semibold text-slate-900">GR Helper</h1> | |
| <p className="text-[11px] text-slate-500">AI Assistant for Government Resolutions</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="text-xs h-8" | |
| onClick={() => navigate('/faq')} | |
| > | |
| <HelpCircle className="h-3.5 w-3.5 mr-1" /> | |
| FAQ | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="text-xs h-8" | |
| onClick={() => navigate('/gr-finder')} | |
| > | |
| <Search className="h-3.5 w-3.5 mr-1" /> | |
| GR Finder | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-hidden px-4 py-4 min-h-0"> | |
| {showWelcome && messages.length === 0 ? ( | |
| <WelcomeScreen /> | |
| ) : ( | |
| <div className="max-w-3xl mx-auto h-full overflow-y-auto pr-2 min-h-0"> | |
| {messages.map((msg, i) => ( | |
| <React.Fragment key={msg.id || String(i)}> | |
| <MessageBubble msg={msg} /> | |
| </React.Fragment> | |
| ))} | |
| {isLoading && ( | |
| <div className="flex items-center gap-3 text-slate-500"> | |
| <div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center"> | |
| <Bot className="h-4 w-4 text-white" /> | |
| </div> | |
| <div className="flex items-center gap-2 bg-white border border-slate-200 px-4 py-2.5 rounded-2xl rounded-tl-sm shadow-sm"> | |
| <Loader2 className="h-4 w-4 animate-spin text-blue-500" /> | |
| <span className="text-sm">Searching GR database...</span> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="bg-white border-t border-slate-200 px-4 py-3 shrink-0"> | |
| <div className="max-w-3xl mx-auto"> | |
| <form onSubmit={(e) => { e.preventDefault(); handleSend(input); }} className="flex gap-3"> | |
| <div className="flex-1 relative"> | |
| <Input | |
| ref={inputRef} | |
| type="text" | |
| placeholder="Ask about GRs, Acts, compensation, flood relief..." | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| className="h-11 pl-4 pr-10 bg-slate-50 border-slate-200 text-sm focus:bg-white focus:border-blue-500 transition-all rounded-xl" | |
| disabled={isLoading} | |
| /> | |
| <div className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400"> | |
| <FileSearch className="h-4 w-4" /> | |
| </div> | |
| </div> | |
| <Button | |
| type="submit" | |
| disabled={!input.trim() || isLoading} | |
| className="h-11 px-5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white text-sm font-medium rounded-xl shadow-sm" | |
| > | |
| <Send className="h-4 w-4 mr-1.5" /> | |
| Send | |
| </Button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Sidebar with Popular GRs */} | |
| <RecentGRsSidebar /> | |
| </div> | |
| {/* Mobile */} | |
| <> | |
| {!isMobileOpen && ( | |
| <button | |
| onClick={() => setIsMobileOpen(true)} | |
| className="fixed bottom-20 right-4 z-50 w-14 h-14 bg-gradient-to-br from-blue-600 to-indigo-600 text-white rounded-full shadow-lg flex items-center justify-center md:hidden" | |
| > | |
| <MessageCircle className="h-6 w-6" /> | |
| </button> | |
| )} | |
| {isMobileOpen && ( | |
| <div className="fixed inset-0 z-[60] bg-slate-50 flex flex-col md:hidden"> | |
| <div className="bg-white border-b border-slate-200 px-4 py-3 flex items-center justify-between"> | |
| <div className="flex items-center gap-2.5"> | |
| <div className="w-9 h-9 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center"> | |
| <Sparkles className="h-4 w-4 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="font-semibold text-slate-900 text-sm">GR Helper</h1> | |
| <p className="text-[10px] text-slate-500">AI Assistant for Government GRs</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => setIsMobileOpen(false)} | |
| className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100" | |
| > | |
| <X className="h-5 w-5 text-slate-500" /> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-hidden px-3 py-1 min-h-0"> | |
| {showWelcome && messages.length === 0 ? ( | |
| <div className="flex flex-col justify-center gap-4 h-full w-full max-w-sm mx-auto"> | |
| {/* Top */} | |
| <div className="text-center"> | |
| <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center mx-auto mb-2 shadow-md"> | |
| <Sparkles className="h-5 w-5 text-white" /> | |
| </div> | |
| <h2 className="text-base font-semibold text-slate-800">GR Helper</h2> | |
| <p className="text-[11px] text-slate-500 mt-1">AI Assistant for Government GRs</p> | |
| </div> | |
| {/* Quick Actions */} | |
| <div className="px-2"> | |
| <p className="text-[10px] font-medium text-slate-500 mb-1.5 px-1">Quick Actions</p> | |
| <div className="grid grid-cols-2 gap-1.5"> | |
| {grQuickActions.slice(0, 4).map((action, idx) => ( | |
| <button | |
| key={idx} | |
| onClick={() => handleSend(action.text)} | |
| className="text-left p-2 bg-white rounded-lg border border-slate-200 active:bg-slate-50" | |
| > | |
| <div className="flex items-center gap-1.5"> | |
| <action.icon className={cn("h-3 w-3", `text-${action.color}-600`)} /> | |
| <span className="text-[10px] font-medium text-slate-700 line-clamp-1">{action.text}</span> | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Middle - Cards */} | |
| <div className="px-2"> | |
| <p className="text-[10px] font-medium text-slate-500 mb-1.5 px-1">Browse by Category</p> | |
| <div className="grid grid-cols-2 gap-1.5"> | |
| {queryCategories.map((cat, idx) => ( | |
| <button | |
| key={idx} | |
| onClick={() => handleSend(cat.queries[0].text)} | |
| className="text-left p-2 bg-white rounded-lg border border-slate-200 active:bg-slate-50" | |
| > | |
| <div className="flex items-center gap-1.5 mb-0.5"> | |
| <div className={cn("p-0.5 rounded", `bg-${cat.color}-100`)}> | |
| <cat.icon className={cn("h-3 w-3", `text-${cat.color}-600`)} /> | |
| </div> | |
| <span className="text-[10px] font-medium text-slate-700">{cat.title}</span> | |
| </div> | |
| <p className="text-[9px] text-slate-500 truncate">{cat.queries[0].text}</p> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Bottom - Quick Links */} | |
| <div className="flex flex-wrap justify-center gap-1.5 px-2"> | |
| {['Konkan rainfall', 'Vidarbha GR', 'Water rights', 'Compensation'].map((s, i) => ( | |
| <button | |
| key={i} | |
| onClick={() => handleSend(s)} | |
| className="text-[10px] px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full" | |
| > | |
| {s} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="w-full h-full overflow-y-auto space-y-3 min-h-0"> | |
| {messages.map((msg, i) => ( | |
| <React.Fragment key={msg.id || String(i)}> | |
| <MessageBubble msg={msg} /> | |
| </React.Fragment> | |
| ))} | |
| {isLoading && ( | |
| <div className="flex items-center gap-2 text-slate-500"> | |
| <div className="w-7 h-7 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center"> | |
| <Bot className="h-3.5 w-3.5 text-white" /> | |
| </div> | |
| <div className="flex items-center gap-2 bg-white border border-slate-200 px-3 py-2 rounded-xl rounded-tl-sm"> | |
| <Loader2 className="h-3 w-3 animate-spin text-blue-500" /> | |
| <span className="text-xs">Searching...</span> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="bg-white border-t border-slate-200 px-4 py-2 pb-4 shrink-0 safe-area-bottom"> | |
| <form onSubmit={(e) => { e.preventDefault(); handleSend(input); }} className="flex gap-2"> | |
| <Input | |
| type="text" | |
| placeholder="Ask about GRs, Acts, compensation..." | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| className="flex-1 h-10 bg-slate-50 text-sm rounded-xl" | |
| disabled={isLoading} | |
| /> | |
| <Button | |
| type="submit" | |
| size="icon" | |
| disabled={!input.trim() || isLoading} | |
| className="h-10 w-10 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl" | |
| > | |
| <Send className="h-4 w-4" /> | |
| </Button> | |
| </form> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| </> | |
| ); | |
| } |