Overview
The useSupportChat hook provides an intelligent support chat system that seamlessly integrates AI (Google Gemini) with human admin support. It features automatic AI responses, admin takeover detection, typing indicators, and real-time message synchronization via Firestore. The AI respects human agents and stays silent when an admin is actively helping.
Import
import { useSupportChat } from '@/hooks/useSupportChat';
Usage
const {
messages,
chatStatus,
inputText,
setInputText,
sendMessage,
initiateChat,
isAdminTyping,
isAiThinking
} = useSupportChat();
// Start a new chat session
await initiateChat("Necesito ayuda con mi reserva");
// Send a message
setInputText("¿Cómo cancelo una reserva?");
await sendMessage();
// Check if admin is responding
if (isAdminTyping) {
console.log("Admin está escribiendo...");
}
State Values
Chat State
Array of chat messages in chronological orderArray<{
id: string;
text: string;
senderRole: 'client' | 'admin';
sessionId: string;
createdAt: Timestamp;
isAi?: boolean; // true if sent by AI, undefined/false if human admin
}>
chatStatus
'loading' | 'open' | 'closed'
Current status of the chat:
loading: Initial state, fetching chat data
open: Active chat session
closed: No active chat session
ID of the current active chat session
Current text in the message input field
Indicator State
Indicates if a human admin is currently typing a response
Indicates if the AI is currently generating a response
Functions
initiateChat
initiateChat: (initialMessage: string | null = null) => Promise<void>
Starts a new support chat session:
Optional first message to send when opening chat
Process:
- Generates unique session ID using timestamp
- Creates or updates chat document in Firestore:
{
status: 'open',
activeSessionId: newSessionId,
adminUnreadCount: 1,
clientUnreadCount: 0,
lastUpdated: serverTimestamp(),
lastMessage: initialMessage || 'El usuario solicitó asistencia.',
adminIsTyping: false,
userInfo: {
uid: user.uid,
name: user.displayName || 'Usuario App',
email: user.email,
photoURL: user.photoURL || ''
}
}
- If initial message provided:
- Saves message to subcollection
- Triggers AI response (no conversation history yet)
Use Cases:
- Open chat without message:
initiateChat()
- Open with greeting:
initiateChat("Hola, necesito ayuda")
sendMessage
sendMessage: () => Promise<void>
Sends a message from the client:
Validation:
- Requires
inputText to be non-empty
- Requires active user session
- Requires active
currentSessionId
Process:
- Captures message text and clears input immediately
- Saves message to Firestore with
senderRole: 'client'
- Updates chat document:
{
lastMessage: msgToSend,
lastUpdated: serverTimestamp(),
adminUnreadCount: 1
}
- Intelligent AI Response Logic:
const adminMessages = messages.filter(m => m.senderRole === 'admin');
const lastAdminMessage = adminMessages[adminMessages.length - 1];
const isHumanInControl = lastAdminMessage && !lastAdminMessage.isAi;
if (!isAdminTyping && !isHumanInControl) {
handleAiResponse(msgToSend, currentSessionId, user.uid);
} else {
console.log("🤫 Shhh... ParkBot guarda silencio porque el humano está a cargo.");
}
AI Response Conditions:
- ✅ AI responds if: Admin is NOT typing AND no recent human messages
- ❌ AI stays silent if: Admin is typing OR human recently responded
handleAiResponse (internal)
handleAiResponse: (userMsg: string, sessionId: string, uid: string) => Promise<void>
Generates and sends an AI response using Google Gemini:
The user’s message to respond to
Process:
- Constructs prompt with system context + user question:
const fullPrompt = `${PARKIN_BOT_SYSTEM_PROMPT}\n\nPregunta del usuario: "${userMsg}"\n\nRespuesta de ParkBot:`;
- Calls Google Gemini API:
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: fullPrompt }] }]
}),
});
- Extracts AI response text from nested structure
- Saves message with
isAi: true flag:
await addDoc(collection(db, 'support_chats', uid, 'messages'), {
text: aiResponseText.trim(),
senderRole: 'admin',
isAi: true,
sessionId: sessionId,
createdAt: serverTimestamp()
});
- Updates chat document for admin dashboard
Error Handling:
- Logs detailed error information
- Shows alert if API fails
- Sets
isAiThinking to false in finally block
setInputText
setInputText: (text: string) => void
Sets the message input text.
AI System Prompt
The AI uses the PARKIN_BOT_SYSTEM_PROMPT which defines:
Personality:
- Amigable y eficiente
- Uses emojis (🚗, 🅿️, 🔧, 🎫)
- Short, mobile-friendly responses
- Spanish (Mexico)
Knowledge Base:
- Reservations: How to book spots, select date/time/vehicle
- Password Recovery: Steps to reset password
- Vehicles: Registering cars, managing plates
- Credits & Payments: Credit system, card management
- Cancellations: Cancellation penalties (100 credits)
- Location: API Seybaplaya, hours 7 AM - 11 PM
- Access Codes: QR/NIP code usage
Limitations:
- Admits when it doesn’t know something
- Suggests human agent escalation when needed
- Never invents information
Full prompt available in /constants/AiContext.ts
Real-time Listeners
Chat Status Listener
Monitors overall chat state and admin typing status:
useEffect(() => {
if (!user) return;
const chatDocRef = doc(db, 'support_chats', user.uid);
const unsubscribe = onSnapshot(chatDocRef, (docSnap) => {
if (docSnap.exists()) {
const data = docSnap.data();
setChatStatus(data.status || 'closed');
setCurrentSessionId(data.activeSessionId || null);
setIsAdminTyping(data.adminIsTyping || false);
// Auto-mark client messages as read
if (data.status === 'open' && data.clientUnreadCount > 0) {
updateDoc(chatDocRef, { clientUnreadCount: 0 });
}
} else {
setChatStatus('closed');
}
});
return () => unsubscribe();
}, [user]);
Messages Listener
Monitors messages for the current session:
useEffect(() => {
if (!user || chatStatus !== 'open' || !currentSessionId) {
if (chatStatus === 'closed') setMessages([]);
return;
}
const q = query(
collection(db, 'support_chats', user.uid, 'messages'),
where('sessionId', '==', currentSessionId),
orderBy('createdAt', 'asc')
);
const unsubscribe = onSnapshot(q, (snapshot) => {
const msgs = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setMessages(msgs);
});
return () => unsubscribe();
}, [user, chatStatus, currentSessionId]);
Features:
- Only listens when chat is open
- Filters by active session ID
- Orders chronologically
- Auto-clears messages when chat closes
AI vs Human Detection
The system intelligently determines when to let AI respond:
// Find the most recent admin message
const adminMessages = messages.filter(m => m.senderRole === 'admin');
const lastAdminMessage = adminMessages[adminMessages.length - 1];
// Check if it's from a human
const isHumanInControl = lastAdminMessage && !lastAdminMessage.isAi;
// AI only responds if:
// 1. Admin is NOT currently typing
// 2. No recent human messages in conversation
if (!isAdminTyping && !isHumanInControl) {
handleAiResponse(msgToSend, currentSessionId, user.uid);
}
Why this matters:
- Prevents AI from interrupting human agents
- Ensures smooth handoff from AI to human
- Human admin maintains control once they engage
Firestore Structure
Chat Document
Path: support_chats/{userId}
{
status: 'open' | 'closed',
activeSessionId: string,
adminUnreadCount: number,
clientUnreadCount: number,
lastUpdated: Timestamp,
lastMessage: string,
adminIsTyping: boolean,
userInfo: {
uid: string,
name: string,
email: string,
photoURL: string
}
}
Message Documents
Path: support_chats/{userId}/messages/{messageId}
{
text: string,
senderRole: 'client' | 'admin',
sessionId: string,
createdAt: Timestamp,
isAi?: boolean // Present and true for AI messages
}
Example: Chat Interface
function SupportChatScreen() {
const {
messages,
chatStatus,
inputText,
setInputText,
sendMessage,
initiateChat,
isAdminTyping,
isAiThinking
} = useSupportChat();
if (chatStatus === 'loading') {
return <ActivityIndicator />;
}
if (chatStatus === 'closed') {
return (
<View>
<Text>¿Necesitas ayuda?</Text>
<Button
title="Iniciar Chat"
onPress={() => initiateChat()}
/>
</View>
);
}
return (
<View style={{ flex: 1 }}>
{/* Messages List */}
<FlatList
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View
style={{
alignSelf: item.senderRole === 'client' ? 'flex-end' : 'flex-start',
backgroundColor: item.senderRole === 'client' ? '#007AFF' : '#E5E5EA',
padding: 10,
borderRadius: 15,
margin: 5
}}
>
<Text
style={{
color: item.senderRole === 'client' ? 'white' : 'black'
}}
>
{item.text}
</Text>
{item.senderRole === 'admin' && item.isAi && (
<Text style={{ fontSize: 10, color: 'gray', marginTop: 4 }}
>
🤖 ParkBot
</Text>
)}
</View>
)}
/>
{/* Typing Indicators */}
{isAdminTyping && (
<View style={{ padding: 10 }}>
<Text>👤 Admin está escribiendo...</Text>
</View>
)}
{isAiThinking && (
<View style={{ padding: 10 }}>
<Text>🤖 ParkBot está pensando...</Text>
</View>
)}
{/* Input */}
<View style={{ flexDirection: 'row', padding: 10 }}>
<TextInput
style={{ flex: 1, borderWidth: 1, borderRadius: 20, padding: 10 }}
value={inputText}
onChangeText={setInputText}
placeholder="Escribe un mensaje..."
multiline
/>
<TouchableOpacity
onPress={sendMessage}
disabled={!inputText.trim()}
style={{
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#007AFF',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10
}}
>
<Text style={{ color: 'white', fontSize: 18 }}>↑</Text>
</TouchableOpacity>
</View>
</View>
);
}
Example: AI Badge Component
function MessageBubble({ message }) {
const isClient = message.senderRole === 'client';
const isAi = message.isAi === true;
return (
<View style={{ alignSelf: isClient ? 'flex-end' : 'flex-start' }}>
<View
style={{
backgroundColor: isClient ? '#007AFF' : '#E5E5EA',
padding: 12,
borderRadius: 16
}}
>
<Text style={{ color: isClient ? 'white' : 'black' }}>
{message.text}
</Text>
</View>
{/* Show badge for AI messages */}
{!isClient && (
<Text
style={{
fontSize: 10,
color: 'gray',
marginTop: 2,
marginLeft: 8
}}
>
{isAi ? '🤖 ParkBot' : '👤 Admin'}
</Text>
)}
{/* Timestamp */}
<Text style={{ fontSize: 10, color: 'gray', marginTop: 2 }}>
{message.createdAt?.toDate().toLocaleTimeString('es-MX', {
hour: '2-digit',
minute: '2-digit'
})}
</Text>
</View>
);
}
Gemini API Configuration
Endpoint:
https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent
API Key: AIzaSyDQagpCrH-AqLRUXp9uLr-LSpJPM-3Fnpo
Request Format:
{
contents: [
{
parts: [
{ text: "System prompt + User question" }
]
}
]
}
Response Format:
{
candidates: [
{
content: {
parts: [
{ text: "AI response text" }
]
}
}
]
}
Error Handling
API Errors
- Logs detailed error response from Google
- Shows user-friendly alert
- Doesn’t break chat functionality
if (!response.ok) {
const errorMsg = data.error?.message || "Error desconocido de Google";
console.error("❌ ERROR DETALLADO GOOGLE:", JSON.stringify(data, null, 2));
Alert.alert("Error de IA", errorMsg);
throw new Error(errorMsg);
}
Message Send Errors
- Catches and logs errors
- User can retry sending
- Doesn’t lose message input
Listener Errors
- Firestore listeners auto-reconnect on network issues
- Missing data handled with fallback values
- Chat gracefully degrades if Firestore unavailable
Message History
Currently loads all messages for active session. For optimization:
- Consider pagination for very long conversations
- Archive old sessions
- Limit query to recent N messages
Real-time Updates
Uses Firestore real-time listeners which:
- Only pay for actual changes (not polling)
- Automatically batch updates
- Handle offline/online transitions
AI Response Time
Gemini Flash model typically responds in:
- Simple queries: 1-2 seconds
- Complex queries: 3-5 seconds
isAiThinking indicator provides user feedback during generation.
Firebase Dependencies
- Firestore Collections:
support_chats, support_chats/{uid}/messages
- Authentication: Requires authenticated user
- Composite Index:
sessionId + createdAt on messages subcollection
Security Rules Recommendation
match /support_chats/{userId} {
// Users can only read/write their own chat
allow read, write: if request.auth.uid == userId;
match /messages/{messageId} {
// Users can only read/write messages in their chat
allow read, write: if request.auth.uid == userId;
}
}
// Admins need separate rules with custom claims
- useLogin - Authentication required for support chat
- useReservar - Common support topic: reservation help
- useHistorial - Common support topic: viewing past reservations