Compare commits
2 Commits
2ee1356189
...
11c1c2b9b4
Author | SHA1 | Date | |
---|---|---|---|
11c1c2b9b4 | |||
6db749d21c |
@ -25,7 +25,7 @@ import { VectorVisualizer } from './VectorVisualizer';
|
|||||||
import { Controls } from './Controls';
|
import { Controls } from './Controls';
|
||||||
import { Conversation, ConversationHandle } from './Conversation';
|
import { Conversation, ConversationHandle } from './Conversation';
|
||||||
import { ChatQuery, QueryOptions } from './ChatQuery';
|
import { ChatQuery, QueryOptions } from './ChatQuery';
|
||||||
import { Scrollable } from './AutoScroll';
|
import { Scrollable } from './Scrollable';
|
||||||
import { BackstoryTab } from './BackstoryTab';
|
import { BackstoryTab } from './BackstoryTab';
|
||||||
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@ -177,26 +177,23 @@ const App = () => {
|
|||||||
iconPosition: "start"
|
iconPosition: "start"
|
||||||
},
|
},
|
||||||
children: (
|
children: (
|
||||||
<Scrollable
|
<Conversation
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: "1024px",
|
maxWidth: "1024px",
|
||||||
height: "calc(100vh - 72px)",
|
height: "calc(100vh - 72px)",
|
||||||
}}
|
}}
|
||||||
>
|
ref={chatRef}
|
||||||
<Conversation
|
{...{
|
||||||
ref={chatRef}
|
type: "chat",
|
||||||
{...{
|
prompt: "What would you like to know about James?",
|
||||||
type: "chat",
|
resetLabel: "chat",
|
||||||
prompt: "What would you like to know about James?",
|
sessionId,
|
||||||
resetLabel: "chat",
|
connectionBase,
|
||||||
sessionId,
|
setSnack,
|
||||||
connectionBase,
|
preamble: backstoryPreamble,
|
||||||
setSnack,
|
defaultPrompts: backstoryQuestions
|
||||||
preamble: backstoryPreamble,
|
}}
|
||||||
defaultPrompts: backstoryQuestions
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Scrollable>
|
|
||||||
)
|
)
|
||||||
}, {
|
}, {
|
||||||
label: "Resume Builder",
|
label: "Resume Builder",
|
||||||
@ -323,7 +320,7 @@ const App = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
|
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
|
||||||
console.log(tabs);
|
|
||||||
if (pathParts.length < 1) {
|
if (pathParts.length < 1) {
|
||||||
console.log("No session id or path -- creating new session");
|
console.log("No session id or path -- creating new session");
|
||||||
fetchSession();
|
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,
|
|
||||||
};
|
|
||||||
|
|
126
frontend/src/BackstoryTextField.tsx
Normal file
126
frontend/src/BackstoryTextField.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useRef, useEffect, ChangeEvent, KeyboardEvent } from 'react';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import './BackstoryTextField.css';
|
||||||
|
|
||||||
|
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.requestAnimationFrame(() => {
|
||||||
|
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`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [value, multiline, textareaRef, shadowRef, placeholder]);
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
|
if (onKeyDown) {
|
||||||
|
onKeyDown(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharedStyle = {
|
||||||
|
width: '100%',
|
||||||
|
padding: '16.5px 14px',
|
||||||
|
resize: 'none' as const,
|
||||||
|
overflow: 'hidden' as const,
|
||||||
|
boxSizing: 'border-box' as const,
|
||||||
|
minHeight: 'calc(1.5rem + 28px)', // lineHeight + padding-top + padding-bottom
|
||||||
|
lineHeight: '1.5',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '16px',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!multiline) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className="BackstoryTextField"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={sharedStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<textarea
|
||||||
|
className="BackstoryTextField"
|
||||||
|
ref={textareaRef}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={{
|
||||||
|
...sharedStyle,
|
||||||
|
height: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="BackgroundTextField"
|
||||||
|
ref={shadowRef}
|
||||||
|
aria-hidden="true"
|
||||||
|
value={placeholder || ""}
|
||||||
|
style={{
|
||||||
|
...sharedStyle,
|
||||||
|
position: 'relative',
|
||||||
|
top: '-9999px',
|
||||||
|
left: '-9999px',
|
||||||
|
visibility: 'hidden',
|
||||||
|
padding: '0px',
|
||||||
|
margin: '0px',
|
||||||
|
overflow: 'auto',
|
||||||
|
height: '0px',
|
||||||
|
minHeight: '0px',
|
||||||
|
}}
|
||||||
|
readOnly
|
||||||
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BackstoryTextField };
|
@ -10,4 +10,5 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 72px);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
@ -7,14 +6,16 @@ import Box from '@mui/material/Box';
|
|||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
import { Message, MessageList, MessageData } from './Message';
|
import { Message, MessageList, MessageData } from './Message';
|
||||||
import { SetSnackType } from './Snack';
|
import { SetSnackType } from './Snack';
|
||||||
import { ContextStatus } from './ContextStatus';
|
import { ContextStatus } from './ContextStatus';
|
||||||
import { useAutoScrollToBottom } from './AutoScroll';
|
import { Scrollable } from './Scrollable';
|
||||||
import { DeleteConfirmation } from './DeleteConfirmation';
|
import { DeleteConfirmation } from './DeleteConfirmation';
|
||||||
import { QueryOptions } from './ChatQuery';
|
import { QueryOptions } from './ChatQuery';
|
||||||
import './Conversation.css';
|
import './Conversation.css';
|
||||||
|
import { BackstoryTextField } from './BackstoryTextField';
|
||||||
|
|
||||||
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." };
|
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 [conversation, setConversation] = useState<MessageList>([]);
|
||||||
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
|
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
|
||||||
const [processingMessage, setProcessingMessage] = useState<MessageData | undefined>(undefined);
|
const [processingMessage, setProcessingMessage] = useState<MessageData | undefined>(undefined);
|
||||||
|
const [streamingMessage, setStreamingMessage] = useState<MessageData | undefined>(undefined);
|
||||||
const timerRef = useRef<any>(null);
|
const timerRef = useRef<any>(null);
|
||||||
const [lastEvalTPS, setLastEvalTPS] = useState<number>(35);
|
const [lastEvalTPS, setLastEvalTPS] = useState<number>(35);
|
||||||
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
||||||
@ -100,7 +102,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||||
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||||
const conversationRef = useRef<MessageList>([]);
|
const conversationRef = useRef<MessageList>([]);
|
||||||
const scrollRef = useAutoScrollToBottom();
|
const viewableElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
// Keep the ref updated whenever items changes
|
// Keep the ref updated whenever items changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -214,6 +217,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
setNoInteractions(false);
|
setNoInteractions(false);
|
||||||
}
|
}
|
||||||
setProcessingMessage(undefined);
|
setProcessingMessage(undefined);
|
||||||
|
setStreamingMessage(undefined);
|
||||||
updateContextStatus();
|
updateContextStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating session ID:', error);
|
console.error('Error generating session ID:', error);
|
||||||
@ -402,6 +406,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
if (onResponse) {
|
if (onResponse) {
|
||||||
update = onResponse(update);
|
update = onResponse(update);
|
||||||
}
|
}
|
||||||
|
setStreamingMessage(undefined);
|
||||||
setProcessingMessage(undefined);
|
setProcessingMessage(undefined);
|
||||||
const backstoryMessage: BackstoryMessage = update;
|
const backstoryMessage: BackstoryMessage = update;
|
||||||
setConversation([
|
setConversation([
|
||||||
@ -425,10 +430,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
// Show error
|
// Show error
|
||||||
setProcessingMessage({ role: 'error', content: update.response });
|
setConversation([
|
||||||
setTimeout(() => {
|
...conversationRef.current, {
|
||||||
setProcessingMessage(undefined);
|
...update,
|
||||||
}, 5000);
|
role: 'error',
|
||||||
|
origin: type,
|
||||||
|
content: update.response || "",
|
||||||
|
}] as MessageList);
|
||||||
|
|
||||||
// Add a small delay to ensure React has time to update the UI
|
// Add a small delay to ensure React has time to update the UI
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
@ -436,7 +444,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
default:
|
default:
|
||||||
// Force an immediate state update based on the message type
|
// Force an immediate state update based on the message type
|
||||||
// Update processing message with immediate re-render
|
// Update processing message with immediate re-render
|
||||||
setProcessingMessage({ role: update.status, content: update.response });
|
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
|
// Add a small delay to ensure React has time to update the UI
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
break;
|
break;
|
||||||
@ -494,13 +506,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={className || "Conversation"}
|
<Scrollable
|
||||||
ref={scrollRef}
|
className={className || "Conversation"}
|
||||||
|
autoscroll
|
||||||
|
textFieldRef={viewableElementRef}
|
||||||
|
fallbackThreshold={0.5}
|
||||||
sx={{
|
sx={{
|
||||||
p: 1,
|
p: 1,
|
||||||
mt: 0,
|
mt: 0,
|
||||||
...sx
|
...sx
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
filteredConversation.map((message, index) =>
|
filteredConversation.map((message, index) =>
|
||||||
<Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack }} />
|
<Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack }} />
|
||||||
@ -510,6 +526,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
processingMessage !== undefined &&
|
processingMessage !== undefined &&
|
||||||
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage }} />
|
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage }} />
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
streamingMessage !== undefined &&
|
||||||
|
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: streamingMessage }} />
|
||||||
|
}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@ -535,18 +555,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
||||||
{prompt &&
|
{prompt &&
|
||||||
<TextField
|
<div
|
||||||
variant="outlined"
|
ref={viewableElementRef}>
|
||||||
disabled={processing}
|
<BackstoryTextField
|
||||||
fullWidth={true}
|
disabled={processing}
|
||||||
multiline={multiline ? true : false}
|
multiline={multiline ? true : false}
|
||||||
type="text"
|
value={query}
|
||||||
value={query}
|
onChange={(e: any) => setQuery(e.target.value)}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onKeyDown={handleKeyPress}
|
||||||
onKeyDown={handleKeyPress}
|
placeholder={prompt}
|
||||||
placeholder={prompt}
|
/>
|
||||||
id="QueryInput"
|
</div>
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||||
@ -585,7 +604,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
|
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
|
||||||
</Box>
|
</Scrollable>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,20 +2,9 @@ import React, { useState, useCallback, useRef } from 'react';
|
|||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Paper,
|
|
||||||
IconButton,
|
|
||||||
Box,
|
Box,
|
||||||
useMediaQuery,
|
|
||||||
Divider,
|
|
||||||
Slider,
|
|
||||||
Stack,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import {
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
SwapHoriz,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
import { ChatQuery } from './ChatQuery';
|
import { ChatQuery } from './ChatQuery';
|
||||||
@ -30,7 +19,6 @@ interface ResumeBuilderProps {
|
|||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResumeBuilder component
|
* ResumeBuilder component
|
||||||
*
|
*
|
||||||
@ -49,13 +37,11 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
const [hasResume, setHasResume] = useState<boolean>(false);
|
const [hasResume, setHasResume] = useState<boolean>(false);
|
||||||
const [hasFacts, setHasFacts] = useState<boolean>(false);
|
const [hasFacts, setHasFacts] = useState<boolean>(false);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const jobConversationRef = useRef<any>(null);
|
const jobConversationRef = useRef<any>(null);
|
||||||
const resumeConversationRef = useRef<any>(null);
|
const resumeConversationRef = useRef<any>(null);
|
||||||
const factsConversationRef = useRef<any>(null);
|
const factsConversationRef = useRef<any>(null);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [splitRatio, setSplitRatio] = useState<number>(100);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle tab change for mobile view
|
* Handle tab change for mobile view
|
||||||
@ -64,20 +50,6 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
setActiveTab(newValue);
|
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) => {
|
const handleJobQuery = (query: string) => {
|
||||||
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler');
|
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler');
|
||||||
jobConversationRef.current?.submitQuery(query);
|
jobConversationRef.current?.submitQuery(query);
|
||||||
@ -250,10 +222,10 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
setHasFacts(false);
|
setHasFacts(false);
|
||||||
}, [setHasFacts]);
|
}, [setHasFacts]);
|
||||||
|
|
||||||
const renderJobDescriptionView = useCallback((small: boolean) => {
|
const renderJobDescriptionView = useCallback((sx: SxProps) => {
|
||||||
console.log('renderJobDescriptionView');
|
console.log('renderJobDescriptionView');
|
||||||
const jobDescriptionQuestions = [
|
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="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} />
|
<ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||||
</Box>,
|
</Box>,
|
||||||
@ -274,6 +246,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
sessionId,
|
sessionId,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
setSnack,
|
setSnack,
|
||||||
|
sx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -292,6 +265,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
sessionId,
|
sessionId,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
setSnack,
|
setSnack,
|
||||||
|
sx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -300,9 +274,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
/**
|
/**
|
||||||
* Renders the resume view with loading indicator
|
* Renders the resume view with loading indicator
|
||||||
*/
|
*/
|
||||||
const renderResumeView = useCallback((small: boolean) => {
|
const renderResumeView = useCallback((sx: SxProps) => {
|
||||||
const resumeQuestions = [
|
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="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} />
|
<ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||||
</Box>,
|
</Box>,
|
||||||
@ -322,6 +296,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
sessionId,
|
sessionId,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
setSnack,
|
setSnack,
|
||||||
|
sx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
@ -339,6 +314,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
connectionBase,
|
connectionBase,
|
||||||
setSnack,
|
setSnack,
|
||||||
defaultPrompts: resumeQuestions,
|
defaultPrompts: resumeQuestions,
|
||||||
|
sx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -347,9 +323,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
/**
|
/**
|
||||||
* Renders the fact check view
|
* Renders the fact check view
|
||||||
*/
|
*/
|
||||||
const renderFactCheckView = useCallback((small: boolean) => {
|
const renderFactCheckView = useCallback((sx: SxProps) => {
|
||||||
const factsQuestions = [
|
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} />
|
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
|
||||||
</Box>,
|
</Box>,
|
||||||
];
|
];
|
||||||
@ -368,169 +344,47 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
|||||||
sessionId,
|
sessionId,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
setSnack,
|
setSnack,
|
||||||
|
sx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]);
|
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]);
|
||||||
|
|
||||||
/**
|
return (
|
||||||
* Gets the appropriate content based on active state for Desktop
|
<Box sx={{
|
||||||
*/
|
p: 0,
|
||||||
const getActiveDesktopContent = useCallback(() => {
|
m: 0,
|
||||||
const hasSlider = hasResume || hasFacts;
|
display: "flex",
|
||||||
const ratio = 75 + 25 * splitRatio / 100;
|
flexGrow: 1,
|
||||||
const otherRatio = hasResume ? ratio / (hasFacts ? 3 : 2) : 100;
|
margin: "0 auto",
|
||||||
const resumeRatio = 100 - otherRatio * (hasFacts ? 2 : 1);
|
overflow: "hidden",
|
||||||
const children = [];
|
backgroundColor: "#F5F5F5",
|
||||||
children.push(
|
flexDirection: "column",
|
||||||
<Box key="JobDescription" className="ChatBox" sx={{
|
maxWidth: "1024px",
|
||||||
display: 'flex',
|
}}
|
||||||
flexDirection: 'column',
|
>
|
||||||
minWidth: `${otherRatio}%`,
|
{/* Tabs */}
|
||||||
width: `${otherRatio}%`,
|
<Tabs
|
||||||
maxWidth: `${otherRatio}%`,
|
value={activeTab}
|
||||||
p: 0,
|
onChange={handleTabChange}
|
||||||
flexGrow: 1,
|
variant="fullWidth"
|
||||||
}}>
|
sx={{ bgcolor: 'background.paper' }}
|
||||||
{renderJobDescriptionView(false)}
|
>
|
||||||
</Box>);
|
<Tab value={0} label="Job Description" />
|
||||||
|
{hasResume && <Tab value={1} label="Resume" />}
|
||||||
|
{hasFacts && <Tab value={2} label="Fact Check" />}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
/* Resume panel - conditionally rendered if resume defined, or processing is in progress */
|
{/* Document display area */}
|
||||||
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={{
|
<Box sx={{
|
||||||
p: 0,
|
display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
|
||||||
m: 0,
|
overflow: "hidden"
|
||||||
display: "flex",
|
|
||||||
flexGrow: 1,
|
|
||||||
flexDirection: "column",
|
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{
|
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||||
display: 'flex',
|
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||||
flexGrow: 1,
|
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||||
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>
|
</Box>
|
||||||
)
|
</Box>
|
||||||
}, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, hasFacts, hasResume]);
|
);
|
||||||
|
|
||||||
// Render mobile view
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Box sx={{
|
|
||||||
p: 0,
|
|
||||||
m: 0,
|
|
||||||
display: "flex",
|
|
||||||
flexGrow: 1,
|
|
||||||
margin: "0 auto",
|
|
||||||
overflow: "hidden",
|
|
||||||
height: "calc(100vh - 72px)",
|
|
||||||
backgroundColor: "#F5F5F5",
|
|
||||||
flexDirection: "column"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Tabs */}
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
variant="fullWidth"
|
|
||||||
sx={{ bgcolor: 'background.paper' }}
|
|
||||||
>
|
|
||||||
<Tab value={0} label="Job Description" />
|
|
||||||
{hasResume && <Tab value={1} label="Resume" />}
|
|
||||||
{hasFacts && <Tab value={2} label="Fact Check" />}
|
|
||||||
</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>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getActiveDesktopContent();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
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 };
|
@ -146,14 +146,6 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
|||||||
|
|
||||||
fetchCollection();
|
fetchCollection();
|
||||||
}, [result, setResult, connectionBase, setSnack, sessionId, view2D])
|
}, [result, setResult, connectionBase, setSnack, sessionId, view2D])
|
||||||
|
|
||||||
/* Trigger a resize event to force Plotly to rescale */
|
|
||||||
useEffect(() => {
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
|
||||||
if (plotlyRef.current) {
|
|
||||||
Plot.Plots.resize(plotlyRef.current);
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!result || !result.embeddings) return;
|
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;
|
||||||
|
};
|
@ -542,13 +542,14 @@ class WebServer:
|
|||||||
agent = context.get_agent(agent_type)
|
agent = context.get_agent(agent_type)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info(f"Attempt to create agent type: {agent_type} failed", e)
|
logger.info(f"Attempt to create agent type: {agent_type} failed", e)
|
||||||
return JSONResponse({ "error": f"{agent_type} is not recognized", "context": context.id }, status_code=404)
|
return JSONResponse({"error": f"{agent_type} is not recognized", "context": context.id}, status_code=404)
|
||||||
|
|
||||||
query = await request.json()
|
query = await request.json()
|
||||||
prompt = query["prompt"]
|
prompt = query["prompt"]
|
||||||
if not isinstance(prompt, str) or len(prompt) == 0:
|
if not isinstance(prompt, str) or len(prompt) == 0:
|
||||||
logger.info(f"Prompt is empty")
|
logger.info(f"Prompt is empty")
|
||||||
return JSONResponse({"error": "Prompt can not be empty"}, status_code=400)
|
return JSONResponse({"error": "Prompt cannot be empty"}, status_code=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
options = Tunables(**query["options"]) if "options" in query else None
|
options = Tunables(**query["options"]) if "options" in query else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -556,45 +557,76 @@ class WebServer:
|
|||||||
return JSONResponse({"error": f"Invalid options: {query['options']}"}, status_code=400)
|
return JSONResponse({"error": f"Invalid options: {query['options']}"}, status_code=400)
|
||||||
|
|
||||||
if not agent:
|
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:
|
match agent_type:
|
||||||
case "job_description":
|
case "job_description":
|
||||||
logger.info(f"Agent {agent_type} not found. Returning empty history.")
|
logger.info(f"Agent {agent_type} not found. Returning empty history.")
|
||||||
agent = context.get_or_create_agent("job_description", job_description=prompt)
|
agent = context.get_or_create_agent("job_description", job_description=prompt)
|
||||||
case _:
|
case _:
|
||||||
logger.info(f"Invalid agent creation sequence for {agent_type}. Returning error.")
|
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)
|
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():
|
async def flush_generator():
|
||||||
logging.info(f"Message starting. Streaming partial results.")
|
logger.info(f"Message starting. Streaming partial results.")
|
||||||
async for message in self.generate_response(context=context, agent=agent, prompt=prompt, options=options):
|
# Create a cancellable task to manage the generator
|
||||||
if message.status != "done":
|
loop = asyncio.get_running_loop()
|
||||||
result = {
|
stop_event = asyncio.Event()
|
||||||
"status": message.status,
|
|
||||||
"response": message.response
|
async def process_generator():
|
||||||
}
|
try:
|
||||||
else:
|
async for message in self.generate_response(context=context, agent=agent, prompt=prompt, options=options):
|
||||||
logging.info(f"Message complete. Providing full response.")
|
if stop_event.is_set():
|
||||||
try:
|
logger.info("Stopping generator due to client disconnection.")
|
||||||
result = message.model_dump(by_alias=True, mode='json')
|
return
|
||||||
except Exception as e:
|
if message.status != "done":
|
||||||
result = { "status": "error", "response": e }
|
result = {
|
||||||
exit(1)
|
"status": message.status,
|
||||||
# Convert to JSON and add newline
|
"response": message.response
|
||||||
result = json.dumps(result) + "\n"
|
}
|
||||||
message.network_packets += 1
|
else:
|
||||||
message.network_bytes += len(result)
|
logger.info(f"Message complete. Providing full response.")
|
||||||
yield result
|
try:
|
||||||
# Explicitly flush after each yield
|
result = message.model_dump(by_alias=True, mode='json')
|
||||||
await asyncio.sleep(0) # Allow the event loop to process the write
|
except Exception as e:
|
||||||
# Save the history once completed
|
result = {"status": "error", "response": str(e)}
|
||||||
self.save_context(context_id)
|
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
|
||||||
|
# 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 with appropriate headers
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
flush_generator(),
|
flush_generator(),
|
||||||
|
@ -421,7 +421,7 @@ class Agent(BaseModel, ABC):
|
|||||||
**message.metadata["options"],
|
**message.metadata["options"],
|
||||||
},
|
},
|
||||||
stream=True,
|
stream=True,
|
||||||
):
|
):
|
||||||
if not response:
|
if not response:
|
||||||
message.status = "error"
|
message.status = "error"
|
||||||
message.response = "No response from LLM."
|
message.response = "No response from LLM."
|
||||||
|
@ -374,6 +374,7 @@ class JobDescription(Agent):
|
|||||||
2. Create a comprehensive inventory of the candidate's actual qualifications.
|
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.
|
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.
|
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:
|
## OUTPUT FORMAT:
|
||||||
|
|
||||||
@ -778,7 +779,6 @@ class JobDescription(Agent):
|
|||||||
|
|
||||||
# Extract JSON from response
|
# Extract JSON from response
|
||||||
json_str = self.extract_json_from_text(message.response)
|
json_str = self.extract_json_from_text(message.response)
|
||||||
verification_results = json.loads(json_str)
|
|
||||||
|
|
||||||
message.status = "done"
|
message.status = "done"
|
||||||
message.response = json_str
|
message.response = json_str
|
||||||
@ -862,12 +862,11 @@ class JobDescription(Agent):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
message.status = "thinking"
|
message.status = "thinking"
|
||||||
message.response = "Starting multi-stage RAG resume generation process"
|
|
||||||
logger.info(message.response)
|
logger.info(message.response)
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
# Stage 1A: Analyze job requirements
|
# 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)
|
logger.info(message.response)
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
@ -878,10 +877,11 @@ class JobDescription(Agent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
job_requirements = json.loads(message.response)
|
job_requirements = json.loads(message.response)
|
||||||
|
message.metadata["job_requirements"] = job_requirements
|
||||||
|
|
||||||
# Stage 1B: Analyze candidate qualifications
|
# Stage 1B: Analyze candidate qualifications
|
||||||
message.status = "thinking"
|
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)
|
logger.info(message.response)
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
@ -892,10 +892,11 @@ class JobDescription(Agent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
candidate_qualifications = json.loads(message.response)
|
candidate_qualifications = json.loads(message.response)
|
||||||
|
message.metadata["candidate_qualifications"] = job_requirements
|
||||||
|
|
||||||
# Stage 1C: Create skills mapping
|
# Stage 1C: Create skills mapping
|
||||||
message.status = "thinking"
|
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)
|
logger.info(message.response)
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
@ -906,13 +907,14 @@ class JobDescription(Agent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
skills_mapping = json.loads(message.response)
|
skills_mapping = json.loads(message.response)
|
||||||
|
message.metadata["skills_mapping"] = skills_mapping
|
||||||
|
|
||||||
# Extract header from original resume
|
# Extract header from original resume
|
||||||
original_header = self.extract_header_from_resume(resume)
|
original_header = self.extract_header_from_resume(resume)
|
||||||
|
|
||||||
# Stage 2: Generate tailored resume
|
# Stage 2: Generate tailored resume
|
||||||
message.status = "thinking"
|
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)
|
logger.info(message.response)
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
@ -923,10 +925,13 @@ class JobDescription(Agent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
generated_resume = message.response
|
generated_resume = message.response
|
||||||
|
message.metadata["generated_resume"] = {
|
||||||
|
"first_pass": generated_resume
|
||||||
|
}
|
||||||
|
|
||||||
# Stage 3: Verify resume
|
# Stage 3: Verify resume
|
||||||
message.status = "thinking"
|
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)
|
logger.info(message.response)
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
@ -937,6 +942,9 @@ class JobDescription(Agent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
verification_results = json.loads(message.response)
|
verification_results = json.loads(message.response)
|
||||||
|
message.metadata["verification_results"] = {
|
||||||
|
"first_pass": verification_results
|
||||||
|
}
|
||||||
|
|
||||||
# Handle corrections if needed
|
# Handle corrections if needed
|
||||||
if verification_results["verification_results"]["overall_assessment"] == "NEEDS REVISION":
|
if verification_results["verification_results"]["overall_assessment"] == "NEEDS REVISION":
|
||||||
@ -959,7 +967,8 @@ class JobDescription(Agent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
generated_resume = message.response
|
generated_resume = message.response
|
||||||
|
message.metadata["generated_resume"]["second_pass"] = generated_resume
|
||||||
|
|
||||||
# Re-verify after corrections
|
# Re-verify after corrections
|
||||||
message.status = "thinking"
|
message.status = "thinking"
|
||||||
message.response = "Re-verifying corrected resume"
|
message.response = "Re-verifying corrected resume"
|
||||||
@ -973,16 +982,12 @@ class JobDescription(Agent):
|
|||||||
yield message
|
yield message
|
||||||
if message.status == "error":
|
if message.status == "error":
|
||||||
return
|
return
|
||||||
|
verification_results = json.loads(message.response)
|
||||||
|
message.metadata["verification_results"]["second_pass"] = verification_results
|
||||||
|
|
||||||
# Return the final results
|
# Return the final results
|
||||||
message.status = "done"
|
message.status = "done"
|
||||||
message.response = json.dumps({
|
message.response = generated_resume
|
||||||
"job_requirements": job_requirements,
|
|
||||||
"candidate_qualifications": candidate_qualifications,
|
|
||||||
"skills_mapping": skills_mapping,
|
|
||||||
"generated_resume": generated_resume,
|
|
||||||
"verification_results": verification_results
|
|
||||||
})
|
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
logger.info("Resume generation process completed successfully")
|
logger.info("Resume generation process completed successfully")
|
||||||
@ -1006,9 +1011,9 @@ class JobDescription(Agent):
|
|||||||
|
|
||||||
self.metrics.generate_count.labels(agent=self.agent_type).inc()
|
self.metrics.generate_count.labels(agent=self.agent_type).inc()
|
||||||
with self.metrics.generate_duration.labels(agent=self.agent_type).time():
|
with self.metrics.generate_duration.labels(agent=self.agent_type).time():
|
||||||
job_description = "You write C and C++ code for 4 years."
|
job_description = message.preamble["job_description"]
|
||||||
resume = "I have worked on Cobol and QuickBasic for 18 years."
|
resume = message.preamble["resume"]
|
||||||
additional_context = ""
|
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):
|
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":
|
if message.status != "done":
|
||||||
|
Loading…
x
Reference in New Issue
Block a user