Added mobile view of resume chat
This commit is contained in:
parent
c2564f5966
commit
794efe0f95
@ -1,6 +1,7 @@
|
|||||||
import React, { forwardRef, useState, useEffect, useRef, JSX } from 'react';
|
import React, { forwardRef, useState, useEffect, useRef, JSX } from 'react';
|
||||||
import { Box, Button, Tooltip, SxProps } from '@mui/material';
|
import { Box, Button, Tooltip, SxProps, useMediaQuery, useTheme, Tabs, Tab } from '@mui/material';
|
||||||
import { Send as SendIcon } from '@mui/icons-material';
|
import { Send as SendIcon, Tune as TuneIcon } from '@mui/icons-material';
|
||||||
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import {
|
import {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@ -70,15 +71,23 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
>(null);
|
>(null);
|
||||||
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
||||||
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
|
|
||||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [streaming, setStreaming] = useState<boolean>(false);
|
const [streaming, setStreaming] = useState<boolean>(false);
|
||||||
|
const [tabValue, setTabValue] = useState<string>('chat');
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||||
|
if (newValue !== tabValue) {
|
||||||
|
setTabValue(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resumeId || resume) return;
|
if (!resumeId || resume) return;
|
||||||
apiClient
|
apiClient
|
||||||
@ -264,8 +273,13 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
content:
|
content:
|
||||||
`Welcome to the Backstory Chat about ${selectedCandidate.fullName}` +
|
`Welcome to the Backstory Chat about ${selectedCandidate.fullName}` +
|
||||||
(resume && ` and the ${resume.job?.title} position at ${resume.job?.company}`) +
|
(resume ? ` and the ${resume.job?.title} position at ${resume.job?.company}` : '') +
|
||||||
`. Enter any questions you have about ${candidatePossessive} resume or skills, or select from the available questions.`,
|
`. Enter any questions you have about ${candidatePossessive} ${
|
||||||
|
resume ? 'resume' : 'work history'
|
||||||
|
} or skills, or select from the available questions.` +
|
||||||
|
(isMobile
|
||||||
|
? `\n\nYou can also click on the "Resume" tab to ${candidatePossessive} resume for this position.`
|
||||||
|
: ''),
|
||||||
metadata: emptyMetadata,
|
metadata: emptyMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -275,183 +289,204 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={ref}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
height: '100%' /* Restrict to main-container's height */,
|
flexGrow: 1,
|
||||||
width: '100%',
|
|
||||||
minHeight: 0 /* Prevent flex overflow */,
|
|
||||||
maxHeight: '100%',
|
|
||||||
'& > *:not(.Scrollable)': {
|
|
||||||
flexShrink: 0 /* Prevent shrinking */,
|
|
||||||
},
|
|
||||||
...sx,
|
|
||||||
p: 0,
|
p: 0,
|
||||||
m: 0,
|
height: '100%',
|
||||||
backgroundColor: '#D3CDBF' /* Warm Gray */,
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{resume && (
|
{isMobile && (
|
||||||
<Box
|
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||||
sx={{
|
<Tab value="resume" icon={<DescriptionIcon />} label="Resume" />
|
||||||
display: 'flex',
|
<Tab value="chat" icon={<TuneIcon />} label="Chat" />
|
||||||
flexDirection: 'column',
|
</Tabs>
|
||||||
width: '50%',
|
|
||||||
maxHeight: '100%',
|
|
||||||
p: 0,
|
|
||||||
m: 0,
|
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
p: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>{resume.job?.title}</strong> at
|
|
||||||
<strong>{resume.job?.company}</strong>. Last updated{' '}
|
|
||||||
{formatDate(resume.updatedAt, false, true)}.
|
|
||||||
</Box>
|
|
||||||
<Scrollable sx={{ m: 0 }}>
|
|
||||||
<ResumePreview shadeMargins={false} resume={resume} />
|
|
||||||
</Scrollable>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
ref={ref}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'row',
|
||||||
width: '50%',
|
height: '100%' /* Restrict to main-container's height */,
|
||||||
p: 1,
|
width: '100%',
|
||||||
m: resume ? 0 : '0 auto',
|
minHeight: 0 /* Prevent flex overflow */,
|
||||||
position: 'relative',
|
maxHeight: '100%',
|
||||||
backgroundColor: 'background.paper',
|
'& > *:not(.Scrollable)': {
|
||||||
|
flexShrink: 0 /* Prevent shrinking */,
|
||||||
|
},
|
||||||
|
...sx,
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
backgroundColor: '#D3CDBF' /* Warm Gray */,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!resume && (
|
{(!isMobile || tabValue === 'resume') && resume && (
|
||||||
<CandidateInfo
|
<Box
|
||||||
key={selectedCandidate.username}
|
|
||||||
action={`Chat with Backstory about ${selectedCandidate.firstName}`}
|
|
||||||
elevation={4}
|
|
||||||
candidate={selectedCandidate}
|
|
||||||
variant="normal"
|
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
display: 'flex',
|
||||||
maxHeight: 0,
|
flexDirection: 'column',
|
||||||
minHeight: 'min-content',
|
width: isMobile ? '100%' : '50%',
|
||||||
}} // Prevent header from shrinking
|
maxHeight: '100%',
|
||||||
/>
|
p: 0,
|
||||||
)}
|
m: 0,
|
||||||
{/* Chat Interface */}
|
position: 'relative',
|
||||||
{/* Scrollable Messages Area */}
|
backgroundColor: '#f5f5f5',
|
||||||
{chatSession && (
|
|
||||||
<Scrollable
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1 }}></Box>
|
<Box
|
||||||
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage }} />}
|
sx={{
|
||||||
{messages.map((message: ChatMessage) => (
|
position: 'relative',
|
||||||
<Message key={message.id} {...{ chatSession, message }} />
|
display: 'flex',
|
||||||
))}
|
justifyContent: 'center',
|
||||||
{processingMessage !== null && (
|
p: 1,
|
||||||
<Message {...{ chatSession, message: processingMessage }} />
|
|
||||||
)}
|
|
||||||
{streamingMessage !== null && (
|
|
||||||
<Message {...{ chatSession, message: streamingMessage }} />
|
|
||||||
)}
|
|
||||||
{streaming && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
m: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PropagateLoader
|
|
||||||
size="10px"
|
|
||||||
loading={streaming}
|
|
||||||
aria-label="Loading Spinner"
|
|
||||||
data-testid="loader"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</Scrollable>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 0.5,
|
|
||||||
p: 0.5,
|
|
||||||
flex: 0,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedCandidate.questions?.map((q, i) => (
|
|
||||||
<BackstoryQuery key={i} question={q} submitQuery={handleSubmitQuestion} />
|
|
||||||
))}
|
|
||||||
{resume &&
|
|
||||||
defaultQuestions.map((question: string, i) => (
|
|
||||||
<BackstoryQuery
|
|
||||||
key={i}
|
|
||||||
question={{ question }}
|
|
||||||
submitQuery={handleSubmitQuestion}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
{/* Fixed Message Input */}
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
||||||
<DeleteConfirmation
|
|
||||||
onDelete={(): void => {
|
|
||||||
chatSession && onDelete(chatSession);
|
|
||||||
}}
|
|
||||||
disabled={!chatSession}
|
|
||||||
sx={{ minWidth: 'auto', px: 2, maxHeight: 'min-content' }}
|
|
||||||
action="reset"
|
|
||||||
label="chat session"
|
|
||||||
title="Reset Chat Session"
|
|
||||||
message={`Are you sure you want to reset the session? This action cannot be undone.`}
|
|
||||||
/>
|
|
||||||
<BackstoryTextField
|
|
||||||
placeholder="Type your message about the candidate..."
|
|
||||||
ref={backstoryTextRef}
|
|
||||||
onEnter={sendMessage}
|
|
||||||
disabled={streaming || loading}
|
|
||||||
/>
|
|
||||||
<Tooltip title="Send">
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
minWidth: 'auto',
|
|
||||||
maxHeight: 'min-content',
|
|
||||||
alignSelf: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<strong>{resume.job?.title}</strong> at
|
||||||
variant="contained"
|
<strong>{resume.job?.company}</strong>. Last updated{' '}
|
||||||
onClick={(): void => {
|
{formatDate(resume.updatedAt, false, true)}.
|
||||||
sendMessage(
|
</Box>
|
||||||
(backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) ||
|
<Scrollable sx={{ m: 0 }}>
|
||||||
''
|
<ResumePreview shadeMargins={false} resume={resume} />
|
||||||
);
|
</Scrollable>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(!isMobile || tabValue === 'chat') && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: isMobile ? '100%' : '50%',
|
||||||
|
p: 1,
|
||||||
|
m: resume ? 0 : '0 auto',
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!resume && (
|
||||||
|
<CandidateInfo
|
||||||
|
key={selectedCandidate.username}
|
||||||
|
action={`Chat with Backstory about ${selectedCandidate.firstName}`}
|
||||||
|
elevation={4}
|
||||||
|
candidate={selectedCandidate}
|
||||||
|
variant="normal"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: 0,
|
||||||
|
minHeight: 'min-content',
|
||||||
|
}} // Prevent header from shrinking
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Chat Interface */}
|
||||||
|
{/* Scrollable Messages Area */}
|
||||||
|
{chatSession && (
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
disabled={streaming || loading}
|
|
||||||
>
|
>
|
||||||
<SendIcon />
|
{messages.length === 0 && (
|
||||||
</Button>
|
<Message {...{ chatSession, message: welcomeMessage }} />
|
||||||
</span>
|
)}
|
||||||
</Tooltip>
|
{messages.map((message: ChatMessage) => (
|
||||||
</Box>
|
<Message key={message.id} {...{ chatSession, message }} />
|
||||||
|
))}
|
||||||
|
{processingMessage !== null && (
|
||||||
|
<Message {...{ chatSession, message: processingMessage }} />
|
||||||
|
)}
|
||||||
|
{streamingMessage !== null && (
|
||||||
|
<Message {...{ chatSession, message: streamingMessage }} />
|
||||||
|
)}
|
||||||
|
{streaming && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
m: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PropagateLoader
|
||||||
|
size="10px"
|
||||||
|
loading={streaming}
|
||||||
|
aria-label="Loading Spinner"
|
||||||
|
data-testid="loader"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</Scrollable>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 0.5,
|
||||||
|
p: 0.5,
|
||||||
|
flex: 0,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedCandidate.questions?.map((q, i) => (
|
||||||
|
<BackstoryQuery key={i} question={q} submitQuery={handleSubmitQuestion} />
|
||||||
|
))}
|
||||||
|
{resume &&
|
||||||
|
defaultQuestions.map((question: string, i) => (
|
||||||
|
<BackstoryQuery
|
||||||
|
key={i}
|
||||||
|
question={{ question }}
|
||||||
|
submitQuery={handleSubmitQuestion}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
{/* Fixed Message Input */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<DeleteConfirmation
|
||||||
|
onDelete={(): void => {
|
||||||
|
chatSession && onDelete(chatSession);
|
||||||
|
}}
|
||||||
|
disabled={!chatSession}
|
||||||
|
sx={{ minWidth: 'auto', px: 2, maxHeight: 'min-content' }}
|
||||||
|
action="reset"
|
||||||
|
label="chat session"
|
||||||
|
title="Reset Chat Session"
|
||||||
|
message={`Are you sure you want to reset the session? This action cannot be undone.`}
|
||||||
|
/>
|
||||||
|
<BackstoryTextField
|
||||||
|
placeholder="Type your message about the candidate..."
|
||||||
|
ref={backstoryTextRef}
|
||||||
|
onEnter={sendMessage}
|
||||||
|
disabled={streaming || loading}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Send">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
minWidth: 'auto',
|
||||||
|
maxHeight: 'min-content',
|
||||||
|
alignSelf: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={(): void => {
|
||||||
|
sendMessage(
|
||||||
|
(backstoryTextRef.current &&
|
||||||
|
backstoryTextRef.current.getAndResetValue()) ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={streaming || loading}
|
||||||
|
>
|
||||||
|
<SendIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -80,7 +80,7 @@ class ResumeChat(Agent):
|
|||||||
)
|
)
|
||||||
yield error_message
|
yield error_message
|
||||||
return
|
return
|
||||||
resume_data = await database.get_resume(user_id=candidate.id, resume_id=extra_context.resume_id)
|
resume_data = await database.get_resume(resume_id=extra_context.resume_id, user_id=candidate.id)
|
||||||
resume = Resume.model_validate(resume_data) if resume_data else None
|
resume = Resume.model_validate(resume_data) if resume_data else None
|
||||||
if not resume:
|
if not resume:
|
||||||
error_message = ChatMessageError(
|
error_message = ChatMessageError(
|
||||||
|
@ -320,7 +320,7 @@ class DatabaseProtocol(Protocol):
|
|||||||
async def get_resumes_by_job(self, user_id: str, job_id: str) -> List[Resume]:
|
async def get_resumes_by_job(self, user_id: str, job_id: str) -> List[Resume]:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def get_resume(self, user_id: str, resume_id: str) -> Optional[Resume]:
|
async def get_resume(self, resume_id: str, user_id: Optional[str] = None) -> Optional[Resume]:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def get_resume_statistics(self, user_id: str) -> Dict[str, Any]:
|
async def get_resume_statistics(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
@ -33,21 +33,52 @@ class ResumeMixin(DatabaseProtocol):
|
|||||||
logger.error(f"❌ Error saving resume for user {user_id}: {e}")
|
logger.error(f"❌ Error saving resume for user {user_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_resume(self, user_id: str, resume_id: str) -> Optional[Resume]:
|
async def get_resume(self, resume_id: str, user_id: Optional[str] = None) -> Optional[Resume]:
|
||||||
"""Get a specific resume for a user"""
|
"""Get a specific resume by resume_id, optionally filtered by user_id"""
|
||||||
try:
|
try:
|
||||||
key = f"{KEY_PREFIXES['resumes']}{user_id}:{resume_id}"
|
if user_id:
|
||||||
data = await self.redis.get(key)
|
# If user_id is provided, use the original key structure
|
||||||
if data:
|
key = f"{KEY_PREFIXES['resumes']}{user_id}:{resume_id}"
|
||||||
resume_data = self._deserialize(data)
|
data = await self.redis.get(key)
|
||||||
logger.info(f"📄 Retrieved resume {resume_id} for user {user_id}")
|
if data:
|
||||||
return Resume.model_validate(resume_data)
|
resume_data = self._deserialize(data)
|
||||||
logger.info(f"📄 Resume {resume_id} not found for user {user_id}")
|
logger.info(f"📄 Retrieved resume {resume_id} for user {user_id}")
|
||||||
return None
|
return Resume.model_validate(resume_data)
|
||||||
except Exception as e:
|
logger.info(f"📄 Resume {resume_id} not found for user {user_id}")
|
||||||
logger.error(f"❌ Error retrieving resume {resume_id} for user {user_id}: {e}")
|
return None
|
||||||
return None
|
else:
|
||||||
|
# If no user_id provided, search for the resume across all users
|
||||||
|
pattern = f"{KEY_PREFIXES['resumes']}*:{resume_id}"
|
||||||
|
|
||||||
|
# Use SCAN to find matching keys
|
||||||
|
matching_keys = []
|
||||||
|
async for key in self.redis.scan_iter(match=pattern):
|
||||||
|
matching_keys.append(key)
|
||||||
|
|
||||||
|
if not matching_keys:
|
||||||
|
logger.info(f"📄 Resume {resume_id} not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(matching_keys) > 1:
|
||||||
|
logger.warning(f"📄 Multiple resumes found with ID {resume_id}, returning first match")
|
||||||
|
|
||||||
|
# Get data from the first matching key
|
||||||
|
target_key = matching_keys[0]
|
||||||
|
data = await self.redis.get(target_key)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
resume_data = self._deserialize(data)
|
||||||
|
# Extract user_id from key for logging
|
||||||
|
key_user_id = target_key.replace(KEY_PREFIXES["resumes"], "").split(":")[0]
|
||||||
|
logger.info(f"📄 Retrieved resume {resume_id} for user {key_user_id}")
|
||||||
|
return Resume.model_validate(resume_data)
|
||||||
|
|
||||||
|
logger.info(f"📄 Resume {resume_id} data not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error retrieving resume {resume_id}: {e}")
|
||||||
|
return None
|
||||||
async def get_all_resumes_for_user(self, user_id: str) -> List[Dict]:
|
async def get_all_resumes_for_user(self, user_id: str) -> List[Dict]:
|
||||||
"""Get all resumes for a specific user"""
|
"""Get all resumes for a specific user"""
|
||||||
try:
|
try:
|
||||||
@ -283,7 +314,7 @@ class ResumeMixin(DatabaseProtocol):
|
|||||||
"""Update specific fields of a resume and save the previous version to history"""
|
"""Update specific fields of a resume and save the previous version to history"""
|
||||||
try:
|
try:
|
||||||
# Get current resume data
|
# Get current resume data
|
||||||
current_resume = await self.get_resume(user_id, resume_id)
|
current_resume = await self.get_resume(resume_id, user_id)
|
||||||
if not current_resume:
|
if not current_resume:
|
||||||
logger.warning(f"📄 Resume {resume_id} not found for user {user_id}")
|
logger.warning(f"📄 Resume {resume_id} not found for user {user_id}")
|
||||||
return None
|
return None
|
||||||
@ -412,7 +443,7 @@ class ResumeMixin(DatabaseProtocol):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Save current version to history before restoring
|
# Save current version to history before restoring
|
||||||
current_resume = await self.get_resume(user_id, resume_id)
|
current_resume = await self.get_resume(resume_id, user_id)
|
||||||
if current_resume:
|
if current_resume:
|
||||||
await self._save_resume_revision(user_id, resume_id, current_resume.model_dump())
|
await self._save_resume_revision(user_id, resume_id, current_resume.model_dump())
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Resume Routes
|
Resume Routes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import List
|
from typing import List
|
||||||
import uuid
|
import uuid
|
||||||
@ -12,12 +13,13 @@ import backstory_traceback as backstory_traceback
|
|||||||
from database.manager import RedisDatabase
|
from database.manager import RedisDatabase
|
||||||
from logger import logger
|
from logger import logger
|
||||||
from models import MOCK_UUID, ChatMessageError, Job, Candidate, Resume, ResumeMessage
|
from models import MOCK_UUID, ChatMessageError, Job, Candidate, Resume, ResumeMessage
|
||||||
from utils.dependencies import get_database, get_current_user
|
from utils.dependencies import get_database, get_current_user, get_current_user_or_guest
|
||||||
from utils.responses import create_success_response
|
from utils.responses import create_success_response
|
||||||
|
|
||||||
# Create router for authentication endpoints
|
# Create router for authentication endpoints
|
||||||
router = APIRouter(prefix="/resumes", tags=["resumes"])
|
router = APIRouter(prefix="/resumes", tags=["resumes"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("")
|
@router.post("")
|
||||||
async def create_candidate_resume(
|
async def create_candidate_resume(
|
||||||
resume: Resume = Body(...),
|
resume: Resume = Body(...),
|
||||||
@ -142,12 +144,12 @@ async def get_user_resumes(current_user=Depends(get_current_user), database: Red
|
|||||||
@router.get("/{resume_id}")
|
@router.get("/{resume_id}")
|
||||||
async def get_resume(
|
async def get_resume(
|
||||||
resume_id: str = Path(..., description="ID of the resume"),
|
resume_id: str = Path(..., description="ID of the resume"),
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user_or_guest),
|
||||||
database: RedisDatabase = Depends(get_database),
|
database: RedisDatabase = Depends(get_database),
|
||||||
):
|
):
|
||||||
"""Get a specific resume by ID"""
|
"""Get a specific resume by ID"""
|
||||||
try:
|
try:
|
||||||
resume_data = await database.get_resume(current_user.id, resume_id)
|
resume_data = await database.get_resume(resume_id)
|
||||||
if not resume_data:
|
if not resume_data:
|
||||||
raise HTTPException(status_code=404, detail="Resume not found")
|
raise HTTPException(status_code=404, detail="Resume not found")
|
||||||
resume = Resume.model_validate(resume_data)
|
resume = Resume.model_validate(resume_data)
|
||||||
@ -242,6 +244,7 @@ async def get_resume_statistics(
|
|||||||
logger.error(f"❌ Error retrieving resume statistics for user {current_user.id}: {e}")
|
logger.error(f"❌ Error retrieving resume statistics for user {current_user.id}: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to retrieve resume statistics")
|
raise HTTPException(status_code=500, detail="Failed to retrieve resume statistics")
|
||||||
|
|
||||||
|
|
||||||
@router.patch("")
|
@router.patch("")
|
||||||
async def update_resume(
|
async def update_resume(
|
||||||
resume: Resume = Body(...),
|
resume: Resume = Body(...),
|
||||||
@ -260,7 +263,7 @@ async def update_resume(
|
|||||||
logger.warning(f"⚠️ Resume {resume.id} could not be updated for user {current_user.id}")
|
logger.warning(f"⚠️ Resume {resume.id} could not be updated for user {current_user.id}")
|
||||||
raise HTTPException(status_code=400, detail="Failed to update resume")
|
raise HTTPException(status_code=400, detail="Failed to update resume")
|
||||||
return create_success_response(updated_resume.model_dump(by_alias=True))
|
return create_success_response(updated_resume.model_dump(by_alias=True))
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -420,4 +423,4 @@ async def compare_resume_revisions(
|
|||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error comparing revisions {revision_id1} and {revision_id2} for resume {resume_id}: {e}")
|
logger.error(f"❌ Error comparing revisions {revision_id1} and {revision_id2} for resume {resume_id}: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to compare resume revisions")
|
raise HTTPException(status_code=500, detail="Failed to compare resume revisions")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user