import React, { useState, useRef, useEffect, useCallback } from 'react' const SUGGESTIONS = [ "What are the top fintech unicorns?", "Tell me about SaaS startups in Bangalore", "What is DPIIT recognition?", "Which sectors are growing fastest?", "Compare edtech vs healthtech", "Latest news about Indian startups 2025", ] // ─── XSS Prevention: escape HTML special chars ──────────────────────────────── function escapeHtml(text) { if (!text) return '' return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } // ─── XSS Prevention: strip script tags and event handlers ───────────────────── function sanitizeChatContent(text) { if (!text) return '' let t = text // Remove script tags t = t.replace(/]*>.*?<\/script>/gi, '') // Remove iframe tags t = t.replace(/]*>.*?<\/iframe>/gi, '') // Remove event handlers (onerror, onclick, etc.) t = t.replace(/\son\w+\s*=\s*["']?[^"'>]*["']?/gi, '') // Remove javascript: and data: URLs t = t.replace(/(javascript|data|vbscript):/gi, '') // Remove meta refresh t = t.replace(/]*http-equiv\s*=\s*["']?refresh["']?[^>]*>/gi, '') return t } // ─── Render sanitized text with basic Markdown (no raw HTML) ────────────────── function SafeMarkdown({ text }) { if (!text) return null const sanitized = sanitizeChatContent(text) // Convert **bold**, *italic*, `code`, and bullet points to HTML safely let html = escapeHtml(sanitized) // Bold .replace(/\*\*(.+?)\*\*/g, '$1') // Italic .replace(/\*(.+?)\*/g, '$1') // Inline code .replace(/`([^`]+)`/g, '$1') // Bullet points (simple) .replace(/^\s*-\s+/gm, '• ') // Numbered lists .replace(/^\s*(\d+)\.\s+/gm, '$1. ') // Line breaks .replace(/\n/g, '
') return } export default function ChatWidget({ onClose }) { const [messages, setMessages] = useState([ { role: 'assistant', content: "👋 Hi! I'm Bharat Tech Atlas AI. Ask me about Indian startups, sectors, funding trends, or any company!" } ]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const scrollRef = useRef(null) // Use a ref to always read the latest messages without stale closure issues const messagesRef = useRef(messages) useEffect(() => { messagesRef.current = messages }, [messages]) useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) const sendMessage = useCallback(async (text) => { if (!text.trim() || loading) return const userMsg = { role: 'user', content: text } // 1) Optimistically add user message to UI setMessages(prev => [...prev, userMsg]) setInput('') setLoading(true) // 2) Build request body from the ref (guaranteed up-to-date) const currentMessages = [...messagesRef.current, userMsg] const payload = { messages: currentMessages.map(m => ({ role: m.role, content: m.content })), stream: false, } try { const resp = await fetch('/api/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) if (!resp.ok) { const errText = await resp.text() throw new Error(`HTTP ${resp.status}: ${errText}`) } const data = await resp.json() // Sanitize response before displaying const safeContent = sanitizeChatContent(data.content) || 'Sorry, I had trouble responding. Try again!' setMessages(prev => [...prev, { role: 'assistant', content: safeContent }]) } catch (err) { console.error('Chat error:', err) setMessages(prev => [...prev, { role: 'assistant', content: '⚠️ Network error. Please try again in a moment.' }]) } finally { setLoading(false) } }, [loading]) return (
{/* Header */}
🤖

Bharat Tech Atlas AI

Powered by Qwen2.5-0.5B + Web Search

{/* Messages */}
{messages.map((m, i) => (
{m.role === 'user' ? ( {m.content} ) : ( )}
))} {loading && (
)}
{/* Suggestions */} {messages.length < 3 && (
{SUGGESTIONS.map(s => ( ))}
)} {/* Input */}
setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && sendMessage(input)} placeholder="Ask about startups, sectors, funding, latest news..." maxLength={2000} className="flex-1 bg-atlas-surface border border-atlas-border rounded-lg px-3 py-2 text-xs text-atlas-text placeholder:text-atlas-muted/50 focus:outline-none focus:border-brand-500/50" />
) }