From bd12aadfb38aa60fb66fa4a432899d0460e44d79 Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 27 Jan 2026 16:13:44 -0800 Subject: [PATCH] Remove LiveKit integration from chat.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all LiveKit imports (registerGlobals, LiveKitRoom, useVoiceAssistant, etc.) - Remove VoiceCallTranscriptHandler component - Remove voice call state management (callState, isCallActive) - Remove voice call functions (startVoiceCall, endVoiceCall, handleVoiceTranscript) - Remove LiveKitRoom component from JSX - Remove debug panel for voice calls - Clean up unused imports (Clipboard, Animated, expo-keep-awake, useAuth, useVoiceTranscript, useVoiceCall) - Remove unused styles (voiceButton, callActiveIndicator, debug panel styles) - Update initial message text to remove voice call references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(tabs)/chat.tsx | 606 +------------------------------------------- 1 file changed, 5 insertions(+), 601 deletions(-) diff --git a/app/(tabs)/chat.tsx b/app/(tabs)/chat.tsx index d596af2..5944961 100644 --- a/app/(tabs)/chat.tsx +++ b/app/(tabs)/chat.tsx @@ -1,7 +1,7 @@ /** * Chat Screen - Text Chat with Julia AI * - * Clean text chat interface with integrated voice calls. + * Clean text chat interface. */ import React, { useState, useCallback, useRef, useEffect } from 'react'; @@ -17,38 +17,16 @@ import { Keyboard, 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, useFocusEffect } from 'expo-router'; -import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'; import { api } from '@/services/api'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; -import { useVoiceTranscript } from '@/contexts/VoiceTranscriptContext'; -import { useVoiceCall } from '@/contexts/VoiceCallContext'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import type { Message, Beneficiary } from '@/types'; -// LiveKit imports -import { - registerGlobals, - LiveKitRoom, - useVoiceAssistant, - useConnectionState, - useTrackTranscription, - useTracks, -} from '@livekit/react-native'; -import { ConnectionState, Track } from 'livekit-client'; -import { getToken, type BeneficiaryData } from '@/services/livekitService'; -import { useAuth } from '@/contexts/AuthContext'; - -// Register LiveKit globals (must be called before using LiveKit) -registerGlobals(); - const API_URL = 'https://eluxnetworks.net/function/well-api/api'; // WellNuo API credentials (same as julia-agent) @@ -128,165 +106,15 @@ function normalizeQuestion(userMessage: string): string { return userMessage; } -// ============================================================================ -// Voice Call Transcript Handler (invisible - just captures transcripts) -// ============================================================================ - -interface VoiceCallTranscriptHandlerProps { - onTranscript: (role: 'user' | 'assistant', text: string) => void; - onDurationUpdate: (seconds: number) => void; - onLog?: (message: string) => void; -} - -// 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, state: agentState } = useVoiceAssistant(); - const [callDuration, setCallDuration] = useState(0); - const [lastProcessedId, setLastProcessedId] = useState(null); - const prevConnectionStateRef = useRef(null); - const prevAgentStateRef = useRef(null); - - // Track all audio tracks for transcription - const tracks = useTracks([Track.Source.Microphone, Track.Source.Unknown], { onlySubscribed: false }); - - // Get transcription from agent's audio track - const { segments: agentSegments } = useTrackTranscription(audioTrack); - - // Get transcription from user's microphone - 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) { - const lastSegment = agentSegments[agentSegments.length - 1]; - if (lastSegment && lastSegment.final && lastSegment.id !== lastProcessedId) { - setLastProcessedId(lastSegment.id); - onTranscript('assistant', lastSegment.text); - const msg = `Julia said: "${lastSegment.text}"`; - console.log('[VoiceCall]', msg); - onLog?.(msg); - } - } - }, [agentSegments, lastProcessedId, onTranscript, onLog]); - - // Process user transcription - const [lastUserSegmentId, setLastUserSegmentId] = useState(null); - useEffect(() => { - if (userSegments && userSegments.length > 0) { - const lastSegment = userSegments[userSegments.length - 1]; - if (lastSegment && lastSegment.final && lastSegment.id !== lastUserSegmentId) { - setLastUserSegmentId(lastSegment.id); - onTranscript('user', lastSegment.text); - const msg = `User said: "${lastSegment.text}"`; - console.log('[VoiceCall]', msg); - onLog?.(msg); - } - } - }, [userSegments, lastUserSegmentId, onTranscript, onLog]); - - // Call duration timer - use ref to avoid state updates during render - const durationRef = useRef(0); - useEffect(() => { - if (connectionState === ConnectionState.Connected) { - const interval = setInterval(() => { - durationRef.current += 1; - onDurationUpdate(durationRef.current); - }, 1000); - return () => clearInterval(interval); - } - }, [connectionState, onDurationUpdate]); - - // Keep screen awake during call - useEffect(() => { - activateKeepAwakeAsync('voice-call'); - return () => { - deactivateKeepAwake('voice-call'); - }; - }, []); - - // This component renders nothing - it just handles transcripts - return null; -} - export default function ChatScreen() { const router = useRouter(); const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); - const { addTranscriptEntry, clearTranscript } = useVoiceTranscript(); - const { user } = useAuth(); - const { - callState, - startCall, - endCall: endVoiceCallContext, - minimizeCall, - maximizeCall, - updateDuration, - isCallActive, - } = useVoiceCall(); // 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.${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.`, + content: `Hello! I'm Julia, your AI wellness companion.${beneficiaryName ? `\n\nI'm here to help you monitor ${beneficiaryName}.` : ''}\n\nType a message below to chat with me.`, timestamp: new Date(), }), []); @@ -298,84 +126,6 @@ export default function ChatScreen() { const [messages, setMessages] = useState([createInitialMessage(null)]); const [sortNewestFirst, setSortNewestFirst] = useState(false); - // Voice call state (local connecting state only) - const [isConnectingVoice, setIsConnectingVoice] = useState(false); - - // Debug logs state - const [debugLogs, setDebugLogs] = useState([]); - 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; - - // Start pulsing animation when call is active - useEffect(() => { - if (isCallActive) { - const pulse = Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1.15, - duration: 600, - useNativeDriver: true, - }), - Animated.timing(pulseAnim, { - toValue: 1, - duration: 600, - useNativeDriver: true, - }), - ]) - ); - pulse.start(); - return () => pulse.stop(); - } else { - pulseAnim.setValue(1); - } - }, [isCallActive, pulseAnim]); - - // Track if we've shown the voice call separator for current call - const [hasShownVoiceSeparator, setHasShownVoiceSeparator] = useState(false); - - // Reset separator flag when starting a new call - useEffect(() => { - if (isCallActive && !hasShownVoiceSeparator) { - // Will show separator on first voice message - } else if (!isCallActive) { - setHasShownVoiceSeparator(false); - } - }, [isCallActive]); const [input, setInput] = useState(''); const [isSending, setIsSending] = useState(false); const inputRef = useRef(''); @@ -428,20 +178,15 @@ export default function ChatScreen() { old: previousDeploymentIdRef.current, new: customDeploymentId, name: deploymentName, - isCallActive, }); - // End any active call - endVoiceCallContext(); - // Clear chat with new initial message (use name instead of ID) setMessages([createInitialMessage(deploymentName)]); - setHasShownVoiceSeparator(false); // Update ref previousDeploymentIdRef.current = customDeploymentId; } - }, [customDeploymentId, deploymentName, createInitialMessage, isCallActive, endVoiceCallContext]); + }, [customDeploymentId, deploymentName, createInitialMessage]); // Update initial message when deploymentName is loaded (but only if chat has just the initial message) useEffect(() => { @@ -515,127 +260,6 @@ export default function ChatScreen() { setShowBeneficiaryPicker(false); }, [setCurrentBeneficiary]); - // ============================================================================ - // Voice Call Functions - // ============================================================================ - - // Start voice call - const startVoiceCall = useCallback(async () => { - if (isConnectingVoice || isCallActive) return; - - setIsConnectingVoice(true); - addDebugLog('Starting voice call...', 'info'); - console.log('[Chat] Starting voice call...'); - - try { - // Build beneficiary data for the agent - // Priority: customDeploymentId from settings > currentBeneficiary > first beneficiary > fallback - const beneficiaryData: BeneficiaryData = { - 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) { - beneficiaries.forEach(b => { - beneficiaryData.beneficiaryNamesDict[b.id.toString()] = b.name; - }); - } - - // Get LiveKit token - addDebugLog('Requesting LiveKit token...', 'info'); - const userIdStr = user?.user_id?.toString() || 'user-' + Date.now(); - const tokenResponse = await getToken(userIdStr, beneficiaryData); - - if (!tokenResponse.success || !tokenResponse.data) { - 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 - const callStartMessage: Message = { - id: `call-start-${Date.now()}`, - role: 'assistant', - content: 'Voice call started', - timestamp: new Date(), - isSystem: true, - }; - setMessages(prev => [...prev, callStartMessage]); - - // 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', - error instanceof Error ? error.message : 'Failed to start voice call' - ); - } finally { - setIsConnectingVoice(false); - } - }, [isConnectingVoice, isCallActive, currentBeneficiary, beneficiaries, user, clearTranscript, startCall, customDeploymentId, addDebugLog]); - - // End voice call and log to chat - const endVoiceCall = useCallback(() => { - console.log('[Chat] Ending voice call...'); - - // Add call end message to chat with duration - const duration = callState.callDuration; - const minutes = Math.floor(duration / 60); - const seconds = duration % 60; - const durationStr = `${minutes}:${seconds.toString().padStart(2, '0')}`; - - const callEndMessage: Message = { - id: `call-end-${Date.now()}-${Math.random().toString(36).slice(2)}`, - role: 'assistant', - content: `Call ended (${durationStr})`, - timestamp: new Date(), - isSystem: true, - }; - setMessages(prev => [...prev, callEndMessage]); - setHasShownVoiceSeparator(false); - - endVoiceCallContext(); - }, [endVoiceCallContext, callState.callDuration]); - - // Handle voice transcript entries - add to chat in real-time - const handleVoiceTranscript = useCallback((role: 'user' | 'assistant', text: string) => { - if (!text.trim()) return; - - // Create voice message and add to chat immediately - const voiceMessage: Message = { - id: `voice-${Date.now()}-${Math.random().toString(36).slice(2)}`, - role, - content: text.trim(), - timestamp: new Date(), - isVoice: true, - }; - - setMessages(prev => [...prev, voiceMessage]); - - // Scroll to latest message (respects sort mode) - setTimeout(() => { - scrollToLatestMessage(true); - }, 100); - - // Also store in transcript context for persistence - addTranscriptEntry(role, text); - }, [hasShownVoiceSeparator, addTranscriptEntry, scrollToLatestMessage]); - // Cached API token for WellNuo const apiTokenRef = useRef(null); @@ -764,7 +388,7 @@ export default function ChatScreen() { const isVoice = item.isVoice; const isSystem = item.isSystem; - // System messages (like "Voice Call Transcript" separator) + // System messages if (isSystem) { return ( @@ -841,7 +465,7 @@ export default function ChatScreen() { { id: '1', role: 'assistant', - content: 'Hello! I\'m Julia, your AI wellness assistant. You can type a message or tap the phone button to start a voice call.', + content: 'Hello! I\'m Julia, your AI wellness assistant. Type a message below to chat with me.', timestamp: new Date(), }, ]); @@ -911,53 +535,6 @@ export default function ChatScreen() { - {/* Debug Logs Modal */} - setShowDebugPanel(false)} - > - - - - Debug Logs ({debugLogs.length}) - - - - - - - - setShowDebugPanel(false)}> - - - - - - - {debugLogs.length === 0 ? ( - No logs yet. Start a voice call to see logs. - ) : ( - debugLogs.map(log => ( - - {log.timestamp} - - {log.message} - - - )) - )} - - - - - {/* Messages */} - {/* Voice Call Button - becomes pulsing bubble during call */} - - - {isConnectingVoice ? ( - - ) : isCallActive ? ( - - - - ) : ( - - )} - - - {/* Call duration badge */} - {isCallActive && ( - - - {Math.floor(callState.callDuration / 60).toString().padStart(2, '0')}: - {(callState.callDuration % 60).toString().padStart(2, '0')} - - - )} - - {/* Invisible LiveKit Room - runs in background during call */} - {isCallActive && callState.token && callState.wsUrl && ( - { - 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(); - }} - > - - - )} ); } @@ -1213,48 +727,6 @@ const styles = StyleSheet.create({ maxHeight: 100, marginRight: Spacing.sm, }, - voiceButton: { - width: 44, - height: 44, - borderRadius: BorderRadius.full, - backgroundColor: AppColors.surface, - justifyContent: 'center', - alignItems: 'center', - marginRight: Spacing.sm, - borderWidth: 1, - borderColor: AppColors.primary, - }, - voiceButtonConnecting: { - borderColor: AppColors.success, - backgroundColor: 'rgba(90, 200, 168, 0.1)', - }, - voiceButtonActive: { - backgroundColor: AppColors.error, - borderColor: AppColors.error, - }, - callActiveIndicator: { - width: '100%', - height: '100%', - justifyContent: 'center', - alignItems: 'center', - }, - callDurationBadge: { - position: 'absolute', - left: 32, - top: -8, - backgroundColor: AppColors.error, - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - minWidth: 42, - alignItems: 'center', - }, - callDurationText: { - fontSize: 10, - fontWeight: '600', - color: AppColors.white, - fontVariant: ['tabular-nums'], - }, sendButton: { width: 44, height: 44, @@ -1376,19 +848,6 @@ const styles = StyleSheet.create({ fontWeight: '500', color: AppColors.textPrimary, }, - // Voice message styles - voiceBubble: { - borderWidth: 1, - borderColor: 'rgba(59, 130, 246, 0.3)', - }, - voiceIndicator: { - position: 'absolute', - top: 6, - right: 6, - }, - voiceIndicatorEmoji: { - fontSize: 10, - }, // System message styles systemMessageContainer: { flexDirection: 'row', @@ -1415,59 +874,4 @@ 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', - }, });