405 lines
13 KiB
TypeScript
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
|
|
};
|
|
|