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:
parent
ad0fe41ee9
commit
85896f442f
@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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__":
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user