GenerateCandidate tweaks to UI

This commit is contained in:
James Ketr 2025-05-22 12:07:16 -07:00
parent 062eccb379
commit 840ad9159b
5 changed files with 36 additions and 268 deletions

View File

@ -98,18 +98,23 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
className="BackstoryPageContainer"
maxWidth="xl"
sx={{
display: "flex",
flexGrow: 1,
pt: 1,
pb: 1,
flexGrow: 1,
width: "100%",
...sx
}}>
<Paper
elevation={2}
sx={{
display: "flex",
flexGrow: 1,
p: 3,
backgroundColor: 'background.paper',
borderRadius: 2,
minHeight: '80vh',
width: "100%",
}}>
{children}
</Paper>
@ -161,13 +166,14 @@ const BackstoryLayout: React.FC<{
<Scrollable
className="BackstoryPageScrollable"
sx={{
mt: "72px", /* Needs to be kept in sync with the height of Header if the Header theme changes */
display: "flex",
flexDirection: "column",
backgroundColor: "background.default",
height: "100%",
maxHeight: "100%",
minHeight: "100%",
mt: "72px" /* Needs to be kept in sync with the height of Header if the Header theme changes */
minWidth: "min-content",
}}
>
<BackstoryPageContainer>

View File

@ -17,7 +17,7 @@ import { BetaPage } from '../Pages/BetaPage';
import { CandidateListingPage } from '../Pages/CandidateListingPage';
import { JobAnalysisPage } from '../Pages/JobAnalysisPage';
import { DemoComponent } from "NewApp/Pages/DemoComponent";
import { GenerateCandidate } from "NewApp/Pages/GenerateCandiate";
import { GenerateCandidate } from "NewApp/Pages/GenerateCandidate";
const DashboardPage = () => (<BetaPage><Typography variant="h4">Dashboard</Typography></BetaPage>);
const ProfilePage = () => (<BetaPage><Typography variant="h4">Profile</Typography></BetaPage>);

View File

@ -7,6 +7,8 @@ import LinkIcon from '@mui/icons-material/Link';
import { CopyBubble } from "../../Components/CopyBubble";
// Styled components
const StyledPaper = styled(Paper)(({ theme }) => ({
display: "flex",
flexDirection: "column",
padding: theme.spacing(2),
marginBottom: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
@ -44,10 +46,10 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
return (
<StyledPaper sx={sx}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minWidth: "80px", maxWidth: "80px" }}>
<Avatar
src={view.has_profile ? `/api/u/${view.username}/profile/${sessionId}` : ''}
alt={`${view.full_name}'s profile`}
src={view.has_profile ? `/api/u/${view.username}/profile/${sessionId}` : ''}
alt={`${view.full_name}'s profile`}
sx={{
width: 80,
height: 80,
@ -55,35 +57,35 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 10 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Box>
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 1, "& > .MuiTypography-root": { m: 0 } }}>
{action !== '' && <Typography variant="body1">{action}</Typography>}
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold' }}>
{view.full_name}
</Typography>
</Box>
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }} >
<Link href={`/u/${view.username}`}>/u/{view.username}</Link>
<CopyBubble
onClick={(event: any) => { event.stopPropagation() }}
tooltip="Copy link" content={`${window.location.origin}/u/{view.username}`} />
</Box>
</Box>
<Box>
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 1, "& > .MuiTypography-root": { m: 0 } }}>
{action !== '' && <Typography variant="body1">{action}</Typography>}
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold' }}>
{view.full_name}
</Typography>
</Box>
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }} >
<Link href={`/u/${view.username}`}>/u/{view.username}</Link>
<CopyBubble
onClick={(event: any) => { event.stopPropagation() }}
tooltip="Copy link" content={`${window.location.origin}/u/{view.username}`} />
</Box>
</Box>
{view.rag_content_size !== undefined && view.rag_content_size > 0 && <Chip
onClick={(event: React.MouseEvent<HTMLDivElement>) => { navigate('/knowledge-explorer'); event.stopPropagation() }}
label={formatRagSize(view.rag_content_size)}
{view.rag_content_size !== undefined && view.rag_content_size > 0 && <Chip
onClick={(event: React.MouseEvent<HTMLDivElement>) => { navigate('/knowledge-explorer'); event.stopPropagation() }}
label={formatRagSize(view.rag_content_size)}
color="primary"
size="small"
sx={{ ml: 2 }}
/>}
/>}
</Box>
<Typography variant="body1" color="text.secondary">
{view.description}
{view.description}
</Typography>
</Grid>
</Grid>

View File

