ai-interviewer-version-2 / frontend /src /services /interviewStorage.js
chowdary1's picture
Deploy AI Interviewer Pro Max v1.1 - Clean build with psutil fix
86d71bd
Raw History Blame Contribute Delete
12.2 kB
/**
* Interview Storage Service
*
* Provides persistent storage for interview completion data.
* Uses backend API as primary source, localStorage as fallback.
*
* CRITICAL: Ensures interview data persists after refresh and
* dashboard counts/history are always accurate.
*/
const STORAGE_KEY = 'ai_interviewer_completed_sessions';
const HISTORY_KEY = 'ai_interviewer_interview_history';
const ANALYTICS_KEY = 'ai_interviewer_analytics_cache';
// ===========================================
// INTERVIEW DATA STRUCTURE
// ===========================================
/**
* Creates a standardized interview record
* @param {Object} data - Interview completion data
* @returns {Object} Standardized interview record
*/
export function createInterviewRecord(data) {
return {
interviewId: data.interviewId || data.session_id || data.id,
date: data.date || data.completed_at || new Date().toISOString(),
score: data.score || data.readiness?.score || 0,
grade: data.grade || data.readiness?.grade || 'C',
targetRole: data.targetRole || data.target_role || 'Interview',
status: 'completed',
strengths: data.strengths || [],
weaknesses: data.weaknesses || [],
improvementTips: data.improvementTips || data.improvements?.areas || [],
pdfUrl: data.pdfUrl || null,
summary: data.summary || data.executive_summary || '',
statistics: data.statistics || {
total_questions: data.total_questions || 0,
answered: data.answered || 0,
skipped: data.skipped || 0,
duration_minutes: data.duration_minutes || 0,
},
skills: data.skills || {},
createdAt: new Date().toISOString(),
};
}
// ===========================================
// LOCAL STORAGE OPERATIONS
// ===========================================
/**
* Get all stored interview records from localStorage
* @returns {Array} Array of interview records
*/
export function getStoredInterviews() {
try {
const stored = localStorage.getItem(HISTORY_KEY);
return stored ? JSON.parse(stored) : [];
} catch (e) {
console.warn('[InterviewStorage] Failed to read from localStorage:', e);
return [];
}
}
/**
* Save interview record to localStorage
* @param {Object} record - Interview record to save
* @returns {boolean} Success status
*/
export function saveInterviewToStorage(record) {
try {
const interviews = getStoredInterviews();
// Check if interview already exists (update) or is new (add)
const existingIndex = interviews.findIndex(
i => i.interviewId === record.interviewId
);
if (existingIndex >= 0) {
// Update existing record
interviews[existingIndex] = {
...interviews[existingIndex],
...record,
updatedAt: new Date().toISOString(),
};
} else {
// Add new record at beginning
interviews.unshift(record);
}
// Keep only last 100 interviews
const trimmedInterviews = interviews.slice(0, 100);
localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmedInterviews));
// Update analytics cache
updateAnalyticsCache(trimmedInterviews);
console.log('[InterviewStorage] Saved interview:', record.interviewId);
return true;
} catch (e) {
console.error('[InterviewStorage] Failed to save:', e);
return false;
}
}
/**
* Get a specific interview by ID
* @param {string} interviewId - Interview ID
* @returns {Object|null} Interview record or null
*/
export function getInterviewById(interviewId) {
const interviews = getStoredInterviews();
return interviews.find(i => i.interviewId === interviewId) || null;
}
/**
* Delete an interview from storage
* @param {string} interviewId - Interview ID
* @returns {boolean} Success status
*/
export function deleteInterviewFromStorage(interviewId) {
try {
const interviews = getStoredInterviews();
const filtered = interviews.filter(i => i.interviewId !== interviewId);
localStorage.setItem(HISTORY_KEY, JSON.stringify(filtered));
updateAnalyticsCache(filtered);
return true;
} catch (e) {
console.error('[InterviewStorage] Failed to delete:', e);
return false;
}
}
// ===========================================
// ANALYTICS CACHE
// ===========================================
/**
* Update local analytics cache based on stored interviews
* @param {Array} interviews - Array of interview records
*/
function updateAnalyticsCache(interviews) {
try {
const completed = interviews.filter(i => i.status === 'completed');
const scores = completed.filter(i => i.score > 0).map(i => i.score);
const analytics = {
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].date : null,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(ANALYTICS_KEY, JSON.stringify(analytics));
} catch (e) {
console.warn('[InterviewStorage] Failed to update analytics cache:', e);
}
}
/**
* Get cached analytics
* @returns {Object} Analytics data
*/
export function getCachedAnalytics() {
try {
const cached = localStorage.getItem(ANALYTICS_KEY);
if (cached) {
return JSON.parse(cached);
}
} catch (e) {
console.warn('[InterviewStorage] Failed to read analytics cache:', e);
}
// Calculate from stored interviews if cache missing
const interviews = getStoredInterviews();
const completed = interviews.filter(i => i.status === 'completed');
const scores = completed.filter(i => i.score > 0).map(i => i.score);
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].date : null,
};
}
// ===========================================
// INTERVIEW COMPLETION HANDLER
// ===========================================
/**
* Handle interview completion - stores data and updates counts
* Call this when an interview finishes (from LiveInterview or InterviewReport)
*
* @param {string} sessionId - Session/Interview ID
* @param {Object} reportData - Report data from backend or local evaluation
* @returns {Object} Saved interview record
*/
export function handleInterviewCompletion(sessionId, reportData) {
console.log('[InterviewStorage] Handling interview completion:', sessionId);
const record = createInterviewRecord({
interviewId: sessionId,
...reportData,
});
saveInterviewToStorage(record);
return record;
}
/**
* Mark session as completed (without full report data)
* Use when navigating to report page before report is generated
*
* @param {string} sessionId - Session ID
* @param {Object} basicData - Basic interview data (targetRole, date, etc.)
*/
export function markSessionCompleted(sessionId, basicData = {}) {
const existing = getInterviewById(sessionId);
if (!existing) {
// Create a placeholder record that will be updated with full report later
const record = createInterviewRecord({
interviewId: sessionId,
status: 'pending_report',
targetRole: basicData.targetRole || 'Interview',
date: basicData.date || new Date().toISOString(),
score: 0, // Will be updated when report is generated
});
saveInterviewToStorage(record);
}
}
/**
* Update existing interview record with report data
* Call this from InterviewReport after report is generated/loaded
*
* @param {string} sessionId - Session ID
* @param {Object} report - Full report data
* @returns {Object} Updated interview record
*/
export function updateWithReportData(sessionId, report) {
console.log('[InterviewStorage] Updating with report data:', sessionId);
const record = createInterviewRecord({
interviewId: sessionId,
score: report.readiness?.score || report.score,
grade: report.readiness?.grade || report.grade,
targetRole: report.target_role || report.targetRole,
date: report.generated_at || report.date || new Date().toISOString(),
strengths: report.strengths || [],
weaknesses: report.weaknesses || [],
improvementTips: report.improvements?.areas || [],
summary: report.executive_summary || '',
statistics: report.statistics || {},
skills: report.skills || {},
status: 'completed',
});
saveInterviewToStorage(record);
return record;
}
// ===========================================
// HISTORY & DISPLAY HELPERS
// ===========================================
/**
* Get interview history for dashboard display
* @param {number} limit - Max number of interviews to return
* @returns {Array} Array of interview history items
*/
export function getInterviewHistory(limit = 10) {
const interviews = getStoredInterviews();
return interviews.slice(0, limit).map(i => ({
id: i.interviewId,
job_role: i.targetRole,
score: i.score,
status: i.status,
created_at: i.date,
grade: i.grade,
}));
}
/**
* Get statistics for dashboard
* @returns {Object} Dashboard statistics
*/
export function getDashboardStats() {
return getCachedAnalytics();
}
// ===========================================
// SYNC WITH BACKEND (Hybrid approach)
// ===========================================
/**
* Merge backend data with local storage
* Call this when backend data is loaded to ensure consistency
*
* @param {Array} backendInterviews - Interviews from backend API
*/
export function syncWithBackend(backendInterviews) {
if (!backendInterviews || !Array.isArray(backendInterviews)) return;
const localInterviews = getStoredInterviews();
const mergedMap = new Map();
// Add local interviews first
localInterviews.forEach(i => {
mergedMap.set(i.interviewId, i);
});
// Overlay backend data (takes precedence)
backendInterviews.forEach(i => {
const id = i.id || i.session_id || i.interviewId;
const existing = mergedMap.get(id);
const record = createInterviewRecord({
interviewId: id,
score: i.score || existing?.score,
grade: i.grade || existing?.grade,
targetRole: i.job_role || i.target_role || existing?.targetRole,
date: i.created_at || i.completed_at || existing?.date,
status: i.status || 'completed',
});
mergedMap.set(id, { ...existing, ...record });
});
// Save merged data
const mergedArray = Array.from(mergedMap.values())
.sort((a, b) => new Date(b.date) - new Date(a.date));
localStorage.setItem(HISTORY_KEY, JSON.stringify(mergedArray.slice(0, 100)));
updateAnalyticsCache(mergedArray);
}
/**
* Clear all interview storage (for logout)
*/
export function clearInterviewStorage() {
localStorage.removeItem(HISTORY_KEY);
localStorage.removeItem(ANALYTICS_KEY);
localStorage.removeItem(STORAGE_KEY);
}
export default {
createInterviewRecord,
getStoredInterviews,
saveInterviewToStorage,
getInterviewById,
deleteInterviewFromStorage,
getCachedAnalytics,
handleInterviewCompletion,
markSessionCompleted,
updateWithReportData,
getInterviewHistory,
getDashboardStats,
syncWithBackend,
clearInterviewStorage,
};