Compare commits
3 Commits
c470d719ea
...
5e37e17724
Author | SHA1 | Date | |
---|---|---|---|
5e37e17724 | |||
1e04f2e070 | |||
4a95c72a6f |
261
frontend/src/components/ui/TransientChat.tsx
Normal file
261
frontend/src/components/ui/TransientChat.tsx
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import React, { forwardRef, useState, useEffect, useRef, JSX, useImperativeHandle } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
import {
|
||||||
|
ChatMessage,
|
||||||
|
ChatSession,
|
||||||
|
ChatMessageUser,
|
||||||
|
ChatMessageError,
|
||||||
|
ChatMessageStreaming,
|
||||||
|
ChatMessageStatus,
|
||||||
|
ChatMessageMetaData,
|
||||||
|
Candidate,
|
||||||
|
ChatQuery,
|
||||||
|
} from 'types/types';
|
||||||
|
import { ConversationHandle } from 'components/Conversation';
|
||||||
|
import { Message } from 'components/Message';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||||
|
import { Scrollable } from 'components/Scrollable';
|
||||||
|
|
||||||
|
const emptyMetadata: ChatMessageMetaData = {
|
||||||
|
model: 'qwen2.5',
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 0,
|
||||||
|
topP: 0,
|
||||||
|
frequencyPenalty: 0,
|
||||||
|
presencePenalty: 0,
|
||||||
|
stopSequences: [],
|
||||||
|
evalCount: 0,
|
||||||
|
evalDuration: 0,
|
||||||
|
promptEvalCount: 0,
|
||||||
|
promptEvalDuration: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultMessage: ChatMessage = {
|
||||||
|
status: 'done',
|
||||||
|
type: 'text',
|
||||||
|
sessionId: '',
|
||||||
|
timestamp: new Date(),
|
||||||
|
content: '',
|
||||||
|
role: 'user',
|
||||||
|
metadata: emptyMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TransientChatProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransientChat = forwardRef<ConversationHandle, TransientChatProps>(
|
||||||
|
(props, ref): JSX.Element => {
|
||||||
|
const { id } = props;
|
||||||
|
const { apiClient, user } = useAuth();
|
||||||
|
const [processingMessage, setProcessingMessage] = useState<
|
||||||
|
ChatMessageStatus | ChatMessageError | null
|
||||||
|
>(null);
|
||||||
|
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
||||||
|
|
||||||
|
const { setSnack } = useAppState();
|
||||||
|
|
||||||
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [streaming, setStreaming] = useState<boolean>(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
submitQuery: (query: ChatQuery): void => {
|
||||||
|
sendMessage(query.prompt);
|
||||||
|
},
|
||||||
|
fetchHistory: (): void => {
|
||||||
|
console.log('fetchHistory called, but not implemented');
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const chatHandlers = {
|
||||||
|
onMessage: (msg: ChatMessage): void => {
|
||||||
|
setMessages(prev => {
|
||||||
|
const filtered = prev.filter(m => m.id !== msg.id);
|
||||||
|
return [...filtered, msg] as ChatMessage[];
|
||||||
|
});
|
||||||
|
setStreamingMessage(null);
|
||||||
|
setProcessingMessage(null);
|
||||||
|
},
|
||||||
|
onError: (error: string | ChatMessageError): void => {
|
||||||
|
console.log('onError:', error);
|
||||||
|
// Type-guard to determine if this is a ChatMessageBase or a string
|
||||||
|
if (typeof error === 'object' && error !== null && 'content' in error) {
|
||||||
|
setProcessingMessage(error);
|
||||||
|
} else {
|
||||||
|
setProcessingMessage({
|
||||||
|
...defaultMessage,
|
||||||
|
status: 'error',
|
||||||
|
content: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setStreaming(false);
|
||||||
|
},
|
||||||
|
onStreaming: (chunk: ChatMessageStreaming): void => {
|
||||||
|
// console.log("onStreaming:", chunk);
|
||||||
|
setStreamingMessage({
|
||||||
|
...chunk,
|
||||||
|
role: 'assistant',
|
||||||
|
metadata: emptyMetadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStatus: (status: ChatMessageStatus): void => {
|
||||||
|
setProcessingMessage(status);
|
||||||
|
},
|
||||||
|
onComplete: (): void => {
|
||||||
|
console.log('onComplete');
|
||||||
|
setStreamingMessage(null);
|
||||||
|
setProcessingMessage(null);
|
||||||
|
setStreaming(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const sendMessage = async (message: string): Promise<void> => {
|
||||||
|
if (!message.trim() || !chatSession?.id || streaming) return;
|
||||||
|
|
||||||
|
const messageContent = message;
|
||||||
|
setStreaming(true);
|
||||||
|
|
||||||
|
const chatMessage: ChatMessageUser = {
|
||||||
|
sessionId: chatSession.id,
|
||||||
|
role: 'user',
|
||||||
|
content: messageContent,
|
||||||
|
status: 'done',
|
||||||
|
type: 'text',
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setProcessingMessage({
|
||||||
|
...defaultMessage,
|
||||||
|
status: 'status',
|
||||||
|
activity: 'info',
|
||||||
|
content: `Establishing connection with chat session.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setMessages(prev => {
|
||||||
|
const filtered = prev.filter(m => m.id !== chatMessage.id);
|
||||||
|
return [...filtered, chatMessage] as ChatMessage[];
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
apiClient.sendMessageStream(chatMessage, chatHandlers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send message:', error);
|
||||||
|
setStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new messages arrive
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Load sessions when username changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
try {
|
||||||
|
apiClient
|
||||||
|
.getOrCreateChatSession(user as Candidate, `Transient chat - ${id}`, 'candidate_chat')
|
||||||
|
.then(session => {
|
||||||
|
setChatSession(session);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setSnack('Unable to load chat session', 'error');
|
||||||
|
}
|
||||||
|
}, [user, apiClient, setSnack]);
|
||||||
|
|
||||||
|
// Load messages when session changes
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMessages = async (): Promise<void> => {
|
||||||
|
if (!chatSession?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiClient.getChatMessages(chatSession.id);
|
||||||
|
const chatMessages: ChatMessage[] = result.data;
|
||||||
|
setMessages(chatMessages);
|
||||||
|
setProcessingMessage(null);
|
||||||
|
setStreamingMessage(null);
|
||||||
|
console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (chatSession?.id) {
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
}, [chatSession, apiClient]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%' /* Restrict to main-container's height */,
|
||||||
|
width: '100%',
|
||||||
|
minHeight: 0 /* Prevent flex overflow */,
|
||||||
|
maxHeight: 'min-content',
|
||||||
|
'& > *:not(.Scrollable)': {
|
||||||
|
flexShrink: 0 /* Prevent shrinking */,
|
||||||
|
},
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Chat Interface */}
|
||||||
|
{/* Scrollable Messages Area */}
|
||||||
|
{chatSession && (
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
maxHeight: '100%',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
flex: 1 /* Take remaining space in some-container */,
|
||||||
|
overflowY: 'auto' /* Scroll if content overflows */,
|
||||||
|
pt: 2,
|
||||||
|
pl: 1,
|
||||||
|
pr: 1,
|
||||||
|
pb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.map((message: ChatMessage) => (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
TransientChat.displayName = 'TransientChat';
|
||||||
|
export { TransientChat };
|
@ -104,7 +104,7 @@ const capitalize = (str: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Main component
|
// Main component
|
||||||
const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProps) => {
|
const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { user, guest } = useAuth();
|
const { user, guest } = useAuth();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Snackbar,
|
|
||||||
Alert,
|
Alert,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -44,13 +43,15 @@ import {
|
|||||||
Phone,
|
Phone,
|
||||||
Email,
|
Email,
|
||||||
AccountCircle,
|
AccountCircle,
|
||||||
|
LiveHelp,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
|
||||||
import { useAppState } from 'hooks/GlobalContext';
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||||
|
import { TransientChat } from 'components/ui/TransientChat';
|
||||||
|
import { ConversationHandle } from 'components/Conversation';
|
||||||
|
|
||||||
// Styled components
|
// Styled components
|
||||||
const VisuallyHiddenInput = styled('input')({
|
const VisuallyHiddenInput = styled('input')({
|
||||||
@ -67,22 +68,22 @@ const VisuallyHiddenInput = styled('input')({
|
|||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
index: number;
|
value: string;
|
||||||
value: number;
|
active: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabPanel(props: TabPanelProps): JSX.Element {
|
function TabPanel(props: TabPanelProps): JSX.Element {
|
||||||
const { children, value, index, ...other } = props;
|
const { children, value, active, ...other } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
hidden={value !== index}
|
hidden={active !== value}
|
||||||
id={`profile-tabpanel-${index}`}
|
id={`profile-tabpanel-${value}`}
|
||||||
aria-labelledby={`profile-tab-${index}`}
|
aria-labelledby={`profile-tab-${value}`}
|
||||||
{...other}
|
{...other}
|
||||||
>
|
>
|
||||||
{value === index && (
|
{value === active && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 1, sm: 3 },
|
p: { xs: 1, sm: 3 },
|
||||||
@ -97,28 +98,20 @@ function TabPanel(props: TabPanelProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPageProps) => {
|
const CandidateProfile: React.FC = () => {
|
||||||
const { setSnack } = useAppState();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { setSnack } = useAppState();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const { user, updateUserData, apiClient } = useAuth();
|
const { user, updateUserData, apiClient } = useAuth();
|
||||||
|
const chatRef = React.useRef<ConversationHandle>(null);
|
||||||
|
|
||||||
// Check if user is a candidate
|
// Check if user is a candidate
|
||||||
const candidate = user?.userType === 'candidate' ? (user as Types.Candidate) : null;
|
const candidate = user?.userType === 'candidate' ? (user as Types.Candidate) : null;
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState<string>('info');
|
||||||
const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({});
|
const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [snackbar, setSnackbar] = useState<{
|
|
||||||
open: boolean;
|
|
||||||
message: string;
|
|
||||||
severity: 'success' | 'error' | 'info' | 'warning';
|
|
||||||
}>({
|
|
||||||
open: false,
|
|
||||||
message: '',
|
|
||||||
severity: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form data state
|
// Form data state
|
||||||
const [formData, setFormData] = useState<Partial<Types.Candidate>>({});
|
const [formData, setFormData] = useState<Partial<Types.Candidate>>({});
|
||||||
@ -126,6 +119,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
|
|
||||||
// Dialog states
|
// Dialog states
|
||||||
const [skillDialog, setSkillDialog] = useState(false);
|
const [skillDialog, setSkillDialog] = useState(false);
|
||||||
|
const [questionDialog, setQuestionDialog] = useState(false);
|
||||||
const [experienceDialog, setExperienceDialog] = useState(false);
|
const [experienceDialog, setExperienceDialog] = useState(false);
|
||||||
|
|
||||||
// New item states
|
// New item states
|
||||||
@ -135,6 +129,11 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
level: 'beginner',
|
level: 'beginner',
|
||||||
yearsOfExperience: 0,
|
yearsOfExperience: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [newQuestion, setNewQuestion] = useState<Partial<Types.CandidateQuestion>>({
|
||||||
|
question: '',
|
||||||
|
});
|
||||||
|
|
||||||
const [newExperience, setNewExperience] = useState<Partial<Types.WorkExperience>>({
|
const [newExperience, setNewExperience] = useState<Partial<Types.WorkExperience>>({
|
||||||
companyName: '',
|
companyName: '',
|
||||||
position: '',
|
position: '',
|
||||||
@ -153,7 +152,6 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
} else {
|
} else {
|
||||||
setProfileImage('');
|
setProfileImage('');
|
||||||
}
|
}
|
||||||
console.log({ isPublic: candidate.isPublic });
|
|
||||||
}
|
}
|
||||||
}, [candidate]);
|
}, [candidate]);
|
||||||
|
|
||||||
@ -166,7 +164,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle tab change
|
// Handle tab change
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number): void => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||||
setTabValue(newValue);
|
setTabValue(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -193,19 +191,11 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
console.log(`Set profile image to: ${candidate.profileImage}`);
|
console.log(`Set profile image to: ${candidate.profileImage}`);
|
||||||
updateUserData(candidate);
|
updateUserData(candidate);
|
||||||
} else {
|
} else {
|
||||||
setSnackbar({
|
setSnack('Failed to upload profile image. Please try again.', 'error');
|
||||||
open: true,
|
|
||||||
message: 'Failed to upload profile image. Please try again.',
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading profile image:', error);
|
console.error('Error uploading profile image:', error);
|
||||||
setSnackbar({
|
setSnack('Failed to upload profile image. Please try again.', 'error');
|
||||||
open: true,
|
|
||||||
message: 'Failed to upload profile image. Please try again.',
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -228,11 +218,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
toggleEditMode(section);
|
toggleEditMode(section);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSnackbar({
|
setSnack('Failed to update profile. Please try again.', 'error');
|
||||||
open: true,
|
|
||||||
message: 'Failed to update profile. Please try again.',
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -256,6 +242,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
yearsOfExperience: 0,
|
yearsOfExperience: 0,
|
||||||
});
|
});
|
||||||
setSkillDialog(false);
|
setSkillDialog(false);
|
||||||
|
setSnack('Skill added successfully!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -263,6 +250,30 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
const handleRemoveSkill = (index: number): void => {
|
const handleRemoveSkill = (index: number): void => {
|
||||||
const updatedSkills = (formData.skills || []).filter((_, i) => i !== index);
|
const updatedSkills = (formData.skills || []).filter((_, i) => i !== index);
|
||||||
setFormData({ ...formData, skills: updatedSkills });
|
setFormData({ ...formData, skills: updatedSkills });
|
||||||
|
setSnack('Skill removed successfully!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new question
|
||||||
|
const handleAddQuestion = (): void => {
|
||||||
|
if (newQuestion.question?.trim()) {
|
||||||
|
const questionToAdd: Types.CandidateQuestion = {
|
||||||
|
question: newQuestion.question.trim(),
|
||||||
|
};
|
||||||
|
const updatedQuestions = [...(formData.questions || []), questionToAdd];
|
||||||
|
setFormData({ ...formData, questions: updatedQuestions });
|
||||||
|
setNewQuestion({
|
||||||
|
question: '',
|
||||||
|
});
|
||||||
|
setQuestionDialog(false);
|
||||||
|
setSnack('Question added successfully!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove question
|
||||||
|
const handleRemoveQuestion = (index: number): void => {
|
||||||
|
const updatedQuestions = (formData.questions || []).filter((_, i) => i !== index);
|
||||||
|
setFormData({ ...formData, questions: updatedQuestions });
|
||||||
|
setSnack('Question removed successfully!');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add new work experience
|
// Add new work experience
|
||||||
@ -283,6 +294,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
location: { city: '', country: '' },
|
location: { city: '', country: '' },
|
||||||
});
|
});
|
||||||
setExperienceDialog(false);
|
setExperienceDialog(false);
|
||||||
|
setSnack('Experience added successfully!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -290,6 +302,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
const handleRemoveExperience = (index: number): void => {
|
const handleRemoveExperience = (index: number): void => {
|
||||||
const updatedExperience = (formData.experience || []).filter((_, i) => i !== index);
|
const updatedExperience = (formData.experience || []).filter((_, i) => i !== index);
|
||||||
setFormData({ ...formData, experience: updatedExperience });
|
setFormData({ ...formData, experience: updatedExperience });
|
||||||
|
setSnack('Experience removed successfully!');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic Information Tab
|
// Basic Information Tab
|
||||||
@ -630,6 +643,104 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSubmitQuestion = (question: Types.CandidateQuestion): void => {
|
||||||
|
console.log('Submitting question:', question);
|
||||||
|
const query: Types.ChatQuery = {
|
||||||
|
prompt: question.question,
|
||||||
|
};
|
||||||
|
chatRef.current?.submitQuery(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Questions Tab
|
||||||
|
const renderQuestions = (): JSX.Element => (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: { xs: 'stretch', sm: 'center' },
|
||||||
|
mb: { xs: 2, sm: 3 },
|
||||||
|
gap: { xs: 1, sm: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant={isMobile ? 'subtitle1' : 'h6'}>Questions for Chat</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={(): void => setQuestionDialog(true)}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? 'small' : 'medium'}
|
||||||
|
>
|
||||||
|
Add Question
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>See how Backstory answers the question about you...</Box>
|
||||||
|
<TransientChat id={Date()} ref={chatRef} />
|
||||||
|
|
||||||
|
<Grid container spacing={{ xs: 1, sm: 2 }} sx={{ maxWidth: '100%' }}>
|
||||||
|
{(formData.questions || []).map((question, index) => (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||||
|
<Card variant="outlined" sx={{ height: '100%' }}>
|
||||||
|
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<BackstoryQuery question={question} submitQuery={handleSubmitQuestion} />
|
||||||
|
{/* Display question text
|
||||||
|
<Typography
|
||||||
|
variant={isMobile ? 'subtitle2' : 'h6'}
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: '0.9rem', sm: '1.25rem' },
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question.question}
|
||||||
|
</Typography> */}
|
||||||
|
{/* {question.category && (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={question.category}
|
||||||
|
color="secondary"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: '0.65rem', sm: '0.75rem' },
|
||||||
|
height: { xs: 20, sm: 24 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(): void => handleRemoveQuestion(index)}
|
||||||
|
color="error"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
>
|
||||||
|
<Delete sx={{ fontSize: { xs: 16, sm: 20 } }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{(!formData.questions || formData.questions.length === 0) && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
No questions added yet. Click "Add Question" to get started.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
// Experience Tab
|
// Experience Tab
|
||||||
const renderExperience = (): JSX.Element => (
|
const renderExperience = (): JSX.Element => (
|
||||||
<Box>
|
<Box>
|
||||||
@ -781,19 +892,17 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{(!formData.experience || formData.experience.length === 0) && (
|
<Typography
|
||||||
<Typography
|
variant="body2"
|
||||||
variant="body2"
|
color="text.secondary"
|
||||||
color="text.secondary"
|
sx={{
|
||||||
sx={{
|
textAlign: 'center',
|
||||||
textAlign: 'center',
|
py: { xs: 2, sm: 4 },
|
||||||
py: { xs: 2, sm: 4 },
|
fontSize: { xs: '0.8rem', sm: '0.875rem' },
|
||||||
fontSize: { xs: '0.8rem', sm: '0.875rem' },
|
}}
|
||||||
}}
|
>
|
||||||
>
|
No education added yet. Click "Add Education" to get started.
|
||||||
No work experience added yet. Click "Add Experience" to get started.
|
</Typography>
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -833,41 +942,55 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
label={isMobile ? 'Info' : 'Basic Info'}
|
label={isMobile ? 'Info' : 'Basic Info'}
|
||||||
|
value="info"
|
||||||
icon={<AccountCircle sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
icon={<AccountCircle sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
iconPosition={isMobile ? 'top' : 'start'}
|
iconPosition={isMobile ? 'top' : 'start'}
|
||||||
/>
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Questions"
|
||||||
|
value="questions"
|
||||||
|
icon={<LiveHelp sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
|
iconPosition={isMobile ? 'top' : 'start'}
|
||||||
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="Skills"
|
label="Skills"
|
||||||
|
value="skills"
|
||||||
icon={<EmojiEvents sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
icon={<EmojiEvents sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
iconPosition={isMobile ? 'top' : 'start'}
|
iconPosition={isMobile ? 'top' : 'start'}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label={isMobile ? 'Work' : 'Experience'}
|
label={isMobile ? 'Work' : 'Experience'}
|
||||||
|
value="experience"
|
||||||
icon={<Work sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
icon={<Work sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
iconPosition={isMobile ? 'top' : 'start'}
|
iconPosition={isMobile ? 'top' : 'start'}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label={isMobile ? 'Edu' : 'Education'}
|
label={isMobile ? 'Edu' : 'Education'}
|
||||||
|
value="education"
|
||||||
icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
iconPosition={isMobile ? 'top' : 'start'}
|
iconPosition={isMobile ? 'top' : 'start'}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel value={tabValue} index={0}>
|
<TabPanel value="info" active={tabValue}>
|
||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={tabValue} index={1}>
|
<TabPanel value="questions" active={tabValue}>
|
||||||
<ComingSoon>{renderSkills()}</ComingSoon>
|
{renderQuestions()}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={tabValue} index={2}>
|
<TabPanel value="skills" active={tabValue}>
|
||||||
<ComingSoon>{renderExperience()}</ComingSoon>
|
{renderSkills()}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={tabValue} index={3}>
|
<TabPanel value="experience" active={tabValue}>
|
||||||
<ComingSoon>{renderEducation()}</ComingSoon>
|
{renderExperience()}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value="education" active={tabValue}>
|
||||||
|
{renderEducation()}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@ -924,7 +1047,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
onChange={(e): void =>
|
onChange={(e): void =>
|
||||||
setNewSkill({
|
setNewSkill({
|
||||||
...newSkill,
|
...newSkill,
|
||||||
level: e.target.value as Types.SkillLevel,
|
level: e.target.value as 'beginner' | 'intermediate' | 'advanced' | 'expert',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
label="Proficiency Level"
|
label="Proficiency Level"
|
||||||
@ -978,6 +1101,104 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Add Question Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={questionDialog}
|
||||||
|
onClose={(): void => setQuestionDialog(false)}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
fullScreen={isMobile}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
...(isMobile && {
|
||||||
|
margin: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add New Question</DialogTitle>
|
||||||
|
<DialogContent
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
pt: { xs: 1, sm: 2 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={isMobile ? 3 : 4}
|
||||||
|
label="Question"
|
||||||
|
value={newQuestion.question || ''}
|
||||||
|
onChange={(e): void => setNewQuestion({ ...newQuestion, question: e.target.value })}
|
||||||
|
placeholder="What would you potential employers to ask about you?"
|
||||||
|
size={isMobile ? 'small' : 'medium'}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{/* <Grid size={{ xs: 12, sm: 8 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Category (Optional)"
|
||||||
|
value={newQuestion.category || ''}
|
||||||
|
onChange={(e): void => setNewQuestion({ ...newQuestion, category: e.target.value })}
|
||||||
|
placeholder="e.g., General, Technical, Behavioral"
|
||||||
|
size={isMobile ? 'small' : 'medium'}
|
||||||
|
/>
|
||||||
|
</Grid> */}
|
||||||
|
{/* <Grid size={{ xs: 12, sm: 4 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={newQuestion.isActive !== false}
|
||||||
|
onChange={(e): void =>
|
||||||
|
setNewQuestion({
|
||||||
|
...newQuestion,
|
||||||
|
isActive: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={isMobile ? 'small' : 'medium'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Active"
|
||||||
|
sx={{
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid> */}
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions
|
||||||
|
sx={{
|
||||||
|
p: { xs: 1.5, sm: 3 },
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
gap: { xs: 1, sm: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={(): void => setQuestionDialog(false)}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? 'small' : 'medium'}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddQuestion}
|
||||||
|
variant="contained"
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? 'small' : 'medium'}
|
||||||
|
disabled={!newQuestion.question?.trim()}
|
||||||
|
>
|
||||||
|
Add Question
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Add Experience Dialog */}
|
{/* Add Experience Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={experienceDialog}
|
open={experienceDialog}
|
||||||
@ -1113,21 +1334,6 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Snackbar for notifications */}
|
|
||||||
<Snackbar
|
|
||||||
open={snackbar.open}
|
|
||||||
autoHideDuration={6000}
|
|
||||||
onClose={(): void => setSnackbar({ ...snackbar, open: false })}
|
|
||||||
>
|
|
||||||
<Alert
|
|
||||||
onClose={(): void => setSnackbar({ ...snackbar, open: false })}
|
|
||||||
severity={snackbar.severity}
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
{snackbar.message}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user