Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
}
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": []
},
Expand Down
1 change: 1 addition & 0 deletions Angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0",
"@google/generative-ai": "^0.21.0",
"bootstrap": "^5.3.3",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.10"
Expand Down
6 changes: 5 additions & 1 deletion Angular/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
<router-outlet />
<div class="container mt-5">
<router-outlet />
</div>

<app-loader [isLoading]="loadingService.isLoading()" />
8 changes: 7 additions & 1 deletion Angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoaderComponent } from './components/loader/loader.component';
import { LoadingService } from './services/loading/loading.service';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
imports: [RouterOutlet, LoaderComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'angular-gemini-recipes';

constructor(
public readonly loadingService: LoadingService
) {}
}
5 changes: 5 additions & 0 deletions Angular/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ export const routes: Routes = [
loadComponent: () =>
import('./pages/chat/chat.component').then((c) => c.ChatComponent),
},
{
path: 'skill-quiz-generator',
loadComponent: () =>
import('./pages/skill-quiz-generator/skill-quiz-generator.component').then(c => c.SkillQuizGeneratorComponent)
}
];
7 changes: 7 additions & 0 deletions Angular/src/app/components/loader/loader.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@if (isLoading) {
<div class="loader-overlay">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
12 changes: 12 additions & 0 deletions Angular/src/app/components/loader/loader.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.loader-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
12 changes: 12 additions & 0 deletions Angular/src/app/components/loader/loader.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Component, Input } from '@angular/core';

@Component({
selector: 'app-loader',
standalone: true,
imports: [],
templateUrl: './loader.component.html',
styleUrl: './loader.component.scss'
})
export class LoaderComponent {
@Input() isLoading: boolean = false;
}
44 changes: 44 additions & 0 deletions Angular/src/app/helpers/quiz-recipe-helper.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { IQuiz, IQuizQuestion } from '../interfaces/iquiz';

@Injectable({
providedIn: 'root'
})
export class QuizRecipeHelperService {

formatAiReponseQuizString(quizString: string): IQuiz {
const quizData: IQuiz = {
title: '',
instructions: '',
questions: []
};

// Split the string by double new lines to separate sections
const sections: string[] = quizString.split(/\n\s*\n/);

// Extract title
quizData.title = sections[0].replace('## ', '').trim();

// Extract instructions
const instructionSection = sections[1]?.trim();
if (instructionSection?.startsWith('**Instructions:**')) {
quizData.instructions = instructionSection.replace('**Instructions:**', '').trim();
}

// Extract questions
const questionSections = sections.slice(2); // All questions are after the first two sections

questionSections.forEach((section: string) => {
const lines: string[] = section.split('\n').map((line: string) => line.trim()).filter((line: string) => line.length > 0);
const mainQuestion: string = lines[0].replace(/^\d+\.\s*/, '').trim();

const quizQuestion: IQuizQuestion = {
question: mainQuestion
};

quizData.questions.push(quizQuestion);
});

return quizData;
}
}
9 changes: 9 additions & 0 deletions Angular/src/app/interfaces/iquiz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface IQuizQuestion {
question: string
}

export interface IQuiz {
title: string,
instructions: string,
questions: IQuizQuestion[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="container mt-5">
<h1 class="text-center">{{ quiz.title }}</h1>
<p class="lead text-center">{{ quiz.instructions }}</p>

<form [formGroup]="quizForm" (ngSubmit)="onSubmit()">
<div formArrayName="answers">
@for (question of quiz.questions; track $index; let i = $index) {
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title" [innerHTML]="question.question"></h5>
<div class="form-group">
<label for="answer-{{ i }}">Your Answer:</label>
<textarea id="answer-{{ i }}" class="form-control" rows="4" formControlName="{{i}}"
required></textarea>
@if (hasError(i)) {
<div class="text-danger">
Answer is required.
</div>
}
</div>
</div>
</div>
}
</div>

@if (quiz.questions.length > 0) {
<div class="text-center mt-4">
<button type="submit" class="btn btn-primary">Submit Answers</button>
</div>
}
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Component, Input, SimpleChanges } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { IQuiz } from '../../../interfaces/iquiz';

@Component({
selector: 'app-generated-quiz',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './generated-quiz.component.html',
styleUrl: './generated-quiz.component.scss'
})
export class GeneratedQuizComponent {
@Input() quiz: IQuiz = {
title: '',
instructions: '',
questions: []
};
quizForm!: FormGroup;

constructor(private fb: FormBuilder) {
this.initQuizForm();
}

ngOnChanges(changes: SimpleChanges): void {
if (changes['quiz'] && this.quiz.questions) {
this.generateQuizForm();
}
}

initQuizForm() {
this.quizForm = this.fb.group({
answers: this.fb.array([])
});
}

generateQuizForm() {
const answersArray = this.fb.array(this.quiz.questions.map(() => this.fb.control('', Validators.required)));
this.quizForm.setControl('answers', answersArray);
}

onSubmit() {
if (this.quizForm.valid) {
console.log(this.quizForm.value);
// TODO: Handle form submission logic here
}
}

hasError(controlIndex: number): boolean {
const control = this.quizForm.get('answers')?.get(`${controlIndex}`);
return !!(control?.invalid || control?.touched);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<div class="quiz-form">
<h2 class="mb-4 pb-4 border-bottom">Skill Quiz Generator</h2>

<form [formGroup]="candidateForm" (ngSubmit)="onSubmit()" class="row" novalidate>
<div class="mb-3 col-md-4 col-12">
<label for="candidateName" class="form-label">Candidate Name</label>
<input type="text" id="candidateName" class="form-control" formControlName="candidateName" required>

@if (hasError('candidateName', ['required'])) {
<div class="text-danger">
Candidate Name is required.
</div>
}
</div>
<div class="mb-3 col-md-4 col-12">
<label for="technology" class="form-label">Technology</label>
<select id="technology" class="form-select" formControlName="technology" required>
<option value="">Select a Technology</option>
@for (tech of technologies; track $index) {
<option [value]="tech">{{ tech }}</option>
}
</select>

@if (hasError('technology', ['required'])) {
<div class="text-danger">
Technology is required.
</div>
}
</div>
<div class="mb-3 col-md-4 col-12">
<label for="questionsLength" class="form-label">Questions Length</label>
<input type="number" id="questionsLength" class="form-control" formControlName="questionsLength" min="5"
required>

@if (hasError('questionsLength', ['min', 'required'])) {
<div class="text-danger">
Minimum value is 5.
</div>
}
</div>

<div class="col-12 text-center">
<button type="submit" class="btn btn-primary" [disabled]="candidateForm.invalid">Generate Quiz</button>
</div>
</form>
</div>

<div class="my-5">
<h2 class="mb-4 pb-4 border-bottom">Ai Generated Quiz</h2>
@if (formattedQuizResponse) {
<app-generated-quiz [quiz]="formattedQuizResponse" />
} @else {
<p>Quiz isn't generated yet.</p>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component } from '@angular/core';
import { GeneratedQuizComponent } from './generated-quiz/generated-quiz.component';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { GeminiGoogleAiService } from '../../services/gemini-google-ai/gemini-google-ai.service';
import { QuizRecipeHelperService } from '../../helpers/quiz-recipe-helper.service';
import { LoadingService } from '../../services/loading/loading.service';
import { IQuiz } from '../../interfaces/iquiz';

@Component({
selector: 'app-skill-quiz-generator',
standalone: true,
imports: [GeneratedQuizComponent, ReactiveFormsModule],
templateUrl: './skill-quiz-generator.component.html',
styleUrl: './skill-quiz-generator.component.scss'
})
export class SkillQuizGeneratorComponent {
candidateForm!: FormGroup;
technologies = ['Angular', 'React', 'JavaScript', 'TypeScript', 'Python', 'Java'];
formattedQuizResponse: IQuiz = {
title: '',
instructions: '',
questions: []
};

constructor(
private fb: FormBuilder,
private geminiService: GeminiGoogleAiService,
private quizHelperService: QuizRecipeHelperService,
private loadingService: LoadingService
) {
this.formInit();
}

formInit() {
this.candidateForm = this.fb.group({
candidateName: ['', Validators.required],
technology: ['', Validators.required],
questionsLength: [5, [Validators.required, Validators.min(5)]]
});
}

hasError(controlName: string, errorNames: string[]) {
const control = this.candidateForm.get(controlName);
return control && control.touched && errorNames.some(errorName => control.hasError(errorName));
}

onSubmit() {
if (this.candidateForm.valid) {
this.loadingService.onLoadingToggle();

const prompt: string = `Generate a technical skill quiz for a candidate named ${this.candidateForm.value.candidateName} who specializes in ${this.candidateForm.value.technology}. The quiz should be tailored to the candidate's proficiency level, aiming for a balanced mix of ${this.candidateForm.value.questionsLength} questions that assess both foundational knowledge and practical application, just return questions strings`;

this.geminiService.askGemini(prompt).then(
(res: string) => {
const formattedResponse = this.quizHelperService.formatAiReponseQuizString(res);
this.formattedQuizResponse = formattedResponse;

this.candidateForm.reset();
this.loadingService.onLoadingToggle();
},
(error: Error) => {
console.error(error);
this.loadingService.onLoadingToggle();
}
);
} else {
console.log('Form is invalid');
}
}
}
16 changes: 16 additions & 0 deletions Angular/src/app/services/loading/loading.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable, Signal, signal } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class LoadingService {

isLoading = signal(false);

/**
* toggle the isLoading boolean signal
*/
onLoadingToggle() {
this.isLoading.set(!this.isLoading());
}
}