diff --git a/frontend/src/hooks/useSecureAuth.tsx b/frontend/src/hooks/useSecureAuth.tsx index 7a2b73d..d36e8d5 100644 --- a/frontend/src/hooks/useSecureAuth.tsx +++ b/frontend/src/hooks/useSecureAuth.tsx @@ -1,10 +1,19 @@ -// Persistent Authentication Hook with localStorage Integration -// Automatically restoring login state on page refresh +// Persistent Authentication Hook with localStorage Integration and Date Conversion +// Automatically restoring login state on page refresh with proper date handling import React, { createContext, useContext,useState, useCallback, useEffect, useRef } from 'react'; import * as Types from 'types/types'; import { useUser } from 'hooks/useUser'; import { CreateCandidateRequest, CreateEmployerRequest, CreateViewerRequest, LoginRequest } from 'services/api-client'; +import { formatApiRequest } from 'types/conversion'; +// Import date conversion functions +import { + convertCandidateFromApi, + convertEmployerFromApi, + convertViewerFromApi, + convertFromApi, +} from 'types/types'; + export interface AuthState { user: Types.User | null; @@ -61,10 +70,59 @@ function clearStoredAuth(): void { localStorage.removeItem(TOKEN_STORAGE.TOKEN_EXPIRY); } +/** + * Convert user data to storage format (dates to ISO strings) + */ +function prepareUserDataForStorage(user: Types.User): string { + try { + // Convert dates to ISO strings for storage + const userForStorage = formatApiRequest(user); + return JSON.stringify(userForStorage); + } catch (error) { + console.error('Failed to prepare user data for storage:', error); + return JSON.stringify(user); // Fallback to direct serialization + } +} + +/** + * Convert stored user data back to proper format (ISO strings to dates) + */ +function parseStoredUserData(userDataStr: string): Types.User | null { + try { + const rawUserData = JSON.parse(userDataStr); + + // Determine user type and apply appropriate conversion + const userType = rawUserData.userType || + (rawUserData.companyName ? 'employer' : + rawUserData.firstName ? 'candidate' : 'viewer'); + + switch (userType) { + case 'candidate': + return convertCandidateFromApi(rawUserData) as Types.Candidate; + case 'employer': + return convertEmployerFromApi(rawUserData) as Types.Employer; + case 'viewer': + return convertViewerFromApi(rawUserData) as Types.Viewer; + default: + // Fallback: try to determine by fields present + if (rawUserData.companyName) { + return convertEmployerFromApi(rawUserData) as Types.Employer; + } else if (rawUserData.skills || rawUserData.experience) { + return convertCandidateFromApi(rawUserData) as Types.Candidate; + } else { + return convertViewerFromApi(rawUserData) as Types.Viewer; + } + } + } catch (error) { + console.error('Failed to parse stored user data:', error); + return null; + } +} + function storeAuthData(authResponse: Types.AuthResponse): void { localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken); localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken); - localStorage.setItem(TOKEN_STORAGE.USER_DATA, JSON.stringify(authResponse.user)); + localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(authResponse.user)); localStorage.setItem(TOKEN_STORAGE.TOKEN_EXPIRY, authResponse.expiresAt.toString()); } @@ -84,18 +142,31 @@ function getStoredAuthData(): { try { if (userDataStr) { - userData = JSON.parse(userDataStr); + userData = parseStoredUserData(userDataStr); } if (expiryStr) { expiresAt = parseInt(expiryStr, 10); } } catch (error) { console.error('Failed to parse stored auth data:', error); + // Clear corrupted data + clearStoredAuth(); } return { accessToken, refreshToken, userData, expiresAt }; } +/** + * Update stored user data (useful when user profile is updated) + */ +function updateStoredUserData(user: Types.User): void { + try { + localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(user)); + } catch (error) { + console.error('Failed to update stored user data:', error); + } +} + export function useSecureAuth() { const [authState, setAuthState] = useState({ user: null, @@ -108,10 +179,20 @@ export function useSecureAuth() { const {apiClient} = useUser(); const initializationCompleted = useRef(false); - // Token refresh function + // Token refresh function with date conversion const refreshAccessToken = useCallback(async (refreshToken: string): Promise => { try { const response = await apiClient.refreshToken(refreshToken); + + // Ensure user data has proper date conversion + if (response.user) { + const userType = response.user.userType || + (response.user.companyName ? 'employer' : + response.user.firstName ? 'candidate' : 'viewer'); + + response.user = convertFromApi(response.user, userType); + } + return response; } catch (error) { console.error('Token refresh failed:', error); @@ -147,11 +228,11 @@ export function useSecureAuth() { const refreshResult = await refreshAccessToken(stored.refreshToken); if (refreshResult) { - // Successfully refreshed + // Successfully refreshed - store with proper date conversion storeAuthData(refreshResult); apiClient.setAuthToken(refreshResult.accessToken); - console.log("User =>", refreshResult.user); + console.log("User (refreshed) =>", refreshResult.user); setAuthState({ user: refreshResult.user, @@ -161,7 +242,7 @@ export function useSecureAuth() { error: null }); - console.log('Token refreshed successfully'); + console.log('Token refreshed successfully with date conversion'); } else { // Refresh failed, clear stored data console.log('Token refresh failed, clearing stored auth'); @@ -177,19 +258,19 @@ export function useSecureAuth() { }); } } else { - // Access token is still valid + // Access token is still valid - user data already has date conversion applied apiClient.setAuthToken(stored.accessToken); - console.log("User =>", stored.userData); + console.log("User (from storage) =>", stored.userData); setAuthState({ - user: stored.userData, + user: stored.userData, // Already converted by parseStoredUserData isAuthenticated: true, isLoading: false, isInitializing: false, error: null }); - console.log('Restored authentication from stored tokens'); + console.log('Restored authentication from stored tokens with date conversion'); } } catch (error) { console.error('Error initializing auth:', error); @@ -250,7 +331,16 @@ export function useSecureAuth() { try { const authResponse = await apiClient.login(loginData); - // Store tokens and user data + // Ensure user data has proper date conversion before storing + if (authResponse.user) { + const userType = authResponse.user.userType || + (authResponse.user.companyName ? 'employer' : + authResponse.user.firstName ? 'candidate' : 'viewer'); + + authResponse.user = convertFromApi(authResponse.user, userType); + } + + // Store tokens and user data with date conversion storeAuthData(authResponse); // Update API client with new token @@ -264,7 +354,7 @@ export function useSecureAuth() { error: null }); - console.log('Login successful'); + console.log('Login successful with date conversion applied'); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Login failed'; @@ -295,6 +385,20 @@ export function useSecureAuth() { console.log('User logged out'); }, [apiClient]); + // Update user data in both state and localStorage (with date conversion) + const updateUserData = useCallback((updatedUser: Types.User) => { + // Update localStorage with proper date formatting + updateStoredUserData(updatedUser); + + // Update state + setAuthState(prev => ({ + ...prev, + user: updatedUser + })); + + console.log('User data updated with date conversion'); + }, []); + const createViewerAccount = useCallback(async (viewerData: CreateViewerRequest): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); @@ -316,7 +420,9 @@ export function useSecureAuth() { throw new Error(usernameValidation.issues.join(', ')); } + // Create viewer - API client automatically applies date conversion const viewer = await apiClient.createViewer(viewerData); + console.log('Viewer created with date conversion:', viewer); // Auto-login after successful registration const loginSuccess = await login({ @@ -357,7 +463,9 @@ export function useSecureAuth() { throw new Error(usernameValidation.issues.join(', ')); } + // Create candidate - API client automatically applies date conversion const candidate = await apiClient.createCandidate(candidateData); + console.log('Candidate created with date conversion:', candidate); // Auto-login after successful registration const loginSuccess = await login({ @@ -398,7 +506,9 @@ export function useSecureAuth() { throw new Error(usernameValidation.issues.join(', ')); } + // Create employer - API client automatically applies date conversion const employer = await apiClient.createEmployer(employerData); + console.log('Employer created with date conversion:', employer); // Auto-login after successful registration const loginSuccess = await login({ @@ -474,7 +584,8 @@ export function useSecureAuth() { createCandidateAccount, createEmployerAccount, requestPasswordReset, - refreshAuth + refreshAuth, + updateUserData // New function to update user data with proper storage }; } @@ -509,7 +620,7 @@ export function useAuth() { interface ProtectedRouteProps { children: React.ReactNode; fallback?: React.ReactNode; - requiredUserType?: 'candidate' | 'employer'; + requiredUserType?: 'candidate' | 'employer' | 'viewer'; } export function ProtectedRoute({ @@ -538,7 +649,7 @@ export function ProtectedRoute({ } // ============================ -// Usage Examples +// Usage Examples with Date Conversion // ============================ /* @@ -580,7 +691,7 @@ function App() { ); } -// Component using auth +// Component using auth with proper date handling function Header() { const { user, isAuthenticated, logout, isInitializing } = useAuth(); @@ -592,7 +703,15 @@ function Header() {
{isAuthenticated ? (
- Welcome, {user?.firstName || user?.companyName}! +
+ Welcome, {user?.firstName || user?.companyName}! + {user?.createdAt && ( + Member since {user.createdAt.toLocaleDateString()} + )} + {user?.lastLogin && ( + Last login: {user.lastLogin.toLocaleString()} + )} +
) : ( @@ -605,6 +724,48 @@ function Header() { ); } +// Profile component with date operations +function UserProfile() { + const { user, updateUserData } = useAuth(); + + if (!user) return null; + + // All date operations work properly because dates are Date objects + const accountAge = user.createdAt ? + Math.floor((Date.now() - user.createdAt.getTime()) / (1000 * 60 * 60 * 24)) : 0; + + const handleProfileUpdate = async (updates: Partial) => { + // When updating user data, it will be properly stored with date conversion + const updatedUser = { ...user, ...updates, updatedAt: new Date() }; + updateUserData(updatedUser); + }; + + return ( +
+

Profile

+

Account created: {user.createdAt?.toLocaleDateString()}

+

Account age: {accountAge} days

+ {user.lastLogin && ( +

Last login: {user.lastLogin.toLocaleString()}

+ )} + + {'availabilityDate' in user && user.availabilityDate && ( +

Available from: {user.availabilityDate.toLocaleDateString()}

+ )} + + {'experience' in user && user.experience?.map((exp, index) => ( +
+

{exp.position} at {exp.companyName}

+

+ {exp.startDate.toLocaleDateString()} - + {exp.endDate ? exp.endDate.toLocaleDateString() : 'Present'} +

+
+ ))} +
+ ); +} + // Auto-redirect based on auth state function LoginPage() { const { isAuthenticated, isInitializing } = useAuth(); diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 474588a..a8ededb 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -290,7 +290,6 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { } }; - console.log(user); // If user is logged in, show their profile if (user) { return ( @@ -321,7 +320,7 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { - Status: {user.status} + {/* Status: {user.status} */} diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts index bf5c4fd..1a809da 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -1,8 +1,9 @@ /** - * Enhanced API Client with Streaming Support + * Enhanced API Client with Streaming Support and Date Conversion * * This demonstrates how to use the generated types with the conversion utilities - * for seamless frontend-backend communication, including streaming responses. + * for seamless frontend-backend communication, including streaming responses and + * automatic date field conversion. */ // Import generated types (from running generate_types.py) @@ -21,6 +22,19 @@ import { PaginatedRequest } from 'types/conversion'; +// Import generated date conversion functions +import { + convertCandidateFromApi, + convertEmployerFromApi, + convertJobFromApi, + convertJobApplicationFromApi, + convertChatSessionFromApi, + convertChatMessageFromApi, + convertViewerFromApi, + convertFromApi, + convertArrayFromApi +} from 'types/types'; + // ============================ // Streaming Types and Interfaces // ============================ @@ -135,6 +149,61 @@ class ApiClient { }; } + // ============================ + // Enhanced Response Handlers with Date Conversion + // ============================ + + /** + * Handle API response with automatic date conversion for specific model types + */ + private async handleApiResponseWithConversion( + response: Response, + modelType?: string + ): Promise { + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const data = await response.json(); + const apiResponse = parseApiResponse(data); + const extractedData = extractApiData(apiResponse); + + // Apply model-specific date conversion if modelType is provided + if (modelType) { + return convertFromApi(extractedData, modelType); + } + + return extractedData; + } + + /** + * Handle paginated API response with automatic date conversion + */ + private async handlePaginatedApiResponseWithConversion( + response: Response, + modelType?: string + ): Promise> { + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const data = await response.json(); + const apiResponse = parsePaginatedResponse(data); + const extractedData = extractApiData(apiResponse); + + // Apply model-specific date conversion to array items if modelType is provided + if (modelType && extractedData.data) { + return { + ...extractedData, + data: convertArrayFromApi(extractedData.data, modelType) + }; + } + + return extractedData; + } + // ============================ // Authentication Methods // ============================ @@ -145,6 +214,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(request)) }); + // AuthResponse doesn't typically have date fields, use standard handler return handleApiResponse(response); } @@ -169,7 +239,7 @@ class ApiClient { } // ============================ - // Viewer Methods + // Viewer Methods with Date Conversion // ============================ async createViewer(request: CreateViewerRequest): Promise { @@ -179,11 +249,11 @@ class ApiClient { body: JSON.stringify(formatApiRequest(request)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Viewer'); } // ============================ - // Candidate Methods + // Candidate Methods with Date Conversion // ============================ async createCandidate(request: CreateCandidateRequest): Promise { @@ -193,7 +263,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(request)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Candidate'); } async getCandidate(username: string): Promise { @@ -201,7 +271,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Candidate'); } async updateCandidate(id: string, updates: Partial): Promise { @@ -211,7 +281,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(updates)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Candidate'); } async getCandidates(request: Partial = {}): Promise> { @@ -222,7 +292,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'Candidate'); } async searchCandidates(query: string, filters?: Record): Promise> { @@ -238,11 +308,11 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'Candidate'); } // ============================ - // Employer Methods + // Employer Methods with Date Conversion // ============================ async createEmployer(request: CreateEmployerRequest): Promise { @@ -252,7 +322,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(request)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Employer'); } async getEmployer(id: string): Promise { @@ -260,7 +330,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Employer'); } async updateEmployer(id: string, updates: Partial): Promise { @@ -270,11 +340,11 @@ class ApiClient { body: JSON.stringify(formatApiRequest(updates)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Employer'); } // ============================ - // Job Methods + // Job Methods with Date Conversion // ============================ async createJob(job: Omit): Promise { @@ -284,7 +354,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(job)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Job'); } async getJob(id: string): Promise { @@ -292,7 +362,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'Job'); } async getJobs(request: Partial = {}): Promise> { @@ -303,7 +373,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'Job'); } async getJobsByEmployer(employerId: string, request: Partial = {}): Promise> { @@ -314,7 +384,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'Job'); } async searchJobs(query: string, filters?: Record): Promise> { @@ -330,11 +400,11 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'Job'); } // ============================ - // Job Application Methods + // Job Application Methods with Date Conversion // ============================ async applyToJob(application: Omit): Promise { @@ -344,7 +414,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(application)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'JobApplication'); } async getJobApplication(id: string): Promise { @@ -352,7 +422,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'JobApplication'); } async getJobApplications(request: Partial = {}): Promise> { @@ -363,7 +433,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'JobApplication'); } async updateApplicationStatus(id: string, status: Types.ApplicationStatus): Promise { @@ -373,14 +443,14 @@ class ApiClient { body: JSON.stringify(formatApiRequest({ status })) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'JobApplication'); } // ============================ - // Chat Methods + // Chat Methods with Date Conversion // ============================ - /** + /** * Create a chat session with optional candidate association */ async createChatSessionWithCandidate( @@ -392,7 +462,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest(request)) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'ChatSession'); } /** @@ -409,7 +479,15 @@ class ApiClient { headers: this.defaultHeaders }); - return handleApiResponse(response); + // Handle the nested sessions with date conversion + const result = await this.handleApiResponseWithConversion(response); + + // Convert the nested sessions array + if (result.sessions && result.sessions.data) { + result.sessions.data = convertArrayFromApi(result.sessions.data, 'ChatSession'); + } + + return result; } /** @@ -439,7 +517,7 @@ class ApiClient { body: JSON.stringify(formatApiRequest({ context })) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'ChatSession'); } async getChatSession(id: string): Promise { @@ -447,7 +525,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'ChatSession'); } /** @@ -460,11 +538,11 @@ class ApiClient { body: JSON.stringify(formatApiRequest({query})) }); - return handleApiResponse(response); + return this.handleApiResponseWithConversion(response, 'ChatMessage'); } /** - * Send message with streaming response support + * Send message with streaming response support and date conversion */ sendMessageStream( sessionId: string, @@ -501,7 +579,7 @@ class ApiClient { const decoder = new TextDecoder(); let buffer = ''; let chatMessage: Types.ChatMessage | null = null; - const chatMessageList : Types.ChatMessage[] = []; + const chatMessageList: Types.ChatMessage[] = []; try { while (true) { @@ -526,30 +604,35 @@ class ApiClient { const data = line.slice(5).trim(); const incoming: Types.ChatMessageBase = JSON.parse(data); + // Convert date fields for incoming messages + const convertedIncoming = convertChatMessageFromApi(incoming); + // Trigger callbacks based on status - if (incoming.status !== chatMessage?.status) { - options.onStatusChange?.(incoming.status); + if (convertedIncoming.status !== chatMessage?.status) { + options.onStatusChange?.(convertedIncoming.status); } // Handle different status types - switch (incoming.status) { + switch (convertedIncoming.status) { case 'streaming': if (chatMessage === null) { - chatMessage = {...incoming}; + chatMessage = {...convertedIncoming}; } else { // Can't do a simple += as typescript thinks .content might not be there - chatMessage.content = (chatMessage?.content || '') + incoming.content; + chatMessage.content = (chatMessage?.content || '') + convertedIncoming.content; + // Update timestamp to latest + chatMessage.timestamp = convertedIncoming.timestamp; } - options.onStreaming?.(incoming); + options.onStreaming?.(convertedIncoming); break; case 'error': - options.onError?.(incoming); + options.onError?.(convertedIncoming); break; default: - chatMessageList.push(incoming); - options.onMessage?.(incoming); + chatMessageList.push(convertedIncoming); + options.onMessage?.(convertedIncoming); break; } } @@ -606,7 +689,7 @@ class ApiClient { } /** - * Get persisted chat messages for a session + * Get persisted chat messages for a session with date conversion */ async getChatMessages( sessionId: string, @@ -621,7 +704,7 @@ class ApiClient { headers: this.defaultHeaders }); - return handlePaginatedApiResponse(response); + return this.handlePaginatedApiResponseWithConversion(response, 'ChatMessage'); } // ============================ @@ -712,10 +795,10 @@ class ApiClient { // Error Handling Helper // ============================ - async handleRequest(requestFn: () => Promise): Promise { + async handleRequest(requestFn: () => Promise, modelType?: string): Promise { try { const response = await requestFn(); - return await handleApiResponse(response); + return await this.handleApiResponseWithConversion(response, modelType); } catch (error) { console.error('API request failed:', error); throw error; @@ -740,10 +823,10 @@ class ApiClient { } // ============================ -// React Hooks for Streaming +// React Hooks for Streaming with Date Conversion // ============================ -/* React Hook Examples for Streaming Chat +/* React Hook Examples for Streaming Chat with proper date handling import { useState, useEffect, useCallback, useRef } from 'react'; export function useStreamingChat(sessionId: string) { @@ -762,31 +845,39 @@ export function useStreamingChat(sessionId: string) { const streamingOptions: StreamingOptions = { onMessage: (message) => { + // Message already has proper Date objects from conversion setCurrentMessage(message); }, - onPartialMessage: (content, messageId) => { + onStreaming: (chunk) => { + // Chunk also has proper Date objects setCurrentMessage(prev => prev ? - { ...prev, content: prev.content + content } : + { + ...prev, + content: prev.content + chunk.content, + timestamp: chunk.timestamp // Update to latest timestamp + } : { - id: messageId || '', + id: chunk.id || '', sessionId, status: 'streaming', sender: 'ai', - content, - timestamp: new Date() + content: chunk.content, + timestamp: chunk.timestamp // Already a Date object } ); }, onStatusChange: (status) => { setCurrentMessage(prev => prev ? { ...prev, status } : null); }, - onComplete: (finalMessage) => { - setMessages(prev => [...prev, finalMessage]); + onComplete: () => { + if (currentMessage) { + setMessages(prev => [...prev, currentMessage]); + } setCurrentMessage(null); setIsStreaming(false); }, onError: (err) => { - setError(err.message); + setError(typeof err === 'string' ? err : err.content); setIsStreaming(false); setCurrentMessage(null); } @@ -799,7 +890,7 @@ export function useStreamingChat(sessionId: string) { setError(err instanceof Error ? err.message : 'Failed to send message'); setIsStreaming(false); } - }, [sessionId, apiClient]); + }, [sessionId, apiClient, currentMessage]); const cancelStreaming = useCallback(() => { if (streamingRef.current) { @@ -819,7 +910,7 @@ export function useStreamingChat(sessionId: string) { }; } -// Usage in React component: +// Usage in React component with proper date handling: function ChatInterface({ sessionId }: { sessionId: string }) { const { messages, @@ -839,14 +930,26 @@ function ChatInterface({ sessionId }: { sessionId: string }) {
{messages.map(message => (
- {message.sender}: {message.content} +
+ {message.sender}: + + {message.timestamp.toLocaleTimeString()} + +
+
{message.content}
))} {currentMessage && (
- {currentMessage.sender}: {currentMessage.content} - {isStreaming && ...} +
+ {currentMessage.sender}: + + {currentMessage.timestamp.toLocaleTimeString()} + + {isStreaming && ...} +
+
{currentMessage.content}
)}
@@ -874,61 +977,96 @@ function ChatInterface({ sessionId }: { sessionId: string }) { */ // ============================ -// Usage Examples +// Usage Examples with Date Conversion // ============================ /* // Initialize API client const apiClient = new ApiClient(); -// Standard message sending (non-streaming) +// All returned objects now have proper Date fields automatically! + +// Create a candidate - createdAt, updatedAt, lastLogin are Date objects try { - const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?'); - console.log('Response:', message.content); + const candidate = await apiClient.createCandidate({ + email: 'jane@example.com', + username: 'jane_doe', + password: 'SecurePassword123!', + firstName: 'Jane', + lastName: 'Doe' + }); + + // These are now Date objects, not strings! + console.log('Created at:', candidate.createdAt.toLocaleDateString()); + console.log('Profile created on:', candidate.createdAt.toDateString()); + + if (candidate.lastLogin) { + console.log('Last seen:', candidate.lastLogin.toRelativeTimeString()); + } } catch (error) { - console.error('Failed to send message:', error); + console.error('Failed to create candidate:', error); } -// Streaming message with callbacks -const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', { - onPartialMessage: (content, messageId) => { - console.log('Partial content:', content); - // Update UI with partial content +// Get jobs with proper date conversion +try { + const jobs = await apiClient.getJobs({ limit: 10 }); + + jobs.data.forEach(job => { + // datePosted, applicationDeadline, featuredUntil are Date objects + console.log(`${job.title} - Posted: ${job.datePosted.toLocaleDateString()}`); + + if (job.applicationDeadline) { + const daysRemaining = Math.ceil( + (job.applicationDeadline.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24) + ); + console.log(`Deadline in ${daysRemaining} days`); + } + }); +} catch (error) { + console.error('Failed to fetch jobs:', error); +} + +// Streaming with proper date conversion +const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me about job opportunities', { + onStreaming: (chunk) => { + // chunk.timestamp is a Date object + console.log(`Streaming at ${chunk.timestamp.toLocaleTimeString()}:`, chunk.content); }, - onStatusChange: (status) => { - console.log('Status changed:', status); - // Update UI status indicator + onMessage: (message) => { + // message.timestamp is a Date object + console.log(`Final message at ${message.timestamp.toLocaleTimeString()}:`, message.content); }, - onComplete: (finalMessage) => { - console.log('Final message:', finalMessage.content); - // Handle completed message - }, - onError: (error) => { - console.error('Streaming error:', error); - // Handle error + onComplete: () => { + console.log('Streaming completed'); } }); -// Can cancel the stream if needed -setTimeout(() => { - streamResponse.cancel(); -}, 10000); // Cancel after 10 seconds - -// Wait for completion +// Chat sessions with date conversion try { - const finalMessage = await streamResponse.promise; - console.log('Stream completed:', finalMessage); + const chatSession = await apiClient.createChatSession({ + type: 'job_search', + additionalContext: {} + }); + + // createdAt and lastActivity are Date objects + console.log('Session created:', chatSession.createdAt.toISOString()); + console.log('Last activity:', chatSession.lastActivity.toLocaleDateString()); } catch (error) { - console.error('Stream failed:', error); + console.error('Failed to create chat session:', error); } -// Auto-detection: streaming if callbacks provided, standard otherwise -await apiClient.sendMessageAuto(sessionId, 'Quick question', { - onPartialMessage: (content) => console.log('Streaming:', content) -}); // Will use streaming - -await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard +// Get chat messages with date conversion +try { + const messages = await apiClient.getChatMessages(sessionId); + + messages.data.forEach(message => { + // timestamp is a Date object + console.log(`[${message.timestamp.toLocaleString()}] ${message.sender}: ${message.content}`); + }); +} catch (error) { + console.error('Failed to fetch messages:', error); +} */ export { ApiClient } -export type { StreamingOptions, StreamingResponse }; \ No newline at end of file +export type { StreamingOptions, StreamingResponse } \ No newline at end of file diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index b570930..1d930ab 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -1,6 +1,6 @@ // Generated TypeScript types from Pydantic models // Source: src/backend/models.py -// Generated on: 2025-05-30T09:14:59.413256 +// Generated on: 2025-05-30T09:39:47.716115 // DO NOT EDIT MANUALLY - This file is auto-generated // ============================ @@ -712,370 +712,400 @@ export interface WorkExperience { /** * Convert Analytics from API response, parsing date fields + * Date fields: timestamp */ export function convertAnalyticsFromApi(data: any): Analytics { if (!data) return data; return { ...data, - entityType: new Date(data.entityType), + // Convert timestamp from ISO string to Date timestamp: new Date(data.timestamp), }; } /** * Convert ApplicationDecision from API response, parsing date fields + * Date fields: date */ export function convertApplicationDecisionFromApi(data: any): ApplicationDecision { if (!data) return data; return { ...data, + // Convert date from ISO string to Date date: new Date(data.date), }; } /** * Convert Attachment from API response, parsing date fields + * Date fields: uploadedAt */ export function convertAttachmentFromApi(data: any): Attachment { if (!data) return data; return { ...data, + // Convert uploadedAt from ISO string to Date uploadedAt: new Date(data.uploadedAt), }; } -/** - * Convert AuthResponse from API response, parsing date fields - */ -export function convertAuthResponseFromApi(data: any): AuthResponse { - if (!data) return data; - - return { - ...data, - user: new Date(data.user), - }; -} /** * Convert Authentication from API response, parsing date fields + * Date fields: resetPasswordExpiry, lastPasswordChange, lockedUntil */ export function convertAuthenticationFromApi(data: any): Authentication { if (!data) return data; return { ...data, + // Convert resetPasswordExpiry from ISO string to Date resetPasswordExpiry: data.resetPasswordExpiry ? new Date(data.resetPasswordExpiry) : undefined, + // Convert lastPasswordChange from ISO string to Date lastPasswordChange: new Date(data.lastPasswordChange), + // Convert lockedUntil from ISO string to Date lockedUntil: data.lockedUntil ? new Date(data.lockedUntil) : undefined, }; } /** * Convert BaseUser from API response, parsing date fields + * Date fields: createdAt, updatedAt, lastLogin */ export function convertBaseUserFromApi(data: any): BaseUser { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), + // Convert lastLogin from ISO string to Date lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, }; } /** * Convert BaseUserWithType from API response, parsing date fields + * Date fields: createdAt, updatedAt, lastLogin */ export function convertBaseUserWithTypeFromApi(data: any): BaseUserWithType { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), + // Convert lastLogin from ISO string to Date lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, }; } /** * Convert Candidate from API response, parsing date fields + * Date fields: createdAt, updatedAt, lastLogin, availabilityDate */ export function convertCandidateFromApi(data: any): Candidate { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), + // Convert lastLogin from ISO string to Date lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, - userType: data.userType ? new Date(data.userType) : undefined, - questions: data.questions ? new Date(data.questions) : undefined, + // Convert availabilityDate from ISO string to Date availabilityDate: data.availabilityDate ? new Date(data.availabilityDate) : undefined, }; } -/** - * Convert CandidateListResponse from API response, parsing date fields - */ -export function convertCandidateListResponseFromApi(data: any): CandidateListResponse { - if (!data) return data; - - return { - ...data, - data: data.data ? new Date(data.data) : undefined, - }; -} -/** - * Convert CandidateResponse from API response, parsing date fields - */ -export function convertCandidateResponseFromApi(data: any): CandidateResponse { - if (!data) return data; - - return { - ...data, - data: data.data ? new Date(data.data) : undefined, - }; -} /** * Convert Certification from API response, parsing date fields + * Date fields: issueDate, expirationDate */ export function convertCertificationFromApi(data: any): Certification { if (!data) return data; return { ...data, + // Convert issueDate from ISO string to Date issueDate: new Date(data.issueDate), + // Convert expirationDate from ISO string to Date expirationDate: data.expirationDate ? new Date(data.expirationDate) : undefined, }; } -/** - * Convert ChatContext from API response, parsing date fields - */ -export function convertChatContextFromApi(data: any): ChatContext { - if (!data) return data; - - return { - ...data, - relatedEntityType: data.relatedEntityType ? new Date(data.relatedEntityType) : undefined, - }; -} /** * Convert ChatMessage from API response, parsing date fields + * Date fields: timestamp */ export function convertChatMessageFromApi(data: any): ChatMessage { if (!data) return data; return { ...data, + // Convert timestamp from ISO string to Date timestamp: new Date(data.timestamp), }; } /** * Convert ChatMessageBase from API response, parsing date fields + * Date fields: timestamp */ export function convertChatMessageBaseFromApi(data: any): ChatMessageBase { if (!data) return data; return { ...data, + // Convert timestamp from ISO string to Date timestamp: new Date(data.timestamp), }; } /** * Convert ChatMessageUser from API response, parsing date fields + * Date fields: timestamp */ export function convertChatMessageUserFromApi(data: any): ChatMessageUser { if (!data) return data; return { ...data, + // Convert timestamp from ISO string to Date timestamp: new Date(data.timestamp), }; } /** * Convert ChatSession from API response, parsing date fields + * Date fields: createdAt, lastActivity */ export function convertChatSessionFromApi(data: any): ChatSession { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: data.createdAt ? new Date(data.createdAt) : undefined, + // Convert lastActivity from ISO string to Date lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined, }; } /** * Convert DataSourceConfiguration from API response, parsing date fields + * Date fields: lastRefreshed */ export function convertDataSourceConfigurationFromApi(data: any): DataSourceConfiguration { if (!data) return data; return { ...data, + // Convert lastRefreshed from ISO string to Date lastRefreshed: data.lastRefreshed ? new Date(data.lastRefreshed) : undefined, }; } /** * Convert EditHistory from API response, parsing date fields + * Date fields: editedAt */ export function convertEditHistoryFromApi(data: any): EditHistory { if (!data) return data; return { ...data, + // Convert editedAt from ISO string to Date editedAt: new Date(data.editedAt), }; } /** * Convert Education from API response, parsing date fields + * Date fields: startDate, endDate */ export function convertEducationFromApi(data: any): Education { if (!data) return data; return { ...data, + // Convert startDate from ISO string to Date startDate: new Date(data.startDate), + // Convert endDate from ISO string to Date endDate: data.endDate ? new Date(data.endDate) : undefined, }; } /** * Convert Employer from API response, parsing date fields + * Date fields: createdAt, updatedAt, lastLogin */ export function convertEmployerFromApi(data: any): Employer { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), + // Convert lastLogin from ISO string to Date lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, }; } /** * Convert Guest from API response, parsing date fields + * Date fields: createdAt, lastActivity */ export function convertGuestFromApi(data: any): Guest { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert lastActivity from ISO string to Date lastActivity: new Date(data.lastActivity), }; } /** * Convert InterviewFeedback from API response, parsing date fields + * Date fields: createdAt, updatedAt */ export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), }; } /** * Convert InterviewSchedule from API response, parsing date fields + * Date fields: scheduledDate, endDate */ export function convertInterviewScheduleFromApi(data: any): InterviewSchedule { if (!data) return data; return { ...data, + // Convert scheduledDate from ISO string to Date scheduledDate: new Date(data.scheduledDate), + // Convert endDate from ISO string to Date endDate: new Date(data.endDate), }; } /** * Convert Job from API response, parsing date fields + * Date fields: datePosted, applicationDeadline, featuredUntil */ export function convertJobFromApi(data: any): Job { if (!data) return data; return { ...data, + // Convert datePosted from ISO string to Date datePosted: new Date(data.datePosted), + // Convert applicationDeadline from ISO string to Date applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined, + // Convert featuredUntil from ISO string to Date featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined, }; } /** * Convert JobApplication from API response, parsing date fields + * Date fields: appliedDate, updatedDate */ export function convertJobApplicationFromApi(data: any): JobApplication { if (!data) return data; return { ...data, + // Convert appliedDate from ISO string to Date appliedDate: new Date(data.appliedDate), + // Convert updatedDate from ISO string to Date updatedDate: new Date(data.updatedDate), - candidateContact: data.candidateContact ? new Date(data.candidateContact) : undefined, }; } /** * Convert MessageReaction from API response, parsing date fields + * Date fields: timestamp */ export function convertMessageReactionFromApi(data: any): MessageReaction { if (!data) return data; return { ...data, + // Convert timestamp from ISO string to Date timestamp: new Date(data.timestamp), }; } /** * Convert RAGConfiguration from API response, parsing date fields + * Date fields: createdAt, updatedAt */ export function convertRAGConfigurationFromApi(data: any): RAGConfiguration { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), }; } /** * Convert RefreshToken from API response, parsing date fields + * Date fields: expiresAt */ export function convertRefreshTokenFromApi(data: any): RefreshToken { if (!data) return data; return { ...data, + // Convert expiresAt from ISO string to Date expiresAt: new Date(data.expiresAt), }; } /** * Convert UserActivity from API response, parsing date fields + * Date fields: timestamp */ export function convertUserActivityFromApi(data: any): UserActivity { if (!data) return data; return { ...data, + // Convert timestamp from ISO string to Date timestamp: new Date(data.timestamp), }; } /** * Convert Viewer from API response, parsing date fields + * Date fields: createdAt, updatedAt, lastLogin */ export function convertViewerFromApi(data: any): Viewer { if (!data) return data; return { ...data, + // Convert createdAt from ISO string to Date createdAt: new Date(data.createdAt), + // Convert updatedAt from ISO string to Date updatedAt: new Date(data.updatedAt), + // Convert lastLogin from ISO string to Date lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, }; } /** * Convert WorkExperience from API response, parsing date fields + * Date fields: startDate, endDate */ export function convertWorkExperienceFromApi(data: any): WorkExperience { if (!data) return data; return { ...data, + // Convert startDate from ISO string to Date startDate: new Date(data.startDate), + // Convert endDate from ISO string to Date endDate: data.endDate ? new Date(data.endDate) : undefined, }; } @@ -1094,8 +1124,6 @@ export function convertFromApi(data: any, modelType: string): T { return convertApplicationDecisionFromApi(data) as T; case 'Attachment': return convertAttachmentFromApi(data) as T; - case 'AuthResponse': - return convertAuthResponseFromApi(data) as T; case 'Authentication': return convertAuthenticationFromApi(data) as T; case 'BaseUser': @@ -1104,14 +1132,8 @@ export function convertFromApi(data: any, modelType: string): T { return convertBaseUserWithTypeFromApi(data) as T; case 'Candidate': return convertCandidateFromApi(data) as T; - case 'CandidateListResponse': - return convertCandidateListResponseFromApi(data) as T; - case 'CandidateResponse': - return convertCandidateResponseFromApi(data) as T; case 'Certification': return convertCertificationFromApi(data) as T; - case 'ChatContext': - return convertChatContextFromApi(data) as T; case 'ChatMessage': return convertChatMessageFromApi(data) as T; case 'ChatMessageBase': diff --git a/src/backend/generate_types.py b/src/backend/generate_types.py index 29a7d3c..92ccd2d 100644 --- a/src/backend/generate_types.py +++ b/src/backend/generate_types.py @@ -101,10 +101,42 @@ def is_date_type(python_type: Any) -> bool: if python_type == datetime: return True - # String representation checks for various datetime types + # Check if it's a datetime type from the datetime module + if hasattr(python_type, '__module__') and hasattr(python_type, '__name__'): + module_name = getattr(python_type, '__module__', '') + type_name = getattr(python_type, '__name__', '') + + # Check for datetime module types + if module_name == 'datetime' and type_name in ('datetime', 'date', 'time'): + return True + + # String representation checks for specific datetime patterns (more restrictive) type_str = str(python_type) - date_patterns = ['datetime', 'date', 'DateTime', 'Date'] - return any(pattern in type_str for pattern in date_patterns) + + # Be very specific about datetime patterns to avoid false positives + specific_date_patterns = [ + 'datetime.datetime', + 'datetime.date', + 'datetime.time', + '', + '', + '', + 'typing.DateTime', + 'pydantic.datetime', + ] + + # Check for exact matches or specific patterns + for pattern in specific_date_patterns: + if pattern in type_str: + return True + + # Additional check for common datetime type aliases + if hasattr(python_type, '__origin__'): + origin_str = str(python_type.__origin__) + if 'datetime' in origin_str and 'datetime.' in origin_str: + return True + + return False def python_type_to_typescript(python_type: Any, debug: bool = False) -> str: """Convert a Python type to TypeScript type string""" @@ -351,7 +383,11 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]: print(f" Raw type: {field_type}") # Check if this is a date field - if is_date_type(field_type): + is_date = is_date_type(field_type) + if debug: + print(f" šŸ“… Date type check for {ts_name}: {is_date} (type: {field_type})") + + if is_date: is_optional = is_field_optional(field_info, field_type, debug) date_fields.append({ 'name': ts_name, @@ -359,6 +395,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]: }) if debug: print(f" šŸ—“ļø Date field detected: {ts_name} (optional: {is_optional})") + elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()): + print(f" āš ļø Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}") ts_type = python_type_to_typescript(field_type, debug) @@ -396,7 +434,11 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]: print(f" Raw type: {field_type}") # Check if this is a date field - if is_date_type(field_type): + is_date = is_date_type(field_type) + if debug: + print(f" šŸ“… Date type check for {ts_name}: {is_date} (type: {field_type})") + + if is_date: is_optional = is_field_optional(field_info, field_type) date_fields.append({ 'name': ts_name, @@ -404,6 +446,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]: }) if debug: print(f" šŸ—“ļø Date field detected: {ts_name} (optional: {is_optional})") + elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()): + print(f" āš ļø Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}") ts_type = python_type_to_typescript(field_type, debug) @@ -471,6 +515,7 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str: func_lines = [ f"/**", f" * Convert {interface_name} from API response, parsing date fields", + f" * Date fields: {', '.join([f['name'] for f in date_fields])}", f" */", f"export function {function_name}(data: any): {interface_name} {{", f" if (!data) return data;", @@ -479,11 +524,14 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str: f" ...data," ] - # Add date field conversions + # Add date field conversions with validation for date_field in date_fields: field_name = date_field['name'] is_optional = date_field['optional'] + # Add a comment for clarity + func_lines.append(f" // Convert {field_name} from ISO string to Date") + if is_optional: func_lines.append(f" {field_name}: data.{field_name} ? new Date(data.{field_name}) : undefined,") else: @@ -502,7 +550,7 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str: # Generate the conversion functions section result = [ "// ============================", - "// Date Conversion Functions", + "// Date Conversion Functions", "// ============================", "", "// These functions convert API responses to properly typed objects", @@ -780,10 +828,27 @@ Generated conversion functions can be used like: file_size = len(ts_content) print(f"āœ… TypeScript types generated: {args.output} ({file_size} characters)") - # Count conversion functions + # Count conversion functions and provide detailed feedback conversion_count = ts_content.count('export function convert') - ts_content.count('convertFromApi') - ts_content.count('convertArrayFromApi') if conversion_count > 0: print(f"šŸ—“ļø Generated {conversion_count} date conversion functions") + if args.debug: + # Show which models have date conversion + models_with_dates = [] + for line in ts_content.split('\n'): + if line.startswith('export function convert') and 'FromApi' in line and 'convertFromApi' not in line: + model_name = line.split('convert')[1].split('FromApi')[0] + models_with_dates.append(model_name) + if models_with_dates: + print(f" Models with date conversion: {', '.join(models_with_dates)}") + + # Provide troubleshooting info if debug mode + if args.debug: + print(f"\nšŸ› Debug mode was enabled. If you see incorrect date conversions:") + print(f" 1. Check the debug output above for 'šŸ“… Date type check' lines") + print(f" 2. Look for 'āš ļø' warnings about false positives") + print(f" 3. Verify your Pydantic model field types are correct") + print(f" 4. Re-run with --debug to see detailed type analysis") # Step 5: Compile TypeScript (unless skipped) if not args.skip_compile: