Spaces:
Sleeping
Sleeping
Upload frontend/src/components/ChatWidget.jsx
Browse files
frontend/src/components/ChatWidget.jsx
CHANGED
|
@@ -9,9 +9,59 @@ const SUGGESTIONS = [
|
|
| 9 |
"Latest news about Indian startups 2025",
|
| 10 |
]
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
export default function ChatWidget({ onClose }) {
|
| 13 |
const [messages, setMessages] = useState([
|
| 14 |
-
{ role: 'assistant', content:
|
| 15 |
])
|
| 16 |
const [input, setInput] = useState('')
|
| 17 |
const [loading, setLoading] = useState(false)
|
|
@@ -51,9 +101,11 @@ export default function ChatWidget({ onClose }) {
|
|
| 51 |
throw new Error(`HTTP ${resp.status}: ${errText}`)
|
| 52 |
}
|
| 53 |
const data = await resp.json()
|
|
|
|
|
|
|
| 54 |
setMessages(prev => [...prev, {
|
| 55 |
role: 'assistant',
|
| 56 |
-
content:
|
| 57 |
}])
|
| 58 |
} catch (err) {
|
| 59 |
console.error('Chat error:', err)
|
|
@@ -77,7 +129,7 @@ export default function ChatWidget({ onClose }) {
|
|
| 77 |
<p className="text-[10px] text-atlas-muted">Powered by Qwen2.5-0.5B + Web Search</p>
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
-
<button onClick={onClose} className="text-atlas-muted hover:text-atlas-text text-lg">β</button>
|
| 81 |
</div>
|
| 82 |
|
| 83 |
{/* Messages */}
|
|
@@ -89,7 +141,11 @@ export default function ChatWidget({ onClose }) {
|
|
| 89 |
? 'bg-brand-500/20 text-brand-300 rounded-br-none'
|
| 90 |
: 'bg-atlas-surface text-atlas-muted rounded-bl-none'
|
| 91 |
}`}>
|
| 92 |
-
{m.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
))}
|
|
@@ -126,6 +182,7 @@ export default function ChatWidget({ onClose }) {
|
|
| 126 |
onChange={e => setInput(e.target.value)}
|
| 127 |
onKeyDown={e => e.key === 'Enter' && sendMessage(input)}
|
| 128 |
placeholder="Ask about startups, sectors, funding, latest news..."
|
|
|
|
| 129 |
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"
|
| 130 |
/>
|
| 131 |
<button onClick={() => sendMessage(input)} disabled={loading || !input.trim()}
|
|
|
|
| 9 |
"Latest news about Indian startups 2025",
|
| 10 |
]
|
| 11 |
|
| 12 |
+
// βββ XSS Prevention: escape HTML special chars ββββββββββββββββββββββββββββββββ
|
| 13 |
+
function escapeHtml(text) {
|
| 14 |
+
if (!text) return ''
|
| 15 |
+
return text
|
| 16 |
+
.replace(/&/g, '&')
|
| 17 |
+
.replace(/</g, '<')
|
| 18 |
+
.replace(/>/g, '>')
|
| 19 |
+
.replace(/"/g, '"')
|
| 20 |
+
.replace(/'/g, ''')
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// βββ XSS Prevention: strip script tags and event handlers βββββββββββββββββββββ
|
| 24 |
+
function sanitizeChatContent(text) {
|
| 25 |
+
if (!text) return ''
|
| 26 |
+
let t = text
|
| 27 |
+
// Remove script tags
|
| 28 |
+
t = t.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
| 29 |
+
// Remove iframe tags
|
| 30 |
+
t = t.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '')
|
| 31 |
+
// Remove event handlers (onerror, onclick, etc.)
|
| 32 |
+
t = t.replace(/\son\w+\s*=\s*["']?[^"'>]*["']?/gi, '')
|
| 33 |
+
// Remove javascript: and data: URLs
|
| 34 |
+
t = t.replace(/(javascript|data|vbscript):/gi, '')
|
| 35 |
+
// Remove meta refresh
|
| 36 |
+
t = t.replace(/<meta[^>]*http-equiv\s*=\s*["']?refresh["']?[^>]*>/gi, '')
|
| 37 |
+
return t
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// βββ Render sanitized text with basic Markdown (no raw HTML) ββββββββββββββββββ
|
| 41 |
+
function SafeMarkdown({ text }) {
|
| 42 |
+
if (!text) return null
|
| 43 |
+
const sanitized = sanitizeChatContent(text)
|
| 44 |
+
// Convert **bold**, *italic*, `code`, and bullet points to HTML safely
|
| 45 |
+
let html = escapeHtml(sanitized)
|
| 46 |
+
// Bold
|
| 47 |
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
| 48 |
+
// Italic
|
| 49 |
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
| 50 |
+
// Inline code
|
| 51 |
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
| 52 |
+
// Bullet points (simple)
|
| 53 |
+
.replace(/^\s*-\s+/gm, 'β’ ')
|
| 54 |
+
// Numbered lists
|
| 55 |
+
.replace(/^\s*(\d+)\.\s+/gm, '$1. ')
|
| 56 |
+
// Line breaks
|
| 57 |
+
.replace(/\n/g, '<br>')
|
| 58 |
+
|
| 59 |
+
return <span dangerouslySetInnerHTML={{ __html: html }} />
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
export default function ChatWidget({ onClose }) {
|
| 63 |
const [messages, setMessages] = useState([
|
| 64 |
+
{ role: 'assistant', content: "π Hi! I'm Bharat Tech Atlas AI. Ask me about Indian startups, sectors, funding trends, or any company!" }
|
| 65 |
])
|
| 66 |
const [input, setInput] = useState('')
|
| 67 |
const [loading, setLoading] = useState(false)
|
|
|
|
| 101 |
throw new Error(`HTTP ${resp.status}: ${errText}`)
|
| 102 |
}
|
| 103 |
const data = await resp.json()
|
| 104 |
+
// Sanitize response before displaying
|
| 105 |
+
const safeContent = sanitizeChatContent(data.content) || 'Sorry, I had trouble responding. Try again!'
|
| 106 |
setMessages(prev => [...prev, {
|
| 107 |
role: 'assistant',
|
| 108 |
+
content: safeContent
|
| 109 |
}])
|
| 110 |
} catch (err) {
|
| 111 |
console.error('Chat error:', err)
|
|
|
|
| 129 |
<p className="text-[10px] text-atlas-muted">Powered by Qwen2.5-0.5B + Web Search</p>
|
| 130 |
</div>
|
| 131 |
</div>
|
| 132 |
+
<button onClick={onClose} className="text-atlas-muted hover:text-atlas-text text-lg" aria-label="Close chat">β</button>
|
| 133 |
</div>
|
| 134 |
|
| 135 |
{/* Messages */}
|
|
|
|
| 141 |
? 'bg-brand-500/20 text-brand-300 rounded-br-none'
|
| 142 |
: 'bg-atlas-surface text-atlas-muted rounded-bl-none'
|
| 143 |
}`}>
|
| 144 |
+
{m.role === 'user' ? (
|
| 145 |
+
<span>{m.content}</span>
|
| 146 |
+
) : (
|
| 147 |
+
<SafeMarkdown text={m.content} />
|
| 148 |
+
)}
|
| 149 |
</div>
|
| 150 |
</div>
|
| 151 |
))}
|
|
|
|
| 182 |
onChange={e => setInput(e.target.value)}
|
| 183 |
onKeyDown={e => e.key === 'Enter' && sendMessage(input)}
|
| 184 |
placeholder="Ask about startups, sectors, funding, latest news..."
|
| 185 |
+
maxLength={2000}
|
| 186 |
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"
|
| 187 |
/>
|
| 188 |
<button onClick={() => sendMessage(input)} disabled={loading || !input.trim()}
|