- App.tsx: full navigation (Auth stack + Main tabs with 5 screens) - Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen - HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors - MapScreen: interactive map with layer toggles (6 layers) - MarketplaceScreen: categories (6), products (5), search - ChatScreen: AI chat with quick prompts (4), bot responses - ProfileScreen: user info, stats, menu (9 items), logout - AlertsScreen: alert list with severity, acknowledge - SensorsScreen: sensor list with type filters (6 types), search - ZonesScreen: zone cards with stats - SettingsScreen: language picker (FR/EN/ES/DE), privacy, about - Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n - Hooks: useSensors, useAlerts, useNotifications, useLocation - Components: Card, Button, LoadingSpinner, ErrorBoundary, Header - Services: iotService, notificationService (with axios API client) - Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN) - Theme: colors.ts with full design system (Blue Ocean palette) - Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
245 lines
9.7 KiB
TypeScript
245 lines
9.7 KiB
TypeScript
// Smart App City — AI Chat Screen
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import {
|
|
View, Text, TextInput, TouchableOpacity,
|
|
ScrollView, StyleSheet, KeyboardAvoidingView, Platform,
|
|
} from 'react-native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
|
|
|
|
type Props = { navigation: NativeStackNavigationProp<any> };
|
|
|
|
interface Message {
|
|
id: string;
|
|
text: string;
|
|
isBot: boolean;
|
|
timestamp: Date;
|
|
}
|
|
|
|
const QUICK_PROMPTS = [
|
|
{ id: '1', text: '🌤️ Météo aujourd\'hui', prompt: 'Quelle est la météo à Martinique aujourd\'hui ?' },
|
|
{ id: '2', text: '🚌 Prochain bus', prompt: 'Quel est le prochain bus pour Fort-de-France ?' },
|
|
{ id: '3', text: '⚡ Consommation énergie', prompt: 'Quelle est ma consommation d\'énergie cette semaine ?' },
|
|
{ id: '4', text: '🚨 Alertes en cours', prompt: 'Y a-t-il des alertes en cours à Martinique ?' },
|
|
];
|
|
|
|
const BOT_RESPONSES: Record<string, string> = {
|
|
'🌤️ Météo': '☀️ Aujourd\'hui à Fort-de-France : 28°C, ensoleillé avec quelques nuages. Humidité 72%, vent 12 km/h NE. Indice UV 8 — pensez à la crème solaire !',
|
|
'🚌 Prochain bus': '🚌 Le prochain bus L1 (Centre-Ville → Schoelcher) arrive dans 8 min. Le L3 (Aimé Césaires) dans 12 min. Source : CFTA temps réel.',
|
|
'⚡ Consommation': '⚡ Votre consommation cette semaine : 45 kWh, soit 12% de moins que la semaine dernière. Estimation mensuelle : 180 kWh. Bon travail !',
|
|
'🚨 Alertes': '🚨 1 alerte en cours : Qualité de l\'air dégradée à Schoelcher (AQI 85). Évitez les efforts prolongés en extérieur. Pas d\'alerte météo.',
|
|
};
|
|
|
|
export default function ChatScreen({ navigation }: Props) {
|
|
const [messages, setMessages] = useState<Message[]>([
|
|
{
|
|
id: '0',
|
|
text: 'Bonjour ! Je suis Smart City IA 🤖\n\nJe peux vous aider avec :\n• 🌤️ Météo en temps réel\n• 🚌 Transports\n• ⚡ Énergie\n• 🚨 Alertes\n\nComment puis-je vous aider ?',
|
|
isBot: true,
|
|
timestamp: new Date(),
|
|
},
|
|
]);
|
|
const [input, setInput] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const scrollRef = useRef<ScrollView>(null);
|
|
|
|
useEffect(() => {
|
|
scrollRef.current?.scrollToEnd({ animated: true });
|
|
}, [messages]);
|
|
|
|
const sendMessage = (text: string) => {
|
|
if (!text.trim()) return;
|
|
|
|
const userMsg: Message = {
|
|
id: Date.now().toString(),
|
|
text: text.trim(),
|
|
isBot: false,
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
setMessages((prev) => [...prev, userMsg]);
|
|
setInput('');
|
|
setIsTyping(true);
|
|
|
|
// Simulate bot response
|
|
setTimeout(() => {
|
|
const response = getBotResponse(text);
|
|
const botMsg: Message = {
|
|
id: (Date.now() + 1).toString(),
|
|
text: response,
|
|
isBot: true,
|
|
timestamp: new Date(),
|
|
};
|
|
setMessages((prev) => [...prev, botMsg]);
|
|
setIsTyping(false);
|
|
}, 1000 + Math.random() * 1000);
|
|
};
|
|
|
|
const getBotResponse = (text: string): string => {
|
|
const lower = text.toLowerCase();
|
|
for (const [key, response] of Object.entries(BOT_RESPONSES)) {
|
|
if (lower.includes(key.slice(2).toLowerCase())) return response;
|
|
}
|
|
if (lower.includes('météo') || lower.includes('weather')) return BOT_RESPONSES['🌤️ Météo'];
|
|
if (lower.includes('bus') || lower.includes('transport')) return BOT_RESPONSES['🚌 Prochain bus'];
|
|
if (lower.includes('énergie') || lower.includes('energie') || lower.includes('consommation')) return BOT_RESPONSES['⚡ Consommation'];
|
|
if (lower.includes('alerte') || lower.includes('danger')) return BOT_RESPONSES['🚨 Alertes'];
|
|
return 'Je comprends votre question. Laissez-moi consulter les données en temps réel... 🔍\n\nPour l\'instant, voici les services disponibles : météo, transports, énergie, et alertes. Essayez l\'un des raccourcis ci-dessous !';
|
|
};
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.container}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
keyboardVerticalOffset={90}
|
|
>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View style={styles.botAvatar}>
|
|
<Text style={styles.botEmoji}>🤖</Text>
|
|
</View>
|
|
<View>
|
|
<Text style={styles.title}>Smart City IA</Text>
|
|
<View style={styles.statusRow}>
|
|
<View style={styles.statusDot} />
|
|
<Text style={styles.statusText}>En ligne</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Quick prompts */}
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.promptsScroll}>
|
|
{QUICK_PROMPTS.map((p) => (
|
|
<TouchableOpacity key={p.id} style={styles.promptChip} onPress={() => sendMessage(p.prompt)}>
|
|
<Text style={styles.promptText}>{p.text}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
|
|
{/* Messages */}
|
|
<ScrollView ref={scrollRef} style={styles.messagesList} showsVerticalScrollIndicator={false}>
|
|
{messages.map((msg) => (
|
|
<View key={msg.id} style={[styles.messageRow, msg.isBot ? styles.botRow : styles.userRow]}>
|
|
{msg.isBot && <View style={styles.botAvatarSmall}><Text style={styles.botEmojiSmall}>🤖</Text></View>}
|
|
<View style={[styles.messageBubble, msg.isBot ? styles.botBubble : styles.userBubble]}>
|
|
<Text style={[styles.messageText, msg.isBot ? styles.botText : styles.userText]}>{msg.text}</Text>
|
|
<Text style={[styles.messageTime, msg.isBot ? styles.botTime : styles.userTime]}>
|
|
{msg.timestamp.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
{isTyping && (
|
|
<View style={[styles.messageRow, styles.botRow]}>
|
|
<View style={styles.botAvatarSmall}><Text style={styles.botEmojiSmall}>🤖</Text></View>
|
|
<View style={[styles.messageBubble, styles.botBubble]}>
|
|
<Text style={styles.typingText}>Écrit...</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
<View style={{ height: 20 }} />
|
|
</ScrollView>
|
|
|
|
{/* Input */}
|
|
<View style={styles.inputBar}>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Posez votre question..."
|
|
placeholderTextColor={Colors.neutral400}
|
|
value={input}
|
|
onChangeText={setInput}
|
|
multiline
|
|
maxLength={500}
|
|
/>
|
|
<TouchableOpacity
|
|
style={[styles.sendBtn, !input.trim() && { opacity: 0.4 }]}
|
|
onPress={() => sendMessage(input)}
|
|
disabled={!input.trim() || isTyping}
|
|
>
|
|
<Text style={styles.sendIcon}>➤</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: Colors.neutral50 },
|
|
header: {
|
|
flexDirection: 'row', alignItems: 'center',
|
|
backgroundColor: Colors.primary[500],
|
|
paddingTop: 50, paddingBottom: Spacing.base,
|
|
paddingHorizontal: Spacing.base,
|
|
gap: Spacing.base,
|
|
},
|
|
botAvatar: {
|
|
width: 44, height: 44, borderRadius: BorderRadius.lg,
|
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
justifyContent: 'center', alignItems: 'center',
|
|
},
|
|
botEmoji: { fontSize: 24 },
|
|
title: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.white },
|
|
statusRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 },
|
|
statusDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: Colors.success },
|
|
statusText: { fontSize: Typography.sizes.xs, color: 'rgba(255,255,255,0.8)' },
|
|
promptsScroll: { padding: Spacing.base },
|
|
promptChip: {
|
|
backgroundColor: Colors.white, borderRadius: BorderRadius.full,
|
|
paddingHorizontal: Spacing.base, paddingVertical: Spacing.sm,
|
|
marginRight: Spacing.sm, borderWidth: 1, borderColor: Colors.neutral200,
|
|
},
|
|
promptText: { fontSize: Typography.sizes.sm, color: Colors.neutral700 },
|
|
messagesList: { flex: 1, paddingHorizontal: Spacing.base },
|
|
messageRow: { flexDirection: 'row', marginBottom: Spacing.sm, gap: Spacing.sm },
|
|
botRow: { justifyContent: 'flex-start' },
|
|
userRow: { justifyContent: 'flex-end' },
|
|
botAvatarSmall: {
|
|
width: 28, height: 28, borderRadius: BorderRadius.md,
|
|
backgroundColor: Colors.primary[100],
|
|
justifyContent: 'center', alignItems: 'center',
|
|
},
|
|
botEmojiSmall: { fontSize: 14 },
|
|
messageBubble: {
|
|
maxWidth: '75%', borderRadius: BorderRadius.lg,
|
|
padding: Spacing.base, ...Shadows.sm,
|
|
},
|
|
botBubble: {
|
|
backgroundColor: Colors.white,
|
|
borderBottomLeftRadius: 4,
|
|
},
|
|
userBubble: {
|
|
backgroundColor: Colors.primary[500],
|
|
borderBottomRightRadius: 4,
|
|
},
|
|
messageText: { fontSize: Typography.sizes.base, lineHeight: 20 },
|
|
botText: { color: Colors.neutral900 },
|
|
userText: { color: Colors.white },
|
|
messageTime: { fontSize: 9, marginTop: 4 },
|
|
botTime: { color: Colors.neutral400 },
|
|
userTime: { color: 'rgba(255,255,255,0.6)' },
|
|
typingText: { color: Colors.neutral400, fontStyle: 'italic' },
|
|
inputBar: {
|
|
flexDirection: 'row', alignItems: 'flex-end',
|
|
backgroundColor: Colors.white,
|
|
paddingHorizontal: Spacing.base,
|
|
paddingVertical: Spacing.sm,
|
|
borderTopWidth: 1, borderTopColor: Colors.neutral200,
|
|
gap: Spacing.sm,
|
|
},
|
|
input: {
|
|
flex: 1, backgroundColor: Colors.neutral50,
|
|
borderRadius: BorderRadius.full,
|
|
paddingHorizontal: Spacing.base,
|
|
paddingVertical: Spacing.sm,
|
|
fontSize: Typography.sizes.base,
|
|
color: Colors.neutral900,
|
|
maxHeight: 100,
|
|
},
|
|
sendBtn: {
|
|
width: 40, height: 40, borderRadius: 20,
|
|
backgroundColor: Colors.primary[500],
|
|
justifyContent: 'center', alignItems: 'center',
|
|
},
|
|
sendIcon: { color: Colors.white, fontSize: 18 },
|
|
});
|