207 lines
7.2 KiB
TypeScript
207 lines
7.2 KiB
TypeScript
import React, {useState, useRef} from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
FlatList,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
ActivityIndicator,
|
|
TextInput,
|
|
Alert,
|
|
} from 'react-native';
|
|
|
|
interface ChatMessage {
|
|
id: string;
|
|
from: 'user' | 'bot';
|
|
text: string;
|
|
time: string;
|
|
}
|
|
|
|
const QUICK_REPLIES = [
|
|
{id: 'status', label: '📊 Kondisi Hari Ini'},
|
|
{id: 'perawatan', label: '🌿 Saran Perawatan'},
|
|
{id: 'nutrisi', label: '🌾 Cek Nutrisi'},
|
|
{id: 'suhu', label: '🌡️ Cek Suhu'},
|
|
];
|
|
|
|
/**
|
|
* PENTING: Gunakan HTTPS untuk Ngrok.
|
|
* Tambahkan 'ngrok-skip-browser-warning': 'true' di headers agar APK bisa tembus.
|
|
*/
|
|
const BOT_API_URL = 'http://202.10.40.129:3000/api/bot/chat';
|
|
|
|
const BotTab: React.FC = () => {
|
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
{
|
|
id: '1',
|
|
from: 'bot',
|
|
text: 'Halo! Saya Bot Greenhouse PT Agrofilia Permata 🌱\n\nAda yang bisa saya bantu terkait budidaya vanili Anda hari ini?',
|
|
time: new Date().toLocaleTimeString('id-ID', {hour: '2-digit', minute: '2-digit'}),
|
|
},
|
|
]);
|
|
const [inputText, setInputText] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const flatListRef = useRef<FlatList>(null);
|
|
|
|
const handleChat = async (input: string) => {
|
|
if (!input.trim()) return;
|
|
|
|
const now = new Date().toLocaleTimeString('id-ID', {hour: '2-digit', minute: '2-digit'});
|
|
|
|
const userMsg: ChatMessage = {
|
|
id: Date.now().toString(),
|
|
from: 'user',
|
|
text: input,
|
|
time: now,
|
|
};
|
|
setMessages(prev => [...prev, userMsg]);
|
|
setInputText('');
|
|
setIsTyping(true);
|
|
|
|
try {
|
|
// Menambahkan timeout agar aplikasi tidak hang jika sinyal buruk
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
|
|
const response = await fetch(BOT_API_URL, {
|
|
method: 'POST',
|
|
signal: controller.signal,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
// Header Wajib untuk Ngrok agar tidak kena blokir halaman peringatan
|
|
'ngrok-skip-browser-warning': 'true',
|
|
},
|
|
body: JSON.stringify({message: input}),
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server Error: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
const botMsg: ChatMessage = {
|
|
id: (Date.now() + 1).toString(),
|
|
from: 'bot',
|
|
text: result.reply || 'Maaf, saya tidak mendapatkan jawaban.',
|
|
time: now,
|
|
};
|
|
setMessages(prev => [...prev, botMsg]);
|
|
} catch (err: any) {
|
|
console.error('Connection Error:', err);
|
|
|
|
let errorText = '⚠️ Koneksi gagal. Pastikan server laptop aktif dan Ngrok berjalan.';
|
|
if (err.name === 'AbortError') {
|
|
errorText = '⚠️ Request Timeout. Koneksi internet terlalu lambat.';
|
|
}
|
|
|
|
const errMsg: ChatMessage = {
|
|
id: (Date.now() + 1).toString(),
|
|
from: 'bot',
|
|
text: errorText,
|
|
time: now,
|
|
};
|
|
setMessages(prev => [...prev, errMsg]);
|
|
} finally {
|
|
setIsTyping(false);
|
|
setTimeout(() => flatListRef.current?.scrollToEnd({animated: true}), 100);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.container}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
keyboardVerticalOffset={90}>
|
|
|
|
<FlatList
|
|
ref={flatListRef}
|
|
data={messages}
|
|
keyExtractor={item => item.id}
|
|
contentContainerStyle={styles.chatList}
|
|
onContentSizeChange={() => flatListRef.current?.scrollToEnd({animated: true})}
|
|
renderItem={({item}) => (
|
|
<View style={[styles.bubbleWrapper, item.from === 'user' ? styles.bubbleRight : styles.bubbleLeft]}>
|
|
{item.from === 'bot' && (
|
|
<View style={styles.botAvatar}>
|
|
<Text>🤖</Text>
|
|
</View>
|
|
)}
|
|
<View style={[styles.bubble, item.from === 'user' ? styles.bubbleUser : styles.bubbleBot]}>
|
|
<Text style={styles.chatText}>{item.text}</Text>
|
|
<Text style={styles.chatTime}>{item.time}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
/>
|
|
|
|
{isTyping && (
|
|
<View style={styles.typingIndicator}>
|
|
<ActivityIndicator size="small" color="#4ADE80" />
|
|
<Text style={styles.typingText}>Bot sedang menganalisis...</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.quickReplyContainer}>
|
|
<FlatList
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
data={QUICK_REPLIES}
|
|
keyExtractor={item => item.id}
|
|
renderItem={({item}) => (
|
|
<TouchableOpacity
|
|
style={styles.quickReplyBtn}
|
|
onPress={() => handleChat(item.label)}>
|
|
<Text style={styles.quickReplyText}>{item.label}</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.inputSection}>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Tanya kondisi vanili..."
|
|
placeholderTextColor="#6B9E7A"
|
|
value={inputText}
|
|
onChangeText={setInputText}
|
|
onSubmitEditing={() => handleChat(inputText)}
|
|
/>
|
|
<TouchableOpacity style={styles.sendBtn} onPress={() => handleChat(inputText)}>
|
|
<Text style={styles.sendBtnText}>Kirim</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
</KeyboardAvoidingView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {flex: 1, backgroundColor: '#0D1A12', paddingBottom: 80},
|
|
chatList: {padding: 16, paddingBottom: 20},
|
|
bubbleWrapper: {flexDirection: 'row', marginBottom: 15, alignItems: 'flex-end'},
|
|
bubbleLeft: {justifyContent: 'flex-start'},
|
|
bubbleRight: {justifyContent: 'flex-end'},
|
|
botAvatar: {width: 30, height: 30, borderRadius: 15, backgroundColor: '#1A3A28', alignItems: 'center', justifyContent: 'center', marginRight: 8},
|
|
bubble: {maxWidth: '80%', borderRadius: 15, padding: 12},
|
|
bubbleBot: {backgroundColor: '#1A3A28', borderBottomLeftRadius: 2},
|
|
bubbleUser: {backgroundColor: '#22C55E', borderBottomRightRadius: 2},
|
|
chatText: {color: '#FFFFFF', fontSize: 14, lineHeight: 20},
|
|
chatTime: {color: '#6B9E7A', fontSize: 10, marginTop: 4, textAlign: 'right'},
|
|
typingIndicator: {flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: 10, gap: 8},
|
|
typingText: {color: '#4ADE80', fontSize: 12, fontStyle: 'italic'},
|
|
quickReplyContainer: {paddingVertical: 10, borderTopWidth: 0.5, borderTopColor: '#2D5A3D'},
|
|
quickReplyBtn: {backgroundColor: '#14532D', paddingHorizontal: 15, paddingVertical: 8, borderRadius: 20, marginLeft: 12, borderWidth: 1, borderColor: '#2D5A3D'},
|
|
quickReplyText: {color: '#4ADE80', fontSize: 12, fontWeight: '600'},
|
|
inputSection: {flexDirection: 'row', padding: 12, backgroundColor: '#1A3A28', alignItems: 'center'},
|
|
input: {flex: 1, backgroundColor: '#0D1A12', borderRadius: 25, paddingHorizontal: 15, color: '#FFF', height: 45},
|
|
sendBtn: {marginLeft: 10, backgroundColor: '#22C55E', paddingVertical: 10, paddingHorizontal: 20, borderRadius: 25},
|
|
sendBtnText: {color: '#FFF', fontWeight: 'bold'},
|
|
});
|
|
|
|
export default BotTab; |