FengShui / frontend /src /components /Chatbot.jsx
Jaosub
Deploy to HF Space
5434776
Raw
History Blame Contribute Delete
8.06 kB
import React, { useState, useEffect, useRef } from 'react';
import { Send, Sparkles, MessageSquare, Compass, ShieldAlert } from 'lucide-react';
export default function Chatbot({ metrics, scores }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);
// Reset chat messages when metrics change (user clicks new location)
useEffect(() => {
if (metrics) {
setMessages([
{
role: 'assistant',
content: `สวัสดีครับ! ผมเป็นผู้ช่วยซินแสปัญญาประดิษฐ์และวิศวกรสิ่งแวดล้อม GIS ผมพร้อมช่วยวิเคราะห์ฮวงจุ้ยและโครงสร้างสิ่งแวดล้อมของ **${metrics.location_name}** (พิกัด: ${metrics.elevation_m}m, NDVI: ${metrics.ndvi}) ให้คุณแล้วครับ มีอะไรต้องการสอบถามเพิ่มเติมเกี่ยวกับการปรับสมดุล Yin-Yang, ปัญหา Sha Qi หรือแนวทางวิศวกรรมป้องกันในจุดนี้ไหมครับ?`
}
]);
}
}, [metrics]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, loading]);
const handleSend = async (e) => {
e.preventDefault();
if (!input.trim() || loading) return;
const userMessage = input;
setInput('');
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);
setLoading(true);
// Prepare history (limit to last 6 messages to keep tokens optimal)
const history = messages
.slice(-6)
.map((msg) => ({ role: msg.role, content: msg.content }));
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userMessage,
history: history,
metrics: metrics,
scores: scores
})
});
if (!response.ok) throw new Error('API server returned error');
const data = await response.json();
setMessages((prev) => [...prev, { role: 'assistant', content: data.response }]);
} catch (err) {
console.error(err);
setMessages((prev) => [
...prev,
{ role: 'assistant', content: 'ขออภัยครับ เกิดข้อผิดพลาดในการเชื่อมต่อระบบวิเคราะห์ AI กรุณาตรวจสอบว่าเซิร์ฟเวอร์ยังเปิดอยู่' }
]);
} finally {
setLoading(false);
}
};
return (
<div className="glass-panel" style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
border: '1px solid var(--border-gold-glass)'
}}>
{/* Chat Header */}
<div style={{
padding: '12px 16px',
background: 'rgba(255, 255, 255, 0.02)',
borderBottom: '1px solid var(--border-glass)',
display: 'flex',
alignItems: 'center',
gap: '10px'
}}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: '#50c878',
boxShadow: '0 0 8px #50c878'
}} />
<MessageSquare size={14} color="var(--color-gold)" />
<span style={{ fontSize: '13px', fontWeight: 'bold', color: 'var(--color-gold)' }}>
Typhoon Feng Shui Master AI
</span>
</div>
{/* Messages Terminal Area */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '16px',
display: 'flex',
flexDirection: 'column',
gap: '12px'
}}>
{messages.map((msg, index) => {
const isUser = msg.role === 'user';
return (
<div
key={index}
style={{
display: 'flex',
justifyContent: isUser ? 'flex-end' : 'flex-start',
width: '100%'
}}
>
<div style={{
maxWidth: '85%',
padding: '10px 14px',
borderRadius: isUser ? '12px 12px 0 12px' : '12px 12px 12px 0',
background: isUser ? 'rgba(212,175,55,0.08)' : 'rgba(255,255,255,0.03)',
border: isUser ? '1px solid rgba(212,175,55,0.15)' : '1px solid var(--border-glass)',
fontSize: '12.5px',
lineHeight: '1.5',
color: isUser ? 'var(--text-primary)' : '#e2e8f0',
whiteSpace: 'pre-wrap'
}}>
{!isUser && index === 0 && (
<div style={{ fontSize: '10px', color: 'var(--color-jade-light)', display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '6px', fontWeight: 'bold' }}>
<Sparkles size={10} /> CONTEXTUAL ANALYSIS
</div>
)}
{msg.content}
</div>
</div>
);
})}
{/* Loading Bubble */}
{loading && (
<div style={{ display: 'flex', justifyContent: 'flex-start', width: '100%' }}>
<div className="shimmer" style={{
maxWidth: '85%',
padding: '12px 24px',
borderRadius: '12px 12px 12px 0',
border: '1px solid var(--border-glass)',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ display: 'flex', gap: '4px' }}>
<span className="pulse-gold-active" style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'var(--color-gold)' }} />
<span className="pulse-gold-active" style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'var(--color-gold)', animationDelay: '0.2s' }} />
<span className="pulse-gold-active" style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'var(--color-gold)', animationDelay: '0.4s' }} />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Message Form */}
<form onSubmit={handleSend} style={{
padding: '12px',
background: 'rgba(0,0,0,0.2)',
borderTop: '1px solid var(--border-glass)',
display: 'flex',
gap: '8px'
}}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about Sha Qi, Yin-Yang, or remedies for this spot..."
disabled={loading}
style={{
flex: 1,
padding: '8px 12px',
backgroundColor: 'rgba(0,0,0,0.4)',
border: '1px solid var(--border-glass)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '12.5px',
fontFamily: 'var(--font-sans)',
outline: 'none'
}}
/>
<button
type="submit"
disabled={loading || !input.trim()}
style={{
padding: '8px 14px',
backgroundColor: 'var(--color-gold)',
color: 'var(--bg-obsidian)',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: (loading || !input.trim()) ? 0.5 : 1,
transition: 'var(--transition-smooth)'
}}
>
<Send size={14} />
</button>
</form>
</div>
);
}