@ -153,7 +153,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
};
return (
<Box sx={{ maxWidth: 1200, margin: '0 auto', p: 2 }}>
<Box>
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
<Grid container spacing={2}>
<Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>

View File

@ -1,240 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import CancelIcon from '@mui/icons-material/Cancel';
import SendIcon from '@mui/icons-material/Send';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { CandidateInfo } from '../Components/CandidateInfo';
import { Query } from '../../Components/ChatQuery'
import { streamQueryResponse, StreamQueryController } from '../Components/streamQueryResponse';
import { connectionBase } from 'Global';
import { UserInfo } from '../Components/UserContext';
import { BackstoryElementProps } from 'Components/BackstoryTab';
import { BackstoryTextField, BackstoryTextFieldRef } from 'Components/BackstoryTextField';
import { jsonrepair } from 'jsonrepair';
import { StyledMarkdown } from 'Components/StyledMarkdown';
import { Scrollable } from 'Components/Scrollable';
import { useForkRef } from '@mui/material';
const emptyUser : UserInfo = {
type: 'candidate',
description: "[blank]",
rag_content_size: 0,
username: "[blank]",
first_name: "[blank]",
last_name: "[blank]",
full_name: "[blank] [blank]",
contact_info: {},
questions: [],
isAuthenticated: false,
has_profile: false
};
const GenerateCandidate = (props: BackstoryElementProps) => {
const {sessionId, setSnack, submitQuery} = props;
const [streaming, setStreaming] = useState<string>('');
const [processing, setProcessing] = useState<boolean>(false);
const [user, setUser] = useState<UserInfo>(emptyUser);
const controllerRef = useRef<StreamQueryController>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const promptRef = useRef<string>(null);
const stateRef = useRef<number>(0); /* Generating persona */
const userRef = useRef<UserInfo>(user);
const [prompt, setPrompt] = useState<string>('');
const [resume, setResume] = useState<string>('');
const processQuery = (query: Query) => {
if (controllerRef.current) {
return;
}
setPrompt(query.prompt);
promptRef.current = query.prompt;
stateRef.current = 0;
setUser(emptyUser);
setStreaming('');
setResume('');
setProcessing(true);
controllerRef.current = streamQueryResponse({
query,
type: "persona",
sessionId,
connectionBase,
onComplete: (msg) => {
console.log({ msg, state: stateRef.current, prompt: promptRef.current || '' });
switch (msg.status) {
case "partial":
case "done":
switch (stateRef.current) {
case 0: /* Generating persona */
let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
if (!partialUser.full_name) {
partialUser.full_name = `${partialUser.first_name} ${partialUser.last_name}`;
}
console.log(partialUser);
setUser(partialUser);
stateRef.current = 1 /* Generating resume */
break;
case 1: /* Generating resume */
stateRef.current = 2 /* RAG generation */
break;
case 2: /* RAG generation */
stateRef.current = 2 /* Image generation */
break;
case 3: /* Generating image */
let imageGeneration = JSON.parse(jsonrepair((msg.response || '').trim()));
console.log(imageGeneration);
if (imageGeneration >= 100) {
setUser({...userRef.current});
} else {
setPrompt(imageGeneration.status);
}
stateRef.current = 3 /* ... */
}
if (msg.status === "done") {
setProcessing(false);
controllerRef.current = null;
stateRef.current = 0;
}
break;
case "thinking":
setPrompt(msg.response || '');
break;
case "error":
console.log(`Error generating persona: ${msg.response}`);
setSnack(msg.response || "", "error");
setProcessing(false);
setUser({...userRef.current});
controllerRef.current = null;
stateRef.current = 0;
break;
}
},
onStreaming: (chunk) => {
setStreaming(chunk);
}
});
};
const cancelQuery = () => {
if (controllerRef.current) {
controllerRef.current.abort();
controllerRef.current = null;
stateRef.current = 0;
setProcessing(false);
}
}
useEffect(() => {
promptRef.current = prompt;
}, [prompt]);
useEffect(() => {
userRef.current = user;
}, [user]);
useEffect(() => {
if (streaming.trim().length === 0) {
return;
}
try {
switch (stateRef.current) {
case 0: /* Generating persona */
const partialUser = {...emptyUser, ...JSON.parse(jsonrepair(`${streaming.trim()}...`))};
if (!partialUser.full_name) {
partialUser.full_name = `${partialUser.first_name} ${partialUser.last_name}`;
}
setUser(partialUser);
break;
case 1: /* Generating resume */
setResume(streaming);
break;
case 3: /* RAG streaming */
break;
case 4: /* Image streaming */
break;
}
} catch {
}
}, [streaming]);
if (!sessionId) {
return <></>;
}
const onEnter = (value: string) => {
if (processing) {
return;
}
const query: Query = {
prompt: value
}
processQuery(query);
};
return (<>
{ user && <CandidateInfo sessionId={sessionId} user={user}/> }
{ resume !== '' && <Scrollable sx={{maxHeight: "20vh"}}><StyledMarkdown {...{content: resume, setSnack, sessionId, submitQuery}}/></Scrollable> }
{processing && <Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
m: 2,
}}>
<Box sx={{flexDirection: "row"}}><Box>Genearating
{stateRef.current === 0 && "persona"}
{stateRef.current === 1 && "resume"}
{stateRef.current === 2 && "RAG"}
{stateRef.current === 3 && "profile image"}
:</Box><Box sx={{fontWeight: "bold"}}>{prompt}</Box></Box>
<PropagateLoader
size="10px"
loading={processing}
aria-label="Loading Spinner"
data-testid="loader"
/>
</Box> }
<BackstoryTextField
ref={backstoryTextRef}
disabled={processing}
onEnter={onEnter}
placeholder='Specify any characteristics you would like the persona to have. For example, "This person likes yo-yos."'
/>
<Box sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
<Tooltip title={"Send"}>
<span style={{ display: "flex", flexGrow: 1 }}>
<Button
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processing}
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
Send<SendIcon />
</Button>
</span>
</Tooltip>
<Tooltip title="Cancel">
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label="cancel"
onClick={() => { cancelQuery(); }}
sx={{ display: "flex", margin: 'auto 0px' }}
size="large"
edge="start"
disabled={controllerRef.current === null || !sessionId || processing === false}
>
<CancelIcon />
</IconButton>
</span>
</Tooltip>
</Box>
</>);
};
export {
GenerateCandidate
};