import React, { useEffect, useState, useRef, useCallback } from 'react'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import Button from '@mui/material/Button'; import Paper from '@mui/material/Paper'; import IconButton from '@mui/material/IconButton'; import CancelIcon from '@mui/icons-material/Cancel'; import SendIcon from '@mui/icons-material/Send'; import PropagateLoader from 'react-spinners/PropagateLoader'; import { CandidateInfo } from '../components/ui/CandidateInfo'; import { Quote } from 'components/Quote'; import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { StyledMarkdown } from 'components/StyledMarkdown'; import { Scrollable } from '../components/Scrollable'; import { Pulse } from 'components/Pulse'; import { StreamingResponse } from 'services/api-client'; import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError, } from 'types/types'; import { useAuth } from 'hooks/AuthContext'; import { Message } from 'components/Message'; import { useAppState } from 'hooks/GlobalContext'; const defaultMessage: ChatMessage = { status: 'done', type: 'text', sessionId: '', timestamp: new Date(), content: '', role: 'user', metadata: null as any, }; const GenerateCandidate = (props: BackstoryElementProps) => { const { apiClient, user } = useAuth(); const { setSnack } = useAppState(); const [processingMessage, setProcessingMessage] = useState(null); const [processing, setProcessing] = useState(false); const [generatedUser, setGeneratedUser] = useState(null); const [prompt, setPrompt] = useState(''); const [resume, setResume] = useState(null); const [canGenImage, setCanGenImage] = useState(false); const [timestamp, setTimestamp] = useState(''); const [shouldGenerateProfile, setShouldGenerateProfile] = useState(false); const [chatSession, setChatSession] = useState(null); const [loading, setLoading] = useState(false); // Only keep refs that are truly necessary const controllerRef = useRef(null); const backstoryTextRef = useRef(null); /* Create the chat session */ useEffect(() => { if (chatSession || loading || !generatedUser) { return; } try { setLoading(true); apiClient .getOrCreateChatSession( generatedUser, `Profile image generator for ${generatedUser.fullName}`, 'generate_image' ) .then(session => { setChatSession(session); setLoading(false); }); } catch (error) { setSnack('Unable to load chat session', 'error'); } finally { setLoading(false); } }, [generatedUser, chatSession, loading, setChatSession, setLoading, setSnack, apiClient]); const cancelQuery = useCallback(() => { if (controllerRef.current) { controllerRef.current.cancel(); controllerRef.current = null; setProcessing(false); } }, []); const onEnter = useCallback( (value: string) => { if (processing) { return; } const generatePersona = async (prompt: string) => { const userMessage: ChatMessageUser = { type: 'text', role: 'user', content: prompt, sessionId: '', status: 'done', timestamp: new Date(), }; setPrompt(prompt || ''); setProcessing(true); setProcessingMessage({ ...defaultMessage, content: 'Generating persona...', }); try { const result = await apiClient.createCandidateAI(userMessage); console.log(result.message, result); setGeneratedUser(result.candidate); setResume(result.resume); setCanGenImage(true); setShouldGenerateProfile(true); // Reset the flag } catch (error) { console.error(error); setPrompt(''); setResume(null); setProcessing(false); setProcessingMessage(null); setSnack('Unable to generate AI persona', 'error'); } }; generatePersona(value); }, [processing, apiClient, setSnack] ); const handleSendClick = useCallback(() => { const value = (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ''; onEnter(value); }, [onEnter]); // Effect to trigger profile image generation when user data is ready useEffect(() => { if (!chatSession || !generatedUser?.username) { return; } const username = generatedUser.username; if ( !shouldGenerateProfile || username === '[blank]' || generatedUser?.firstName === '[blank]' ) { return; } if (controllerRef.current) { console.log('Controller already active, skipping profile generation'); return; } setProcessingMessage({ ...defaultMessage, content: 'Starting image generation...', }); setProcessing(true); setCanGenImage(false); const chatMessage: ChatMessageUser = { sessionId: chatSession.id || '', role: 'user', status: 'done', type: 'text', timestamp: new Date(), content: prompt, }; controllerRef.current = apiClient.sendMessageStream(chatMessage, { onMessage: async (msg: ChatMessage) => { console.log(`onMessage: ${msg.type} ${msg.content}`, msg); controllerRef.current = null; try { await apiClient.updateCandidate(generatedUser.id || '', { profileImage: 'profile.png', }); const { success, message } = await apiClient.deleteChatSession(chatSession.id || ''); console.log( `Profile generated for ${username} and chat session was ${ !success ? 'not ' : '' } deleted: ${message}}` ); setGeneratedUser({ ...generatedUser, profileImage: 'profile.png', } as CandidateAI); setCanGenImage(true); setShouldGenerateProfile(false); } catch (error) { console.error(error); setSnack( `Unable to update ${username} to indicate they have a profile picture.`, 'error' ); } }, onError: (error: string | ChatMessageError) => { console.log('onError:', error); // Type-guard to determine if this is a ChatMessageBase or a string if (typeof error === 'object' && error !== null && 'content' in error) { setSnack(error.content || 'Unknown error generating profile image', 'error'); } else { setSnack(error as string, 'error'); } setProcessingMessage(null); setProcessing(false); controllerRef.current = null; setCanGenImage(true); setShouldGenerateProfile(false); }, onComplete: () => { setProcessingMessage(null); setProcessing(false); controllerRef.current = null; setCanGenImage(true); setShouldGenerateProfile(false); }, onStatus: (status: ChatMessageStatus) => { if (status.activity === 'heartbeat' && status.content) { setTimestamp(status.timestamp?.toISOString() || ''); } else if (status.content) { setProcessingMessage({ ...defaultMessage, content: status.content }); } console.log(`onStatusChange: ${status}`); }, }); }, [chatSession, shouldGenerateProfile, generatedUser, prompt, setSnack, apiClient]); if (!user?.isAdmin) { return You must be logged in as an admin to generate AI candidates.; } return ( {generatedUser && } {prompt && } {processing && ( {processingMessage && chatSession && ( )} )} {processing && ( )} {resume && ( )} {' '} {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} ); }; export { GenerateCandidate };