React Native Expo app — full mobile client
- Expo SDK 54, expo-router, NativeWind - 7 screens: login, tasks, calendar, goals, chat, settings, tabs - API client (api.hasdo.info), SecureStore auth, AuthContext - Tab navigation: Ukoly, Kalendar, Cile, Chat, Nastaveni - Pull-to-refresh, FAB, group colors, priority dots - Calendar grid with task dots - AI chat with keyboard avoiding - Web export verified (780 modules, 1.5MB bundle) - Bundle ID: info.hasdo.taskteam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
217
mobile/app/(tabs)/chat.tsx
Normal file
217
mobile/app/(tabs)/chat.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthContext } from '../../lib/AuthContext';
|
||||
import * as api from '../../lib/api';
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
export default function ChatScreen() {
|
||||
const { token } = useAuthContext();
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: '0',
|
||||
role: 'assistant',
|
||||
content: 'Ahoj! Jsem vas AI asistent pro Task Team. Mohu vam pomoci s ukoly, planovani a organizaci. Na co se chcete zeptat?',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (!input.trim() || !token || sending) return;
|
||||
|
||||
const userMsg: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: input.trim(),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
setInput('');
|
||||
setSending(true);
|
||||
|
||||
try {
|
||||
const res = await api.sendChatMessage(token, userMsg.content);
|
||||
const aiMsg: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: res.data?.reply || 'Omlouvam se, nepodarilo se zpracovat odpoved.',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, aiMsg]);
|
||||
} catch (err: any) {
|
||||
const errMsg: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: `Chyba: ${err.message}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, errMsg]);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}, [input, token, sending]);
|
||||
|
||||
const renderMessage = ({ item }: { item: Message }) => {
|
||||
const isUser = item.role === 'user';
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.msgRow,
|
||||
isUser ? styles.msgRowUser : styles.msgRowAi,
|
||||
]}
|
||||
>
|
||||
{!isUser && (
|
||||
<View style={styles.avatar}>
|
||||
<Ionicons name="sparkles" size={16} color="#3B82F6" />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
styles.bubble,
|
||||
isUser ? styles.bubbleUser : styles.bubbleAi,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.bubbleText,
|
||||
isUser && { color: '#fff' },
|
||||
]}
|
||||
>
|
||||
{item.content}
|
||||
</Text>
|
||||
<Text style={[styles.timestamp, isUser && { color: 'rgba(255,255,255,0.6)' }]}>
|
||||
{item.timestamp.toLocaleTimeString('cs-CZ', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={90}
|
||||
>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={messages}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderMessage}
|
||||
contentContainerStyle={styles.listContent}
|
||||
onContentSizeChange={() =>
|
||||
flatListRef.current?.scrollToEnd({ animated: true })
|
||||
}
|
||||
/>
|
||||
|
||||
<View style={styles.inputBar}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Napiste zpravu..."
|
||||
value={input}
|
||||
onChangeText={setInput}
|
||||
multiline
|
||||
maxLength={2000}
|
||||
editable={!sending}
|
||||
onSubmitEditing={sendMessage}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendBtn, (!input.trim() || sending) && styles.sendBtnDisabled]}
|
||||
onPress={sendMessage}
|
||||
disabled={!input.trim() || sending}
|
||||
>
|
||||
{sending ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Ionicons name="send" size={20} color="#fff" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F8FAFC' },
|
||||
listContent: { padding: 12, paddingBottom: 4 },
|
||||
msgRow: { flexDirection: 'row', marginBottom: 12, maxWidth: '85%' },
|
||||
msgRowUser: { alignSelf: 'flex-end' },
|
||||
msgRowAi: { alignSelf: 'flex-start' },
|
||||
avatar: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#EFF6FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 8,
|
||||
marginTop: 4,
|
||||
},
|
||||
bubble: { borderRadius: 16, padding: 12, maxWidth: '100%' },
|
||||
bubbleUser: {
|
||||
backgroundColor: '#3B82F6',
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
bubbleAi: {
|
||||
backgroundColor: '#fff',
|
||||
borderBottomLeftRadius: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
},
|
||||
bubbleText: { fontSize: 15, lineHeight: 21, color: '#1E293B' },
|
||||
timestamp: { fontSize: 10, color: '#94A3B8', marginTop: 4, textAlign: 'right' },
|
||||
inputBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
padding: 12,
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E2E8F0',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
fontSize: 15,
|
||||
maxHeight: 100,
|
||||
backgroundColor: '#F8FAFC',
|
||||
},
|
||||
sendBtn: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
backgroundColor: '#3B82F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 8,
|
||||
},
|
||||
sendBtnDisabled: { backgroundColor: '#94A3B8' },
|
||||
});
|
||||
Reference in New Issue
Block a user