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'; const defaultMessage: ChatMessage = { status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user" }; const GenerateCandidate = (props: BackstoryElementProps) => { const { apiClient, user } = useAuth(); const { setSnack, submitQuery } = props; 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 };