From 85896f442f5dfb481f679d62089e8138c4b408ac Mon Sep 17 00:00:00 2001 From: Sergei Date: Sun, 25 Jan 2026 10:30:01 -0800 Subject: [PATCH] Show beneficiary name instead of deployment ID in chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/(tabs)/chat.tsx | 400 ++++++++++++++++++++++++++---- app/(tabs)/profile.tsx | 15 +- julia-agent/julia-ai/src/agent.py | 6 +- services/api.ts | 50 ++++ 4 files changed, 418 insertions(+), 53 deletions(-) diff --git a/app/(tabs)/chat.tsx b/app/(tabs)/chat.tsx index 8a58554..f018e3a 100644 --- a/app/(tabs)/chat.tsx +++ b/app/(tabs)/chat.tsx @@ -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(null); + const prevConnectionStateRef = useRef(null); + const prevAgentStateRef = useRef(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(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(null); + const [deploymentName, setDeploymentName] = useState(null); // Chat state - initialized after deployment ID is loaded const [messages, setMessages] = useState([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([]); + 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(null); + // Keep inputRef in sync with input state + useEffect(() => { + inputRef.current = input; + }, [input]); + // Beneficiary picker const [showBeneficiaryPicker, setShowBeneficiaryPicker] = useState(false); const [beneficiaries, setBeneficiaries] = useState([]); 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(null); + // Track previous value to detect actual changes (not just re-renders) + const previousDeploymentIdRef = useRef(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() { + {/* 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 */} + {/* Typing indicator */} + {isSending && ( + + + + + + + Julia is typing... + + )} + {/* Input */} {/* 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() { )} @@ -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', + }, }); diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index f27f0f4..7dcce9f 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -60,17 +60,27 @@ export default function ProfileScreen() { const [isValidating, setIsValidating] = useState(false); const [validationError, setValidationError] = useState(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); diff --git a/julia-agent/julia-ai/src/agent.py b/julia-agent/julia-ai/src/agent.py index 65ba516..9bf7c53 100644 --- a/julia-agent/julia-ai/src/agent.py +++ b/julia-agent/julia-ai/src/agent.py @@ -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__": diff --git a/services/api.ts b/services/api.ts index 6d811eb..3fd11a0 100644 --- a/services/api.ts +++ b/services/api.ts @@ -213,6 +213,20 @@ class ApiService { async clearDeploymentId(): Promise { await SecureStore.deleteItemAsync('deploymentId'); + await SecureStore.deleteItemAsync('deploymentName'); + } + + // Deployment Name management + async setDeploymentName(name: string): Promise { + await SecureStore.setItemAsync('deploymentName', name); + } + + async getDeploymentName(): Promise { + try { + return await SecureStore.getItemAsync('deploymentName'); + } catch { + return null; + } } async validateDeploymentId(deploymentId: string): Promise> { @@ -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> { + 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> { const token = await this.getToken();