/
telthy
/
SOVA_AI
Обзор
Документация
Войти
/
telthy
/
SOVA_AI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
child
App.js
891 строка
26 KB
telthy
upload files
18 окт 2025, 16:47
18 окт 2025, 16:47
3c5c9ef
Код
Авторство
О чём код?
import React, { useState, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, SafeAreaView, FlatList, TouchableOpacity, TextInput, Modal, Switch, Alert, ScrollView, Dimensions, StatusBar, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; const { width, height } = Dimensions.get('window'); // === ЦВЕТА === const COLORS = { primary: '#000000ff', // Зелёный secondary: '#5e1e6aff', button: '#ffffffff', white: '#FFFFFF', black: '#000000', grey: '#808080', card: '#FFFFFF', textPrimary: '#000000', textSecondary: '#666666', }; export default function ChildApp() { const [messages, setMessages] = useState([ { id: '0', text: '👋 Привет! Я твой друг-помощник!', sender: 'bot' } ]); const [buttons, setButtons] = useState([ "Запланировать", "Покажи мой день"]); const [slotButtons, setSlotButtons] = useState([]); const [botMood, setBotMood] = useState('😊'); const [showSettings, setShowSettings] = useState(false); const [showStats, setShowStats] = useState(false); const [showRequests, setShowRequests] = useState(false); // ✅ Новое состояние const [serverIP, setServerIP] = useState('192.168.68.58'); const [textInputEnabled, setTextInputEnabled] = useState(true); const [textInput, setTextInput] = useState(''); const [statsData, setStatsData] = useState(null); const [requests, setRequests] = useState([]); // ✅ Запросы от родителей const flatListRef = useRef(null); const scrollToBottom = () => { setTimeout(() => { flatListRef.current?.scrollToEnd({ animated: true }); }, 100); }; useEffect(() => { scrollToBottom(); }, [messages]); const sendMessage = async (text) => { if (!text.trim()) return; const userMsg = { id: Date.now().toString(), text, sender: 'user' }; setMessages(prev => [...prev, userMsg]); setTextInput(''); setButtons([]); setSlotButtons([]); try { const response = await fetch(`http://${serverIP}:5000/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ message: text, user_id: "default" }), }); const data = await response.json(); const botMsg = { id: (Date.now() + 1).toString(), text: data.reply, sender: 'bot' }; setMessages(prev => [...prev, botMsg]); if (data.reply.includes('отлично') || data.reply.includes('ура') || data.reply.includes('✅')) { setBotMood('🎉'); } else if (data.reply.includes('ошибка') || data.reply.includes('❌')) { setBotMood('😢'); } else { setBotMood('😊'); } if (data.show_buttons) { setButtons(data.show_buttons); } if (data.show_slot_buttons) { const lines = data.reply.split('\n'); const slotLines = lines.filter(line => /^\d+\./.test(line)); const slots = slotLines.map((line, i) => ({ id: i + 1, text: line.replace(/^\d+\.\s*/, ''), })); setSlotButtons(slots); } } catch (error) { const errorMsg = { id: (Date.now() + 1).toString(), text: '🤖 Ой! Что-то сломалось. Попробуй ещё раз!', sender: 'bot', }; setMessages(prev => [...prev, errorMsg]); setBotMood('🤔'); } }; const renderMessage = ({ item }) => ( <View style={[ styles.message, item.sender === 'user' ? styles.userMessage : styles.botMessage ]}> <Text style ={[item.sender === 'user' ? styles.messageText : styles.messageText_bot]} >{item.text}</Text> </View> ); // ✅ Очистка чата const clearChat = async () => { console.log("Очистка чата..."); setMessages([]); // ✅ Это работает? try { console.log("Очистка запрос..."); const response = await fetch(`http://${serverIP}:5000/clear_chat`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ user_id: "default" }), }); const data = await response.json(); console.log("Ответ сервера:", data); // ✅ Добавь это } catch (error) { console.log("Ошибка:", error); // ✅ Добавь это } }; // ✅ Сохранение настроек const saveSettings = () => { if (!serverIP.trim()) { Alert.alert("Ошибка", "Введите IP-адрес сервера"); return; } setShowSettings(false); Alert.alert("Готово", "Настройки сохранены!"); }; // ✅ Получение статистики const loadStats = async () => { try { const response = await fetch(`http://${serverIP}:5000/stats`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ user_id: "default" }), }); const data = await response.json(); if (data.success) { setStatsData(data.stats); setShowStats(true); } else { Alert.alert("Ошибка", data.error || "Не удалось загрузить статистику"); } } catch (error) { Alert.alert("Ошибка", "Не удалось подключиться к серверу"); } }; // ✅ Получение запросов от родителей const loadRequests = async () => { try { console.log("Загружаю запросы..."); const response = await fetch(`http://${serverIP}:5000/get_child_requests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // ❌ Убираем body }); const data = await response.json(); console.log("Ответ сервера:", data); if (data.success) { setRequests(data.requests); setShowRequests(true); } else { Alert.alert("Ошибка", data.error); } } catch (error) { console.log("Ошибка загрузки запросов:", error); Alert.alert("Ошибка", "Не удалось подключиться к серверу"); } }; const respondToRequest = async (requestId, response) => { try { const res = await fetch(`http://${serverIP}:5000/respond_to_request`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ child_id: "default", request_id: requestId, response: response, }), }); const data = await res.json(); if (data.success) { Alert.alert("Готово", "Ответ отправлен"); loadRequests(); // Обновляем список } else { Alert.alert("Ошибка", data.error); } } catch (error) { Alert.alert("Ошибка", "Не удалось отправить ответ"); } }; const openDialog = () => { sendMessage("Привет!"); }; return ( <SafeAreaView style={styles.container}> <LinearGradient colors={['rgba(78, 172, 240, 1)', 'rgba(47, 106, 227, 1)', 'rgba(52, 120, 230, 1)', 'rgba(117, 52, 230, 1)']} style={styles.linearGradient} useAngle={true} angle={45} angleCenter={{x:0.5,y:0.5}}> {/* Статус-бар */} <StatusBar barStyle="light-content" backgroundColor={COLORS.primary} text= "СОВА"/> {/* Шапка с ботом */} <View style={styles.header}> <Text style={styles.botTitle}>Сова</Text> <Text style={styles.botMood}>{botMood}</Text> </View> <FlatList ref={flatListRef} data={messages} keyExtractor={item => item.id} renderItem={renderMessage} contentContainerStyle={styles.list} onContentSizeChange={scrollToBottom} onLayout={scrollToBottom} /> {/* Кнопки слотов */} {slotButtons.length > 0 && ( <View style={styles.slotContainer}> <Text style={styles.slotTitle}>⏰ Выбери время:</Text> {slotButtons.map(slot => ( <TouchableOpacity key={slot.id} style={styles.slotButton} onPress={() => sendMessage(String(slot.id))} > <Text style={styles.slotText}>{slot.text}</Text> </TouchableOpacity> ))} </View> )} {/* Кнопки от бота */} {buttons.length > 0 && ( <View style={styles.buttonContainer}> {buttons.map((btn, index) => ( <TouchableOpacity key={index} style={styles.button} onPress={() => sendMessage(btn)} > <Text style={styles.buttonText}>{btn}</Text> </TouchableOpacity> ))} </View> )} {/* ✅ Поле ввода (если включено) */} {textInputEnabled && ( <View style={styles.inputContainer}> <TextInput style={styles.textInput} value={textInput} onChangeText={setTextInput} placeholder="💬 Напиши сообщение..." onSubmitEditing={() => sendMessage(textInput)} /> <TouchableOpacity style={styles.sendButton} onPress={() => sendMessage(textInput)} > <Text style={styles.sendButtonText}>➤</Text> </TouchableOpacity> </View> )} {(textInputEnabled == false) && (<View style={styles.inputContainer}> <TextInput style={styles.textInput} value={textInput} onChangeText={setTextInput} placeholder="🎙️" onSubmitEditing={() => sendMessage(textInput)} /> </View>)} {/* ✅ НИЖНЯЯ ПАНЕЛЬ (всегда видна) */} <View style={styles.bottomBar}> {/* Слева: Настройки */} <TouchableOpacity style={styles.bottomBarButton} onPress={() => setShowSettings(true)} > <Text style={styles.bottomBarIcon}>⚙️</Text> </TouchableOpacity> {/* Центр: Статистика */} <TouchableOpacity style={styles.bottomBarButton} onPress={loadStats} > <Text style={styles.bottomBarIcon}>📊</Text> </TouchableOpacity> {/* Справа: Диалог */} <TouchableOpacity style={styles.bottomBarButton} onPress={openDialog} > <Text style={styles.bottomBarIcon}>💬</Text> </TouchableOpacity> {/* Ещё правее: Запросы */} <TouchableOpacity style={styles.bottomBarButton} onPress={loadRequests} > <Text style={styles.bottomBarIcon}>📬</Text> </TouchableOpacity> </View> </LinearGradient> {/* ✅ Модальное окно настроек */} <Modal visible={showSettings} animationType="slide" transparent={true} > <View style={styles.modalOverlay}> <View style={styles.modalContent}> <Text style={styles.modalTitle}>⚙️ Настройки</Text> {/* IP сервера */} <Text style={styles.settingLabel}>🌐 IP-адрес сервера:</Text> <TextInput style={styles.ipInput} value={serverIP} onChangeText={setServerIP} placeholder="192.168.1.105" /> {/* Вкл/Выкл ввод */} <View style={styles.switchContainer}> <Text style={styles.settingLabel}>✏️ Показывать строку ввода:</Text> <Switch value={textInputEnabled} onValueChange={setTextInputEnabled} /> </View> {/* Кнопки */} <View style={styles.modalButtonRow}> <TouchableOpacity style={[styles.modalButton, { backgroundColor: '#dc3545' }]} onPress={clearChat} > <Text style={styles.modalButtonText}>🧹 Очистить чат</Text> </TouchableOpacity> <TouchableOpacity style={[styles.modalButton, { backgroundColor: COLORS.primary }]} onPress={saveSettings} > <Text style={styles.modalButtonText}>💾 Сохранить</Text> </TouchableOpacity> <TouchableOpacity style={[styles.modalButton, { backgroundColor: '#6c757d' }]} onPress={() => setShowSettings(false)} > <Text style={styles.modalButtonText}>❌ Закрыть</Text> </TouchableOpacity> </View> </View> </View> </Modal> {/* ✅ Модальное окно статистики */} <Modal visible={showStats} animationType="slide" transparent={true} > <View style={styles.modalOverlay}> <View style={styles.statsModalContent}> <Text style={styles.modalTitle}>📊 Твоя статистика за неделю</Text> {statsData ? ( <ScrollView style={styles.statsScroll}> {/* Общая статистика */} <View style={styles.statCard}> <Text style={styles.statTitle}>📈 Всего встреч:</Text> <Text style={styles.statValue}>{statsData.total_events}</Text> </View> <View style={styles.statCard}> <Text style={styles.statTitle}>🕒 Всего времени:</Text> <Text style={styles.statValue}>{statsData.total_duration} часов</Text> </View> <View style={styles.statCard}> <Text style={styles.statTitle}>🎮 Игры/Видео:</Text> <Text style={styles.statValue}>{statsData.game_time} часов</Text> </View> <View style={styles.statCard}> <Text style={styles.statTitle}>📚 Учёба:</Text> <Text style={styles.statValue}>{statsData.study_time} часов</Text> </View> <View style={styles.statCard}> <Text style={styles.statTitle}>😴 Отдых:</Text> <Text style={styles.statValue}>{statsData.rest_time} часов</Text> </View> {/* Статистика по дням */} <Text style={styles.sectionTitle}>📅 По дням недели:</Text> {Object.entries(statsData.daily_stats).map(([day, hours]) => ( <View key={day} style={styles.dailyStatRow}> <Text style={styles.dailyStatDay}>{day}</Text> <View style={styles.progressBarContainer}> <View style={[ styles.progressBarFill, { width: `${Math.min(hours * 10, 100)}%` } ]} /> </View> <Text style={styles.dailyStatHours}>{hours.toFixed(1)} ч</Text> </View> ))} </ScrollView> ) : ( <Text style={styles.loadingText}>Загрузка...</Text> )} <TouchableOpacity style={[styles.modalButton, { backgroundColor: '#6c757d', marginTop: 20 }]} onPress={() => setShowStats(false)} > <Text style={styles.modalButtonText}>❌ Закрыть</Text> </TouchableOpacity> </View> </View> </Modal> {/* ✅ Модальное окно запросов от родителей */} <Modal visible={showRequests} animationType="slide" transparent={true} > <View style={styles.modalOverlay}> <View style={styles.requestsModalContent}> <Text style={styles.modalTitle}>📬 Запросы от родителей</Text> {requests.length > 0 ? ( <ScrollView style={styles.requestsScroll}> {requests.map(req => ( <View key={req.id} style={styles.requestCard}> <Text style={styles.requestTitle}>{req.title}</Text> <Text style={styles.requestMeta}> {req.date} | {req.time} | {req.type} </Text> <Text style={styles.requestStatus}>Статус: {req.status}</Text> {req.status === "ожидание" && ( <View style={styles.requestActions}> <TouchableOpacity style={[styles.requestButton, { backgroundColor: '#28a745' }]} onPress={() => respondToRequest(req.id, "accepted")} > <Text style={styles.requestButtonText}>✅ Принять</Text> </TouchableOpacity> <TouchableOpacity style={[styles.requestButton, { backgroundColor: '#dc3545' }]} onPress={() => respondToRequest(req.id, "rejected")} > <Text style={styles.requestButtonText}>❌ Отклонить</Text> </TouchableOpacity> </View> )} </View> ))} </ScrollView> ) : ( <Text style={styles.noRequestsText}>Нет запросов</Text> )} <TouchableOpacity style={[styles.modalButton, { backgroundColor: '#6c757d', marginTop: 20 }]} onPress={() => setShowRequests(false)} > <Text style={styles.modalButtonText}>❌ Закрыть</Text> </TouchableOpacity> </View> </View> </Modal> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f0f8ff', }, linearGradient: { flex: 1, paddingLeft: 0, paddingRight: 0, borderRadius: 0, height: '50%', }, linearGradient_bottom: { flex: 1, paddingLeft: 0, paddingRight: 0, borderRadius: 0, backgroundColor: '#9234e0ff', }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 15, borderBottomLeftRadius: 20, borderBottomRightRadius: 20, }, botTitle: { fontSize: 18, fontWeight: 'bold', color: COLORS.white, }, botMood: { fontSize: 24, }, list: { paddingHorizontal: 10, paddingVertical: 5, paddingBottom: 120, // ✅ Меньше, потому что панель теперь 80px }, message: { maxWidth: '80%', padding: 15, marginVertical: 8, borderRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, userMessage: { alignSelf: 'flex-end', backgroundColor: COLORS.white, borderBottomRightRadius: 5, }, botMessage: { alignSelf: 'flex-start', backgroundColor: COLORS.secondary, borderBottomLeftRadius: 5, }, messageText: { color: COLORS.black, fontSize: 16, lineHeight: 22, }, messageText_bot: { color: COLORS.white, fontSize: 16, lineHeight: 22, }, slotContainer: { padding: 15, backgroundColor: '#fff', borderTopWidth: 1, borderColor: '#e0e0e0', }, slotTitle: { fontSize: 16, fontWeight: '600', marginBottom: 10, color: COLORS.primary, }, slotButton: { backgroundColor: COLORS.secondary, padding: 12, borderRadius: 15, marginBottom: 10, alignItems: 'center', }, slotText: { color: COLORS.white, fontSize: 14, fontWeight: '500', }, buttonContainer: { flexDirection: 'row', flexWrap: 'wrap', padding: 10, gap: 10, }, button: { flex: 1, minWidth: '45%', backgroundColor: COLORS.button, paddingVertical: 15, paddingHorizontal: 10, borderRadius: 15, alignItems: 'center', margin: 2, }, buttonText: { color: COLORS.black, fontSize: 15, fontWeight: '600', }, inputContainer: { flexDirection: 'row', padding: 10, borderTopWidth: 1, borderColor: '#ccc', }, textInput: { flex: 1, borderWidth: 1, borderColor: '#ccc', borderRadius: 20, paddingHorizontal: 15, paddingVertical: 10, fontSize: 16, backgroundColor: COLORS.white, }, sendButton: { justifyContent: 'center', alignItems: 'center', marginLeft: 10, }, sendButtonText: { fontSize: 24, color: COLORS.primary, }, // === НИЖНЯЯ ПАНЕЛЬ (всегда видна) === bottomBar: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', paddingVertical: 0, height: 80 }, bottomBarButton: { padding: 0, alignItems: 'center', justifyContent: 'center', }, bottomBarIcon: { fontSize: 28, }, // === МОДАЛЬНЫЕ ОКНА === modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.5)', }, modalContent: { width: '90%', backgroundColor: COLORS.card, borderRadius: 20, padding: 20, gap: 15, }, statsModalContent: { width: '90%', maxHeight: '80%', backgroundColor: COLORS.card, borderRadius: 20, padding: 20, }, requestsModalContent: { width: '90%', maxHeight: '80%', backgroundColor: COLORS.card, borderRadius: 20, padding: 20, }, modalTitle: { fontSize: 20, fontWeight: 'bold', textAlign: 'center', marginBottom: 20, color: COLORS.textPrimary, }, settingLabel: { fontSize: 16, fontWeight: '600', color: COLORS.textPrimary, }, ipInput: { borderWidth: 1, borderColor: '#ccc', borderRadius: 10, padding: 10, fontSize: 16, backgroundColor: COLORS.white, }, switchContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, modalButtonRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 10, }, modalButton: { flex: 1, paddingVertical: 12, paddingHorizontal: 10, borderRadius: 15, alignItems: 'center', }, modalButtonText: { color: COLORS.white, fontSize: 14, fontWeight: '600', }, // === СТАТИСТИКА === statsScroll: { maxHeight: 400, }, statCard: { backgroundColor: '#f8f9fa', padding: 15, borderRadius: 15, marginBottom: 15, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, statTitle: { fontSize: 16, fontWeight: '600', color: '#495057', }, statValue: { fontSize: 18, fontWeight: 'bold', color: COLORS.primary, }, sectionTitle: { fontSize: 18, fontWeight: 'bold', marginTop: 20, marginBottom: 15, color: '#343a40', }, dailyStatRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, dailyStatDay: { width: 40, fontSize: 14, fontWeight: '600', color: '#495057', }, progressBarContainer: { flex: 1, height: 10, backgroundColor: '#e9ecef', borderRadius: 5, marginHorizontal: 10, overflow: 'hidden', }, progressBarFill: { height: '100%', backgroundColor: COLORS.primary, }, dailyStatHours: { width: 60, textAlign: 'right', fontSize: 14, color: '#495057', }, loadingText: { textAlign: 'center', fontSize: 16, color: '#6c757d', marginTop: 20, }, // === ЗАПРОСЫ ОТ РОДИТЕЛЕЙ === requestsScroll: { maxHeight: 400, }, requestCard: { backgroundColor: '#f8f9fa', padding: 15, borderRadius: 15, marginBottom: 15, }, requestTitle: { fontSize: 16, fontWeight: '600', color: '#343a40', }, requestMeta: { fontSize: 14, color: '#6c757d', marginTop: 5, }, requestStatus: { fontSize: 14, fontWeight: '600', color: '#007BFF', marginTop: 5, }, requestActions: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 10, gap: 10, }, requestButton: { flex: 1, paddingVertical: 10, paddingHorizontal: 15, borderRadius: 10, alignItems: 'center', }, requestButtonText: { color: COLORS.white, fontSize: 14, fontWeight: '600', }, noRequestsText: { textAlign: 'center', fontSize: 16, color: '#6c757d', marginTop: 20, }, });