backstory/frontend/src/ResumeBuilder.tsx
2025-05-09 13:39:49 -07:00

405 lines
13 KiB
TypeScript

import React, { useState, useCallback, useRef } from 'react';
import {
Tabs,
Tab,
Box,
} from '@mui/material';
import { SxProps, Theme } from '@mui/material';
import { ChatQuery } from './ChatQuery';
import { MessageList, MessageData } from './Message';
import { SetSnackType } from './Snack';
import { Conversation } from './Conversation';
interface ResumeBuilderProps {
connectionBase: string,
sessionId: string | undefined,
setSnack: SetSnackType,
sx?: SxProps<Theme>;
};
/**
* ResumeBuilder component
*
* A responsive component that displays job descriptions, generated resumes and fact checks
* with different layouts for mobile and desktop views.
*/
const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
sx,
connectionBase,
sessionId,
setSnack
}) => {
// State for editing job description
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
const [hasResume, setHasResume] = useState<boolean>(false);
const [hasFacts, setHasFacts] = useState<boolean>(false);
const jobConversationRef = useRef<any>(null);
const resumeConversationRef = useRef<any>(null);
const factsConversationRef = useRef<any>(null);
const [activeTab, setActiveTab] = useState<number>(0);
/**
* Handle tab change for mobile view
*/
const handleTabChange = (_event: React.SyntheticEvent, newValue: number): void => {
setActiveTab(newValue);
};
const handleJobQuery = (query: string) => {
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler');
jobConversationRef.current?.submitQuery(query);
};
const handleResumeQuery = (query: string) => {
console.log(`handleResumeQuery: ${query} -- `, resumeConversationRef.current ? ' sending' : 'no handler');
resumeConversationRef.current?.submitQuery(query);
};
const handleFactsQuery = (query: string) => {
console.log(`handleFactsQuery: ${query} -- `, factsConversationRef.current ? ' sending' : 'no handler');
factsConversationRef.current?.submitQuery(query);
};
const filterJobDescriptionMessages = useCallback((messages: MessageList): MessageList => {
if (messages === undefined || messages.length === 0) {
return [];
}
console.log("filterJobDescriptionMessages disabled", messages)
if (messages.length > 1) {
setHasResume(true);
}
messages[0].role = 'content';
messages[0].title = 'Job Description';
messages[0].disableCopy = false;
return messages;
// let reduced = messages.filter((m, i) => {
// const keep = (m.metadata?.origin || m.origin || "no origin") === 'job_description';
// if ((m.metadata?.origin || m.origin || "no origin") === 'resume') {
// setHasResume(true);
// }
// // if (!keep) {
// // console.log(`filterJobDescriptionMessages: ${i + 1} filtered:`, m);
// // } else {
// // console.log(`filterJobDescriptionMessages: ${i + 1}:`, m);
// // }
// return keep;
// });
// /* If Resume hasn't occurred yet and there is still more than one message,
// * resume has been generated. */
// if (!hasResume && reduced.length > 1) {
// setHasResume(true);
// }
// if (reduced.length > 0) {
// // First message is always 'content'
// reduced[0].title = 'Job Description';
// reduced[0].role = 'content';
// setHasJobDescription(true);
// }
// /* Filter out any messages which the server injected for state management */
// reduced = reduced.filter(m => m.display !== "hide");
// return reduced;
}, [setHasResume/*, setHasJobDescription, hasResume*/]);
const filterResumeMessages = useCallback((messages: MessageList): MessageList => {
if (messages === undefined || messages.length === 0) {
return [];
}
console.log("filterResumeMessages disabled")
if (messages.length > 3) {
setHasFacts(true);
}
return messages;
// let reduced = messages.filter((m, i) => {
// const keep = (m.metadata?.origin || m.origin || "no origin") === 'resume';
// if ((m.metadata?.origin || m.origin || "no origin") === 'fact_check') {
// setHasFacts(true);
// }
// if (!keep) {
// console.log(`filterResumeMessages: ${i + 1} filtered:`, m);
// } else {
// console.log(`filterResumeMessages: ${i + 1}:`, m);
// }
// return keep;
// });
// /* If there is more than one message, it is user: "...JOB_DESCRIPTION...", assistant: "...RESUME..."
// * which means a resume has been generated. */
// if (reduced.length > 1) {
// /* Remove the assistant message from the UI */
// if (reduced[0].role === "user") {
// reduced.splice(0, 1);
// }
// }
// /* If Fact Check hasn't occurred yet and there is still more than one message,
// * facts have have been generated. */
// if (!hasFacts && reduced.length > 1) {
// setHasFacts(true);
// }
// /* Filter out any messages which the server injected for state management */
// reduced = reduced.filter(m => m.display !== "hide");
// /* If there are any messages, there is a resume */
// if (reduced.length > 0) {
// // First message is always 'content'
// reduced[0].title = 'Resume';
// reduced[0].role = 'content';
// setHasResume(true);
// }
// return reduced;
}, [/*setHasResume, hasFacts,*/ setHasFacts]);
const filterFactsMessages = useCallback((messages: MessageList): MessageList => {
if (messages === undefined || messages.length === 0) {
return [];
}
console.log("filterFactsMessages disabled")
return messages;
// messages.forEach((m, i) => console.log(`filterFactsMessages: ${i + 1}:`, m))
// const reduced = messages.filter(m => {
// return (m.metadata?.origin || m.origin || "no origin") === 'fact_check';
// });
// /* If there is more than one message, it is user: "Fact check this resume...", assistant: "...FACT CHECK..."
// * which means facts have been generated. */
// if (reduced.length > 1) {
// /* Remove the user message from the UI */
// if (reduced[0].role === "user") {
// reduced.splice(0, 1);
// }
// // First message is always 'content'
// reduced[0].title = 'Fact Check';
// reduced[0].role = 'content';
// setHasFacts(true);
// }
// return reduced;
}, [/*setHasFacts*/]);
const jobResponse = useCallback(async (message: MessageData) => {
console.log('onJobResponse', message);
if (message.actions && message.actions.includes("resume_generated")) {
await resumeConversationRef.current.fetchHistory();
setHasResume(true);
setActiveTab(1); // Switch to Resume tab
}
if (message.actions && message.actions.includes("facts_checked")) {
await factsConversationRef.current.fetchHistory();
setHasFacts(true);
}
}, [setHasFacts, setHasResume, setActiveTab]);
const resumeResponse = useCallback((message: MessageData): void => {
console.log('onResumeResponse', message);
setHasFacts(true);
}, [setHasFacts]);
const factsResponse = useCallback((message: MessageData): void => {
console.log('onFactsResponse', message);
}, []);
const resetJobDescription = useCallback(() => {
setHasJobDescription(false);
setHasResume(false);
setHasFacts(false);
}, [setHasJobDescription, setHasResume, setHasFacts]);
const resetResume = useCallback(() => {
setHasResume(false);
setHasFacts(false);
}, [setHasResume, setHasFacts]);
const resetFacts = useCallback(() => {
setHasFacts(false);
}, [setHasFacts]);
const renderJobDescriptionView = useCallback((sx: SxProps) => {
console.log('renderJobDescriptionView');
const jobDescriptionQuestions = [
<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>,
];
if (!hasJobDescription) {
return <Conversation
ref={jobConversationRef}
{...{
type: "job_description",
actionLabel: "Generate Resume",
prompt: "Paste a job description, then click Generate...",
multiline: true,
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterJobDescriptionMessages,
resetAction: resetJobDescription,
onResponse: jobResponse,
sessionId,
connectionBase,
setSnack,
sx,
}}
/>
} else {
return <Conversation
ref={jobConversationRef}
{...{
type: "job_description",
actionLabel: "Send",
prompt: "Ask a question about this job description...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterJobDescriptionMessages,
defaultPrompts: jobDescriptionQuestions,
resetAction: resetJobDescription,
onResponse: jobResponse,
sessionId,
connectionBase,
setSnack,
sx,
}}
/>
}
}, [connectionBase, filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume]);
/**
* Renders the resume view with loading indicator
*/
const renderResumeView = useCallback((sx: SxProps) => {
const resumeQuestions = [
<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>,
];
if (!hasFacts) {
return <Conversation
ref={resumeConversationRef}
{...{
type: "resume",
actionLabel: "Fact Check",
defaultQuery: "Fact check the resume.",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterResumeMessages,
onResponse: resumeResponse,
resetAction: resetResume,
sessionId,
connectionBase,
setSnack,
sx,
}}
/>
} else {
return <Conversation
ref={resumeConversationRef}
{...{
type: "resume",
actionLabel: "Send",
prompt: "Ask a question about this job resume...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterResumeMessages,
onResponse: resumeResponse,
resetAction: resetResume,
sessionId,
connectionBase,
setSnack,
defaultPrompts: resumeQuestions,
sx,
}}
/>
}
}, [connectionBase, filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume]);
/**
* Renders the fact check view
*/
const renderFactCheckView = useCallback((sx: SxProps) => {
const factsQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
</Box>,
];
return <Conversation
ref={factsConversationRef}
{...{
type: "fact_check",
actionLabel: "Send",
prompt: "Ask a question about any discrepencies...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterFactsMessages,
defaultPrompts: factsQuestions,
resetAction: resetFacts,
onResponse: factsResponse,
sessionId,
connectionBase,
setSnack,
sx,
}}
/>
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]);
return (
<Box sx={{
p: 0,
m: 0,
display: "flex",
flexGrow: 1,
margin: "0 auto",
overflow: "hidden",
backgroundColor: "#F5F5F5",
flexDirection: "column",
maxWidth: "1024px",
}}
>
{/* 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,
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>
);
};
export type {
ResumeBuilderProps
};
export {
ResumeBuilder
};