Working on Job Analysis

This commit is contained in:
James Ketr 2025-06-02 20:24:46 -07:00
parent 0994c95f91
commit 138f61b777
13 changed files with 104 additions and 87 deletions

View File

@ -1,20 +1,20 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { ChatQuery } from "types/types";
import { CandidateQuestion } from "types/types";
type ChatSubmitQueryInterface = (query: ChatQuery) => void;
type ChatSubmitQueryInterface = (query: CandidateQuestion) => void;
interface BackstoryQueryInterface {
query: ChatQuery,
question: CandidateQuestion,
submitQuery?: ChatSubmitQueryInterface
}
const BackstoryQuery = (props : BackstoryQueryInterface) => {
const { query, submitQuery } = props;
const { question, submitQuery } = props;
if (submitQuery === undefined) {
return (<Box>{query.prompt}</Box>);
return (<Box>{question.question}</Box>);
}
return (
<Button variant="outlined" sx={{
@ -22,8 +22,8 @@ const BackstoryQuery = (props : BackstoryQueryInterface) => {
borderColor: theme => theme.palette.custom.highlight,
m: 1
}}
size="small" onClick={(e: any) => { submitQuery(query); }}>
{query.prompt}
size="small" onClick={(e: any) => { submitQuery(question); }}>
{question.question}
</Button>
);
}

View File

@ -9,6 +9,7 @@ import {
Button,
useMediaQuery,
Tooltip,
SxProps,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import ResetIcon from '@mui/icons-material/History';
@ -20,7 +21,7 @@ interface DeleteConfirmationProps {
label?: string;
action?: "delete" | "reset";
color?: "inherit" | "default" | "primary" | "secondary" | "error" | "info" | "success" | "warning" | undefined;
sx?: SxProps;
// New props for controlled mode
open?: boolean;
onClose?: () => void;
@ -54,7 +55,8 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
message,
hideButton = false,
confirmButtonText,
cancelButtonText = "Cancel"
cancelButtonText = "Cancel",
sx
} = props;
// Internal state for uncontrolled mode
@ -104,7 +106,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
aria-label={action}
onClick={handleClickOpen}
color={color || "inherit"}
sx={{ display: "flex", margin: 'auto 0px' }}
sx={{ display: "flex", margin: 'auto 0px', ...sx }}
size="large"
edge="start"
disabled={disabled}

View File

@ -13,7 +13,7 @@ import { GenerateImage } from 'components/GenerateImage';
import './StyledMarkdown.css';
import { BackstoryElementProps } from './BackstoryTab';
import { ChatSession } from 'types/types';
import { CandidateQuestion, ChatSession } from 'types/types';
interface StyledMarkdownProps extends BackstoryElementProps {
className?: string,
@ -106,8 +106,11 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
const queryString = props.query.replace(/(\w+):/g, '"$1":');
try {
const query = JSON.parse(queryString);
const backstoryQuestion: CandidateQuestion = {
question: queryString
}
return <BackstoryQuery submitQuery={submitQuery} query={query} />
return <BackstoryQuery submitQuery={submitQuery} question={query} />
} catch (e) {
console.log("StyledMarkdown error:", queryString, e);
return props.query;

View File

@ -44,7 +44,7 @@ const ViewerNavItems: NavigationLinkType[] = [
const CandidateNavItems : NavigationLinkType[]= [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
// { name: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
{ name: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
{ name: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
// { name: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },

View File

@ -21,6 +21,7 @@ import { LoginPage } from "pages/LoginPage";
import { CandidateDashboardPage } from "pages/CandidateDashboardPage"
import { EmailVerificationPage } from "components/EmailVerificationComponents";
import { CandidateProfilePage } from "pages/candidate/Profile";
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
const ResumesPage = () => (<BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>);
@ -70,6 +71,7 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
routes.splice(-1, 0, ...[
<Route key={`${index++}`} path="/candidate/dashboard" element={<BetaPage><CandidateDashboardPage {...backstoryProps} /></BetaPage>} />,
<Route key={`${index++}`} path="/candidate/profile" element={<CandidateProfilePage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/backstory" element={<BackstoryPage />} />,
<Route key={`${index++}`} path="/candidate/resumes" element={<ResumesPage />} />,
<Route key={`${index++}`} path="/candidate/qa-setup" element={<QASetupPage />} />,

View File

@ -6,6 +6,7 @@ import {
Divider,
useTheme,
useMediaQuery,
Tooltip,
} from '@mui/material';
import {
Send as SendIcon
@ -21,6 +22,7 @@ import { useNavigate } from 'react-router-dom';
import { useSelectedCandidate } from 'hooks/GlobalContext';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryQuery } from 'components/BackstoryQuery';
const defaultMessage: ChatMessage = {
type: "preparing", status: "done", sender: "system", sessionId: "", timestamp: new Date(), content: ""
@ -279,12 +281,13 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
</Box>
</>}
</Paper>
{selectedCandidate.questions?.length !== 0 && selectedCandidate.questions?.map(q => <BackstoryQuery question={q} />)}
{/* Fixed Message Input */}
<Box sx={{ display: 'flex', flexShrink: 0, gap: 1 }}>
<DeleteConfirmation
onDelete={() => { chatSession && onDelete(chatSession); }}
disabled={!chatSession}
sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content" }}
action="reset"
label="chat session"
title="Reset Chat Session"
@ -296,14 +299,19 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
onEnter={sendMessage}
disabled={streaming || loading}
/>
<Tooltip title="Send">
<span style={{ minWidth: 'auto', maxHeight: "min-content", alignSelf: "center" }}
>
<Button
variant="contained"
onClick={() => { sendMessage((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }}
disabled={streaming || loading}
sx={{ minWidth: 'auto', px: 2 }}
disabled={streaming || loading}
>
<SendIcon />
</Button>
</span>
</Tooltip>
</Box>
</Box>
);

View File

@ -145,7 +145,6 @@ const documents : DocType[] = [
{ title: "Authentication Architecture", route: "authentication.md", description: "Complete authentication architecture", icon: <LayersIcon /> },
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions", icon: <DashboardIcon /> },
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts", icon: <DashboardIcon /> },
{ title: "Chat Mockup", route: "mockup-chat-system", description: "Mockup of chat system", icon: <DashboardIcon /> },
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles", icon: <PaletteIcon /> },
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application", icon: <AnalyticsIcon /> },
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." },

View File

@ -55,7 +55,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
Generate your own perfect AI candidate!
</Button>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
{candidates?.map((u, i) =>
<Box key={`${u.username}`}
onClick={() => { setSelectedCandidate(u); navigate("/chat"); }}

View File

@ -38,15 +38,16 @@ import { Candidate } from "types/types";
import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext';
// Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const theme = useTheme();
const { user } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
// State management
const [activeStep, setActiveStep] = useState(0);
const [selectedCandidate, setSelectedCandidate] = useState<Candidate | null>(null);
const [jobDescription, setJobDescription] = useState('');
const [jobTitle, setJobTitle] = useState('');
const [jobLocation, setJobLocation] = useState('');
@ -58,7 +59,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
useEffect(() => {
if (candidates !== null) {
if (candidates !== null || selectedCandidate) {
return;
}
const getCandidates = async () => {
@ -85,11 +86,20 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
getCandidates();
}, [candidates, setSnack]);
useEffect(() => {
if (selectedCandidate && activeStep === 0) {
setActiveStep(1);
}
}, [selectedCandidate, activeStep]);
// Steps in our process
const steps = [
{ label: 'Select Candidate', icon: <PersonIcon /> },
{ label: 'Job Description', icon: <WorkIcon /> },
{ label: 'View Analysis', icon: <AssessmentIcon /> }
const steps = selectedCandidate === null ? [
{ index: 0, label: 'Select Candidate', icon: <PersonIcon /> },
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
{ index: 2, label: 'View Analysis', icon: <AssessmentIcon /> }
] : [
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
{ index: 2, label: 'View Analysis', icon: <AssessmentIcon /> }
];
// Mock handlers for our analysis APIs
@ -235,7 +245,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return;
}
if (activeStep === 1 && (!jobTitle || !jobDescription)) {
if (activeStep === 1 && (/*(extraInfo && !jobTitle) || */!jobDescription)) {
setError('Please provide both job title and description before continuing.');
return;
}
@ -252,11 +262,12 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
};
const handleReset = () => {
setActiveStep(0);
setSelectedCandidate(null);
// setActiveStep(0);
setActiveStep(1);
// setSelectedCandidate(null);
setJobDescription('');
setJobTitle('');
setJobLocation('');
// setJobTitle('');
// setJobLocation('');
setAnalysisStarted(false);
};
@ -341,50 +352,56 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
</Paper>
);
const extraInfo = false;
// Render function for the job description step
const renderJobDescription = () => (
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
<Typography variant="h5" gutterBottom>
Enter Job Details
</Typography>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Job Title"
variant="outlined"
value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)}
required
margin="normal"
/>
{extraInfo && <>
<Typography variant="h5" gutterBottom>
Enter Job Details
</Typography>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Job Title"
variant="outlined"
value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)}
required
margin="normal"
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Job Location"
variant="outlined"
value={jobLocation}
onChange={(e) => setJobLocation(e.target.value)}
margin="normal"
/>
</Grid>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Job Location"
variant="outlined"
value={jobLocation}
onChange={(e) => setJobLocation(e.target.value)}
margin="normal"
/>
</Grid>
</>
}
<Grid size={{ xs: 12 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
<Typography variant="subtitle1" sx={{ mr: 2 }}>
Job Description
</Typography>
<Button
{extraInfo && <Button
variant="outlined"
startIcon={<FileUploadIcon />}
size="small"
onClick={() => setOpenUploadDialog(true)}
>
Upload
</Button>
</Button>}
</Box>
<TextField
@ -406,8 +423,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
The job description will be used to extract requirements for candidate matching.
</Typography>
</Grid>
</Typography>
</Grid>
</Paper>
);
@ -455,13 +471,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Box sx={{ mt: 4, mb: 4 }}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((step, index) => (
<Step key={index}>
{steps.map(step => (
<Step key={step.index}>
<StepLabel slots={{
stepIcon: () => (
<Avatar
sx={{
bgcolor: activeStep >= index ? theme.palette.primary.main : theme.palette.grey[300],
bgcolor: activeStep >= step.index ? theme.palette.primary.main : theme.palette.grey[300],
color: 'white'
}}
>
@ -484,7 +500,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
disabled={activeStep === steps[0].index}
onClick={handleBack}
sx={{ mr: 1 }}
>
@ -492,13 +508,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
</Button>
<Box sx={{ flex: '1 1 auto' }} />
{activeStep === steps.length - 1 ? (
{activeStep === steps[steps.length - 1].index ? (
<Button onClick={handleReset} variant="outlined">
Start New Analysis
</Button>
) : (
<Button onClick={handleNext} variant="contained">
{activeStep === steps.length - 2 ? 'Start Analysis' : 'Next'}
{activeStep === steps[steps.length - 1].index - 1 ? 'Start Analysis' : 'Next'}
</Button>
)}
</Box>

View File

@ -27,8 +27,8 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
setQuestions([
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
{candidate.questions?.map(({ question, tunables }, i: number) =>
<BackstoryQuery key={i} query={{ prompt: question, tunables: tunables }} submitQuery={submitQuery} />
{candidate.questions?.map((q, i: number) =>
<BackstoryQuery key={i} question={q} submitQuery={submitQuery} />
)}
</Box>,
<Box sx={{ p: 1 }}>

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// Source: src/backend/models.py
// Generated on: 2025-06-02T23:24:36.213957
// Generated on: 2025-06-03T02:54:38.566349
// DO NOT EDIT MANUALLY - This file is auto-generated
// ============================
@ -367,7 +367,6 @@ export interface ChatSession {
lastActivity?: Date;
title?: string;
context: ChatContext;
messages?: Array<ChatMessage>;
isArchived: boolean;
systemPrompt?: string;
}

View File

@ -3032,15 +3032,9 @@ async def delete_chat_session(
# Delete all messages associated with this session
try:
await database.delete_chat_messages(session_id)
chat_messages = await database.get_chat_messages(session_id)
message_count = len(chat_messages)
# Delete each message
for message_data in chat_messages:
message_id = message_data.get("id")
if message_id:
await database.delete_chat_message(session_id, message_id)
message_count = len(chat_messages)
logger.info(f"🗑️ Deleted {message_count} messages from session {session_id}")
except Exception as e:
@ -3092,15 +3086,9 @@ async def reset_chat_session(
# Delete all messages associated with this session
try:
await database.delete_chat_messages(session_id)
chat_messages = await database.get_chat_messages(session_id)
message_count = len(chat_messages)
# Delete each message
for message_data in chat_messages:
message_id = message_data.get("id")
if message_id:
await database.delete_chat_message(session_id, message_id)
message_count = len(chat_messages)
logger.info(f"🗑️ Deleted {message_count} messages from session {session_id}")
except Exception as e:

View File

@ -774,7 +774,7 @@ class ChatSession(BaseModel):
last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="lastActivity")
title: Optional[str] = None
context: ChatContext
messages: Optional[List[ChatMessage]] = None
# messages: Optional[List[ChatMessage]] = None
is_archived: bool = Field(False, alias="isArchived")
system_prompt: Optional[str] = Field(None, alias="systemPrompt")
model_config = {