import { useState, useCallback, } from 'react'; import Box from '@mui/material/Box'; import { SeverityType } from './Snack'; import { ContextStatus } from './ContextStatus'; import { MessageData } from './MessageMeta'; import { DocumentViewer } from './DocumentViewer'; interface ResumeBuilderProps { scrollToBottom: () => void, isScrolledToBottom: () => boolean, setProcessing: (processing: boolean) => void, processing: boolean, connectionBase: string, sessionId: string | undefined, setSnack: (message: string, severity?: SeverityType) => void, }; const ResumeBuilder = ({ scrollToBottom, isScrolledToBottom, setProcessing, processing, connectionBase, sessionId, setSnack }: ResumeBuilderProps) => { const [lastEvalTPS, setLastEvalTPS] = useState(35); const [lastPromptTPS, setLastPromptTPS] = useState(430); const [contextStatus, setContextStatus] = useState({ context_used: 0, max_context: 0 }); // const [countdown, setCountdown] = useState(0); const [resume, setResume] = useState(undefined); const [facts, setFacts] = useState(undefined); // const timerRef = useRef(null); const updateContextStatus = useCallback(() => { fetch(connectionBase + `/api/context-status/${sessionId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => { setContextStatus(data); }) .catch(error => { console.error('Error getting context status:', error); setSnack("Unable to obtain context status.", "error"); }); }, [setContextStatus, connectionBase, setSnack, sessionId]); // const startCountdown = (seconds: number) => { // if (timerRef.current) clearInterval(timerRef.current); // setCountdown(seconds); // timerRef.current = setInterval(() => { // setCountdown((prev) => { // if (prev <= 1) { // clearInterval(timerRef.current); // timerRef.current = null; // if (isScrolledToBottom()) { // setTimeout(() => { // scrollToBottom(); // }, 50) // } // return 0; // } // return prev - 1; // }); // }, 1000); // }; // const stopCountdown = () => { // if (timerRef.current) { // clearInterval(timerRef.current); // timerRef.current = null; // setCountdown(0); // } // }; if (sessionId === undefined) { return (<>); } const generateResume = async (jobDescription: string) => { if (!jobDescription.trim()) return; setResume(undefined); try { setProcessing(true); // Add initial processing message //setGenerateStatus({ role: 'assistant', content: 'Processing request...' }); // Make the fetch request with proper headers const response = await fetch(connectionBase + `/api/generate-resume/${sessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ content: jobDescription.trim() }), }); // We'll guess that the response will be around 500 tokens... const token_guess = 500; const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS); setSnack(`Job description sent. Response estimated in ${estimate}s.`, "info"); //startCountdown(Math.round(estimate)); if (!response.ok) { throw new Error(`Server responded with ${response.status}: ${response.statusText}`); } if (!response.body) { throw new Error('Response body is null'); } // Set up stream processing with explicit chunking const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } const chunk = decoder.decode(value, { stream: true }); // Process each complete line immediately buffer += chunk; let lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (!line.trim()) continue; try { const update = JSON.parse(line); // Force an immediate state update based on the message type if (update.status === 'processing') { // Update processing message with immediate re-render //setGenerateStatus({ role: 'info', content: update.message }); console.log(update.num_ctx); // Add a small delay to ensure React has time to update the UI await new Promise(resolve => setTimeout(resolve, 0)); } else if (update.status === 'done') { // Replace processing message with final result //setGenerateStatus(undefined); setResume(update.message); const metadata = update.message.metadata; const evalTPS = metadata.eval_count * 10 ** 9 / metadata.eval_duration; const promptTPS = metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration; setLastEvalTPS(evalTPS ? evalTPS : 35); setLastPromptTPS(promptTPS ? promptTPS : 35); updateContextStatus(); } else if (update.status === 'error') { // Show error //setGenerateStatus({ role: 'error', content: update.message }); } } catch (e) { setSnack("Error generating resume", "error") console.error('Error parsing JSON:', e, line); } } } // Process any remaining buffer content if (buffer.trim()) { try { const update = JSON.parse(buffer); if (update.status === 'done') { //setGenerateStatus(undefined); setResume(update.message); } } catch (e) { setSnack("Error processing job description", "error") } } //stopCountdown(); setProcessing(false); } catch (error) { console.error('Fetch error:', error); setSnack("Unable to process job description", "error"); //setGenerateStatus({ role: 'error', content: `Error: ${error}` }); setProcessing(false); //stopCountdown(); } }; const factCheck = async (resume: string) => { if (!resume.trim()) return; setFacts(undefined); try { setProcessing(true); const response = await fetch(connectionBase + `/api/fact-check/${sessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ content: resume.trim() }), }); // We'll guess that the response will be around 500 tokens... const token_guess = 500; const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS); setSnack(`Resume sent for Fact Check. Response estimated in ${estimate}s.`, "info"); //startCountdown(Math.round(estimate)); if (!response.ok) { throw new Error(`Server responded with ${response.status}: ${response.statusText}`); } if (!response.body) { throw new Error('Response body is null'); } // Set up stream processing with explicit chunking const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } const chunk = decoder.decode(value, { stream: true }); // Process each complete line immediately buffer += chunk; let lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (!line.trim()) continue; try { const update = JSON.parse(line); // Force an immediate state update based on the message type if (update.status === 'processing') { // Add a small delay to ensure React has time to update the UI await new Promise(resolve => setTimeout(resolve, 0)); } else if (update.status === 'done') { // Replace processing message with final result setFacts(update.message); const metadata = update.message.metadata; const evalTPS = metadata.eval_count * 10 ** 9 / metadata.eval_duration; const promptTPS = metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration; setLastEvalTPS(evalTPS ? evalTPS : 35); setLastPromptTPS(promptTPS ? promptTPS : 35); updateContextStatus(); } else if (update.status === 'error') { } } catch (e) { setSnack("Error generating resume", "error") console.error('Error parsing JSON:', e, line); } } } // Process any remaining buffer content if (buffer.trim()) { try { const update = JSON.parse(buffer); if (update.status === 'done') { //setGenerateStatus(undefined); setFacts(update.message); } } catch (e) { setSnack("Error processing resume", "error") } } //stopCountdown(); setProcessing(false); } catch (error) { console.error('Fetch error:', error); setSnack("Unable to process resume", "error"); //setGenerateStatus({ role: 'error', content: `Error: ${error}` }); setProcessing(false); //stopCountdown(); } }; return ( ); } export type { ResumeBuilderProps }; export { ResumeBuilder };