Working on model conversion
This commit is contained in:
parent
a8a8d3738d
commit
b5b3a1f5dc
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
import { Box } from '@mui/material';
|
||||
import { backstoryTheme } from './BackstoryTheme';
|
||||
|
||||
import { SeverityType } from 'components/Snack';
|
||||
@ -17,13 +17,7 @@ import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
import { debugConversion } from 'types/conversion';
|
||||
import { User, Guest, Candidate } from 'types/types';
|
||||
|
||||
const BackstoryApp = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [guest, setGuest] = useState<Guest | null>(null);
|
||||
const [candidate, setCandidate] = useState<Candidate | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const snackRef = useRef<any>(null);
|
||||
@ -38,51 +32,6 @@ const BackstoryApp = () => {
|
||||
};
|
||||
const [page, setPage] = useState<string>("");
|
||||
|
||||
const createGuestSession = () => {
|
||||
console.log("TODO: Convert this to query the server for the session instead of generating it.");
|
||||
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const guest: Guest = {
|
||||
sessionId,
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
ipAddress: 'unknown',
|
||||
userAgent: navigator.userAgent
|
||||
};
|
||||
setGuest(guest);
|
||||
debugConversion(guest, 'Guest Session');
|
||||
};
|
||||
|
||||
const checkExistingAuth = () => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const userData = localStorage.getItem('userData');
|
||||
if (token && userData) {
|
||||
try {
|
||||
const user = JSON.parse(userData);
|
||||
// Convert dates back to Date objects if they're stored as strings
|
||||
if (user.createdAt && typeof user.createdAt === 'string') {
|
||||
user.createdAt = new Date(user.createdAt);
|
||||
}
|
||||
if (user.updatedAt && typeof user.updatedAt === 'string') {
|
||||
user.updatedAt = new Date(user.updatedAt);
|
||||
}
|
||||
if (user.lastLogin && typeof user.lastLogin === 'string') {
|
||||
user.lastLogin = new Date(user.lastLogin);
|
||||
}
|
||||
setUser(user);
|
||||
} catch (e) {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('userData');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create guest session on component mount
|
||||
useEffect(() => {
|
||||
createGuestSession();
|
||||
checkExistingAuth();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
||||
setPage(currentRoute);
|
||||
@ -91,27 +40,20 @@ const BackstoryApp = () => {
|
||||
// Render appropriate routes based on user type
|
||||
return (
|
||||
<ThemeProvider theme={backstoryTheme}>
|
||||
<UserProvider {...{ guest, user, candidate, setSnack }}>
|
||||
<UserProvider {...{ setSnack }}>
|
||||
<Routes>
|
||||
<Route path="/u/:username" element={<CandidateRoute {...{ guest, candidate, setCandidate, setSnack }} />} />
|
||||
<Route path="/u/:username" element={<CandidateRoute {...{ setSnack }} />} />
|
||||
{/* Static/shared routes */}
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<BackstoryLayout
|
||||
setSnack={setSnack}
|
||||
page={page}
|
||||
chatRef={chatRef}
|
||||
snackRef={snackRef}
|
||||
submitQuery={submitQuery}
|
||||
/>
|
||||
<BackstoryLayout {...{ setSnack, page, chatRef, snackRef, submitQuery }} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</UserProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export {
|
||||
|
@ -1,57 +0,0 @@
|
||||
|
||||
.PhoneInput:disabled {
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
/* .PhoneInput:not(:active):not(:focus):not(:hover) {
|
||||
} */
|
||||
|
||||
.PhoneInput::placeholder {
|
||||
color: rgba(46, 46, 46, 0.38);
|
||||
}
|
||||
|
||||
.PhoneInput:focus,
|
||||
.PhoneInput:active {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.PhoneInput:hover:not(:active):not(:focus) {
|
||||
outline: 1px solid black;
|
||||
}
|
||||
|
||||
.PhoneInputInput {
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: currentColor;
|
||||
padding: 4px 0 5px;
|
||||
border: 0;
|
||||
box-sizing: content-box;
|
||||
background: none;
|
||||
height: 1.4375em;
|
||||
margin: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
-webkit-animation-name: mui-auto-fill-cancel;
|
||||
animation-name: mui-auto-fill-cancel;
|
||||
-webkit-animation-duration: 10ms;
|
||||
animation-duration: 10ms;
|
||||
padding: 16.5px 14px;
|
||||
}
|
||||
|
||||
.PhoneInputCountry {
|
||||
min-width: 64px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.PhoneInputCountry:focus,
|
||||
.PhoneInputCountry:active {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.PhoneInput {
|
||||
display: flex;
|
||||
outline: 1px solid rgba(46, 46, 46, 0.38);
|
||||
border: none;
|
||||
}
|
@ -1,540 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Paper,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
Avatar
|
||||
} from '@mui/material';
|
||||
import { Person, PersonAdd, AccountCircle, ExitToApp } from '@mui/icons-material';
|
||||
import 'react-phone-number-input/style.css';
|
||||
import PhoneInput from 'react-phone-number-input';
|
||||
import { E164Number } from 'libphonenumber-js/core';
|
||||
import './PhoneInput.css';
|
||||
|
||||
import { ApiClient } from 'types/api-client';
|
||||
|
||||
// Import conversion utilities
|
||||
import {
|
||||
formatApiRequest,
|
||||
parseApiResponse,
|
||||
handleApiResponse,
|
||||
extractApiData,
|
||||
isSuccessResponse,
|
||||
debugConversion,
|
||||
type ApiResponse
|
||||
} from './types/conversion';
|
||||
|
||||
import {
|
||||
AuthResponse, User, Guest, Candidate
|
||||
} from './types/types'
|
||||
|
||||
interface LoginRequest {
|
||||
login: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const BackstoryTestApp: React.FC = () => {
|
||||
const apiClient = new ApiClient();
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [guestSession, setGuestSession] = useState<Guest | null>(null);
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [phone, setPhone] = useState<E164Number | null>(null);
|
||||
const name = (currentUser?.userType === 'candidate' ? (currentUser as Candidate).username : currentUser?.email) || '';
|
||||
|
||||
// Login form state
|
||||
const [loginForm, setLoginForm] = useState<LoginRequest>({
|
||||
login: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
// Register form state
|
||||
const [registerForm, setRegisterForm] = useState<RegisterRequest>({
|
||||
username: '',
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
password: '',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
// Create guest session on component mount
|
||||
useEffect(() => {
|
||||
createGuestSession();
|
||||
checkExistingAuth();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (phone !== registerForm.phone && phone) {
|
||||
console.log({ phone });
|
||||
setRegisterForm({ ...registerForm, phone });
|
||||
}
|
||||
}, [phone, registerForm]);
|
||||
|
||||
const createGuestSession = () => {
|
||||
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const guest: Guest = {
|
||||
sessionId,
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
ipAddress: 'unknown',
|
||||
userAgent: navigator.userAgent
|
||||
};
|
||||
setGuestSession(guest);
|
||||
debugConversion(guest, 'Guest Session');
|
||||
};
|
||||
|
||||
const checkExistingAuth = () => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const userData = localStorage.getItem('userData');
|
||||
if (token && userData) {
|
||||
try {
|
||||
const user = JSON.parse(userData);
|
||||
// Convert dates back to Date objects if they're stored as strings
|
||||
if (user.createdAt && typeof user.createdAt === 'string') {
|
||||
user.createdAt = new Date(user.createdAt);
|
||||
}
|
||||
if (user.updatedAt && typeof user.updatedAt === 'string') {
|
||||
user.updatedAt = new Date(user.updatedAt);
|
||||
}
|
||||
if (user.lastLogin && typeof user.lastLogin === 'string') {
|
||||
user.lastLogin = new Date(user.lastLogin);
|
||||
}
|
||||
setCurrentUser(user);
|
||||
} catch (e) {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('userData');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const authResponse = await apiClient.login(loginForm.login, loginForm.password)
|
||||
|
||||
debugConversion(authResponse, 'Login Response');
|
||||
|
||||
// Store tokens in localStorage
|
||||
localStorage.setItem('accessToken', authResponse.accessToken);
|
||||
localStorage.setItem('refreshToken', authResponse.refreshToken);
|
||||
localStorage.setItem('userData', JSON.stringify(authResponse.user));
|
||||
|
||||
setCurrentUser(authResponse.user);
|
||||
setSuccess('Login successful!');
|
||||
|
||||
// Clear form
|
||||
setLoginForm({ login: '', password: '' });
|
||||
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Login failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const candidate: Candidate = {
|
||||
username: registerForm.username,
|
||||
email: registerForm.email,
|
||||
firstName: registerForm.firstName,
|
||||
lastName: registerForm.lastName,
|
||||
fullName: `${registerForm.firstName} ${registerForm.lastName}`,
|
||||
phone: registerForm.phone || undefined,
|
||||
userType: 'candidate',
|
||||
status: 'active',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
skills: [],
|
||||
experience: [],
|
||||
education: [],
|
||||
preferredJobTypes: [],
|
||||
languages: [],
|
||||
certifications: [],
|
||||
location: {
|
||||
city: '',
|
||||
country: '',
|
||||
remote: true
|
||||
}
|
||||
};
|
||||
|
||||
const result = await apiClient.createCandidate(candidate);
|
||||
|
||||
debugConversion(result, 'Registration Response');
|
||||
|
||||
setSuccess('Registration successful! You can now login.');
|
||||
|
||||
// Clear form and switch to login tab
|
||||
setRegisterForm({
|
||||
username: '',
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
password: '',
|
||||
phone: ''
|
||||
});
|
||||
setTabValue(0);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Registration error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Registration failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('userData');
|
||||
setCurrentUser(null);
|
||||
setSuccess('Logged out successfully');
|
||||
createGuestSession();
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
};
|
||||
|
||||
// API helper function for authenticated requests
|
||||
const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { 'Authorization': `Bearer ${token}` }),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
return handleApiResponse(response);
|
||||
};
|
||||
|
||||
// If user is logged in, show their profile
|
||||
if (currentUser) {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<AccountCircle sx={{ mr: 2 }} />
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Welcome, {name}
|
||||
</Typography>
|
||||
<Button
|
||||
color="inherit"
|
||||
onClick={handleLogout}
|
||||
startIcon={<ExitToApp />}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Container maxWidth="md" sx={{ mt: 4 }}>
|
||||
<Card elevation={3}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
||||
<Avatar sx={{ mr: 2, bgcolor: 'primary.main' }}>
|
||||
<AccountCircle />
|
||||
</Avatar>
|
||||
<Typography variant="h4" component="h1">
|
||||
User Profile
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||
<strong>Username:</strong> {name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||
<strong>Email:</strong> {currentUser.email}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||
<strong>Status:</strong> {currentUser.status}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||
<strong>Phone:</strong> {currentUser.phone || 'Not provided'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||
<strong>Last Login:</strong> {
|
||||
currentUser.lastLogin
|
||||
? currentUser.lastLogin.toLocaleString()
|
||||
: 'N/A'
|
||||
}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||
<strong>Member Since:</strong> {currentUser.createdAt.toLocaleDateString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const validateInput = (value: string) => {
|
||||
if (!value) return 'This field is required';
|
||||
|
||||
// Username: alphanumeric, 3-20 characters, no @
|
||||
const usernameRegex = /^[a-zA-Z0-9]{3,20}$/;
|
||||
// Email: basic email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (usernameRegex.test(value)) return '';
|
||||
if (emailRegex.test(value)) return '';
|
||||
return 'Enter a valid username (3-20 alphanumeric characters) or email';
|
||||
};
|
||||
|
||||
const handleLoginChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
setLoginForm({ ...loginForm, login: value });
|
||||
setError(validateInput(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
<Typography variant="h4" component="h1" gutterBottom align="center" color="primary">
|
||||
Backstory Platform
|
||||
</Typography>
|
||||
|
||||
{guestSession && (
|
||||
<Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom color="primary">
|
||||
Guest Session Active
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
|
||||
Session ID: {guestSession.sessionId}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Created: {guestSession.createdAt.toLocaleString()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||
<Tab icon={<Person />} label="Login" />
|
||||
<Tab icon={<PersonAdd />} label="Register" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{tabValue === 0 && (
|
||||
<Box component="form" onSubmit={handleLogin}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Sign In
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username or Email"
|
||||
type="text"
|
||||
value={loginForm.login}
|
||||
onChange={handleLoginChange}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
placeholder="Enter username or email"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type="password"
|
||||
value={loginForm.password}
|
||||
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
autoComplete='current-password'
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Person />}
|
||||
>
|
||||
{loading ? 'Signing In...' : 'Sign In'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tabValue === 1 && (
|
||||
<Box component="form" onSubmit={handleRegister}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Create Account
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2} sx={{ mb: 2 }}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="First Name"
|
||||
value={registerForm.firstName}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, firstName: e.target.value })}
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Last Name"
|
||||
value={registerForm.lastName}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, lastName: e.target.value })}
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username"
|
||||
value={registerForm.username}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, username: e.target.value })}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
type="email"
|
||||
value={registerForm.email}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<PhoneInput
|
||||
label="Phone (Optional)"
|
||||
placeholder="Enter phone number"
|
||||
defaultCountry='US'
|
||||
value={registerForm.phone}
|
||||
disabled={loading}
|
||||
onChange={(v) => setPhone(v as E164Number)} />
|
||||
{/* <TextField
|
||||
fullWidth
|
||||
label="Phone (Optional)"
|
||||
type="tel"
|
||||
value={registerForm.phone}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, phone: e.target.value })}
|
||||
margin="normal"
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
/> */}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type="password"
|
||||
value={registerForm.password}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, password: e.target.value })}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <PersonAdd />}
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Create Account'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export { BackstoryTestApp };
|
@ -69,8 +69,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
||||
sx,
|
||||
type,
|
||||
} = props;
|
||||
const apiClient = new ApiClient();
|
||||
const { candidate } = useUser()
|
||||
const { candidate, apiClient } = useUser()
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
const [conversation, setConversation] = useState<ChatMessage[]>([]);
|
||||
@ -127,9 +126,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
||||
try {
|
||||
const aiParameters: AIParameters = {
|
||||
name: '',
|
||||
model: 'custom',
|
||||
model: 'qwen2.5',
|
||||
temperature: 0.7,
|
||||
maxTokens: -1,
|
||||
topP: 1,
|
||||
frequencyPenalty: 0,
|
||||
presencePenalty: 0,
|
||||
|
@ -38,17 +38,17 @@ const DefaultNavItems: NavigationLinkType[] = [
|
||||
|
||||
const CandidateNavItems : NavigationLinkType[]= [
|
||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ name: 'Job Analysis', path: '/job-analysis', icon: <WorkIcon /> },
|
||||
// { name: 'Job Analysis', path: '/job-analysis', icon: <WorkIcon /> },
|
||||
{ name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
|
||||
{ name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
|
||||
// { name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
|
||||
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
|
||||
// { name: 'Profile', icon: <PersonIcon />, path: '/profile' },
|
||||
// { name: 'Backstory', icon: <HistoryIcon />, path: '/backstory' },
|
||||
{ name: 'Resumes', icon: <DescriptionIcon />, path: '/resumes' },
|
||||
// { name: 'Resumes', icon: <DescriptionIcon />, path: '/resumes' },
|
||||
// { name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' },
|
||||
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
|
||||
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
|
||||
// { name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
|
||||
// { name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
|
||||
];
|
||||
|
||||
const EmployerNavItems: NavigationLinkType[] = [
|
||||
@ -121,13 +121,16 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const BackstoryLayout: React.FC<{
|
||||
interface BackstoryLayoutProps {
|
||||
setSnack: SetSnackType;
|
||||
page: string;
|
||||
chatRef: React.Ref<any>;
|
||||
snackRef: React.Ref<any>;
|
||||
submitQuery: any;
|
||||
}> = ({ setSnack, page, chatRef, snackRef, submitQuery }) => {
|
||||
};
|
||||
|
||||
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
|
||||
const { setSnack, page, chatRef, snackRef, submitQuery } = props;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, guest, candidate } = useUser();
|
||||
|
@ -17,6 +17,7 @@ import { CandidateListingPage } from 'pages/FindCandidatePage';
|
||||
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
|
||||
import { GenerateCandidate } from "pages/GenerateCandidate";
|
||||
import { ControlsPage } from 'pages/ControlsPage';
|
||||
import { LoginPage } from "pages/LoginPage";
|
||||
|
||||
const ProfilePage = () => (<BetaPage><Typography variant="h4">Profile</Typography></BetaPage>);
|
||||
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
||||
@ -27,7 +28,6 @@ const SavedPage = () => (<BetaPage><Typography variant="h4">Saved</Typography></
|
||||
const JobsPage = () => (<BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>);
|
||||
const CompanyPage = () => (<BetaPage><Typography variant="h4">Company</Typography></BetaPage>);
|
||||
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>);
|
||||
const LoginPage = () => (<BetaPage><Typography variant="h4">Login page...</Typography></BetaPage>);
|
||||
// const DashboardPage = () => (<BetaPage><Typography variant="h4">Dashboard</Typography></BetaPage>);
|
||||
// const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>);
|
||||
// const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
|
||||
|
@ -87,7 +87,6 @@ const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
||||
|
||||
interface HeaderProps {
|
||||
transparent?: boolean;
|
||||
onLogout?: () => void;
|
||||
className?: string;
|
||||
navigate: NavigateFunction;
|
||||
navigationLinks: NavigationLinkType[];
|
||||
@ -98,7 +97,7 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
const { user } = useUser();
|
||||
const { user, setUser } = useUser();
|
||||
const candidate: Candidate | null = (user && user.userType === "candidate") ? user as Candidate : null;
|
||||
const employer: Employer | null = (user && user.userType === "employer") ? user as Employer : null;
|
||||
const {
|
||||
@ -108,7 +107,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
navigationLinks,
|
||||
showLogin,
|
||||
sessionId,
|
||||
onLogout,
|
||||
setSnack,
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
@ -177,9 +175,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
|
||||
const handleLogout = () => {
|
||||
handleUserMenuClose();
|
||||
if (onLogout) {
|
||||
onLogout();
|
||||
}
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
@ -245,14 +241,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => { navigate("/register"); }}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
@ -279,14 +267,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
onClick={() => { navigate("/register"); }}
|
||||
sx={{ display: { xs: 'none', sm: 'block' } }}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { SetSnackType } from '../components/Snack';
|
||||
import { User, Guest, Candidate } from 'types/types';
|
||||
import { ApiClient } from "types/api-client";
|
||||
import { debugConversion } from "types/conversion";
|
||||
|
||||
type UserContextType = {
|
||||
apiClient: ApiClient;
|
||||
user: User | null;
|
||||
guest: Guest;
|
||||
candidate: Candidate | null;
|
||||
setUser: (user: User | null) => void;
|
||||
setCandidate: (candidate: Candidate | null) => void;
|
||||
};
|
||||
|
||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||
@ -18,19 +23,118 @@ const useUser = () => {
|
||||
|
||||
interface UserProviderProps {
|
||||
children: React.ReactNode;
|
||||
candidate: Candidate | null;
|
||||
user: User | null;
|
||||
guest: Guest | null;
|
||||
setSnack: SetSnackType;
|
||||
};
|
||||
|
||||
const UserProvider: React.FC<UserProviderProps> = (props: UserProviderProps) => {
|
||||
const { guest, user, children, candidate, setSnack } = props;
|
||||
const { children, setSnack } = props;
|
||||
const [apiClient, setApiClient] = useState<ApiClient>(new ApiClient());
|
||||
const [candidate, setCandidate] = useState<Candidate | null>(null);
|
||||
const [guest, setGuest] = useState<Guest | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [activeUser, setActiveUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Candidate =>", candidate);
|
||||
}, [candidate]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Guest =>", guest);
|
||||
}, [guest]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("User => ", user);
|
||||
}, [user]);
|
||||
|
||||
/* Handle logout if any consumers of UserProvider setUser to NULL */
|
||||
useEffect(() => {
|
||||
/* If there is an active user and it is the same as the
|
||||
* new user, do nothing */
|
||||
if (activeUser && activeUser.email === user?.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
console.log(`Logging out ${user.email}`);
|
||||
try {
|
||||
const results = await apiClient.logout();
|
||||
if (results.error) {
|
||||
console.error(results.error);
|
||||
setSnack(results.error.message, "error")
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setSnack(`Unable to logout: ${e}`, "error")
|
||||
}
|
||||
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('userData');
|
||||
createGuestSession();
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
setActiveUser(user);
|
||||
if (!user) {
|
||||
logout();
|
||||
}
|
||||
}, [user, apiClient, activeUser]);
|
||||
|
||||
const createGuestSession = () => {
|
||||
console.log("TODO: Convert this to query the server for the session instead of generating it.");
|
||||
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const guest: Guest = {
|
||||
sessionId,
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
ipAddress: 'unknown',
|
||||
userAgent: navigator.userAgent
|
||||
};
|
||||
setGuest(guest);
|
||||
debugConversion(guest, 'Guest Session');
|
||||
};
|
||||
|
||||
const checkExistingAuth = () => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const userData = localStorage.getItem('userData');
|
||||
if (token && userData) {
|
||||
try {
|
||||
const user = JSON.parse(userData);
|
||||
// Convert dates back to Date objects if they're stored as strings
|
||||
if (user.createdAt && typeof user.createdAt === 'string') {
|
||||
user.createdAt = new Date(user.createdAt);
|
||||
}
|
||||
if (user.updatedAt && typeof user.updatedAt === 'string') {
|
||||
user.updatedAt = new Date(user.updatedAt);
|
||||
}
|
||||
if (user.lastLogin && typeof user.lastLogin === 'string') {
|
||||
user.lastLogin = new Date(user.lastLogin);
|
||||
}
|
||||
setApiClient(new ApiClient(token));
|
||||
setUser(user);
|
||||
} catch (e) {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('userData');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create guest session on component mount
|
||||
useEffect(() => {
|
||||
createGuestSession();
|
||||
checkExistingAuth();
|
||||
}, []);
|
||||
|
||||
if (guest === null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ candidate, user, guest }}>
|
||||
<UserContext.Provider value={{ apiClient, candidate, setCandidate, user, setUser, guest }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
@ -9,16 +9,15 @@ import { Conversation, ConversationHandle } from '../components/Conversation';
|
||||
import { ChatQuery } from '../components/ChatQuery';
|
||||
import { CandidateInfo } from 'components/CandidateInfo';
|
||||
import { useUser } from "../hooks/useUser";
|
||||
import { Candidate } from "../types/types";
|
||||
|
||||
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||
const { setSnack, submitQuery } = props;
|
||||
const { candidate } = useUser();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
||||
const { user } = useUser();
|
||||
const candidate: Candidate | null = (user && user.userType === 'candidate') ? user as Candidate : null;
|
||||
|
||||
console.log("ChatPage candidate =>", candidate);
|
||||
useEffect(() => {
|
||||
if (!candidate) {
|
||||
return;
|
||||
@ -38,8 +37,8 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
||||
}, [candidate, isMobile, submitQuery]);
|
||||
|
||||
if (!candidate) {
|
||||
return (<></>);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<CandidateInfo candidate={candidate} action="Chat with Backstory AI about " />
|
||||
|
@ -100,7 +100,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
}
|
||||
const sendSystemPrompt = async (prompt: string) => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||
const response = await fetch(connectionBase + `/api/1.0/tunables`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -126,7 +126,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt")[], message: string = "Update successful.") => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/reset/`, {
|
||||
const response = await fetch(connectionBase + `/api/1.0/reset/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -178,7 +178,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
}
|
||||
const fetchSystemInfo = async () => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/system-info`, {
|
||||
const response = await fetch(connectionBase + `/api/1.0/system-info`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -210,13 +210,16 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
}, [systemInfo, setSystemInfo, setSnack])
|
||||
|
||||
useEffect(() => {
|
||||
if (!systemPrompt) {
|
||||
return;
|
||||
}
|
||||
setEditSystemPrompt(systemPrompt.trim());
|
||||
}, [systemPrompt, setEditSystemPrompt]);
|
||||
|
||||
const toggleRag = async (tool: Tool) => {
|
||||
tool.enabled = !tool.enabled
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||
const response = await fetch(connectionBase + `/api/1.0/tunables`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -238,7 +241,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
const toggleTool = async (tool: Tool) => {
|
||||
tool.enabled = !tool.enabled
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||
const response = await fetch(connectionBase + `/api/1.0/tunables`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -265,7 +268,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
const fetchTunables = async () => {
|
||||
try {
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||
const response = await fetch(connectionBase + `/api/1.0/tunables`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -5,11 +5,11 @@ import Box from '@mui/material/Box';
|
||||
|
||||
import { BackstoryPageProps } from '../components/BackstoryTab';
|
||||
import { CandidateInfo } from 'components/CandidateInfo';
|
||||
import { connectionBase } from '../utils/Global';
|
||||
import { Candidate } from "../types/types";
|
||||
import { ApiClient } from 'types/api-client';
|
||||
import { useUser } from 'hooks/useUser';
|
||||
|
||||
const CandidateListingPage = (props: BackstoryPageProps) => {
|
||||
const apiClient = new ApiClient();
|
||||
const { apiClient, setCandidate } = useUser();
|
||||
const navigate = useNavigate();
|
||||
const { setSnack } = props;
|
||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||
@ -44,27 +44,24 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
|
||||
|
||||
return (
|
||||
<Box sx={{display: "flex", flexDirection: "column"}}>
|
||||
<Box sx={{ p: 1, textAlign: "center" }}>
|
||||
Not seeing a candidate you like?
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{m: 1}}
|
||||
onClick={() => { navigate('/generate-candidate')}}>
|
||||
Generate your own perfect AI candidate!
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap"}}>
|
||||
<Box sx={{ p: 1, textAlign: "center" }}>
|
||||
Not seeing a candidate you like?
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ m: 1 }}
|
||||
onClick={() => { navigate('/generate-candidate') }}>
|
||||
Generate your own perfect AI candidate!
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{candidates?.map((u, i) =>
|
||||
<Box key={`${u.username}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) : void => {
|
||||
navigate(`/u/${u.username}`)
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
onClick={() => { setCandidate(u); navigate("/chat"); }}
|
||||
sx={{ cursor: "pointer" }}>
|
||||
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} candidate={u} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -10,9 +10,7 @@ import SendIcon from '@mui/icons-material/Send';
|
||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
|
||||
|
||||
import { CandidateInfo } from '../components/CandidateInfo';
|
||||
import { Query } from '../types/types'
|
||||
import { Quote } from 'components/Quote';
|
||||
import { Candidate } from '../types/types';
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
@ -21,6 +19,8 @@ import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||
import { Scrollable } from '../components/Scrollable';
|
||||
import { Pulse } from 'components/Pulse';
|
||||
import { StreamingResponse } from 'types/api-client';
|
||||
import { ChatContext, ChatSession, AIParameters, Query } from 'types/types';
|
||||
import { useUser } from 'hooks/useUser';
|
||||
|
||||
const emptyUser: Candidate = {
|
||||
description: "[blank]",
|
||||
@ -46,6 +46,7 @@ const emptyUser: Candidate = {
|
||||
};
|
||||
|
||||
const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||
const { apiClient } = useUser();
|
||||
const { setSnack, submitQuery } = props;
|
||||
const [streaming, setStreaming] = useState<string>('');
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
@ -57,16 +58,45 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||
const [timestamp, setTimestamp] = useState<number>(0);
|
||||
const [state, setState] = useState<number>(0); // Replaced stateRef
|
||||
const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false);
|
||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||
|
||||
// Only keep refs that are truly necessary
|
||||
const controllerRef = useRef<StreamingResponse>(null);
|
||||
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||
|
||||
const generatePersona = useCallback((query: Query) => {
|
||||
if (controllerRef.current) {
|
||||
/* Create the chat session */
|
||||
useEffect(() => {
|
||||
if (chatSession) {
|
||||
return;
|
||||
}
|
||||
setPrompt(query.prompt);
|
||||
|
||||
const createChatSession = async () => {
|
||||
try {
|
||||
const aiParameters: AIParameters = { model: 'qwen2.5' };
|
||||
|
||||
const chatContext: ChatContext = {
|
||||
type: "generate_persona",
|
||||
aiParameters
|
||||
};
|
||||
const response: ChatSession = await apiClient.createChatSession(chatContext);
|
||||
setChatSession(response);
|
||||
setSnack(`Chat session created for generate_persona: ${response.id}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setSnack("Unable to create chat session.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
createChatSession();
|
||||
}, [chatSession, setChatSession]);
|
||||
|
||||
const generatePersona = useCallback((query: Query) => {
|
||||
if (!chatSession || !chatSession.id) {
|
||||
return;
|
||||
}
|
||||
const sessionId: string = chatSession.id;
|
||||
|
||||
setPrompt(query.prompt || '');
|
||||
setState(0);
|
||||
setStatus("Generating persona...");
|
||||
setUser(emptyUser);
|
||||
@ -76,6 +106,24 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||
setCanGenImage(false);
|
||||
setShouldGenerateProfile(false); // Reset the flag
|
||||
|
||||
const streamResponse = apiClient.sendMessageStream(sessionId, query, {
|
||||
onPartialMessage: (content, messageId) => {
|
||||
console.log('Partial content:', content);
|
||||
// Update UI with partial content
|
||||
},
|
||||
onStatusChange: (status) => {
|
||||
console.log('Status changed:', status);
|
||||
// Update UI status indicator
|
||||
},
|
||||
onComplete: (finalMessage) => {
|
||||
console.log('Final message:', finalMessage.content);
|
||||
// Handle completed message
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Streaming error:', error);
|
||||
// Handle error
|
||||
}
|
||||
});
|
||||
// controllerRef.current = streamQueryResponse({
|
||||
// query,
|
||||
// type: "persona",
|
||||
|
@ -1,12 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useUser } from "../hooks/useUser";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import { SetSnackType } from '../components/Snack';
|
||||
import { LoadingComponent } from "../components/LoadingComponent";
|
||||
import { User, Guest, Candidate } from 'types/types';
|
||||
import { ApiClient } from "types/api-client";
|
||||
import { useUser } from "hooks/useUser";
|
||||
|
||||
interface CandidateRouteProps {
|
||||
guest?: Guest | null;
|
||||
@ -15,7 +14,7 @@ interface CandidateRouteProps {
|
||||
};
|
||||
|
||||
const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => {
|
||||
const apiClient = new ApiClient();
|
||||
const { apiClient } = useUser();
|
||||
const { setSnack } = props;
|
||||
const { username } = useParams<{ username: string }>();
|
||||
const [candidate, setCandidate] = useState<Candidate|null>(null);
|
||||
@ -32,11 +31,12 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
|
||||
navigate('/chat');
|
||||
} catch {
|
||||
setSnack(`Unable to obtain information for ${username}.`, "error");
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
|
||||
getCandidate(username);
|
||||
}, [candidate, username, setCandidate]);
|
||||
}, [candidate, username, setCandidate, navigate, setSnack]);
|
||||
|
||||
if (candidate === null) {
|
||||
return (<Box>
|
||||
|
@ -9,14 +9,14 @@
|
||||
import * as Types from './types';
|
||||
import {
|
||||
formatApiRequest,
|
||||
parseApiResponse,
|
||||
parsePaginatedResponse,
|
||||
// parseApiResponse,
|
||||
// parsePaginatedResponse,
|
||||
handleApiResponse,
|
||||
handlePaginatedApiResponse,
|
||||
createPaginatedRequest,
|
||||
toUrlParams,
|
||||
extractApiData,
|
||||
ApiResponse,
|
||||
// extractApiData,
|
||||
// ApiResponse,
|
||||
PaginatedResponse,
|
||||
PaginatedRequest
|
||||
} from './conversion';
|
||||
@ -76,16 +76,25 @@ class ApiClient {
|
||||
// Authentication Methods
|
||||
// ============================
|
||||
|
||||
async login(email: string, password: string): Promise<Types.AuthResponse> {
|
||||
async login(login: string, password: string): Promise<Types.AuthResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: this.defaultHeaders,
|
||||
body: JSON.stringify(formatApiRequest({ email, password }))
|
||||
body: JSON.stringify(formatApiRequest({ login, password }))
|
||||
});
|
||||
|
||||
return handleApiResponse<Types.AuthResponse>(response);
|
||||
}
|
||||
|
||||
async logout(): Promise<Types.ApiResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: this.defaultHeaders,
|
||||
});
|
||||
|
||||
return handleApiResponse<Types.ApiResponse>(response);
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<Types.AuthResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
@ -110,8 +119,8 @@ class ApiClient {
|
||||
return handleApiResponse<Types.Candidate>(response);
|
||||
}
|
||||
|
||||
async getCandidate(id: string): Promise<Types.Candidate> {
|
||||
const response = await fetch(`${this.baseUrl}/candidates/${id}`, {
|
||||
async getCandidate(username: string): Promise<Types.Candidate> {
|
||||
const response = await fetch(`${this.baseUrl}/candidates/${username}`, {
|
||||
headers: this.defaultHeaders
|
||||
});
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
// Generated TypeScript types from Pydantic models
|
||||
// Source: src/backend/models.py
|
||||
// Generated on: 2025-05-28T21:47:08.590102
|
||||
// Generated on: 2025-05-29T02:05:50.622601
|
||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||
|
||||
// ============================
|
||||
// Enums
|
||||
// ============================
|
||||
|
||||
export type AIModelType = "gpt-4" | "gpt-3.5-turbo" | "claude-3" | "claude-3-opus" | "custom";
|
||||
export type AIModelType = "qwen2.5" | "flux-schnell";
|
||||
|
||||
export type ActivityType = "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat";
|
||||
|
||||
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
||||
|
||||
export type ChatContextType = "job_search" | "candidate_screening" | "interview_prep" | "resume_review" | "general";
|
||||
export type ChatContextType = "job_search" | "candidate_screening" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile";
|
||||
|
||||
export type ChatSenderType = "user" | "ai" | "system";
|
||||
|
||||
@ -66,138 +66,138 @@ export type VectorStoreType = "pinecone" | "qdrant" | "faiss" | "milvus" | "weav
|
||||
export interface AIParameters {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
model: "gpt-4" | "gpt-3.5-turbo" | "claude-3" | "claude-3-opus" | "custom";
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
topP: number;
|
||||
frequencyPenalty: number;
|
||||
presencePenalty: number;
|
||||
model?: "qwen2.5" | "flux-schnell";
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
topP?: number;
|
||||
frequencyPenalty?: number;
|
||||
presencePenalty?: number;
|
||||
systemPrompt?: string;
|
||||
isDefault: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isDefault?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
customModelConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AccessibilitySettings {
|
||||
fontSize: "small" | "medium" | "large";
|
||||
highContrast: boolean;
|
||||
reduceMotion: boolean;
|
||||
screenReader: boolean;
|
||||
fontSize?: "small" | "medium" | "large";
|
||||
highContrast?: boolean;
|
||||
reduceMotion?: boolean;
|
||||
screenReader?: boolean;
|
||||
colorBlindMode?: "protanopia" | "deuteranopia" | "tritanopia" | "none";
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
id?: string;
|
||||
entityType: "job" | "candidate" | "chat" | "system" | "employer";
|
||||
entityId: string;
|
||||
metricType: string;
|
||||
value: number;
|
||||
timestamp: Date;
|
||||
entityType?: "job" | "candidate" | "chat" | "system" | "employer";
|
||||
entityId?: string;
|
||||
metricType?: string;
|
||||
value?: number;
|
||||
timestamp?: Date;
|
||||
dimensions?: Record<string, any>;
|
||||
segment?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data?: any;
|
||||
error?: ErrorDetail;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ApplicationDecision {
|
||||
status: "accepted" | "rejected";
|
||||
status?: "accepted" | "rejected";
|
||||
reason?: string;
|
||||
date: Date;
|
||||
by: string;
|
||||
date?: Date;
|
||||
by?: string;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
id?: string;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
fileSize: number;
|
||||
fileUrl: string;
|
||||
uploadedAt: Date;
|
||||
isProcessed: boolean;
|
||||
fileName?: string;
|
||||
fileType?: string;
|
||||
fileSize?: number;
|
||||
fileUrl?: string;
|
||||
uploadedAt?: Date;
|
||||
isProcessed?: boolean;
|
||||
processingResult?: any;
|
||||
thumbnailUrl?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: any;
|
||||
expiresAt: number;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
user?: any;
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
export interface Authentication {
|
||||
userId: string;
|
||||
passwordHash: string;
|
||||
salt: string;
|
||||
refreshTokens: Array<RefreshToken>;
|
||||
userId?: string;
|
||||
passwordHash?: string;
|
||||
salt?: string;
|
||||
refreshTokens?: Array<RefreshToken>;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiry?: Date;
|
||||
lastPasswordChange: Date;
|
||||
mfaEnabled: boolean;
|
||||
lastPasswordChange?: Date;
|
||||
mfaEnabled?: boolean;
|
||||
mfaMethod?: "app" | "sms" | "email";
|
||||
mfaSecret?: string;
|
||||
loginAttempts: number;
|
||||
loginAttempts?: number;
|
||||
lockedUntil?: Date;
|
||||
}
|
||||
|
||||
export interface BaseUser {
|
||||
id?: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
lastLogin?: Date;
|
||||
profileImage?: string;
|
||||
status: "active" | "inactive" | "pending" | "banned";
|
||||
status?: "active" | "inactive" | "pending" | "banned";
|
||||
}
|
||||
|
||||
export interface BaseUserWithType {
|
||||
id?: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
lastLogin?: Date;
|
||||
profileImage?: string;
|
||||
status: "active" | "inactive" | "pending" | "banned";
|
||||
userType: "candidate" | "employer" | "guest";
|
||||
status?: "active" | "inactive" | "pending" | "banned";
|
||||
userType?: "candidate" | "employer" | "guest";
|
||||
}
|
||||
|
||||
export interface Candidate {
|
||||
id?: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
lastLogin?: Date;
|
||||
profileImage?: string;
|
||||
status: "active" | "inactive" | "pending" | "banned";
|
||||
status?: "active" | "inactive" | "pending" | "banned";
|
||||
userType?: "candidate";
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string;
|
||||
username?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
fullName?: string;
|
||||
description?: string;
|
||||
resume?: string;
|
||||
skills: Array<Skill>;
|
||||
experience: Array<WorkExperience>;
|
||||
skills?: Array<Skill>;
|
||||
experience?: Array<WorkExperience>;
|
||||
questions?: Array<CandidateQuestion>;
|
||||
education: Array<Education>;
|
||||
preferredJobTypes: Array<"full-time" | "part-time" | "contract" | "internship" | "freelance">;
|
||||
education?: Array<Education>;
|
||||
preferredJobTypes?: Array<"full-time" | "part-time" | "contract" | "internship" | "freelance">;
|
||||
desiredSalary?: DesiredSalary;
|
||||
location: Location;
|
||||
location?: Location;
|
||||
availabilityDate?: Date;
|
||||
summary?: string;
|
||||
languages: Array<Language>;
|
||||
certifications: Array<Certification>;
|
||||
languages?: Array<Language>;
|
||||
certifications?: Array<Certification>;
|
||||
jobApplications?: Array<JobApplication>;
|
||||
hasProfile?: boolean;
|
||||
age?: number;
|
||||
@ -206,24 +206,24 @@ export interface Candidate {
|
||||
}
|
||||
|
||||
export interface CandidateContact {
|
||||
email: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface CandidateListResponse {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data?: Array<Candidate>;
|
||||
error?: ErrorDetail;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface CandidateQuestion {
|
||||
question: string;
|
||||
question?: string;
|
||||
tunables?: Tunables;
|
||||
}
|
||||
|
||||
export interface CandidateResponse {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data?: Candidate;
|
||||
error?: ErrorDetail;
|
||||
meta?: Record<string, any>;
|
||||
@ -231,30 +231,30 @@ export interface CandidateResponse {
|
||||
|
||||
export interface Certification {
|
||||
id?: string;
|
||||
name: string;
|
||||
issuingOrganization: string;
|
||||
issueDate: Date;
|
||||
name?: string;
|
||||
issuingOrganization?: string;
|
||||
issueDate?: Date;
|
||||
expirationDate?: Date;
|
||||
credentialId?: string;
|
||||
credentialUrl?: string;
|
||||
}
|
||||
|
||||
export interface ChatContext {
|
||||
type: "job_search" | "candidate_screening" | "interview_prep" | "resume_review" | "general";
|
||||
type?: "job_search" | "candidate_screening" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile";
|
||||
relatedEntityId?: string;
|
||||
relatedEntityType?: "job" | "candidate" | "employer";
|
||||
aiParameters: AIParameters;
|
||||
aiParameters?: AIParameters;
|
||||
additionalContext?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id?: string;
|
||||
sessionId: string;
|
||||
status: "partial" | "done" | "streaming" | "thinking" | "error";
|
||||
sender: "user" | "ai" | "system";
|
||||
sessionId?: string;
|
||||
status?: "partial" | "done" | "streaming" | "thinking" | "error";
|
||||
sender?: "user" | "ai" | "system";
|
||||
senderId?: string;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
content?: string;
|
||||
timestamp?: Date;
|
||||
attachments?: Array<Attachment>;
|
||||
reactions?: Array<MessageReaction>;
|
||||
isEdited?: boolean;
|
||||
@ -266,54 +266,54 @@ export interface ChatSession {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
guestId?: string;
|
||||
createdAt: Date;
|
||||
lastActivity: Date;
|
||||
createdAt?: Date;
|
||||
lastActivity?: Date;
|
||||
title?: string;
|
||||
context: ChatContext;
|
||||
context?: ChatContext;
|
||||
messages?: Array<ChatMessage>;
|
||||
isArchived?: boolean;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
export interface CustomQuestion {
|
||||
question: string;
|
||||
answer: string;
|
||||
question?: string;
|
||||
answer?: string;
|
||||
}
|
||||
|
||||
export interface DataSourceConfiguration {
|
||||
id?: string;
|
||||
ragConfigId: string;
|
||||
name: string;
|
||||
sourceType: "document" | "website" | "api" | "database" | "internal";
|
||||
connectionDetails: Record<string, any>;
|
||||
processingPipeline: Array<ProcessingStep>;
|
||||
ragConfigId?: string;
|
||||
name?: string;
|
||||
sourceType?: "document" | "website" | "api" | "database" | "internal";
|
||||
connectionDetails?: Record<string, any>;
|
||||
processingPipeline?: Array<ProcessingStep>;
|
||||
refreshSchedule?: string;
|
||||
lastRefreshed?: Date;
|
||||
status: "active" | "pending" | "error" | "processing";
|
||||
status?: "active" | "pending" | "error" | "processing";
|
||||
errorDetails?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface DesiredSalary {
|
||||
amount: number;
|
||||
currency: string;
|
||||
period: "hour" | "day" | "month" | "year";
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
period?: "hour" | "day" | "month" | "year";
|
||||
}
|
||||
|
||||
export interface EditHistory {
|
||||
content: string;
|
||||
editedAt: Date;
|
||||
editedBy: string;
|
||||
content?: string;
|
||||
editedAt?: Date;
|
||||
editedBy?: string;
|
||||
}
|
||||
|
||||
export interface Education {
|
||||
id?: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
fieldOfStudy: string;
|
||||
startDate: Date;
|
||||
institution?: string;
|
||||
degree?: string;
|
||||
fieldOfStudy?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isCurrent: boolean;
|
||||
isCurrent?: boolean;
|
||||
gpa?: number;
|
||||
achievements?: Array<string>;
|
||||
location?: Location;
|
||||
@ -321,45 +321,45 @@ export interface Education {
|
||||
|
||||
export interface Employer {
|
||||
id?: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
lastLogin?: Date;
|
||||
profileImage?: string;
|
||||
status: "active" | "inactive" | "pending" | "banned";
|
||||
status?: "active" | "inactive" | "pending" | "banned";
|
||||
userType?: "employer";
|
||||
companyName: string;
|
||||
industry: string;
|
||||
companyName?: string;
|
||||
industry?: string;
|
||||
description?: string;
|
||||
companySize: string;
|
||||
companyDescription: string;
|
||||
companySize?: string;
|
||||
companyDescription?: string;
|
||||
websiteUrl?: string;
|
||||
jobs?: Array<Job>;
|
||||
location: Location;
|
||||
location?: Location;
|
||||
companyLogo?: string;
|
||||
socialLinks?: Array<SocialLink>;
|
||||
poc?: PointOfContact;
|
||||
}
|
||||
|
||||
export interface EmployerResponse {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data?: Employer;
|
||||
error?: ErrorDetail;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ErrorDetail {
|
||||
code: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
export interface Guest {
|
||||
id?: string;
|
||||
sessionId: string;
|
||||
createdAt: Date;
|
||||
lastActivity: Date;
|
||||
sessionId?: string;
|
||||
createdAt?: Date;
|
||||
lastActivity?: Date;
|
||||
convertedToUserId?: string;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
@ -367,49 +367,49 @@ export interface Guest {
|
||||
|
||||
export interface InterviewFeedback {
|
||||
id?: string;
|
||||
interviewId: string;
|
||||
reviewerId: string;
|
||||
technicalScore: number;
|
||||
culturalScore: number;
|
||||
overallScore: number;
|
||||
strengths: Array<string>;
|
||||
weaknesses: Array<string>;
|
||||
recommendation: "strong_hire" | "hire" | "no_hire" | "strong_no_hire";
|
||||
comments: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isVisible: boolean;
|
||||
interviewId?: string;
|
||||
reviewerId?: string;
|
||||
technicalScore?: number;
|
||||
culturalScore?: number;
|
||||
overallScore?: number;
|
||||
strengths?: Array<string>;
|
||||
weaknesses?: Array<string>;
|
||||
recommendation?: "strong_hire" | "hire" | "no_hire" | "strong_no_hire";
|
||||
comments?: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
isVisible?: boolean;
|
||||
skillAssessments?: Array<SkillAssessment>;
|
||||
}
|
||||
|
||||
export interface InterviewSchedule {
|
||||
id?: string;
|
||||
applicationId: string;
|
||||
scheduledDate: Date;
|
||||
endDate: Date;
|
||||
interviewType: "phone" | "video" | "onsite" | "technical" | "behavioral";
|
||||
interviewers: Array<string>;
|
||||
applicationId?: string;
|
||||
scheduledDate?: Date;
|
||||
endDate?: Date;
|
||||
interviewType?: "phone" | "video" | "onsite" | "technical" | "behavioral";
|
||||
interviewers?: Array<string>;
|
||||
location?: string | Location;
|
||||
notes?: string;
|
||||
feedback?: InterviewFeedback;
|
||||
status: "scheduled" | "completed" | "cancelled" | "rescheduled";
|
||||
status?: "scheduled" | "completed" | "cancelled" | "rescheduled";
|
||||
meetingLink?: string;
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
responsibilities: Array<string>;
|
||||
requirements: Array<string>;
|
||||
title?: string;
|
||||
description?: string;
|
||||
responsibilities?: Array<string>;
|
||||
requirements?: Array<string>;
|
||||
preferredSkills?: Array<string>;
|
||||
employerId: string;
|
||||
location: Location;
|
||||
employerId?: string;
|
||||
location?: Location;
|
||||
salaryRange?: SalaryRange;
|
||||
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
|
||||
datePosted: Date;
|
||||
employmentType?: "full-time" | "part-time" | "contract" | "internship" | "freelance";
|
||||
datePosted?: Date;
|
||||
applicationDeadline?: Date;
|
||||
isActive: boolean;
|
||||
isActive?: boolean;
|
||||
applicants?: Array<JobApplication>;
|
||||
department?: string;
|
||||
reportsTo?: string;
|
||||
@ -422,12 +422,12 @@ export interface Job {
|
||||
|
||||
export interface JobApplication {
|
||||
id?: string;
|
||||
jobId: string;
|
||||
candidateId: string;
|
||||
status: "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
||||
appliedDate: Date;
|
||||
updatedDate: Date;
|
||||
resumeVersion: string;
|
||||
jobId?: string;
|
||||
candidateId?: string;
|
||||
status?: "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
||||
appliedDate?: Date;
|
||||
updatedDate?: Date;
|
||||
resumeVersion?: string;
|
||||
coverLetter?: string;
|
||||
notes?: string;
|
||||
interviewSchedules?: Array<InterviewSchedule>;
|
||||
@ -437,28 +437,28 @@ export interface JobApplication {
|
||||
}
|
||||
|
||||
export interface JobListResponse {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data?: Array<Job>;
|
||||
error?: ErrorDetail;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface JobResponse {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data?: Job;
|
||||
error?: ErrorDetail;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
language: string;
|
||||
proficiency: "basic" | "conversational" | "fluent" | "native";
|
||||
language?: string;
|
||||
proficiency?: "basic" | "conversational" | "fluent" | "native";
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
city: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country: string;
|
||||
country?: string;
|
||||
postalCode?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
@ -468,15 +468,15 @@ export interface Location {
|
||||
}
|
||||
|
||||
export interface MessageReaction {
|
||||
userId: string;
|
||||
reaction: string;
|
||||
timestamp: Date;
|
||||
userId?: string;
|
||||
reaction?: string;
|
||||
timestamp?: Date;
|
||||
}
|
||||
|
||||
export interface NotificationPreference {
|
||||
type: "email" | "push" | "in_app";
|
||||
events: Array<string>;
|
||||
isEnabled: boolean;
|
||||
type?: "email" | "push" | "in_app";
|
||||
events?: Array<string>;
|
||||
isEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedRequest {
|
||||
@ -488,80 +488,80 @@ export interface PaginatedRequest {
|
||||
}
|
||||
|
||||
export interface PaginatedResponse {
|
||||
data: Array<any>;
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
hasMore: boolean;
|
||||
data?: Array<any>;
|
||||
total?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
totalPages?: number;
|
||||
hasMore?: boolean;
|
||||
}
|
||||
|
||||
export interface PointOfContact {
|
||||
name: string;
|
||||
position: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
position?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface ProcessingStep {
|
||||
id?: string;
|
||||
type: "extract" | "transform" | "chunk" | "embed" | "filter" | "summarize";
|
||||
parameters: Record<string, any>;
|
||||
order: number;
|
||||
type?: "extract" | "transform" | "chunk" | "embed" | "filter" | "summarize";
|
||||
parameters?: Record<string, any>;
|
||||
order?: number;
|
||||
dependsOn?: Array<string>;
|
||||
}
|
||||
|
||||
export interface Query {
|
||||
prompt: string;
|
||||
prompt?: string;
|
||||
tunables?: Tunables;
|
||||
agentOptions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface RAGConfiguration {
|
||||
id?: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
userId?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
dataSourceConfigurations: Array<DataSourceConfiguration>;
|
||||
embeddingModel: string;
|
||||
vectorStoreType: "pinecone" | "qdrant" | "faiss" | "milvus" | "weaviate";
|
||||
retrievalParameters: RetrievalParameters;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isDefault: boolean;
|
||||
version: number;
|
||||
isActive: boolean;
|
||||
dataSourceConfigurations?: Array<DataSourceConfiguration>;
|
||||
embeddingModel?: string;
|
||||
vectorStoreType?: "pinecone" | "qdrant" | "faiss" | "milvus" | "weaviate";
|
||||
retrievalParameters?: RetrievalParameters;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
isDefault?: boolean;
|
||||
version?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface RefreshToken {
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
device: string;
|
||||
ipAddress: string;
|
||||
isRevoked: boolean;
|
||||
token?: string;
|
||||
expiresAt?: Date;
|
||||
device?: string;
|
||||
ipAddress?: string;
|
||||
isRevoked?: boolean;
|
||||
revokedReason?: string;
|
||||
}
|
||||
|
||||
export interface RetrievalParameters {
|
||||
searchType: "similarity" | "mmr" | "hybrid" | "keyword";
|
||||
topK: number;
|
||||
searchType?: "similarity" | "mmr" | "hybrid" | "keyword";
|
||||
topK?: number;
|
||||
similarityThreshold?: number;
|
||||
rerankerModel?: string;
|
||||
useKeywordBoost: boolean;
|
||||
useKeywordBoost?: boolean;
|
||||
filterOptions?: Record<string, any>;
|
||||
contextWindow: number;
|
||||
contextWindow?: number;
|
||||
}
|
||||
|
||||
export interface SalaryRange {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
period: "hour" | "day" | "month" | "year";
|
||||
isVisible: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
currency?: string;
|
||||
period?: "hour" | "day" | "month" | "year";
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchQuery {
|
||||
query: string;
|
||||
query?: string;
|
||||
filters?: Record<string, any>;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
@ -571,21 +571,21 @@ export interface SearchQuery {
|
||||
|
||||
export interface Skill {
|
||||
id?: string;
|
||||
name: string;
|
||||
category: string;
|
||||
level: "beginner" | "intermediate" | "advanced" | "expert";
|
||||
name?: string;
|
||||
category?: string;
|
||||
level?: "beginner" | "intermediate" | "advanced" | "expert";
|
||||
yearsOfExperience?: number;
|
||||
}
|
||||
|
||||
export interface SkillAssessment {
|
||||
skillName: string;
|
||||
score: number;
|
||||
skillName?: string;
|
||||
score?: number;
|
||||
comments?: string;
|
||||
}
|
||||
|
||||
export interface SocialLink {
|
||||
platform: "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
|
||||
url: string;
|
||||
platform?: "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface Tunables {
|
||||
@ -598,35 +598,35 @@ export interface UserActivity {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
guestId?: string;
|
||||
activityType: "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat";
|
||||
timestamp: Date;
|
||||
metadata: Record<string, any>;
|
||||
activityType?: "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat";
|
||||
timestamp?: Date;
|
||||
metadata?: Record<string, any>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface UserPreference {
|
||||
userId: string;
|
||||
theme: "light" | "dark" | "system";
|
||||
notifications: Array<NotificationPreference>;
|
||||
accessibility: AccessibilitySettings;
|
||||
userId?: string;
|
||||
theme?: "light" | "dark" | "system";
|
||||
notifications?: Array<NotificationPreference>;
|
||||
accessibility?: AccessibilitySettings;
|
||||
dashboardLayout?: Record<string, any>;
|
||||
language: string;
|
||||
timezone: string;
|
||||
emailFrequency: "immediate" | "daily" | "weekly" | "never";
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
emailFrequency?: "immediate" | "daily" | "weekly" | "never";
|
||||
}
|
||||
|
||||
export interface WorkExperience {
|
||||
id?: string;
|
||||
companyName: string;
|
||||
position: string;
|
||||
startDate: Date;
|
||||
companyName?: string;
|
||||
position?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isCurrent: boolean;
|
||||
description: string;
|
||||
skills: Array<string>;
|
||||
location: Location;
|
||||
isCurrent?: boolean;
|
||||
description?: string;
|
||||
skills?: Array<string>;
|
||||
location?: Location;
|
||||
achievements?: Array<string>;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ from models import (
|
||||
Candidate, Employer, Location, Skill, AIParameters, AIModelType
|
||||
)
|
||||
|
||||
|
||||
def test_model_creation():
|
||||
"""Test that we can create models successfully"""
|
||||
print("🧪 Testing model creation...")
|
||||
@ -122,7 +123,7 @@ def test_validation_constraints():
|
||||
# Test AI Parameters with constraints
|
||||
valid_params = AIParameters(
|
||||
name="Test Config",
|
||||
model=AIModelType.GPT_4,
|
||||
model=AIModelType.QWEN2_5,
|
||||
temperature=0.7, # Valid: 0-1
|
||||
maxTokens=2000, # Valid: > 0
|
||||
topP=0.95, # Valid: 0-1
|
||||
@ -138,7 +139,7 @@ def test_validation_constraints():
|
||||
try:
|
||||
invalid_params = AIParameters(
|
||||
name="Invalid Config",
|
||||
model=AIModelType.GPT_4,
|
||||
model=AIModelType.QWEN2_5,
|
||||
temperature=1.5, # Invalid: > 1
|
||||
maxTokens=2000,
|
||||
topP=0.95,
|
||||
|
@ -13,6 +13,7 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
import stat
|
||||
|
||||
def run_command(command: str, description: str, cwd: str | None = None) -> bool:
|
||||
"""Run a command and return success status"""
|
||||
try:
|
||||
@ -68,9 +69,34 @@ except ImportError as e:
|
||||
print("Make sure pydantic is installed: pip install pydantic")
|
||||
sys.exit(1)
|
||||
|
||||
def python_type_to_typescript(python_type: Any) -> str:
|
||||
def unwrap_annotated_type(python_type: Any) -> Any:
|
||||
"""Unwrap Annotated types to get the actual type"""
|
||||
# Handle typing_extensions.Annotated and typing.Annotated
|
||||
origin = get_origin(python_type)
|
||||
args = get_args(python_type)
|
||||
|
||||
# Check for Annotated types - more robust detection
|
||||
if origin is not None and args:
|
||||
origin_str = str(origin)
|
||||
if 'Annotated' in origin_str or (hasattr(origin, '__name__') and origin.__name__ == 'Annotated'):
|
||||
# Return the first argument (the actual type)
|
||||
return unwrap_annotated_type(args[0]) # Recursive unwrap in case of nested annotations
|
||||
|
||||
return python_type
|
||||
|
||||
def python_type_to_typescript(python_type: Any, debug: bool = False) -> str:
|
||||
"""Convert a Python type to TypeScript type string"""
|
||||
|
||||
if debug:
|
||||
print(f" 🔍 Converting type: {python_type} (type: {type(python_type)})")
|
||||
|
||||
# First unwrap any Annotated types
|
||||
original_type = python_type
|
||||
python_type = unwrap_annotated_type(python_type)
|
||||
|
||||
if debug and original_type != python_type:
|
||||
print(f" 🔄 Unwrapped: {original_type} -> {python_type}")
|
||||
|
||||
# Handle None/null
|
||||
if python_type is type(None):
|
||||
return "null"
|
||||
@ -79,6 +105,8 @@ def python_type_to_typescript(python_type: Any) -> str:
|
||||
if python_type == str:
|
||||
return "string"
|
||||
elif python_type == int or python_type == float:
|
||||
if debug:
|
||||
print(f" ✅ Converting {python_type} to number")
|
||||
return "number"
|
||||
elif python_type == bool:
|
||||
return "boolean"
|
||||
@ -91,30 +119,33 @@ def python_type_to_typescript(python_type: Any) -> str:
|
||||
origin = get_origin(python_type)
|
||||
args = get_args(python_type)
|
||||
|
||||
if debug and origin:
|
||||
print(f" 🔍 Generic type - origin: {origin}, args: {args}")
|
||||
|
||||
if origin is Union:
|
||||
# Handle Optional (Union[T, None])
|
||||
if len(args) == 2 and type(None) in args:
|
||||
non_none_type = next(arg for arg in args if arg is not type(None))
|
||||
return python_type_to_typescript(non_none_type)
|
||||
return python_type_to_typescript(non_none_type, debug)
|
||||
|
||||
# Handle other unions
|
||||
union_types = [python_type_to_typescript(arg) for arg in args if arg is not type(None)]
|
||||
union_types = [python_type_to_typescript(arg, debug) for arg in args if arg is not type(None)]
|
||||
return " | ".join(union_types)
|
||||
|
||||
elif origin is list or origin is List:
|
||||
if args:
|
||||
item_type = python_type_to_typescript(args[0])
|
||||
item_type = python_type_to_typescript(args[0], debug)
|
||||
return f"Array<{item_type}>"
|
||||
return "Array<any>"
|
||||
|
||||
elif origin is dict or origin is Dict:
|
||||
if len(args) == 2:
|
||||
key_type = python_type_to_typescript(args[0])
|
||||
value_type = python_type_to_typescript(args[1])
|
||||
key_type = python_type_to_typescript(args[0], debug)
|
||||
value_type = python_type_to_typescript(args[1], debug)
|
||||
return f"Record<{key_type}, {value_type}>"
|
||||
return "Record<string, any>"
|
||||
|
||||
# Handle Literal types - UPDATED SECTION
|
||||
# Handle Literal types
|
||||
if hasattr(python_type, '__origin__') and str(python_type.__origin__).endswith('Literal'):
|
||||
if args:
|
||||
literal_values = []
|
||||
@ -155,6 +186,8 @@ def python_type_to_typescript(python_type: Any) -> str:
|
||||
return "string"
|
||||
|
||||
# Default fallback
|
||||
if debug:
|
||||
print(f" ⚠️ Falling back to 'any' for type: {python_type}")
|
||||
return "any"
|
||||
|
||||
def snake_to_camel(snake_str: str) -> str:
|
||||
@ -162,32 +195,104 @@ def snake_to_camel(snake_str: str) -> str:
|
||||
components = snake_str.split('_')
|
||||
return components[0] + ''.join(x.title() for x in components[1:])
|
||||
|
||||
def process_pydantic_model(model_class) -> Dict[str, Any]:
|
||||
def is_field_optional(field_info: Any, field_type: Any) -> bool:
|
||||
"""Determine if a field should be optional in TypeScript"""
|
||||
|
||||
# First, check if the type itself is Optional (Union with None)
|
||||
origin = get_origin(field_type)
|
||||
args = get_args(field_type)
|
||||
is_union_with_none = origin is Union and type(None) in args
|
||||
|
||||
# If the type is Optional[T], it's always optional regardless of Field settings
|
||||
if is_union_with_none:
|
||||
return True
|
||||
|
||||
# For non-Optional types, check Field settings and defaults
|
||||
|
||||
# Check for default factory (makes field optional)
|
||||
if hasattr(field_info, 'default_factory') and field_info.default_factory is not None:
|
||||
return True
|
||||
|
||||
# Check the default value
|
||||
if hasattr(field_info, 'default'):
|
||||
default_val = field_info.default
|
||||
|
||||
# Field(...) or Ellipsis means REQUIRED (not optional)
|
||||
if default_val is ...:
|
||||
return False
|
||||
|
||||
# Any other default value (including None) makes it optional
|
||||
# This covers: Field(None), Field("some_value"), = "some_value", = None, etc.
|
||||
else:
|
||||
return True
|
||||
|
||||
# If no default is set at all, check if field is explicitly marked as not required
|
||||
# This is for edge cases in Pydantic v2
|
||||
if hasattr(field_info, 'is_required'):
|
||||
try:
|
||||
return not field_info.is_required()
|
||||
except:
|
||||
pass
|
||||
elif hasattr(field_info, 'required'):
|
||||
return not field_info.required
|
||||
|
||||
# Default: if type is not Optional and no explicit default, it's required (not optional)
|
||||
return False
|
||||
|
||||
def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
|
||||
"""Process a Pydantic model and return TypeScript interface definition"""
|
||||
interface_name = model_class.__name__
|
||||
properties = []
|
||||
|
||||
if debug:
|
||||
print(f" 🔍 Processing model: {interface_name}")
|
||||
|
||||
# Get fields from the model
|
||||
if hasattr(model_class, 'model_fields'):
|
||||
# Pydantic v2
|
||||
fields = model_class.model_fields
|
||||
for field_name, field_info in fields.items():
|
||||
ts_name = snake_to_camel(field_name)
|
||||
if debug:
|
||||
print(f" 📝 Field: {field_name}")
|
||||
print(f" Field info: {field_info}")
|
||||
print(f" Default: {getattr(field_info, 'default', 'NO_DEFAULT')}")
|
||||
|
||||
# Check for alias
|
||||
# Use alias if available, otherwise convert snake_case to camelCase
|
||||
if hasattr(field_info, 'alias') and field_info.alias:
|
||||
ts_name = field_info.alias
|
||||
else:
|
||||
ts_name = snake_to_camel(field_name)
|
||||
|
||||
# Get type annotation
|
||||
field_type = getattr(field_info, 'annotation', str)
|
||||
ts_type = python_type_to_typescript(field_type)
|
||||
if debug:
|
||||
print(f" Raw type: {field_type}")
|
||||
|
||||
ts_type = python_type_to_typescript(field_type, debug)
|
||||
|
||||
# Check if optional
|
||||
is_optional = False
|
||||
if hasattr(field_info, 'is_required'):
|
||||
is_optional = not field_info.is_required()
|
||||
elif hasattr(field_info, 'default'):
|
||||
is_optional = field_info.default is not None
|
||||
is_optional = is_field_optional(field_info, field_type)
|
||||
|
||||
if debug:
|
||||
print(f" TS name: {ts_name}")
|
||||
print(f" TS type: {ts_type}")
|
||||
print(f" Optional: {is_optional}")
|
||||
|
||||
# Debug the optional logic
|
||||
origin = get_origin(field_type)
|
||||
args = get_args(field_type)
|
||||
is_union_with_none = origin is Union and type(None) in args
|
||||
has_default = hasattr(field_info, 'default')
|
||||
has_default_factory = hasattr(field_info, 'default_factory') and field_info.default_factory is not None
|
||||
|
||||
print(f" └─ Type is Optional: {is_union_with_none}")
|
||||
if has_default:
|
||||
default_val = field_info.default
|
||||
print(f" └─ Has default: {default_val} (is ...? {default_val is ...})")
|
||||
else:
|
||||
print(f" └─ No default attribute")
|
||||
print(f" └─ Has default factory: {has_default_factory}")
|
||||
print()
|
||||
|
||||
properties.append({
|
||||
'name': ts_name,
|
||||
@ -199,17 +304,45 @@ def process_pydantic_model(model_class) -> Dict[str, Any]:
|
||||
# Pydantic v1
|
||||
fields = model_class.__fields__
|
||||
for field_name, field_info in fields.items():
|
||||
ts_name = snake_to_camel(field_name)
|
||||
if debug:
|
||||
print(f" 📝 Field: {field_name} (Pydantic v1)")
|
||||
print(f" Field info: {field_info}")
|
||||
|
||||
# Use alias if available, otherwise convert snake_case to camelCase
|
||||
if hasattr(field_info, 'alias') and field_info.alias:
|
||||
ts_name = field_info.alias
|
||||
else:
|
||||
ts_name = snake_to_camel(field_name)
|
||||
|
||||
field_type = getattr(field_info, 'annotation', getattr(field_info, 'type_', str))
|
||||
ts_type = python_type_to_typescript(field_type)
|
||||
if debug:
|
||||
print(f" Raw type: {field_type}")
|
||||
|
||||
is_optional = not getattr(field_info, 'required', True)
|
||||
if hasattr(field_info, 'default') and field_info.default is not None:
|
||||
is_optional = True
|
||||
ts_type = python_type_to_typescript(field_type, debug)
|
||||
|
||||
# For Pydantic v1, check required and default
|
||||
is_optional = is_field_optional(field_info, field_type)
|
||||
|
||||
if debug:
|
||||
print(f" TS name: {ts_name}")
|
||||
print(f" TS type: {ts_type}")
|
||||
print(f" Optional: {is_optional}")
|
||||
|
||||
# Debug the optional logic
|
||||
origin = get_origin(field_type)
|
||||
args = get_args(field_type)
|
||||
is_union_with_none = origin is Union and type(None) in args
|
||||
has_default = hasattr(field_info, 'default')
|
||||
has_default_factory = hasattr(field_info, 'default_factory') and field_info.default_factory is not None
|
||||
|
||||
print(f" └─ Type is Optional: {is_union_with_none}")
|
||||
if has_default:
|
||||
default_val = field_info.default
|
||||
print(f" └─ Has default: {default_val} (is ...? {default_val is ...})")
|
||||
else:
|
||||
print(f" └─ No default attribute")
|
||||
print(f" └─ Has default factory: {has_default_factory}")
|
||||
print()
|
||||
|
||||
properties.append({
|
||||
'name': ts_name,
|
||||
@ -233,7 +366,7 @@ def process_enum(enum_class) -> Dict[str, Any]:
|
||||
'values': " | ".join(values)
|
||||
}
|
||||
|
||||
def generate_typescript_interfaces(source_file: str):
|
||||
def generate_typescript_interfaces(source_file: str, debug: bool = False):
|
||||
"""Generate TypeScript interfaces from models"""
|
||||
|
||||
print(f"📖 Scanning {source_file} for Pydantic models and enums...")
|
||||
@ -270,7 +403,7 @@ def generate_typescript_interfaces(source_file: str):
|
||||
issubclass(obj, BaseModel) and
|
||||
obj != BaseModel):
|
||||
|
||||
interface = process_pydantic_model(obj)
|
||||
interface = process_pydantic_model(obj, debug)
|
||||
interfaces.append(interface)
|
||||
print(f" ✅ Found Pydantic model: {name}")
|
||||
|
||||
@ -284,6 +417,9 @@ def generate_typescript_interfaces(source_file: str):
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Warning: Error processing {name}: {e}")
|
||||
if debug:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
print(f"\n📊 Found {len(interfaces)} interfaces and {len(enums)} enums")
|
||||
@ -362,7 +498,8 @@ Examples:
|
||||
python generate_types.py --source models.py --output types.ts # Specify files
|
||||
python generate_types.py --skip-test # Skip model validation
|
||||
python generate_types.py --skip-compile # Skip TS compilation
|
||||
python generate_types.py --source models.py --output types.ts --skip-test --skip-compile
|
||||
python generate_types.py --debug # Enable debug output
|
||||
python generate_types.py --source models.py --output types.ts --skip-test --skip-compile --debug
|
||||
"""
|
||||
)
|
||||
|
||||
@ -390,6 +527,12 @@ Examples:
|
||||
help='Skip TypeScript compilation check after generation'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Enable debug output to troubleshoot type conversion issues'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--version', '-v',
|
||||
action='version',
|
||||
@ -422,7 +565,11 @@ Examples:
|
||||
|
||||
# Step 3: Generate TypeScript content
|
||||
print("🔄 Generating TypeScript types...")
|
||||
ts_content = generate_typescript_interfaces(args.source)
|
||||
if args.debug:
|
||||
print("🐛 Debug mode enabled - detailed output follows:")
|
||||
print()
|
||||
|
||||
ts_content = generate_typescript_interfaces(args.source, args.debug)
|
||||
|
||||
if ts_content is None:
|
||||
print("❌ Failed to generate TypeScript content")
|
||||
|
@ -140,18 +140,43 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
async def verify_token_with_blacklist(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
"""Verify token and check if it's blacklisted"""
|
||||
try:
|
||||
# First decode the token
|
||||
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
|
||||
|
||||
# Check if token is blacklisted
|
||||
redis_client = redis_manager.get_client()
|
||||
blacklist_key = f"blacklisted_token:{credentials.credentials}"
|
||||
|
||||
is_blacklisted = await redis_client.exists(blacklist_key)
|
||||
if is_blacklisted:
|
||||
logger.warning(f"🚫 Attempt to use blacklisted token for user {user_id}")
|
||||
raise HTTPException(status_code=401, detail="Token has been revoked")
|
||||
|
||||
# Optional: Check if all user tokens are revoked (for "logout from all devices")
|
||||
# user_revoked_key = f"user_tokens_revoked:{user_id}"
|
||||
# user_tokens_revoked_at = await redis_client.get(user_revoked_key)
|
||||
# if user_tokens_revoked_at:
|
||||
# revoked_timestamp = datetime.fromisoformat(user_tokens_revoked_at.decode())
|
||||
# token_issued_at = datetime.fromtimestamp(payload.get("iat", 0), UTC)
|
||||
# if token_issued_at < revoked_timestamp:
|
||||
# raise HTTPException(status_code=401, detail="All user tokens have been revoked")
|
||||
|
||||
return user_id
|
||||
|
||||
except jwt.PyJWTError:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification error: {e}")
|
||||
raise HTTPException(status_code=401, detail="Token verification failed")
|
||||
|
||||
async def get_current_user(
|
||||
user_id: str = Depends(verify_token),
|
||||
user_id: str = Depends(verify_token_with_blacklist),
|
||||
database: RedisDatabase = Depends(lambda: db_manager.get_database())
|
||||
):
|
||||
"""Get current user from database"""
|
||||
@ -321,12 +346,146 @@ async def login(
|
||||
return create_success_response(auth_response.model_dump(by_alias=True))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Login error: {e}")
|
||||
logger.error(f"⚠️ Login error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=create_error_response("LOGIN_ERROR", str(e))
|
||||
)
|
||||
|
||||
@api_router.post("/auth/logout")
|
||||
async def logout(
|
||||
refreshToken: str = Body(..., alias="refreshToken"),
|
||||
accessToken: Optional[str] = Body(None, alias="accessToken"),
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Logout endpoint - revokes both access and refresh tokens"""
|
||||
try:
|
||||
# Verify refresh token
|
||||
try:
|
||||
refresh_payload = jwt.decode(refreshToken, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id = refresh_payload.get("sub")
|
||||
token_type = refresh_payload.get("type")
|
||||
refresh_exp = refresh_payload.get("exp")
|
||||
|
||||
if not user_id or token_type != "refresh":
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content=create_error_response("INVALID_TOKEN", "Invalid refresh token")
|
||||
)
|
||||
except jwt.PyJWTError as e:
|
||||
logger.warning(f"Invalid refresh token during logout: {e}")
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content=create_error_response("INVALID_TOKEN", "Invalid refresh token")
|
||||
)
|
||||
|
||||
# Verify that the refresh token belongs to the current user
|
||||
if user_id != current_user.id:
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content=create_error_response("FORBIDDEN", "Token does not belong to current user")
|
||||
)
|
||||
|
||||
# Get Redis client
|
||||
redis_client = redis_manager.get_client()
|
||||
|
||||
# Revoke refresh token (blacklist it until its natural expiration)
|
||||
refresh_ttl = max(0, refresh_exp - int(datetime.now(UTC).timestamp()))
|
||||
if refresh_ttl > 0:
|
||||
await redis_client.setex(
|
||||
f"blacklisted_token:{refreshToken}",
|
||||
refresh_ttl,
|
||||
json.dumps({
|
||||
"user_id": user_id,
|
||||
"token_type": "refresh",
|
||||
"revoked_at": datetime.now(UTC).isoformat(),
|
||||
"reason": "user_logout"
|
||||
})
|
||||
)
|
||||
logger.info(f"🔒 Blacklisted refresh token for user {user_id}")
|
||||
|
||||
# If access token is provided, revoke it too
|
||||
if accessToken:
|
||||
try:
|
||||
access_payload = jwt.decode(accessToken, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
access_user_id = access_payload.get("sub")
|
||||
access_exp = access_payload.get("exp")
|
||||
|
||||
# Verify access token belongs to same user
|
||||
if access_user_id == user_id:
|
||||
access_ttl = max(0, access_exp - int(datetime.now(UTC).timestamp()))
|
||||
if access_ttl > 0:
|
||||
await redis_client.setex(
|
||||
f"blacklisted_token:{accessToken}",
|
||||
access_ttl,
|
||||
json.dumps({
|
||||
"user_id": user_id,
|
||||
"token_type": "access",
|
||||
"revoked_at": datetime.now(UTC).isoformat(),
|
||||
"reason": "user_logout"
|
||||
})
|
||||
)
|
||||
logger.info(f"🔒 Blacklisted access token for user {user_id}")
|
||||
else:
|
||||
logger.warning(f"Access token user mismatch during logout: {access_user_id} != {user_id}")
|
||||
except jwt.PyJWTError as e:
|
||||
logger.warning(f"Invalid access token during logout (non-critical): {e}")
|
||||
# Don't fail logout if access token is invalid
|
||||
|
||||
# Optional: Revoke all tokens for this user (for "logout from all devices")
|
||||
# Uncomment the following lines if you want to implement this feature:
|
||||
#
|
||||
# await redis_client.setex(
|
||||
# f"user_tokens_revoked:{user_id}",
|
||||
# timedelta(days=30).total_seconds(), # Max refresh token lifetime
|
||||
# datetime.now(UTC).isoformat()
|
||||
# )
|
||||
|
||||
logger.info(f"🔑 User {user_id} logged out successfully")
|
||||
return create_success_response({
|
||||
"message": "Logged out successfully",
|
||||
"tokensRevoked": {
|
||||
"refreshToken": True,
|
||||
"accessToken": bool(accessToken)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Logout error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=create_error_response("LOGOUT_ERROR", str(e))
|
||||
)
|
||||
|
||||
@api_router.post("/auth/logout-all")
|
||||
async def logout_all_devices(
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Logout from all devices by revoking all tokens for the user"""
|
||||
try:
|
||||
redis_client = redis_manager.get_client()
|
||||
|
||||
# Set a timestamp that invalidates all tokens issued before this moment
|
||||
await redis_client.setex(
|
||||
f"user_tokens_revoked:{current_user.id}",
|
||||
int(timedelta(days=30).total_seconds()), # Max refresh token lifetime
|
||||
datetime.now(UTC).isoformat()
|
||||
)
|
||||
|
||||
logger.info(f"🔒 All tokens revoked for user {current_user.id}")
|
||||
return create_success_response({
|
||||
"message": "Logged out from all devices successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Logout all devices error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=create_error_response("LOGOUT_ALL_ERROR", str(e))
|
||||
)
|
||||
|
||||
@api_router.post("/auth/refresh")
|
||||
async def refresh_token_endpoint(
|
||||
refreshToken: str = Body(..., alias="refreshToken"),
|
||||
@ -427,21 +586,33 @@ async def create_candidate(
|
||||
content=create_error_response("CREATION_FAILED", str(e))
|
||||
)
|
||||
|
||||
@api_router.get("/candidates/{candidate_id}")
|
||||
@api_router.get("/candidates/{username}")
|
||||
async def get_candidate(
|
||||
candidate_id: str = Path(...),
|
||||
username: str = Path(...),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Get a candidate by ID"""
|
||||
"""Get a candidate by username"""
|
||||
try:
|
||||
candidate_data = await database.get_candidate(candidate_id)
|
||||
if not candidate_data:
|
||||
all_candidates_data = await database.get_all_candidates()
|
||||
candidates_list = [Candidate.model_validate(data) for data in all_candidates_data.values()]
|
||||
|
||||
# Normalize username to lowercase for case-insensitive search
|
||||
query_lower = username.lower()
|
||||
|
||||
# Filter by search query
|
||||
candidates_list = [
|
||||
c for c in candidates_list
|
||||
if (query_lower == c.email.lower() or
|
||||
query_lower == c.username.lower())
|
||||
]
|
||||
|
||||
if not len(candidates_list):
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content=create_error_response("NOT_FOUND", "Candidate not found")
|
||||
)
|
||||
|
||||
candidate = Candidate.model_validate(candidate_data)
|
||||
candidate = Candidate.model_validate(candidates_list[0])
|
||||
return create_success_response(candidate.model_dump(by_alias=True))
|
||||
|
||||
except Exception as e:
|
||||
@ -558,6 +729,7 @@ async def search_candidates(
|
||||
if (query_lower in c.first_name.lower() or
|
||||
query_lower in c.last_name.lower() or
|
||||
query_lower in c.email.lower() or
|
||||
query_lower in c.username.lower() or
|
||||
any(query_lower in skill.name.lower() for skill in c.skills))
|
||||
]
|
||||
|
||||
@ -727,6 +899,98 @@ async def search_jobs(
|
||||
content=create_error_response("SEARCH_FAILED", str(e))
|
||||
)
|
||||
|
||||
# ============================
|
||||
# Chat Endpoints
|
||||
# ============================
|
||||
@api_router.post("/chat/sessions")
|
||||
async def create_chat_session(
|
||||
session_data: Dict[str, Any] = Body(...),
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Create a new chat session"""
|
||||
try:
|
||||
# Add required fields
|
||||
session_data["id"] = str(uuid.uuid4())
|
||||
session_data["createdAt"] = datetime.now(UTC).isoformat()
|
||||
session_data["updatedAt"] = datetime.now(UTC).isoformat()
|
||||
|
||||
# Create chat session
|
||||
chat_session = ChatSession.model_validate(session_data)
|
||||
await database.set_chat_session(chat_session.id, chat_session.model_dump())
|
||||
|
||||
return create_success_response(chat_session.model_dump(by_alias=True))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Chat session creation error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=create_error_response("CREATION_FAILED", str(e))
|
||||
)
|
||||
|
||||
@api_router.get("/chat/sessions/{session_id}")
|
||||
async def get_chat_session(
|
||||
session_id: str = Path(...),
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Get a chat session by ID"""
|
||||
try:
|
||||
chat_session_data = await database.get_chat_session(session_id)
|
||||
if not chat_session_data:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content=create_error_response("NOT_FOUND", "Chat session not found")
|
||||
)
|
||||
|
||||
chat_session = ChatSession.model_validate(chat_session_data)
|
||||
return create_success_response(chat_session.model_dump(by_alias=True))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get chat session error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=create_error_response("FETCH_ERROR", str(e))
|
||||
)
|
||||
|
||||
@api_router.get("/chat/sessions")
|
||||
async def get_chat_sessions(
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
sortBy: Optional[str] = Query(None, alias="sortBy"),
|
||||
sortOrder: str = Query("desc", pattern="^(asc|desc)$", alias="sortOrder"),
|
||||
filters: Optional[str] = Query(None),
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Get paginated list of chat sessions"""
|
||||
try:
|
||||
filter_dict = None
|
||||
if filters:
|
||||
filter_dict = json.loads(filters)
|
||||
|
||||
# Get all chat sessions from Redis
|
||||
all_sessions_data = await database.get_all_chat_sessions()
|
||||
sessions_list = [ChatSession.model_validate(data) for data in all_sessions_data.values()]
|
||||
|
||||
paginated_sessions, total = filter_and_paginate(
|
||||
sessions_list, page, limit, sortBy, sortOrder, filter_dict
|
||||
)
|
||||
|
||||
paginated_response = create_paginated_response(
|
||||
[s.model_dump(by_alias=True) for s in paginated_sessions],
|
||||
page, limit, total
|
||||
)
|
||||
|
||||
return create_success_response(paginated_response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get chat sessions error: {e}")
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=create_error_response("FETCH_FAILED", str(e))
|
||||
)
|
||||
|
||||
# ============================
|
||||
# Health Check and Info Endpoints
|
||||
# ============================
|
||||
@ -790,6 +1054,11 @@ async def redis_stats(redis_client: redis.Redis = Depends(get_redis)):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=503, detail=f"Redis stats unavailable: {e}")
|
||||
|
||||
@api_router.get("/system-info")
|
||||
async def get_system_info(request: Request):
|
||||
from system_info import system_info # Import system_info function from system_info module
|
||||
return JSONResponse(system_info())
|
||||
|
||||
@api_router.get("/")
|
||||
async def api_info():
|
||||
"""API information endpoint"""
|
||||
|
@ -80,13 +80,12 @@ class ChatContextType(str, Enum):
|
||||
INTERVIEW_PREP = "interview_prep"
|
||||
RESUME_REVIEW = "resume_review"
|
||||
GENERAL = "general"
|
||||
GENERATE_PERSONA = "generate_persona"
|
||||
GENERATE_PROFILE = "generate_profile"
|
||||
|
||||
class AIModelType(str, Enum):
|
||||
GPT_4 = "gpt-4"
|
||||
GPT_35_TURBO = "gpt-3.5-turbo"
|
||||
CLAUDE_3 = "claude-3"
|
||||
CLAUDE_3_OPUS = "claude-3-opus"
|
||||
CUSTOM = "custom"
|
||||
QWEN2_5 = "qwen2.5"
|
||||
FLUX_SCHNELL = "flux-schnell"
|
||||
|
||||
class MFAMethod(str, Enum):
|
||||
APP = "app"
|
||||
@ -526,15 +525,15 @@ class AIParameters(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
model: AIModelType
|
||||
temperature: Annotated[float, Field(ge=0, le=1)]
|
||||
max_tokens: Annotated[int, Field(gt=0)] = Field(..., alias="maxTokens")
|
||||
top_p: Annotated[float, Field(ge=0, le=1)] = Field(..., alias="topP")
|
||||
frequency_penalty: Annotated[float, Field(ge=-2, le=2)] = Field(..., alias="frequencyPenalty")
|
||||
presence_penalty: Annotated[float, Field(ge=-2, le=2)] = Field(..., alias="presencePenalty")
|
||||
temperature: Optional[Annotated[float, Field(ge=0, le=1)]] = 0.7
|
||||
max_tokens: Optional[Annotated[int, Field(gt=0)]] = Field(..., alias="maxTokens")
|
||||
top_p: Optional[Annotated[float, Field(ge=0, le=1)]] = Field(..., alias="topP")
|
||||
frequency_penalty: Optional[Annotated[float, Field(ge=-2, le=2)]] = Field(..., alias="frequencyPenalty")
|
||||
presence_penalty: Optional[Annotated[float, Field(ge=-2, le=2)]] = Field(..., alias="presencePenalty")
|
||||
system_prompt: Optional[str] = Field(None, alias="systemPrompt")
|
||||
is_default: bool = Field(..., alias="isDefault")
|
||||
created_at: datetime = Field(..., alias="createdAt")
|
||||
updated_at: datetime = Field(..., alias="updatedAt")
|
||||
is_default: Optional[bool] = Field(..., alias="isDefault")
|
||||
created_at: Optional[datetime] = Field(..., alias="createdAt")
|
||||
updated_at: Optional[datetime] = Field(..., alias="updatedAt")
|
||||
custom_model_config: Optional[Dict[str, Any]] = Field(None, alias="customModelConfig")
|
||||
class Config:
|
||||
populate_by_name = True # Allow both field names and aliases
|
||||
|
@ -1,207 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Focused test script that tests the most important functionality
|
||||
without getting caught up in serialization format complexities
|
||||
"""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from models import (
|
||||
UserStatus, UserType, SkillLevel, EmploymentType,
|
||||
Candidate, Employer, Location, Skill, AIParameters, AIModelType
|
||||
)
|
||||
|
||||
def test_model_creation():
|
||||
"""Test that we can create models successfully"""
|
||||
print("🧪 Testing model creation...")
|
||||
|
||||
# Create supporting objects
|
||||
location = Location(city="Austin", country="USA")
|
||||
skill = Skill(name="Python", category="Programming", level=SkillLevel.ADVANCED)
|
||||
|
||||
# Create candidate
|
||||
candidate = Candidate(
|
||||
email="test@example.com",
|
||||
username="test_candidate",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
status=UserStatus.ACTIVE,
|
||||
firstName="John",
|
||||
lastName="Doe",
|
||||
fullName="John Doe",
|
||||
skills=[skill],
|
||||
experience=[],
|
||||
education=[],
|
||||
preferredJobTypes=[EmploymentType.FULL_TIME],
|
||||
location=location,
|
||||
languages=[],
|
||||
certifications=[]
|
||||
)
|
||||
|
||||
# Create employer
|
||||
employer = Employer(
|
||||
email="hr@company.com",
|
||||
username="test_employer",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
status=UserStatus.ACTIVE,
|
||||
companyName="Test Company",
|
||||
industry="Technology",
|
||||
companySize="50-200",
|
||||
companyDescription="A test company",
|
||||
location=location
|
||||
)
|
||||
|
||||
print(f"✅ Candidate: {candidate.first_name} {candidate.last_name}")
|
||||
print(f"✅ Employer: {employer.company_name}")
|
||||
print(f"✅ User types: {candidate.user_type}, {employer.user_type}")
|
||||
|
||||
return candidate, employer
|
||||
|
||||
def test_json_api_format():
|
||||
"""Test JSON serialization in API format (the most important use case)"""
|
||||
print("\n📡 Testing JSON API format...")
|
||||
|
||||
candidate, employer = test_model_creation()
|
||||
|
||||
# Serialize to JSON (API format)
|
||||
candidate_json = candidate.model_dump_json(by_alias=True)
|
||||
employer_json = employer.model_dump_json(by_alias=True)
|
||||
|
||||
print(f"✅ Candidate JSON: {len(candidate_json)} chars")
|
||||
print(f"✅ Employer JSON: {len(employer_json)} chars")
|
||||
|
||||
# Deserialize from JSON
|
||||
candidate_back = Candidate.model_validate_json(candidate_json)
|
||||
employer_back = Employer.model_validate_json(employer_json)
|
||||
|
||||
# Verify data integrity
|
||||
assert candidate_back.email == candidate.email
|
||||
assert candidate_back.first_name == candidate.first_name
|
||||
assert employer_back.company_name == employer.company_name
|
||||
|
||||
print(f"✅ JSON round-trip successful")
|
||||
print(f"✅ Data integrity verified")
|
||||
|
||||
return True
|
||||
|
||||
def test_api_dict_format():
|
||||
"""Test dictionary format with aliases (for API requests/responses)"""
|
||||
print("\n📊 Testing API dictionary format...")
|
||||
|
||||
candidate, employer = test_model_creation()
|
||||
|
||||
# Create API format dictionaries
|
||||
candidate_dict = candidate.model_dump(by_alias=True)
|
||||
employer_dict = employer.model_dump(by_alias=True)
|
||||
|
||||
# Verify camelCase aliases are used
|
||||
assert "firstName" in candidate_dict
|
||||
assert "lastName" in candidate_dict
|
||||
assert "createdAt" in candidate_dict
|
||||
assert "companyName" in employer_dict
|
||||
|
||||
print(f"✅ API format dictionaries created")
|
||||
print(f"✅ CamelCase aliases verified")
|
||||
|
||||
# Test deserializing from API format
|
||||
candidate_back = Candidate.model_validate(candidate_dict)
|
||||
employer_back = Employer.model_validate(employer_dict)
|
||||
|
||||
assert candidate_back.email == candidate.email
|
||||
assert employer_back.company_name == employer.company_name
|
||||
|
||||
print(f"✅ API format round-trip successful")
|
||||
|
||||
return True
|
||||
|
||||
def test_validation_constraints():
|
||||
"""Test that validation constraints work"""
|
||||
print("\n🔒 Testing validation constraints...")
|
||||
|
||||
# Test AI Parameters with constraints
|
||||
valid_params = AIParameters(
|
||||
name="Test Config",
|
||||
model=AIModelType.GPT_4,
|
||||
temperature=0.7, # Valid: 0-1
|
||||
maxTokens=2000, # Valid: > 0
|
||||
topP=0.95, # Valid: 0-1
|
||||
frequencyPenalty=0.0, # Valid: -2 to 2
|
||||
presencePenalty=0.0, # Valid: -2 to 2
|
||||
isDefault=True,
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now()
|
||||
)
|
||||
print(f"✅ Valid AI parameters created")
|
||||
|
||||
# Test constraint violation
|
||||
try:
|
||||
invalid_params = AIParameters(
|
||||
name="Invalid Config",
|
||||
model=AIModelType.GPT_4,
|
||||
temperature=1.5, # Invalid: > 1
|
||||
maxTokens=2000,
|
||||
topP=0.95,
|
||||
frequencyPenalty=0.0,
|
||||
presencePenalty=0.0,
|
||||
isDefault=True,
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now()
|
||||
)
|
||||
print("❌ Should have rejected invalid temperature")
|
||||
return False
|
||||
except Exception:
|
||||
print(f"✅ Constraint validation working")
|
||||
|
||||
return True
|
||||
|
||||
def test_enum_values():
|
||||
"""Test that enum values work correctly"""
|
||||
print("\n📋 Testing enum values...")
|
||||
|
||||
# Test that enum values are properly handled
|
||||
candidate, employer = test_model_creation()
|
||||
|
||||
# Check enum values in serialization
|
||||
candidate_dict = candidate.model_dump(by_alias=True)
|
||||
|
||||
assert candidate_dict["status"] == "active"
|
||||
assert candidate_dict["userType"] == "candidate"
|
||||
assert employer.user_type == UserType.EMPLOYER
|
||||
|
||||
print(f"✅ Enum values correctly serialized")
|
||||
print(f"✅ User types: candidate={candidate.user_type}, employer={employer.user_type}")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run all focused tests"""
|
||||
print("🎯 Focused Pydantic Model Tests")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
test_model_creation()
|
||||
test_json_api_format()
|
||||
test_api_dict_format()
|
||||
test_validation_constraints()
|
||||
test_enum_values()
|
||||
|
||||
print(f"\n🎉 All focused tests passed!")
|
||||
print("=" * 40)
|
||||
print("✅ Models work correctly")
|
||||
print("✅ JSON API format works")
|
||||
print("✅ Validation constraints work")
|
||||
print("✅ Enum values work")
|
||||
print("✅ Ready for type generation!")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
2
update-types.sh
Executable file
2
update-types.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker compose exec backstory shell "python src/backend/generate_types.py --source src/backend/models.py --output frontend/src/types/types.ts ${*}"
|
Loading…
x
Reference in New Issue
Block a user