Show beneficiary name instead of deployment ID in chat

- Add deploymentName state to chat screen
- Load and display beneficiary name in initial welcome message
- Save deployment name to SecureStore when validating in profile
- End call and clear chat when deployment changes
- Fix text input not clearing after sending message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2026-01-25 10:30:01 -08:00
parent ad0fe41ee9
commit 85896f442f
4 changed files with 418 additions and 53 deletions

View File

@ -18,11 +18,13 @@ import {
Platform,
Alert,
Animated,
ScrollView,
} from 'react-native';
import * as Clipboard from 'expo-clipboard';
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { useRouter, useFocusEffect } from 'expo-router';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import { api } from '@/services/api';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
@ -133,16 +135,27 @@ function normalizeQuestion(userMessage: string): string {
interface VoiceCallTranscriptHandlerProps {
onTranscript: (role: 'user' | 'assistant', text: string) => void;
onDurationUpdate: (seconds: number) => void;
onLog?: (message: string) => void;
}
function VoiceCallTranscriptHandler({ onTranscript, onDurationUpdate }: VoiceCallTranscriptHandlerProps) {
// Debug log entry type
interface DebugLogEntry {
id: string;
timestamp: string;
level: 'info' | 'warn' | 'error' | 'success';
message: string;
}
function VoiceCallTranscriptHandler({ onTranscript, onDurationUpdate, onLog }: VoiceCallTranscriptHandlerProps) {
const connectionState = useConnectionState();
const { audioTrack } = useVoiceAssistant();
const { audioTrack, state: agentState } = useVoiceAssistant();
const [callDuration, setCallDuration] = useState(0);
const [lastProcessedId, setLastProcessedId] = useState<string | null>(null);
const prevConnectionStateRef = useRef<ConnectionState | null>(null);
const prevAgentStateRef = useRef<string | null>(null);
// Track all audio tracks for transcription
const tracks = useTracks([Track.Source.Microphone], { onlySubscribed: true });
const tracks = useTracks([Track.Source.Microphone, Track.Source.Unknown], { onlySubscribed: false });
// Get transcription from agent's audio track
const { segments: agentSegments } = useTrackTranscription(audioTrack);
@ -151,6 +164,56 @@ function VoiceCallTranscriptHandler({ onTranscript, onDurationUpdate }: VoiceCal
const localTrack = tracks.find(t => t.participant?.isLocal);
const { segments: userSegments } = useTrackTranscription(localTrack);
// Log connection state changes
useEffect(() => {
if (prevConnectionStateRef.current !== connectionState) {
const msg = `Connection: ${prevConnectionStateRef.current || 'initial'} -> ${connectionState}`;
console.log('[VoiceCall]', msg);
onLog?.(msg);
prevConnectionStateRef.current = connectionState;
}
}, [connectionState, onLog]);
// Log agent state changes
useEffect(() => {
if (agentState && prevAgentStateRef.current !== agentState) {
const msg = `Agent state: ${prevAgentStateRef.current || 'initial'} -> ${agentState}`;
console.log('[VoiceCall]', msg);
onLog?.(msg);
prevAgentStateRef.current = agentState;
}
}, [agentState, onLog]);
// Log audio track info
useEffect(() => {
if (audioTrack) {
// audioTrack may have different properties depending on LiveKit version
const trackInfo = JSON.stringify({
hasTrack: !!audioTrack,
publication: (audioTrack as any)?.publication?.sid || 'no-pub',
trackSid: (audioTrack as any)?.sid || (audioTrack as any)?.trackSid || 'unknown',
});
const msg = `Audio track received: ${trackInfo}`;
console.log('[VoiceCall]', msg);
onLog?.(msg);
}
}, [audioTrack, onLog]);
// Log all tracks
useEffect(() => {
if (tracks.length > 0) {
const trackInfo = tracks.map(t => {
const participant = t.participant?.identity || 'unknown';
const source = t.source || 'unknown';
const isLocal = t.participant?.isLocal ? 'local' : 'remote';
return `${participant}(${isLocal}):${source}`;
}).join(', ');
const msg = `Tracks (${tracks.length}): ${trackInfo}`;
console.log('[VoiceCall]', msg);
onLog?.(msg);
}
}, [tracks, onLog]);
// Process agent transcription
useEffect(() => {
if (agentSegments && agentSegments.length > 0) {
@ -158,10 +221,12 @@ function VoiceCallTranscriptHandler({ onTranscript, onDurationUpdate }: VoiceCal
if (lastSegment && lastSegment.final && lastSegment.id !== lastProcessedId) {
setLastProcessedId(lastSegment.id);
onTranscript('assistant', lastSegment.text);
console.log('[VoiceCall] Agent said:', lastSegment.text);
const msg = `Julia said: "${lastSegment.text}"`;
console.log('[VoiceCall]', msg);
onLog?.(msg);
}
}
}, [agentSegments, lastProcessedId, onTranscript]);
}, [agentSegments, lastProcessedId, onTranscript, onLog]);
// Process user transcription
const [lastUserSegmentId, setLastUserSegmentId] = useState<string | null>(null);
@ -171,10 +236,12 @@ function VoiceCallTranscriptHandler({ onTranscript, onDurationUpdate }: VoiceCal
if (lastSegment && lastSegment.final && lastSegment.id !== lastUserSegmentId) {
setLastUserSegmentId(lastSegment.id);
onTranscript('user', lastSegment.text);
console.log('[VoiceCall] User said:', lastSegment.text);
const msg = `User said: "${lastSegment.text}"`;
console.log('[VoiceCall]', msg);
onLog?.(msg);
}
}
}, [userSegments, lastUserSegmentId, onTranscript]);
}, [userSegments, lastUserSegmentId, onTranscript, onLog]);
// Call duration timer - use ref to avoid state updates during render
const durationRef = useRef(0);
@ -215,16 +282,17 @@ export default function ChatScreen() {
isCallActive,
} = useVoiceCall();
// Helper to create initial message with deployment ID
const createInitialMessage = useCallback((deploymentId?: string | null): Message => ({
// Helper to create initial message with beneficiary name
const createInitialMessage = useCallback((beneficiaryName?: string | null): Message => ({
id: '1',
role: 'assistant',
content: `Hello! I'm Julia, your AI wellness companion.${deploymentId ? `\n\nDeployment ID: ${deploymentId}` : ''}\n\nTap the phone button to start a voice call, or type a message below.`,
content: `Hello! I'm Julia, your AI wellness companion.${beneficiaryName ? `\n\nI'm here to help you monitor ${beneficiaryName}.` : ''}\n\nTap the phone button to start a voice call, or type a message below.`,
timestamp: new Date(),
}), []);
// Custom deployment ID from settings
// Custom deployment ID and name from settings
const [customDeploymentId, setCustomDeploymentId] = useState<string | null>(null);
const [deploymentName, setDeploymentName] = useState<string | null>(null);
// Chat state - initialized after deployment ID is loaded
const [messages, setMessages] = useState<Message[]>([createInitialMessage(null)]);
@ -233,6 +301,43 @@ export default function ChatScreen() {
// Voice call state (local connecting state only)
const [isConnectingVoice, setIsConnectingVoice] = useState(false);
// Debug logs state
const [debugLogs, setDebugLogs] = useState<DebugLogEntry[]>([]);
const [showDebugPanel, setShowDebugPanel] = useState(false);
const debugLogIdRef = useRef(0);
// Add debug log entry
const addDebugLog = useCallback((message: string, level: DebugLogEntry['level'] = 'info') => {
const now = new Date();
const timestamp = now.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}) + '.' + now.getMilliseconds().toString().padStart(3, '0');
const entry: DebugLogEntry = {
id: `log-${++debugLogIdRef.current}`,
timestamp,
level,
message,
};
setDebugLogs(prev => [...prev.slice(-100), entry]); // Keep last 100 logs
}, []);
// Copy logs to clipboard
const copyLogsToClipboard = useCallback(async () => {
const logsText = debugLogs.map(log => `[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}`).join('\n');
await Clipboard.setStringAsync(logsText);
Alert.alert('Copied', `${debugLogs.length} log entries copied to clipboard`);
}, [debugLogs]);
// Clear debug logs
const clearDebugLogs = useCallback(() => {
setDebugLogs([]);
addDebugLog('Logs cleared', 'info');
}, [addDebugLog]);
// Pulsing animation for active call
const pulseAnim = useRef(new Animated.Value(1)).current;
@ -273,47 +378,77 @@ export default function ChatScreen() {
}, [isCallActive]);
const [input, setInput] = useState('');
const [isSending, setIsSending] = useState(false);
const inputRef = useRef('');
const flatListRef = useRef<FlatList>(null);
// Keep inputRef in sync with input state
useEffect(() => {
inputRef.current = input;
}, [input]);
// Beneficiary picker
const [showBeneficiaryPicker, setShowBeneficiaryPicker] = useState(false);
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
const [loadingBeneficiaries, setLoadingBeneficiaries] = useState(false);
// Load custom deployment ID from settings and update initial message
useEffect(() => {
const loadCustomDeploymentId = async () => {
const saved = await api.getDeploymentId();
setCustomDeploymentId(saved);
// Update initial message with deployment ID
if (saved) {
setMessages([createInitialMessage(saved)]);
}
};
loadCustomDeploymentId();
}, [createInitialMessage]);
// Load custom deployment ID and name from settings
// Use useFocusEffect to reload when returning from profile screen
useFocusEffect(
useCallback(() => {
const loadDeploymentData = async () => {
const savedId = await api.getDeploymentId();
const savedName = await api.getDeploymentName();
console.log('[Chat] useFocusEffect: loaded deployment ID:', savedId, 'name:', savedName);
setCustomDeploymentId(savedId);
setDeploymentName(savedName);
};
loadDeploymentData();
}, [])
);
// When deployment ID changes, end call and clear chat
const previousDeploymentId = useRef<string | null>(null);
// Track previous value to detect actual changes (not just re-renders)
const previousDeploymentIdRef = useRef<string | null | undefined>(undefined);
useEffect(() => {
// Skip initial load
if (previousDeploymentId.current === null) {
previousDeploymentId.current = customDeploymentId;
// undefined means "not yet initialized" - store current value and skip
if (previousDeploymentIdRef.current === undefined) {
console.log('[Chat] Initializing deployment tracking:', customDeploymentId, 'name:', deploymentName);
previousDeploymentIdRef.current = customDeploymentId;
// Update initial message with deployment name if we have one
if (customDeploymentId || deploymentName) {
setMessages([createInitialMessage(deploymentName)]);
}
return;
}
// If deployment ID actually changed
if (previousDeploymentId.current !== customDeploymentId) {
console.log('[Chat] Deployment ID changed, ending call and clearing chat');
// Check if deployment actually changed
if (previousDeploymentIdRef.current !== customDeploymentId) {
console.log('[Chat] Deployment changed!', {
old: previousDeploymentIdRef.current,
new: customDeploymentId,
name: deploymentName,
isCallActive,
});
// End any active call
if (isCallActive) {
endVoiceCallContext();
}
// Clear chat with new initial message
setMessages([createInitialMessage(customDeploymentId)]);
endVoiceCallContext();
// Clear chat with new initial message (use name instead of ID)
setMessages([createInitialMessage(deploymentName)]);
setHasShownVoiceSeparator(false);
previousDeploymentId.current = customDeploymentId;
// Update ref
previousDeploymentIdRef.current = customDeploymentId;
}
}, [customDeploymentId, createInitialMessage, isCallActive, endVoiceCallContext]);
}, [customDeploymentId, deploymentName, createInitialMessage, isCallActive, endVoiceCallContext]);
// Update initial message when deploymentName is loaded (but only if chat has just the initial message)
useEffect(() => {
if (deploymentName && messages.length === 1 && messages[0].id === '1') {
setMessages([createInitialMessage(deploymentName)]);
}
}, [deploymentName, createInitialMessage]);
// Load beneficiaries
const loadBeneficiaries = useCallback(async () => {
@ -389,6 +524,7 @@ export default function ChatScreen() {
if (isConnectingVoice || isCallActive) return;
setIsConnectingVoice(true);
addDebugLog('Starting voice call...', 'info');
console.log('[Chat] Starting voice call...');
try {
@ -398,6 +534,7 @@ export default function ChatScreen() {
deploymentId: customDeploymentId || currentBeneficiary?.id?.toString() || beneficiaries[0]?.id?.toString() || '21',
beneficiaryNamesDict: {},
};
addDebugLog(`Deployment ID: ${beneficiaryData.deploymentId}`, 'info');
// Add names dict if not in single deployment mode
if (!SINGLE_DEPLOYMENT_MODE) {
@ -407,6 +544,7 @@ export default function ChatScreen() {
}
// Get LiveKit token
addDebugLog('Requesting LiveKit token...', 'info');
const userIdStr = user?.user_id?.toString() || 'user-' + Date.now();
const tokenResponse = await getToken(userIdStr, beneficiaryData);
@ -414,6 +552,8 @@ export default function ChatScreen() {
throw new Error(tokenResponse.error || 'Failed to get voice token');
}
addDebugLog(`Token received! Room: ${tokenResponse.data.roomName}`, 'success');
addDebugLog(`WS URL: ${tokenResponse.data.wsUrl}`, 'info');
console.log('[Chat] Got voice token, connecting to room:', tokenResponse.data.roomName);
// Add call start message to chat
@ -428,13 +568,17 @@ export default function ChatScreen() {
// Clear previous transcript and start call via context
clearTranscript();
addDebugLog('Calling startCall with token and wsUrl...', 'info');
startCall({
token: tokenResponse.data.token,
wsUrl: tokenResponse.data.wsUrl,
beneficiaryName: currentBeneficiary?.name,
beneficiaryId: currentBeneficiary?.id?.toString(),
});
addDebugLog('startCall called, waiting for LiveKitRoom to connect...', 'success');
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
addDebugLog(`Voice call error: ${errorMsg}`, 'error');
console.error('[Chat] Voice call error:', error);
Alert.alert(
'Voice Call Error',
@ -443,7 +587,7 @@ export default function ChatScreen() {
} finally {
setIsConnectingVoice(false);
}
}, [isConnectingVoice, isCallActive, currentBeneficiary, beneficiaries, user, clearTranscript, startCall, customDeploymentId]);
}, [isConnectingVoice, isCallActive, currentBeneficiary, beneficiaries, user, clearTranscript, startCall, customDeploymentId, addDebugLog]);
// End voice call and log to chat
const endVoiceCall = useCallback(() => {
@ -456,7 +600,7 @@ export default function ChatScreen() {
const durationStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;
const callEndMessage: Message = {
id: `call-end-${Date.now()}`,
id: `call-end-${Date.now()}-${Math.random().toString(36).slice(2)}`,
role: 'assistant',
content: `Call ended (${durationStr})`,
timestamp: new Date(),
@ -525,7 +669,7 @@ export default function ChatScreen() {
// Text chat - send message via API (same as julia-agent)
const sendTextMessage = useCallback(async () => {
const trimmedInput = input.trim();
const trimmedInput = inputRef.current.trim();
if (!trimmedInput || isSending) return;
const userMessage: Message = {
@ -535,8 +679,11 @@ export default function ChatScreen() {
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
// Clear input immediately before any async operations
setInput('');
inputRef.current = '';
setMessages(prev => [...prev, userMessage]);
setIsSending(true);
Keyboard.dismiss();
@ -609,7 +756,7 @@ export default function ChatScreen() {
} finally {
setIsSending(false);
}
}, [input, isSending, getWellNuoToken, customDeploymentId, currentBeneficiary, beneficiaries]);
}, [isSending, getWellNuoToken, customDeploymentId, currentBeneficiary, beneficiaries]);
// Render message bubble
const renderMessage = ({ item }: { item: Message }) => {
@ -769,6 +916,53 @@ export default function ChatScreen() {
</View>
</Modal>
{/* Debug Logs Modal */}
<Modal
visible={showDebugPanel}
transparent
animationType="slide"
onRequestClose={() => setShowDebugPanel(false)}
>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, styles.debugModalContent]}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Debug Logs ({debugLogs.length})</Text>
<View style={styles.debugHeaderButtons}>
<TouchableOpacity style={styles.debugHeaderBtn} onPress={copyLogsToClipboard}>
<Ionicons name="copy-outline" size={20} color={AppColors.primary} />
</TouchableOpacity>
<TouchableOpacity style={styles.debugHeaderBtn} onPress={clearDebugLogs}>
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
</TouchableOpacity>
<TouchableOpacity onPress={() => setShowDebugPanel(false)}>
<Ionicons name="close" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
</View>
</View>
<ScrollView style={styles.debugLogsContainer}>
{debugLogs.length === 0 ? (
<Text style={styles.debugEmptyText}>No logs yet. Start a voice call to see logs.</Text>
) : (
debugLogs.map(log => (
<View key={log.id} style={styles.debugLogEntry}>
<Text style={styles.debugTimestamp}>{log.timestamp}</Text>
<Text style={[
styles.debugMessage,
log.level === 'error' && styles.debugError,
log.level === 'warn' && styles.debugWarn,
log.level === 'success' && styles.debugSuccess,
]}>
{log.message}
</Text>
</View>
))
)}
</ScrollView>
</View>
</View>
</Modal>
{/* Messages */}
<KeyboardAvoidingView
style={styles.chatContainer}
@ -786,6 +980,18 @@ export default function ChatScreen() {
}}
/>
{/* Typing indicator */}
{isSending && (
<View style={styles.typingIndicator}>
<View style={styles.typingDots}>
<View style={[styles.typingDot, styles.typingDot1]} />
<View style={[styles.typingDot, styles.typingDot2]} />
<View style={[styles.typingDot, styles.typingDot3]} />
</View>
<Text style={styles.typingText}>Julia is typing...</Text>
</View>
)}
{/* Input */}
<View style={styles.inputContainer}>
{/* Voice Call Button - becomes pulsing bubble during call */}
@ -852,9 +1058,17 @@ export default function ChatScreen() {
connect={true}
audio={true}
video={false}
onConnected={() => console.log('[Chat] LiveKit connected')}
onDisconnected={endVoiceCall}
onConnected={() => {
console.log('[Chat] LiveKit connected');
addDebugLog('LiveKitRoom: CONNECTED to server!', 'success');
}}
onDisconnected={() => {
addDebugLog('LiveKitRoom: DISCONNECTED', 'warn');
endVoiceCall();
}}
onError={(error) => {
const errorMsg = error?.message || 'Unknown error';
addDebugLog(`LiveKitRoom ERROR: ${errorMsg}`, 'error');
console.error('[Chat] LiveKit error:', error);
Alert.alert('Voice Call Error', error.message);
endVoiceCall();
@ -863,6 +1077,7 @@ export default function ChatScreen() {
<VoiceCallTranscriptHandler
onTranscript={handleVoiceTranscript}
onDurationUpdate={updateDuration}
onLog={addDebugLog}
/>
</LiveKitRoom>
)}
@ -1020,8 +1235,8 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(90, 200, 168, 0.1)',
},
voiceButtonActive: {
backgroundColor: AppColors.success,
borderColor: AppColors.success,
backgroundColor: AppColors.error,
borderColor: AppColors.error,
},
callActiveIndicator: {
width: '100%',
@ -1033,7 +1248,7 @@ const styles = StyleSheet.create({
position: 'absolute',
left: 32,
top: -8,
backgroundColor: AppColors.success,
backgroundColor: AppColors.error,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
@ -1057,6 +1272,40 @@ const styles = StyleSheet.create({
sendButtonDisabled: {
backgroundColor: AppColors.surface,
},
// Typing indicator
typingIndicator: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
gap: 8,
},
typingDots: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
typingDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: AppColors.primary,
opacity: 0.4,
},
typingDot1: {
opacity: 0.4,
},
typingDot2: {
opacity: 0.6,
},
typingDot3: {
opacity: 0.8,
},
typingText: {
fontSize: 13,
color: AppColors.textSecondary,
fontStyle: 'italic',
},
// Modal styles
modalOverlay: {
flex: 1,
@ -1172,4 +1421,59 @@ const styles = StyleSheet.create({
color: AppColors.textMuted,
marginLeft: 4,
},
// Debug panel styles
debugButtonActive: {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
},
debugModalContent: {
maxHeight: '80%',
},
debugHeaderButtons: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
debugHeaderBtn: {
padding: Spacing.xs,
},
debugLogsContainer: {
flex: 1,
padding: Spacing.sm,
backgroundColor: '#1a1a2e',
},
debugEmptyText: {
color: AppColors.textMuted,
textAlign: 'center',
padding: Spacing.lg,
fontSize: FontSizes.sm,
},
debugLogEntry: {
flexDirection: 'row',
paddingVertical: 3,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.05)',
},
debugTimestamp: {
color: '#6b7280',
fontSize: 11,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
marginRight: Spacing.sm,
minWidth: 90,
},
debugMessage: {
color: '#e5e7eb',
fontSize: 11,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
flex: 1,
flexWrap: 'wrap',
},
debugError: {
color: '#ef4444',
},
debugWarn: {
color: '#f59e0b',
},
debugSuccess: {
color: '#10b981',
},
});

View File

@ -60,17 +60,27 @@ export default function ProfileScreen() {
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
// Load saved deployment ID and validate to get name
// Load saved deployment ID or auto-populate from first available
useEffect(() => {
const loadDeploymentId = async () => {
const saved = await api.getDeploymentId();
if (saved) {
// Use saved deployment ID
setDeploymentId(saved);
// Validate to get the deployment name
const result = await api.validateDeploymentId(saved);
if (result.ok && result.data?.valid && result.data.name) {
setDeploymentName(result.data.name);
}
} else {
// No saved ID - auto-populate from first available deployment
const firstResult = await api.getFirstDeploymentId();
if (firstResult.ok && firstResult.data) {
setDeploymentId(firstResult.data.deploymentId);
setDeploymentName(firstResult.data.name);
// Also save it so it persists
await api.setDeploymentId(firstResult.data.deploymentId);
}
}
};
loadDeploymentId();
@ -92,6 +102,9 @@ export default function ProfileScreen() {
const result = await api.validateDeploymentId(trimmed);
if (result.ok && result.data?.valid) {
await api.setDeploymentId(trimmed);
if (result.data.name) {
await api.setDeploymentName(result.data.name);
}
setDeploymentId(trimmed);
setDeploymentName(result.data.name || '');
setShowDeploymentModal(false);

View File

@ -441,10 +441,8 @@ async def entrypoint(ctx: JobContext):
),
)
# Generate initial greeting
await session.generate_reply(
instructions="Greet the user warmly as Julia. Briefly introduce yourself as their AI care assistant and ask how you can help them today."
)
# Generate initial greeting - simple and direct
await session.say("Hi! I'm Julia, your AI care assistant. How can I help you today?")
if __name__ == "__main__":

View File

@ -213,6 +213,20 @@ class ApiService {
async clearDeploymentId(): Promise<void> {
await SecureStore.deleteItemAsync('deploymentId');
await SecureStore.deleteItemAsync('deploymentName');
}
// Deployment Name management
async setDeploymentName(name: string): Promise<void> {
await SecureStore.setItemAsync('deploymentName', name);
}
async getDeploymentName(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('deploymentName');
} catch {
return null;
}
}
async validateDeploymentId(deploymentId: string): Promise<ApiResponse<{ valid: boolean; name?: string }>> {
@ -352,6 +366,42 @@ class ApiService {
return { data: beneficiaries, ok: true };
}
// Get the first available deployment ID for the user (for auto-population)
async getFirstDeploymentId(): Promise<ApiResponse<{ deploymentId: string; name: string } | null>> {
const token = await this.getToken();
const userName = await this.getUserName();
if (!token || !userName) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
const response = await this.makeRequest<{ result_list: Array<{
deployment_id: number;
email: string;
first_name: string;
last_name: string;
}> }>({
function: 'deployments_list',
user_name: userName,
token: token,
first: '0',
last: '100',
});
if (!response.ok || !response.data?.result_list || response.data.result_list.length === 0) {
return { ok: true, data: null };
}
const first = response.data.result_list[0];
return {
ok: true,
data: {
deploymentId: String(first.deployment_id),
name: `${first.first_name} ${first.last_name}`.trim(),
},
};
}
// AI Chat
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
const token = await this.getToken();