diff --git a/frontend/src/app/components/application-list/application-list.component.ts b/frontend/src/app/components/application-list/application-list.component.ts
index 2bd209b..d0276ea 100644
--- a/frontend/src/app/components/application-list/application-list.component.ts
+++ b/frontend/src/app/components/application-list/application-list.component.ts
@@ -17,6 +17,8 @@ import {
Subscription,
} from 'rxjs';
import { AuthService } from '../../services/auth.service';
+import { Router } from '@angular/router';
+import { InterviewService } from '../../services/interview.service';
type SortField = 'company' | 'role' | 'date' | 'status' | 'location';
type SortDirection = 'asc' | 'desc';
@@ -31,6 +33,8 @@ type SortDirection = 'asc' | 'desc';
export class ApplicationListComponent implements OnInit, OnDestroy {
private jobService = inject(JobService);
public authService = inject(AuthService);
+ private interviewService = inject(InterviewService);
+ private router = inject(Router);
searchQuery = signal('');
statusFilter = signal('All Statuses');
@@ -42,6 +46,7 @@ export class ApplicationListComponent implements OnInit, OnDestroy {
successMessage = signal('');
errorMessage = signal('');
+ startingInterviewId = signal
(null);
activeMenuId = signal(null);
@@ -198,4 +203,20 @@ export class ApplicationListComponent implements OnInit, OnDestroy {
this.jobService.deleteJob(id);
this.closeMenu();
}
+
+ async prepareInterview(job: Job) {
+ if (this.startingInterviewId()) return;
+
+ this.startingInterviewId.set(job.id);
+ this.errorMessage.set('');
+ try {
+ const session = await this.interviewService.startInterview(job.id);
+ this.closeMenu();
+ await this.router.navigate(['/app/interviews', session.sessionId]);
+ } catch {
+ this.showMessage('error', 'Unable to start interview prep right now.');
+ } finally {
+ this.startingInterviewId.set(null);
+ }
+ }
}
diff --git a/frontend/src/app/components/interview-prep/interview-prep.component.html b/frontend/src/app/components/interview-prep/interview-prep.component.html
new file mode 100644
index 0000000..1539c48
--- /dev/null
+++ b/frontend/src/app/components/interview-prep/interview-prep.component.html
@@ -0,0 +1,86 @@
+
+
+
+ @if (error()) {
+
+ {{ error() }}
+
+ }
+
+
+
+
+
+ @if (uploadStatus() === 'uploading') { Uploading resume... }
+ @else if (uploadStatus() === 'processing') { Processing resume in background... }
+ @else if (uploadStatus() === 'done') { Resume processed successfully. }
+ @else if (uploadStatus() === 'error') { Resume upload failed. Try again. }
+ @else { Upload a resume to enrich answer feedback. }
+
+
+
+ @if (report()) {
+
+
Final Report
+
{{ report()?.overallScore }}/10
+
+
+
Weak Areas
+
+ @for (item of report()?.weakAreas || []; track item) { - {{ item }}
}
+
+
+
+
Improvement Suggestions
+
+ @for (item of report()?.improvementSuggestions || []; track item) { - {{ item }}
}
+
+
+
+ } @else {
+
+
+
Questions
+ @for (q of questions(); track q.index) {
+
+ Q{{ q.index + 1 }}
+
+ }
+
+
+
+ @if (isLoading()) {
+
Loading interview questions...
+ } @else {
+
{{ currentQuestionText }}
+
+ @if (lastScore() !== null) {
+
+
Score: {{ lastScore() }}/10
+
{{ lastFeedback() }}
+
Gap: {{ lastGap() }}
+
+ }
+
+
+
+
+
+
+ }
+
+
+ }
+
diff --git a/frontend/src/app/components/interview-prep/interview-prep.component.ts b/frontend/src/app/components/interview-prep/interview-prep.component.ts
new file mode 100644
index 0000000..faf4042
--- /dev/null
+++ b/frontend/src/app/components/interview-prep/interview-prep.component.ts
@@ -0,0 +1,111 @@
+import { CommonModule } from '@angular/common';
+import { Component, inject, OnInit, signal } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+import { InterviewQuestion, InterviewReportResponse, InterviewService } from '../../services/interview.service';
+
+@Component({
+ selector: 'app-interview-prep',
+ standalone: true,
+ imports: [CommonModule, FormsModule, RouterLink],
+ templateUrl: './interview-prep.component.html',
+})
+export class InterviewPrepComponent implements OnInit {
+ private route = inject(ActivatedRoute);
+ private interviewService = inject(InterviewService);
+
+ sessionId = '';
+ isLoading = signal(true);
+ isSubmitting = signal(false);
+ error = signal('');
+
+ questions = signal([]);
+ currentIndex = signal(0);
+ answer = signal('');
+
+ lastScore = signal(null);
+ lastFeedback = signal('');
+ lastGap = signal('');
+
+ report = signal(null);
+ uploadStatus = signal<'idle' | 'uploading' | 'processing' | 'done' | 'error'>('idle');
+
+ async ngOnInit() {
+ this.sessionId = this.route.snapshot.paramMap.get('sessionId') || '';
+ if (!this.sessionId) {
+ this.error.set('Interview session not found.');
+ this.isLoading.set(false);
+ return;
+ }
+ await this.loadQuestions();
+ }
+
+ async loadQuestions() {
+ this.isLoading.set(true);
+ this.error.set('');
+ try {
+ const res = await this.interviewService.getQuestions(this.sessionId);
+ this.questions.set(res.questions || []);
+ this.currentIndex.set(res.currentIndex || 0);
+ if (!res.questions?.length) {
+ await this.loadReport();
+ }
+ } catch (e) {
+ this.error.set('Could not load interview questions. Please retry.');
+ } finally {
+ this.isLoading.set(false);
+ }
+ }
+
+
+ get currentQuestionText() {
+ const current = this.questions()[this.currentIndex()];
+ return current ? current.question : '';
+ }
+
+ async submitAnswer() {
+ if (this.isSubmitting() || !this.answer().trim()) return;
+
+ this.isSubmitting.set(true);
+ this.error.set('');
+ try {
+ const res = await this.interviewService.submitAnswer(this.sessionId, this.currentIndex(), this.answer());
+ this.lastScore.set(res.score);
+ this.lastFeedback.set(res.feedback);
+ this.lastGap.set(res.improvementGap);
+ this.answer.set('');
+ this.currentIndex.set(res.nextQuestionIndex);
+ if (res.completed) {
+ await this.loadReport();
+ }
+ } catch (e) {
+ this.error.set('Answer submission failed. Please retry.');
+ } finally {
+ this.isSubmitting.set(false);
+ }
+ }
+
+ async loadReport() {
+ try {
+ const report = await this.interviewService.getReport(this.sessionId);
+ this.report.set(report);
+ } catch (e) {
+ this.error.set('Unable to load final report.');
+ }
+ }
+
+ async onResumeSelected(event: Event) {
+ const input = event.target as HTMLInputElement;
+ const file = input.files?.[0];
+ if (!file) return;
+
+ this.uploadStatus.set('uploading');
+ try {
+ await this.interviewService.uploadResume(this.sessionId, file);
+ this.uploadStatus.set('processing');
+ setTimeout(() => this.uploadStatus.set('done'), 800);
+ } catch {
+ this.uploadStatus.set('error');
+ }
+ }
+}
diff --git a/frontend/src/app/services/interview.service.ts b/frontend/src/app/services/interview.service.ts
new file mode 100644
index 0000000..6766501
--- /dev/null
+++ b/frontend/src/app/services/interview.service.ts
@@ -0,0 +1,81 @@
+import { Injectable, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { firstValueFrom } from 'rxjs';
+import { environment } from '../../environments/environment';
+
+export interface InterviewStartResponse {
+ sessionId: string;
+ status: string;
+ message: string;
+}
+
+export interface InterviewQuestion {
+ index: number;
+ question: string;
+}
+
+export interface InterviewQuestionsResponse {
+ questions: InterviewQuestion[];
+ currentIndex: number;
+ totalQuestions: number;
+}
+
+export interface InterviewAnswerResponse {
+ score: number;
+ feedback: string;
+ improvementGap: string;
+ nextQuestionIndex: number;
+ completed: boolean;
+}
+
+export interface InterviewReportResponse {
+ overallScore: number;
+ answeredQuestions: number;
+ totalQuestions: number;
+ weakAreas: string[];
+ improvementSuggestions: string[];
+}
+
+@Injectable({ providedIn: 'root' })
+export class InterviewService {
+ private http = inject(HttpClient);
+ private readonly apiUrl = `${environment.apiBaseUrl}/api/interviews`;
+
+ private async withRetry(fn: () => Promise, retries = 1): Promise {
+ try {
+ return await fn();
+ } catch (err) {
+ if (retries <= 0) throw err;
+ return this.withRetry(fn, retries - 1);
+ }
+ }
+
+ async startInterview(jobId: string) {
+ return this.withRetry(() => firstValueFrom(this.http.post(`${this.apiUrl}/start/${jobId}`, {})));
+ }
+
+ async getQuestions(sessionId: string) {
+ return this.withRetry(() => firstValueFrom(this.http.get(`${this.apiUrl}/${sessionId}/questions`)));
+ }
+
+ async submitAnswer(sessionId: string, questionIndex: number, answer: string) {
+ return this.withRetry(() =>
+ firstValueFrom(
+ this.http.post(`${this.apiUrl}/${sessionId}/answers`, {
+ questionIndex,
+ answer,
+ }),
+ ),
+ );
+ }
+
+ async getReport(sessionId: string) {
+ return this.withRetry(() => firstValueFrom(this.http.get(`${this.apiUrl}/${sessionId}/report`)));
+ }
+
+ async uploadResume(sessionId: string, file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+ return this.withRetry(() => firstValueFrom(this.http.post<{ status: string }>(`${this.apiUrl}/${sessionId}/resume`, formData)));
+ }
+}