- 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>
218 lines
5.7 KiB
TypeScript
218 lines
5.7 KiB
TypeScript
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' },
|
|
});
|