No more observer resize errors
This commit is contained in:
parent
2ee1356189
commit
6db749d21c
@ -25,7 +25,7 @@ import { VectorVisualizer } from './VectorVisualizer';
|
||||
import { Controls } from './Controls';
|
||||
import { Conversation, ConversationHandle } from './Conversation';
|
||||
import { ChatQuery, QueryOptions } from './ChatQuery';
|
||||
import { Scrollable } from './AutoScroll';
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { BackstoryTab } from './BackstoryTab';
|
||||
|
||||
import './App.css';
|
||||
@ -177,13 +177,11 @@ const App = () => {
|
||||
iconPosition: "start"
|
||||
},
|
||||
children: (
|
||||
<Scrollable
|
||||
<Conversation
|
||||
sx={{
|
||||
maxWidth: "1024px",
|
||||
height: "calc(100vh - 72px)",
|
||||
}}
|
||||
>
|
||||
<Conversation
|
||||
ref={chatRef}
|
||||
{...{
|
||||
type: "chat",
|
||||
@ -196,7 +194,6 @@ const App = () => {
|
||||
defaultPrompts: backstoryQuestions
|
||||
}}
|
||||
/>
|
||||
</Scrollable>
|
||||
)
|
||||
}, {
|
||||
label: "Resume Builder",
|
||||
@ -323,7 +320,7 @@ const App = () => {
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
|
||||
console.log(tabs);
|
||||
|
||||
if (pathParts.length < 1) {
|
||||
console.log("No session id or path -- creating new session");
|
||||
fetchSession();
|
||||
|
@ -1,156 +0,0 @@
|
||||
import { useEffect, useRef, RefObject } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
/**
|
||||
* Hook that automatically scrolls a container to the bottom when content changes
|
||||
* or when the container is resized, but only if the user is already near the bottom.
|
||||
*
|
||||
* @param threshold - Distance from bottom (px) to consider "near bottom" (default: 100)
|
||||
* @param smooth - Whether to use smooth scrolling (default: true)
|
||||
* @returns Ref to attach to the scrollable container
|
||||
*/
|
||||
const useAutoScrollToBottom = (
|
||||
threshold: number = 0.33, // Percentage of viewport to trigger threshold
|
||||
smooth: boolean = true
|
||||
): RefObject<HTMLDivElement> => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const lastScrollTop = useRef<number>(0);
|
||||
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
// console.log("No ref");
|
||||
return;
|
||||
}
|
||||
const lastScrollHeight = container.scrollHeight;
|
||||
var isUserScrollingUp = false;
|
||||
|
||||
// Function to check if we should scroll to bottom
|
||||
const checkAndScrollToBottom = (priorScrollHeight?: number | undefined): void => {
|
||||
if (!container) return;
|
||||
|
||||
const scrollHeight = (priorScrollHeight !== undefined) ? priorScrollHeight : container.scrollHeight;
|
||||
|
||||
// Only auto-scroll if the user is near the bottom and not actively scrolling up
|
||||
const isNearBottom: boolean =
|
||||
scrollHeight - container.scrollTop - container.clientHeight <= container.clientHeight * threshold;
|
||||
|
||||
if (isNearBottom && !isUserScrollingUp) {
|
||||
// console.log('Scrolling', {
|
||||
// isNearBottom,
|
||||
// isUserScrollingUp,
|
||||
// scrollHeightToUser: scrollHeight,
|
||||
// scrollHeight: container.scrollHeight,
|
||||
// scrollTop: container.scrollTop,
|
||||
// clientHeight: container.clientHeight,
|
||||
// threshold,
|
||||
// delta: container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
// });
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
});
|
||||
} else {
|
||||
// console.log('Not scrolling', {
|
||||
// isNearBottom,
|
||||
// isUserScrollingUp,
|
||||
// scrollHeight: container.scrollHeight,
|
||||
// scrollTop: container.scrollTop,
|
||||
// clientHeight: container.clientHeight,
|
||||
// threshold,
|
||||
// delta: container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
// Set up ResizeObserver to detect content size changes
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkAndScrollToBottom(lastScrollHeight);
|
||||
});
|
||||
|
||||
// Observe the container and its children
|
||||
resizeObserver.observe(container);
|
||||
Array.from(container.children).forEach((child) => {
|
||||
resizeObserver.observe(child);
|
||||
});
|
||||
|
||||
// Track user scrolling behavior
|
||||
const handleScroll = (): void => {
|
||||
if (!container) {
|
||||
// console.log("No ref in handleScroll");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (scrollTimeout.current) {
|
||||
clearTimeout(scrollTimeout.current);
|
||||
}
|
||||
|
||||
// Determine scroll direction
|
||||
const currentScrollTop = container.scrollTop;
|
||||
isUserScrollingUp = currentScrollTop < lastScrollTop.current;
|
||||
lastScrollTop.current = currentScrollTop;
|
||||
|
||||
// Reset the scrolling flag after user stops scrolling
|
||||
scrollTimeout.current = setTimeout(() => {
|
||||
//setIsUserScrollingUp(false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Add scroll event listener
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
|
||||
// Run initial check
|
||||
checkAndScrollToBottom();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
if (scrollTimeout.current) {
|
||||
clearTimeout(scrollTimeout.current);
|
||||
}
|
||||
};
|
||||
}, [smooth, threshold]); // Re-run when dependencies change or scrolling state changes
|
||||
|
||||
return containerRef as RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
interface ScrollableProps {
|
||||
children?: React.ReactNode;
|
||||
sx?: SxProps<Theme>,
|
||||
autoscroll?: boolean,
|
||||
}
|
||||
|
||||
const Scrollable = (props: ScrollableProps) => {
|
||||
const { sx, children, autoscroll } = props;
|
||||
const scrollRef = useAutoScrollToBottom();
|
||||
return <Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
margin: "0 auto",
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
backgroundColor: "#F5F5F5",
|
||||
...sx
|
||||
}}
|
||||
ref={(autoscroll !== undefined && autoscroll !== false) ? scrollRef : undefined}
|
||||
>{children}</Box>;
|
||||
};
|
||||
|
||||
export {
|
||||
useAutoScrollToBottom,
|
||||
Scrollable,
|
||||
};
|
||||
|
124
frontend/src/BackstoryTextField.tsx
Normal file
124
frontend/src/BackstoryTextField.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useRef, useEffect, ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
interface BackstoryTextFieldProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
multiline?: boolean;
|
||||
placeholder?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
onKeyDown?: (e: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const BackstoryTextField: React.FC<BackstoryTextFieldProps> = ({
|
||||
value,
|
||||
disabled = false,
|
||||
multiline = false,
|
||||
placeholder,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const shadowRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (multiline && textareaRef.current && shadowRef.current) {
|
||||
const shadow = shadowRef.current;
|
||||
const textarea = textareaRef.current;
|
||||
|
||||
shadow.value = value || placeholder || '';
|
||||
|
||||
window.setTimeout(() => {
|
||||
const computed = getComputedStyle(textarea);
|
||||
const paddingTop = parseFloat(computed.paddingTop || '0');
|
||||
const paddingBottom = parseFloat(computed.paddingBottom || '0');
|
||||
|
||||
const totalPadding = paddingTop + paddingBottom;
|
||||
const newHeight = shadow.scrollHeight + totalPadding;
|
||||
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
}, 100);
|
||||
}
|
||||
}, [value, multiline, placeholder]);
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
if (!multiline && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (onKeyDown) {
|
||||
onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
const sharedStyle = {
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
resize: 'none' as const,
|
||||
overflow: 'hidden' as const,
|
||||
boxSizing: 'border-box' as const,
|
||||
lineHeight: '1.5',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '16px',
|
||||
opacity: disabled ? "0.38" : "1",
|
||||
color: disabled ? 'rgba(0, 0, 0, 0.38)' : 'rgb(46, 46, 46)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
};
|
||||
|
||||
if (!multiline) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={sharedStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
...sharedStyle,
|
||||
height: 'auto',
|
||||
}}
|
||||
/>
|
||||
<textarea
|
||||
ref={shadowRef}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
...sharedStyle,
|
||||
position: 'relative',
|
||||
top: '-9999px',
|
||||
left: '-9999px',
|
||||
visibility: 'hidden',
|
||||
padding: '0px',
|
||||
margin: '0px',
|
||||
overflow: 'auto',
|
||||
height: '0px',
|
||||
}}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { BackstoryTextField };
|
@ -10,4 +10,5 @@
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 72px);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Button from '@mui/material/Button';
|
||||
@ -7,14 +6,16 @@ import Box from '@mui/material/Box';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
import { Message, MessageList, MessageData } from './Message';
|
||||
import { SetSnackType } from './Snack';
|
||||
import { ContextStatus } from './ContextStatus';
|
||||
import { useAutoScrollToBottom } from './AutoScroll';
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { DeleteConfirmation } from './DeleteConfirmation';
|
||||
import { QueryOptions } from './ChatQuery';
|
||||
import './Conversation.css';
|
||||
import { BackstoryTextField } from './BackstoryTextField';
|
||||
|
||||
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." };
|
||||
|
||||
@ -93,6 +94,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
const [conversation, setConversation] = useState<MessageList>([]);
|
||||
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
|
||||
const [processingMessage, setProcessingMessage] = useState<MessageData | undefined>(undefined);
|
||||
const [streamingMessage, setStreamingMessage] = useState<MessageData | undefined>(undefined);
|
||||
const timerRef = useRef<any>(null);
|
||||
const [lastEvalTPS, setLastEvalTPS] = useState<number>(35);
|
||||
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
||||
@ -100,7 +102,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||
const conversationRef = useRef<MessageList>([]);
|
||||
const scrollRef = useAutoScrollToBottom();
|
||||
const viewableElementRef = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
// Keep the ref updated whenever items changes
|
||||
useEffect(() => {
|
||||
@ -214,6 +217,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
setNoInteractions(false);
|
||||
}
|
||||
setProcessingMessage(undefined);
|
||||
setStreamingMessage(undefined);
|
||||
updateContextStatus();
|
||||
} catch (error) {
|
||||
console.error('Error generating session ID:', error);
|
||||
@ -402,6 +406,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
if (onResponse) {
|
||||
update = onResponse(update);
|
||||
}
|
||||
setStreamingMessage(undefined);
|
||||
setProcessingMessage(undefined);
|
||||
const backstoryMessage: BackstoryMessage = update;
|
||||
setConversation([
|
||||
@ -425,10 +430,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
break;
|
||||
case 'error':
|
||||
// Show error
|
||||
setProcessingMessage({ role: 'error', content: update.response });
|
||||
setTimeout(() => {
|
||||
setProcessingMessage(undefined);
|
||||
}, 5000);
|
||||
setConversation([
|
||||
...conversationRef.current, {
|
||||
...update,
|
||||
role: 'error',
|
||||
origin: type,
|
||||
content: update.response || "",
|
||||
}] as MessageList);
|
||||
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
@ -436,7 +444,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
default:
|
||||
// Force an immediate state update based on the message type
|
||||
// Update processing message with immediate re-render
|
||||
if (update.status === "streaming") {
|
||||
setStreamingMessage({ role: update.status, content: update.response });
|
||||
} else {
|
||||
setProcessingMessage({ role: update.status, content: update.response });
|
||||
}
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
break;
|
||||
@ -494,13 +506,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={className || "Conversation"}
|
||||
ref={scrollRef}
|
||||
<Scrollable
|
||||
className={className || "Conversation"}
|
||||
autoscroll
|
||||
textFieldRef={viewableElementRef}
|
||||
fallbackThreshold={0.5}
|
||||
sx={{
|
||||
p: 1,
|
||||
mt: 0,
|
||||
...sx
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{
|
||||
filteredConversation.map((message, index) =>
|
||||
<Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack }} />
|
||||
@ -510,6 +526,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
processingMessage !== undefined &&
|
||||
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage }} />
|
||||
}
|
||||
{
|
||||
streamingMessage !== undefined &&
|
||||
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: streamingMessage }} />
|
||||
}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -535,18 +555,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
</Box>
|
||||
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
||||
{prompt &&
|
||||
<TextField
|
||||
variant="outlined"
|
||||
<div
|
||||
ref={viewableElementRef}>
|
||||
<BackstoryTextField
|
||||
disabled={processing}
|
||||
fullWidth={true}
|
||||
multiline={multiline ? true : false}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onChange={(e: any) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder={prompt}
|
||||
id="QueryInput"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||
@ -585,7 +604,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
|
||||
</Box>
|
||||
</Scrollable>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -2,20 +2,9 @@ import React, { useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Paper,
|
||||
IconButton,
|
||||
Box,
|
||||
useMediaQuery,
|
||||
Divider,
|
||||
Slider,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
SwapHoriz,
|
||||
} from '@mui/icons-material';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
import { ChatQuery } from './ChatQuery';
|
||||
@ -30,7 +19,6 @@ interface ResumeBuilderProps {
|
||||
sx?: SxProps<Theme>;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* ResumeBuilder component
|
||||
*
|
||||
@ -49,13 +37,11 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
const [hasResume, setHasResume] = useState<boolean>(false);
|
||||
const [hasFacts, setHasFacts] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const jobConversationRef = useRef<any>(null);
|
||||
const resumeConversationRef = useRef<any>(null);
|
||||
const factsConversationRef = useRef<any>(null);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const [splitRatio, setSplitRatio] = useState<number>(100);
|
||||
|
||||
/**
|
||||
* Handle tab change for mobile view
|
||||
@ -64,20 +50,6 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjust split ratio for desktop view
|
||||
*/
|
||||
const handleSliderChange = (_event: Event, newValue: number | number[]): void => {
|
||||
setSplitRatio(newValue as number);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset split ratio to default
|
||||
*/
|
||||
const resetSplit = (): void => {
|
||||
setSplitRatio(50);
|
||||
};
|
||||
|
||||
const handleJobQuery = (query: string) => {
|
||||
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler');
|
||||
jobConversationRef.current?.submitQuery(query);
|
||||
@ -250,10 +222,10 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
setHasFacts(false);
|
||||
}, [setHasFacts]);
|
||||
|
||||
const renderJobDescriptionView = useCallback((small: boolean) => {
|
||||
const renderJobDescriptionView = useCallback((sx: SxProps) => {
|
||||
console.log('renderJobDescriptionView');
|
||||
const jobDescriptionQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="What are the key skills necessary for this position?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
</Box>,
|
||||
@ -274,6 +246,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -292,6 +265,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@ -300,9 +274,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
/**
|
||||
* Renders the resume view with loading indicator
|
||||
*/
|
||||
const renderResumeView = useCallback((small: boolean) => {
|
||||
const renderResumeView = useCallback((sx: SxProps) => {
|
||||
const resumeQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="Is this resume a good fit for the provided job description?" tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
</Box>,
|
||||
@ -322,6 +296,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
} else {
|
||||
@ -339,6 +314,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
connectionBase,
|
||||
setSnack,
|
||||
defaultPrompts: resumeQuestions,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@ -347,9 +323,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
/**
|
||||
* Renders the fact check view
|
||||
*/
|
||||
const renderFactCheckView = useCallback((small: boolean) => {
|
||||
const renderFactCheckView = useCallback((sx: SxProps) => {
|
||||
const factsQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
|
||||
</Box>,
|
||||
];
|
||||
@ -368,133 +344,11 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]);
|
||||
|
||||
/**
|
||||
* Gets the appropriate content based on active state for Desktop
|
||||
*/
|
||||
const getActiveDesktopContent = useCallback(() => {
|
||||
const hasSlider = hasResume || hasFacts;
|
||||
const ratio = 75 + 25 * splitRatio / 100;
|
||||
const otherRatio = hasResume ? ratio / (hasFacts ? 3 : 2) : 100;
|
||||
const resumeRatio = 100 - otherRatio * (hasFacts ? 2 : 1);
|
||||
const children = [];
|
||||
children.push(
|
||||
<Box key="JobDescription" className="ChatBox" sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: `${otherRatio}%`,
|
||||
width: `${otherRatio}%`,
|
||||
maxWidth: `${otherRatio}%`,
|
||||
p: 0,
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
{renderJobDescriptionView(false)}
|
||||
</Box>);
|
||||
|
||||
/* Resume panel - conditionally rendered if resume defined, or processing is in progress */
|
||||
if (hasResume) {
|
||||
children.push(
|
||||
<Box key="ResumeView"
|
||||
className="ChatBox"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: `${resumeRatio}%`,
|
||||
width: `${resumeRatio}%`,
|
||||
maxWidth: `${resumeRatio}%`,
|
||||
p: 0,
|
||||
flexGrow: 1
|
||||
}}>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
{renderResumeView(false)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */
|
||||
if (hasFacts) {
|
||||
children.push(
|
||||
<Box key="FactCheckView"
|
||||
className="ChatBox"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: `${otherRatio}%`,
|
||||
width: `${otherRatio}%`,
|
||||
maxWidth: `${otherRatio}%`,
|
||||
p: 0,
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
{renderFactCheckView(false)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/* Split control panel - conditionally rendered if either facts or resume is set */
|
||||
let slider = <Box key="slider"></Box>;
|
||||
if (hasSlider) {
|
||||
slider = (
|
||||
<Paper key="slider" sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Stack direction="row" spacing={2} alignItems="center" sx={{ width: '60%' }}>
|
||||
<IconButton onClick={() => setSplitRatio(s => Math.max(0, s - 10))}>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
|
||||
<Slider
|
||||
value={splitRatio}
|
||||
onChange={handleSliderChange}
|
||||
aria-label="Split ratio"
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
|
||||
<IconButton onClick={() => setSplitRatio(s => Math.min(100, s + 10))}>
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={resetSplit}>
|
||||
<SwapHoriz />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 0,
|
||||
m: 0,
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
p: 0,
|
||||
m: 0,
|
||||
margin: "0 auto",
|
||||
maxWidth: hasSlider ? "100%" : "1024px",
|
||||
width: hasSlider ? "100%" : "1024px",
|
||||
height: `calc(100vh - ${hasSlider ? 144 : 72}px)`,
|
||||
backgroundColor: "#F5F5F5",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
{slider}
|
||||
</Box>
|
||||
)
|
||||
}, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, hasFacts, hasResume]);
|
||||
|
||||
// Render mobile view
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 0,
|
||||
@ -503,9 +357,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
flexGrow: 1,
|
||||
margin: "0 auto",
|
||||
overflow: "hidden",
|
||||
height: "calc(100vh - 72px)",
|
||||
backgroundColor: "#F5F5F5",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
maxWidth: "1024px",
|
||||
}}
|
||||
>
|
||||
{/* Tabs */}
|
||||
@ -521,16 +375,16 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
</Tabs>
|
||||
|
||||
{/* Document display area */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx }}>
|
||||
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(true)}</Box>
|
||||
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView(true)}</Box>
|
||||
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(true)}</Box>
|
||||
<Box sx={{
|
||||
display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return getActiveDesktopContent();
|
||||
};
|
||||
|
||||
export type {
|
||||
|
40
frontend/src/Scrollable.tsx
Normal file
40
frontend/src/Scrollable.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import { RefObject, useRef } from 'react';
|
||||
import { useAutoScrollToBottom } from './useAutoScrollToBottom';
|
||||
|
||||
interface ScrollableProps {
|
||||
children?: React.ReactNode;
|
||||
sx?: SxProps<Theme>;
|
||||
autoscroll?: boolean;
|
||||
textFieldRef?: RefObject<HTMLElement | null>; // Reference to the element that triggers auto-scroll
|
||||
fallbackThreshold?: number;
|
||||
contentUpdateTrigger?: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Scrollable = (props: ScrollableProps) => {
|
||||
const { sx, className, children, autoscroll, textFieldRef, fallbackThreshold = 0.33, contentUpdateTrigger } = props;
|
||||
// Create a default ref if textFieldRef is not provided
|
||||
const defaultTextFieldRef = useRef<HTMLElement | null>(null);
|
||||
const scrollRef = useAutoScrollToBottom(textFieldRef ?? defaultTextFieldRef, true, fallbackThreshold, contentUpdateTrigger);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className || "Scrollable"}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
margin: '0 auto',
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#F5F5F5',
|
||||
...sx,
|
||||
}}
|
||||
ref={autoscroll !== undefined && autoscroll !== false ? scrollRef : undefined}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { useAutoScrollToBottom, Scrollable };
|
@ -153,7 +153,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
||||
if (plotlyRef.current) {
|
||||
Plot.Plots.resize(plotlyRef.current);
|
||||
}
|
||||
}, [])
|
||||
}, [plotlyRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (!result || !result.embeddings) return;
|
||||
|
173
frontend/src/useAutoScrollToBottom.tsx
Normal file
173
frontend/src/useAutoScrollToBottom.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { useEffect, useRef, RefObject, useCallback } from 'react';
|
||||
|
||||
type ResizeCallback = () => void;
|
||||
|
||||
const useResizeObserverAndMutationObserver = (
|
||||
targetRef: RefObject<HTMLElement | null>,
|
||||
scrollToRef: RefObject<HTMLElement | null>,
|
||||
callback: ResizeCallback
|
||||
) => {
|
||||
const callbackRef = useRef(callback);
|
||||
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||
const mutationObserverRef = useRef<MutationObserver | null>(null);
|
||||
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = targetRef.current;
|
||||
const scrollTo = scrollToRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const debouncedCallback = (entries: ResizeObserverEntry[] | undefined) => {
|
||||
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
requestAnimationFrame(() => callbackRef.current());
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(debouncedCallback);
|
||||
const mutationObserver = new MutationObserver(() => { debouncedCallback(undefined); });
|
||||
|
||||
// Observe container size
|
||||
resizeObserver.observe(container);
|
||||
// Observe TextField size if available
|
||||
if (scrollTo) {
|
||||
resizeObserver.observe(scrollTo);
|
||||
}
|
||||
|
||||
// Observe child additions/removals and text content changes
|
||||
mutationObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
resizeObserverRef.current = resizeObserver;
|
||||
mutationObserverRef.current = mutationObserver;
|
||||
|
||||
return () => {
|
||||
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
||||
resizeObserver.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
}, [targetRef, scrollToRef]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto-scroll hook for scrollable containers.
|
||||
* Scrolls to the bottom of the container on paste or when TextField is fully/partially visible.
|
||||
*/
|
||||
export const useAutoScrollToBottom = (
|
||||
scrollToRef: RefObject<HTMLElement | null>,
|
||||
smooth: boolean = true,
|
||||
fallbackThreshold: number = 0.33,
|
||||
contentUpdateTrigger?: any
|
||||
): RefObject<HTMLDivElement | null> => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const lastScrollTop = useRef(0);
|
||||
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const isUserScrollingUpRef = useRef(false);
|
||||
|
||||
const checkAndScrollToBottom = useCallback((isPasteEvent: boolean = false) => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let shouldScroll = false;
|
||||
const scrollTo = scrollToRef.current;
|
||||
|
||||
if (scrollTo) {
|
||||
// Get positions
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const scrollToRect = scrollTo.getBoundingClientRect();
|
||||
const containerTop = containerRect.top;
|
||||
const containerBottom = containerTop + container.clientHeight;
|
||||
|
||||
// Check if TextField is fully or partially visible (for non-paste events)
|
||||
const isTextFieldVisible =
|
||||
scrollToRect.top < containerBottom && scrollToRect.bottom > containerTop;
|
||||
|
||||
// Scroll on paste or if TextField is visible and user isn't scrolling up
|
||||
shouldScroll = (isPasteEvent || isTextFieldVisible) && !isUserScrollingUpRef.current;
|
||||
|
||||
if (shouldScroll) {
|
||||
requestAnimationFrame(() => {
|
||||
console.debug('Scrolling to container bottom:', {
|
||||
scrollHeight: container.scrollHeight,
|
||||
scrollToHeight: scrollToRect.height,
|
||||
containerHeight: container.clientHeight,
|
||||
isPasteEvent,
|
||||
isTextFieldVisible,
|
||||
isUserScrollingUp: isUserScrollingUpRef.current,
|
||||
});
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto',
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback to threshold-based check
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const isNearBottom =
|
||||
scrollHeight - container.scrollTop - container.clientHeight <=
|
||||
container.clientHeight * fallbackThreshold;
|
||||
shouldScroll = isNearBottom && !isUserScrollingUpRef.current;
|
||||
|
||||
if (shouldScroll) {
|
||||
requestAnimationFrame(() => {
|
||||
console.debug('Scrolling to container bottom (fallback):', { scrollHeight });
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [fallbackThreshold, smooth, scrollToRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const scrollTo = scrollToRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollTop = container.scrollTop;
|
||||
isUserScrollingUpRef.current = currentScrollTop < lastScrollTop.current;
|
||||
lastScrollTop.current = currentScrollTop;
|
||||
|
||||
if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
|
||||
scrollTimeout.current = setTimeout(() => {
|
||||
isUserScrollingUpRef.current = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handlePaste = () => {
|
||||
// Delay scroll check to ensure DOM updates
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => checkAndScrollToBottom(true));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
if (scrollTo) {
|
||||
scrollTo.addEventListener('paste', handlePaste);
|
||||
}
|
||||
checkAndScrollToBottom();
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
if (scrollTo) {
|
||||
scrollTo.removeEventListener('paste', handlePaste);
|
||||
}
|
||||
if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
|
||||
};
|
||||
}, [smooth, scrollToRef, fallbackThreshold, contentUpdateTrigger, checkAndScrollToBottom]);
|
||||
|
||||
// Observe container and TextField size, plus DOM changes
|
||||
useResizeObserverAndMutationObserver(containerRef, scrollToRef, checkAndScrollToBottom);
|
||||
|
||||
return containerRef;
|
||||
};
|
@ -549,6 +549,7 @@ class WebServer:
|
||||
if not isinstance(prompt, str) or len(prompt) == 0:
|
||||
logger.info(f"Prompt is empty")
|
||||
return JSONResponse({"error": "Prompt cannot be empty"}, status_code=400)
|
||||
|
||||
try:
|
||||
options = Tunables(**query["options"]) if "options" in query else None
|
||||
except Exception as e:
|
||||
@ -556,11 +557,6 @@ class WebServer:
|
||||
return JSONResponse({"error": f"Invalid options: {query['options']}"}, status_code=400)
|
||||
|
||||
if not agent:
|
||||
# job_description is the only agent that is dynamically generated from a
|
||||
# Rest API endpoint.
|
||||
# - 'chat' is created on context creation.
|
||||
# - 'resume' is created on actions by 'job_description'
|
||||
# - 'fact_check' is created on ations by 'fact_check'
|
||||
match agent_type:
|
||||
case "job_description":
|
||||
logger.info(f"Agent {agent_type} not found. Returning empty history.")
|
||||
@ -569,32 +565,68 @@ class WebServer:
|
||||
logger.info(f"Invalid agent creation sequence for {agent_type}. Returning error.")
|
||||
return JSONResponse({"error": f"{agent_type} is not recognized", "context": context.id}, status_code=404)
|
||||
|
||||
# Create a custom generator that ensures flushing
|
||||
async def flush_generator():
|
||||
logging.info(f"Message starting. Streaming partial results.")
|
||||
logger.info(f"Message starting. Streaming partial results.")
|
||||
# Create a cancellable task to manage the generator
|
||||
loop = asyncio.get_running_loop()
|
||||
stop_event = asyncio.Event()
|
||||
|
||||
async def process_generator():
|
||||
try:
|
||||
async for message in self.generate_response(context=context, agent=agent, prompt=prompt, options=options):
|
||||
if stop_event.is_set():
|
||||
logger.info("Stopping generator due to client disconnection.")
|
||||
return
|
||||
if message.status != "done":
|
||||
result = {
|
||||
"status": message.status,
|
||||
"response": message.response
|
||||
}
|
||||
else:
|
||||
logging.info(f"Message complete. Providing full response.")
|
||||
logger.info(f"Message complete. Providing full response.")
|
||||
try:
|
||||
result = message.model_dump(by_alias=True, mode='json')
|
||||
except Exception as e:
|
||||
result = { "status": "error", "response": e }
|
||||
exit(1)
|
||||
result = {"status": "error", "response": str(e)}
|
||||
yield json.dumps(result) + "\n"
|
||||
return
|
||||
|
||||
# Convert to JSON and add newline
|
||||
result = json.dumps(result) + "\n"
|
||||
message.network_packets += 1
|
||||
message.network_bytes += len(result)
|
||||
yield result
|
||||
# Explicitly flush after each yield
|
||||
await asyncio.sleep(0) # Allow the event loop to process the write
|
||||
# Save the history once completed
|
||||
# Allow the event loop to process the write
|
||||
await asyncio.sleep(0)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in process_generator: {e}")
|
||||
yield json.dumps({"status": "error", "response": str(e)}) + "\n"
|
||||
finally:
|
||||
# Save context on completion or error
|
||||
self.save_context(context_id)
|
||||
|
||||
# Create a generator iterator
|
||||
gen = process_generator()
|
||||
|
||||
try:
|
||||
async for result in gen:
|
||||
# Check if client has disconnected
|
||||
if await request.is_disconnected():
|
||||
logger.info("Client disconnected, stopping generator.")
|
||||
stop_event.set() # Signal the generator to stop
|
||||
return
|
||||
yield result
|
||||
except Exception as e:
|
||||
logger.error(f"Error in flush_generator: {e}")
|
||||
yield json.dumps({"status": "error", "response": str(e)}) + "\n"
|
||||
finally:
|
||||
stop_event.set() # Ensure generator stops if not already stopped
|
||||
# Ensure generator is fully closed
|
||||
try:
|
||||
await gen.aclose()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing generator: {e}")
|
||||
|
||||
# Return StreamingResponse with appropriate headers
|
||||
return StreamingResponse(
|
||||
flush_generator(),
|
||||
|
@ -374,6 +374,7 @@ class JobDescription(Agent):
|
||||
2. Create a comprehensive inventory of the candidate's actual qualifications.
|
||||
3. DO NOT consider any job requirements - this is a pure candidate analysis task.
|
||||
4. For each qualification, cite exactly where in the materials it appears.
|
||||
5. DO NOT duplicate or repeat time periods or skills once listed.
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
|
||||
@ -778,7 +779,6 @@ class JobDescription(Agent):
|
||||
|
||||
# Extract JSON from response
|
||||
json_str = self.extract_json_from_text(message.response)
|
||||
verification_results = json.loads(json_str)
|
||||
|
||||
message.status = "done"
|
||||
message.response = json_str
|
||||
@ -862,12 +862,11 @@ class JobDescription(Agent):
|
||||
"""
|
||||
try:
|
||||
message.status = "thinking"
|
||||
message.response = "Starting multi-stage RAG resume generation process"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
# Stage 1A: Analyze job requirements
|
||||
message.response = "Stage 1A: Analyzing job requirements"
|
||||
message.response = "Multi-stage RAG resume generation process: Stage 1A: Analyzing job requirements"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
@ -878,10 +877,11 @@ class JobDescription(Agent):
|
||||
return
|
||||
|
||||
job_requirements = json.loads(message.response)
|
||||
message.metadata["job_requirements"] = job_requirements
|
||||
|
||||
# Stage 1B: Analyze candidate qualifications
|
||||
message.status = "thinking"
|
||||
message.response = "Stage 1B: Analyzing candidate qualifications"
|
||||
message.response = "Multi-stage RAG resume generation process: Stage 1B: Analyzing candidate qualifications"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
@ -892,10 +892,11 @@ class JobDescription(Agent):
|
||||
return
|
||||
|
||||
candidate_qualifications = json.loads(message.response)
|
||||
message.metadata["candidate_qualifications"] = job_requirements
|
||||
|
||||
# Stage 1C: Create skills mapping
|
||||
message.status = "thinking"
|
||||
message.response = "Stage 1C: Creating skills mapping"
|
||||
message.response = "Multi-stage RAG resume generation process: Stage 1C: Creating skills mapping"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
@ -906,13 +907,14 @@ class JobDescription(Agent):
|
||||
return
|
||||
|
||||
skills_mapping = json.loads(message.response)
|
||||
message.metadata["skills_mapping"] = skills_mapping
|
||||
|
||||
# Extract header from original resume
|
||||
original_header = self.extract_header_from_resume(resume)
|
||||
|
||||
# Stage 2: Generate tailored resume
|
||||
message.status = "thinking"
|
||||
message.response = "Stage 2: Generating tailored resume"
|
||||
message.response = "Multi-stage RAG resume generation process: Stage 2: Generating tailored resume"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
@ -923,10 +925,13 @@ class JobDescription(Agent):
|
||||
return
|
||||
|
||||
generated_resume = message.response
|
||||
message.metadata["generated_resume"] = {
|
||||
"first_pass": generated_resume
|
||||
}
|
||||
|
||||
# Stage 3: Verify resume
|
||||
message.status = "thinking"
|
||||
message.response = "Stage 3: Verifying resume for accuracy"
|
||||
message.response = "Multi-stage RAG resume generation process: Stage 3: Verifying resume for accuracy"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
@ -937,6 +942,9 @@ class JobDescription(Agent):
|
||||
return
|
||||
|
||||
verification_results = json.loads(message.response)
|
||||
message.metadata["verification_results"] = {
|
||||
"first_pass": verification_results
|
||||
}
|
||||
|
||||
# Handle corrections if needed
|
||||
if verification_results["verification_results"]["overall_assessment"] == "NEEDS REVISION":
|
||||
@ -959,6 +967,7 @@ class JobDescription(Agent):
|
||||
return
|
||||
|
||||
generated_resume = message.response
|
||||
message.metadata["generated_resume"]["second_pass"] = generated_resume
|
||||
|
||||
# Re-verify after corrections
|
||||
message.status = "thinking"
|
||||
@ -973,16 +982,12 @@ class JobDescription(Agent):
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
verification_results = json.loads(message.response)
|
||||
message.metadata["verification_results"]["second_pass"] = verification_results
|
||||
|
||||
# Return the final results
|
||||
message.status = "done"
|
||||
message.response = json.dumps({
|
||||
"job_requirements": job_requirements,
|
||||
"candidate_qualifications": candidate_qualifications,
|
||||
"skills_mapping": skills_mapping,
|
||||
"generated_resume": generated_resume,
|
||||
"verification_results": verification_results
|
||||
})
|
||||
message.response = generated_resume
|
||||
yield message
|
||||
|
||||
logger.info("Resume generation process completed successfully")
|
||||
@ -1006,9 +1011,9 @@ class JobDescription(Agent):
|
||||
|
||||
self.metrics.generate_count.labels(agent=self.agent_type).inc()
|
||||
with self.metrics.generate_duration.labels(agent=self.agent_type).time():
|
||||
job_description = "You write C and C++ code for 4 years."
|
||||
resume = "I have worked on Cobol and QuickBasic for 18 years."
|
||||
additional_context = ""
|
||||
job_description = message.preamble["job_description"]
|
||||
resume = message.preamble["resume"]
|
||||
additional_context = message.preamble["context"]
|
||||
|
||||
async for message in self.generate_factual_tailored_resume(message=message, job_description=job_description, resume=resume, additional_context=additional_context):
|
||||
if message.status != "done":
|
||||
|
Loading…
x
Reference in New Issue
Block a user