Spaces:
Sleeping
Sleeping
Download frontend/src/services/userDataService.js from chowdary1/ai-interviewer-version-2: direct link, hf CLI and curl.
- Browser
- Download file 8.44 kB
-
https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/services/userDataService.js
- Command line
-
hf download hf://spaces/chowdary1/ai-interviewer-version-2/frontend/src/services/userDataService.js
-
curl -L -o userDataService.js https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/services/userDataService.js
8.44 kB
| /** | |
| * User Data Service - SINGLE SOURCE OF TRUTH | |
| * | |
| * Provides unified data access for Dashboard, Profile, and Analytics pages. | |
| * Uses backend API as primary source with localStorage fallback. | |
| * | |
| * CRITICAL: Both Dashboard and Profile MUST use this service. | |
| * DO NOT fetch data separately in components. | |
| */ | |
| import { analyticsApi, profileApi, resumeApi, userApi } from './api'; | |
| import { | |
| getCachedAnalytics, | |
| getStoredInterviews, | |
| getInterviewHistory, | |
| syncWithBackend, | |
| } from './interviewStorage'; | |
| // =========================================== | |
| // DATA SHAPE DEFINITIONS | |
| // =========================================== | |
| /** | |
| * @typedef {Object} UserAnalytics | |
| * @property {number} total_interviews | |
| * @property {number} completed_interviews | |
| * @property {number|null} average_score | |
| * @property {number|null} best_score | |
| * @property {string|null} last_interview_at | |
| * @property {string} trend - 'improving' | 'declining' | 'stable' | |
| * @property {number} completion_rate | |
| * @property {number} total_questions_answered | |
| * @property {Object} skill_summary | |
| */ | |
| /** | |
| * @typedef {Object} InterviewRecord | |
| * @property {string} id | |
| * @property {string} target_role | |
| * @property {number|null} score | |
| * @property {string} status | |
| * @property {string} created_at | |
| * @property {string} session_type | |
| * @property {string} difficulty | |
| * @property {number} duration | |
| * @property {Array} strengths | |
| * @property {Array} weaknesses | |
| * @property {boolean} has_recording | |
| */ | |
| // =========================================== | |
| // ANALYTICS COMPUTATION (Used by Dashboard & Profile) | |
| // =========================================== | |
| /** | |
| * Compute analytics from interview array | |
| * SINGLE function used by both Dashboard and Profile | |
| * | |
| * @param {Array} interviews - Array of interview records | |
| * @returns {UserAnalytics} | |
| */ | |
| export function computeAnalytics(interviews = []) { | |
| const completed = interviews.filter(i => | |
| i.status === 'completed' || i.status === 'completing' | |
| ); | |
| const scores = completed | |
| .filter(i => i.score != null && i.score > 0) | |
| .map(i => Number(i.score)); | |
| // Calculate trend (compare last 3 vs previous 3) | |
| let trend = 'stable'; | |
| if (scores.length >= 6) { | |
| const recent = scores.slice(0, 3).reduce((a, b) => a + b, 0) / 3; | |
| const older = scores.slice(3, 6).reduce((a, b) => a + b, 0) / 3; | |
| if (recent > older + 5) trend = 'improving'; | |
| else if (recent < older - 5) trend = 'declining'; | |
| } | |
| // Calculate completion rate | |
| const completionRate = interviews.length > 0 | |
| ? Math.round((completed.length / interviews.length) * 100) | |
| : 0; | |
| // Calculate total questions | |
| const totalQuestions = completed.reduce((sum, i) => { | |
| return sum + (i.total_questions || i.questions_answered || 0); | |
| }, 0); | |
| return { | |
| total_interviews: interviews.length, | |
| completed_interviews: completed.length, | |
| average_score: scores.length > 0 | |
| ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) | |
| : null, | |
| best_score: scores.length > 0 ? Math.max(...scores) : null, | |
| last_interview_at: interviews.length > 0 ? interviews[0].created_at : null, | |
| trend, | |
| completion_rate: completionRate, | |
| total_questions_answered: totalQuestions, | |
| }; | |
| } | |
| // =========================================== | |
| // UNIFIED DATA LOADERS | |
| // =========================================== | |
| /** | |
| * Load all user data for Dashboard/Profile | |
| * Uses backend API with localStorage fallback | |
| * | |
| * @returns {Promise<{analytics: UserAnalytics, interviews: Array, resumes: Array}>} | |
| */ | |
| export async function loadUserData() { | |
| let analytics = getCachedAnalytics(); // Start with cache | |
| let interviews = []; | |
| let resumes = []; | |
| // Try backend first | |
| try { | |
| // Load all data in parallel | |
| const [analyticsRes, historyRes, resumesRes] = await Promise.allSettled([ | |
| userApi.getAnalytics(), | |
| profileApi.getInterviewHistory(50, 0), | |
| resumeApi.getAll(), | |
| ]); | |
| // Process analytics | |
| if (analyticsRes.status === 'fulfilled' && analyticsRes.value.success) { | |
| analytics = analyticsRes.value.analytics || analytics; | |
| } | |
| // Process interview history | |
| if (historyRes.status === 'fulfilled' && historyRes.value.success) { | |
| interviews = historyRes.value.interviews || []; | |
| // Sync to localStorage for future fallback | |
| syncWithBackend(interviews); | |
| } else { | |
| // Fallback to localStorage | |
| interviews = getStoredInterviews().map(normalizeInterviewRecord); | |
| } | |
| // Process resumes | |
| if (resumesRes.status === 'fulfilled' && resumesRes.value.success) { | |
| resumes = resumesRes.value.resumes || []; | |
| } | |
| // If backend analytics failed but we have interviews, compute locally | |
| if (!analytics.total_interviews && interviews.length > 0) { | |
| analytics = computeAnalytics(interviews); | |
| } | |
| } catch (error) { | |
| console.error('[UserDataService] Failed to load data:', error); | |
| // Full fallback to localStorage | |
| interviews = getStoredInterviews().map(normalizeInterviewRecord); | |
| analytics = computeAnalytics(interviews); | |
| } | |
| return { analytics, interviews, resumes }; | |
| } | |
| /** | |
| * Load dashboard-specific data (quick stats + recent interviews) | |
| * Optimized for dashboard which needs less data | |
| * | |
| * @returns {Promise<{analytics: UserAnalytics, recentInterviews: Array}>} | |
| */ | |
| export async function loadDashboardData() { | |
| let analytics = getCachedAnalytics(); | |
| let recentInterviews = getInterviewHistory(5); // Start with cache | |
| try { | |
| // Load analytics from backend | |
| const analyticsRes = await userApi.getAnalytics(); | |
| if (analyticsRes.success && analyticsRes.analytics) { | |
| analytics = analyticsRes.analytics; | |
| } | |
| // Load recent history | |
| const historyRes = await userApi.getHistory(5, 0); | |
| if (historyRes.success && historyRes.interviews?.length > 0) { | |
| recentInterviews = historyRes.interviews; | |
| syncWithBackend(historyRes.interviews); | |
| } | |
| } catch (error) { | |
| console.warn('[UserDataService] Dashboard API failed, using cache'); | |
| // Fallback already set | |
| } | |
| return { analytics, recentInterviews }; | |
| } | |
| /** | |
| * Load profile page data (full history + resumes) | |
| * | |
| * @returns {Promise<{analytics: UserAnalytics, interviews: Array, resumes: Array, user: Object}>} | |
| */ | |
| export async function loadProfileData() { | |
| return loadUserData(); | |
| } | |
| // =========================================== | |
| // HELPERS | |
| // =========================================== | |
| /** | |
| * Normalize interview record to consistent shape | |
| * Handles variations between backend and localStorage formats | |
| */ | |
| function normalizeInterviewRecord(record) { | |
| return { | |
| id: record.id || record.interviewId || record.session_id, | |
| target_role: record.target_role || record.targetRole || record.job_role || 'Interview', | |
| score: record.score ?? null, | |
| status: record.status || 'completed', | |
| created_at: record.created_at || record.date || record.createdAt, | |
| session_type: record.session_type || 'mixed', | |
| difficulty: record.difficulty || record.difficulty_level || 'medium', | |
| duration: record.duration || record.duration_minutes || 0, | |
| strengths: record.strengths || [], | |
| weaknesses: record.weaknesses || [], | |
| has_recording: record.has_recording || false, | |
| total_questions: record.total_questions || record.statistics?.total_questions || 0, | |
| questions_answered: record.questions_answered || record.statistics?.answered || 0, | |
| }; | |
| } | |
| /** | |
| * Get formatted analytics for Profile stats display | |
| */ | |
| export function getFormattedStats(analytics, resumeCount = 0) { | |
| return { | |
| total_interviews: analytics?.total_interviews || 0, | |
| completed_interviews: analytics?.completed_interviews || 0, | |
| average_score: analytics?.average_score || 0, | |
| total_resumes: resumeCount, | |
| best_score: analytics?.best_score || 0, | |
| completion_rate: analytics?.completion_rate || 0, | |
| trend: analytics?.trend || 'stable', | |
| }; | |
| } | |
| export default { | |
| computeAnalytics, | |
| loadUserData, | |
| loadDashboardData, | |
| loadProfileData, | |
| getFormattedStats, | |
| }; | |