Files
task-team/mobile/app/(tabs)/chat.tsx
Admin db81100b5b 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>
2026-03-29 15:09:35 +00:00

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' },
});