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 { jsonrepair } from 'jsonrepair'; import { CandidateInfo } from '../components/CandidateInfo'; import { Quote } from 'components/Quote'; import { Candidate } from '../types/types'; 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 { ChatContext, ChatMessage, ChatMessageUser, ChatMessageBase, ChatSession, ChatQuery } from 'types/types'; import { useAuth } from 'hooks/AuthContext'; const emptyUser: Candidate = { userType: "candidate", description: "[blank]", username: "[blank]", firstName: "[blank]", lastName: "[blank]", fullName: "[blank] [blank]", questions: [], location: { city: '[blank]', country: '[blank]' }, email: '[blank]', createdAt: new Date(), updatedAt: new Date(), status: "pending", skills: [], experience: [], education: [], preferredJobTypes: [], languages: [], certifications: [] }; const GenerateCandidate = (props: BackstoryElementProps) => { const { apiClient } = useAuth(); const { setSnack, submitQuery } = props; const [streaming, setStreaming] = useState(''); const [processing, setProcessing] = useState(false); const [user, setUser] = useState(null); const [prompt, setPrompt] = useState(''); const [resume, setResume] = useState(''); const [canGenImage, setCanGenImage] = useState(false); const [status, setStatus] = useState(''); const [timestamp, setTimestamp] = useState(0); const [state, setState] = useState(0); // Replaced stateRef const [shouldGenerateProfile, setShouldGenerateProfile] = useState(false); const [chatSession, setChatSession] = useState(null); // Only keep refs that are truly necessary const controllerRef = useRef(null); const backstoryTextRef = useRef(null); /* Create the chat session */ useEffect(() => { if (chatSession) { return; } const createChatSession = async () => { try { const chatContext: ChatContext = { type: "generate_persona" }; const response: ChatSession = await apiClient.createChatSession(chatContext); setChatSession(response); setSnack(`Chat session created for generate_persona: ${response.id}`); } catch (e) { console.error(e); setSnack("Unable to create chat session.", "error"); } }; createChatSession(); }, [chatSession, setChatSession]); const generatePersona = useCallback((query: ChatQuery) => { if (!chatSession || !chatSession.id) { return; } const sessionId: string = chatSession.id; setPrompt(query.prompt || ''); setState(0); setStatus("Generating persona..."); setUser(emptyUser); setStreaming(''); setResume(''); setProcessing(true); setCanGenImage(false); setShouldGenerateProfile(false); // Reset the flag const chatMessage: ChatMessageUser = { sessionId: chatSession.id, content: query.prompt, tunables: query.tunables, status: "done", type: "user", sender: "user", timestamp: new Date() }; const streamResponse = apiClient.sendMessageStream(chatMessage, { onMessage: (chatMessage: ChatMessage) => { console.log('Message:', chatMessage); // Update UI with partial content }, onStatusChange: (status) => { console.log('Status changed:', status); // Update UI status indicator }, onComplete: () => { console.log('Content complete'); }, onWarn: (warning) => { console.log("Warning:", warning); }, onError: (error: string | ChatMessageBase) => { // Type-guard to determine if this is a ChatMessageBase or a string if (typeof error === "object" && error !== null && "content" in error) { console.log("Error message:", error); } else { console.log("Error string:", error); } }, onStreaming: (chunk) => { console.log("Streaming: ", chunk); } }); // controllerRef.current = streamQueryResponse({ // query, // type: "persona", // connectionBase, // onComplete: (msg) => { // switch (msg.status) { // case "partial": // case "done": // setState(currentState => { // switch (currentState) { // case 0: /* Generating persona */ // let partialUser = JSON.parse(jsonrepair((msg.response || '').trim())); // if (!partialUser.fullName) { // partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`; // } // console.log("Setting final user data:", partialUser); // setUser({ ...partialUser }); // return 1; /* Generating resume */ // case 1: /* Generating resume */ // setResume(msg.response || ''); // return 2; /* RAG generation */ // case 2: /* RAG generation */ // return 3; /* Image generation */ // default: // return currentState; // } // }); // if (msg.status === "done") { // setProcessing(false); // setCanGenImage(true); // setStatus(''); // controllerRef.current = null; // setState(0); // // Set flag to trigger profile generation after user state updates // console.log("Persona generation complete, setting shouldGenerateProfile flag"); // setShouldGenerateProfile(true); // } // break; // case "thinking": // setStatus(msg.response || ''); // break; // case "error": // console.log(`Error generating persona: ${msg.response}`); // setSnack(msg.response || "", "error"); // setProcessing(false); // setUser(emptyUser); // controllerRef.current = null; // setState(0); // break; // } // }, // onStreaming: (chunk) => { // setStreaming(chunk); // } // }); }, [setSnack]); const cancelQuery = useCallback(() => { if (controllerRef.current) { controllerRef.current.cancel(); controllerRef.current = null; setState(0); setProcessing(false); } }, []); const onEnter = useCallback((value: string) => { if (processing) { return; } const query: ChatQuery = { prompt: value, } generatePersona(query); }, [processing, generatePersona]); const handleSendClick = useCallback(() => { const value = (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""; generatePersona({ prompt: value }); }, [generatePersona]); // Effect to trigger profile generation when user data is ready useEffect(() => { console.log("useEffect triggered - shouldGenerateProfile:", shouldGenerateProfile, "user:", user?.username, user?.firstName); if (shouldGenerateProfile && user?.username !== "[blank]" && user?.firstName !== "[blank]") { console.log("Triggering profile generation with updated user data:", user); if (controllerRef.current) { console.log("Controller already active, skipping profile generation"); return; } // Don't generate if we still have blank user data if (user?.username === "[blank]" || user?.firstName === "[blank]") { console.log("Cannot generate profile: user data not ready"); return; } const imagePrompt = `A photorealistic profile picture of a ${user?.age} year old ${user?.gender?.toLocaleLowerCase()} ${user?.ethnicity?.toLocaleLowerCase()} person. ${prompt}` setStatus('Starting image generation...'); setProcessing(true); setCanGenImage(false); setState(3); const start = Date.now(); // controllerRef.current = streamQueryResponse({ // query: { // prompt: imagePrompt, // agentOptions: { // username: user?.username, // filename: "profile.png" // } // }, // type: "image", // sessionId, // connectionBase, // onComplete: (msg) => { // // console.log("Profile generation response:", msg); // switch (msg.status) { // case "partial": // case "done": // if (msg.status === "done") { // setProcessing(false); // controllerRef.current = null; // setState(0); // setCanGenImage(true); // setShouldGenerateProfile(false); // setUser({ // ...(user ? user : emptyUser), // hasProfile: true // }); // } // break; // case "error": // console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`); // setSnack(msg.response || "", "error"); // setProcessing(false); // controllerRef.current = null; // setState(0); // setCanGenImage(true); // setShouldGenerateProfile(false); // break; // default: // let data: any = {}; // try { // data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response; // } catch (e) { // data = { message: msg.response }; // } // if (msg.status !== "heartbeat") { // console.log(data); // } // if (data.timestamp) { // setTimestamp(data.timestamp); // } else { // setTimestamp(Date.now()) // } // if (data.message) { // setStatus(data.message); // } // break; // } // } // }); } }, [shouldGenerateProfile, user, prompt, setSnack]); // Handle streaming updates based on current state useEffect(() => { if (streaming.trim().length === 0) { return; } try { switch (state) { case 0: /* Generating persona */ const partialUser = {...emptyUser, ...JSON.parse(jsonrepair(`${streaming.trim()}...`))}; if (!partialUser.fullName) { partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`; } setUser(partialUser); break; case 1: /* Generating resume */ setResume(streaming); break; case 3: /* RAG streaming */ break; case 4: /* Image streaming */ break; } } catch { // Ignore JSON parsing errors during streaming } }, [streaming, state]); return ( {user && } { prompt && } {processing && { status && Generation status {status} } } {processing && } { resume !== '' && } { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} ); }; export { GenerateCandidate };