From c6a802458bc50afe4c5550420b4b160db488a3ad Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Wed, 6 Nov 2024 21:48:50 -0500 Subject: [PATCH 1/2] refactor: refactor chat component and gemini service --- Angular/src/app/pages/chat/chat.component.ts | 8 ++++---- .../gemini-google-ai/gemini-google-ai.service.ts | 10 ++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Angular/src/app/pages/chat/chat.component.ts b/Angular/src/app/pages/chat/chat.component.ts index 7827495..2787b8b 100644 --- a/Angular/src/app/pages/chat/chat.component.ts +++ b/Angular/src/app/pages/chat/chat.component.ts @@ -1,9 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { GeminiGoogleAiService } from '../../services/gemini-google-ai/gemini-google-ai.service'; -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClientModule } from '@angular/common/http'; @Component({ selector: 'app-chat', @@ -17,7 +17,7 @@ export class ChatComponent { userInput: string = ''; isLoading: boolean = false; - constructor(private geminiService: GeminiGoogleAiService) {} + readonly #geminiService = inject(GeminiGoogleAiService); async onSubmit() { if (!this.userInput.trim()) return; @@ -31,7 +31,7 @@ export class ChatComponent { // TODO: Handle errors try { - const response = await this.geminiService.askGemini(userMessage); + const response = await this.#geminiService.askGemini(userMessage); this.messages.push({ text: response, isUser: false }); } catch (err) { this.messages.push({ diff --git a/Angular/src/app/services/gemini-google-ai/gemini-google-ai.service.ts b/Angular/src/app/services/gemini-google-ai/gemini-google-ai.service.ts index 3f446d8..57b80f6 100644 --- a/Angular/src/app/services/gemini-google-ai/gemini-google-ai.service.ts +++ b/Angular/src/app/services/gemini-google-ai/gemini-google-ai.service.ts @@ -7,17 +7,15 @@ import { environment } from '../../../environments/environment'; }) export class GeminiGoogleAiService { // instance to initiate Gemini - Google Ai with API_KEY - private genAI: GoogleGenerativeAI; - - constructor() { - this.genAI = new GoogleGenerativeAI(environment.googleAiKey); - } + readonly #genAI: GoogleGenerativeAI = new GoogleGenerativeAI( + environment.googleAiKey, + ); /** * Communicate with Gemini - Google Ai using text prompt */ async askGemini(prompt: string): Promise { - const model: GenerativeModel = this.genAI.getGenerativeModel({ + const model: GenerativeModel = this.#genAI.getGenerativeModel({ model: 'gemini-1.5-flash', }); From 9887b72416d3ac270acaeca9c3ee868f282950a7 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Wed, 6 Nov 2024 22:13:10 -0500 Subject: [PATCH 2/2] feat(sentiment-analyzer-receipe) add receipe --- Angular/src/app/app.config.ts | 7 +- Angular/src/app/app.routes.ts | 17 +++- Angular/src/app/model/sentiment.response.ts | 26 ++++++ .../src/app/pages/chat/chat.component.scss | 18 +++- Angular/src/app/pages/chat/chat.component.ts | 2 - .../sentiment-analyzer.component.html | 38 ++++++++ .../sentiment-analyzer.component.scss | 92 +++++++++++++++++++ .../sentiment-analyzer.component.ts | 74 +++++++++++++++ .../gemini-google-sentiment-ai.service.ts | 28 ++++++ Angular/src/environments/environment.ts | 1 + 10 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 Angular/src/app/model/sentiment.response.ts create mode 100644 Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.html create mode 100644 Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.scss create mode 100644 Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.ts create mode 100644 Angular/src/app/services/gemini-google-ai/gemini-google-sentiment-ai.service.ts diff --git a/Angular/src/app/app.config.ts b/Angular/src/app/app.config.ts index a1e7d6f..1ff934a 100644 --- a/Angular/src/app/app.config.ts +++ b/Angular/src/app/app.config.ts @@ -2,7 +2,12 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; +import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideHttpClient(), + ], }; diff --git a/Angular/src/app/app.routes.ts b/Angular/src/app/app.routes.ts index e901880..2ce2073 100644 --- a/Angular/src/app/app.routes.ts +++ b/Angular/src/app/app.routes.ts @@ -9,9 +9,7 @@ export const routes: Routes = [ { path: 'home', loadComponent: () => - import('./pages/home/home.component').then( - (c) => c.HomeComponent, - ), + import('./pages/home/home.component').then((c) => c.HomeComponent), }, { path: 'starter', @@ -28,6 +26,15 @@ export const routes: Routes = [ { path: 'skill-quiz-generator', loadComponent: () => - import('./pages/skill-quiz-generator/skill-quiz-generator.component').then(c => c.SkillQuizGeneratorComponent) - } + import( + './pages/skill-quiz-generator/skill-quiz-generator.component' + ).then((c) => c.SkillQuizGeneratorComponent), + }, + { + path: 'sentiment-analyzer', + loadComponent: () => + import('./pages/sentiment-analyzer/sentiment-analyzer.component').then( + (c) => c.SentimentAnalyzerComponent, + ), + }, ]; diff --git a/Angular/src/app/model/sentiment.response.ts b/Angular/src/app/model/sentiment.response.ts new file mode 100644 index 0000000..81ef23f --- /dev/null +++ b/Angular/src/app/model/sentiment.response.ts @@ -0,0 +1,26 @@ +export interface SentimentResponse { + documentSentiment: DocumentSentiment; + languageCode: string; + sentences: Sentence[]; + languageSupported: boolean; +} + +export interface DocumentSentiment { + magnitude: number; + score: number; +} + +export interface Sentence { + text: Text; + sentiment: Sentiment; +} + +export interface Text { + content: string; + beginOffset: number; +} + +export interface Sentiment { + magnitude: number; + score: number; +} diff --git a/Angular/src/app/pages/chat/chat.component.scss b/Angular/src/app/pages/chat/chat.component.scss index d3e45a2..b04dc83 100644 --- a/Angular/src/app/pages/chat/chat.component.scss +++ b/Angular/src/app/pages/chat/chat.component.scss @@ -44,7 +44,8 @@ padding: 10px; } -input { +input, +textarea { flex: 1; padding: 12px; font-size: 1rem; @@ -74,3 +75,18 @@ input { transform: rotate(360deg); } } + +.bot-message.positive { + background-color: #28a745; + width: fit-content; +} + +.bot-message.negative { + background-color: #dc3545; + width: fit-content; +} + +.bot-message.neutral { + background-color: #6c757d; + width: fit-content; +} diff --git a/Angular/src/app/pages/chat/chat.component.ts b/Angular/src/app/pages/chat/chat.component.ts index 2787b8b..cfd037c 100644 --- a/Angular/src/app/pages/chat/chat.component.ts +++ b/Angular/src/app/pages/chat/chat.component.ts @@ -3,7 +3,6 @@ import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { GeminiGoogleAiService } from '../../services/gemini-google-ai/gemini-google-ai.service'; -import { HttpClientModule } from '@angular/common/http'; @Component({ selector: 'app-chat', @@ -29,7 +28,6 @@ export class ChatComponent { const userMessage = this.userInput; this.userInput = ''; - // TODO: Handle errors try { const response = await this.#geminiService.askGemini(userMessage); this.messages.push({ text: response, isUser: false }); diff --git a/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.html b/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.html new file mode 100644 index 0000000..d0e85a3 --- /dev/null +++ b/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.html @@ -0,0 +1,38 @@ +
+
+ @for (msg of messages; track msg) { + @if (msg.isUser) { +
+ {{ msg.text }} +
+ } @else { + @if (msg.error) { +
+ {{ msg.error }} +
+ } @else { +
+ Score: {{ msg.response?.documentSentiment?.score }}
+ Magnitude: {{ msg.response?.documentSentiment?.magnitude }} +
+ } + } + } + + @if (isLoading) { +
+ } +
+ +
+
+
diff --git a/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.scss b/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.scss new file mode 100644 index 0000000..b04dc83 --- /dev/null +++ b/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.scss @@ -0,0 +1,92 @@ +.chat-container { + display: flex; + flex-direction: column; + height: 100%; + max-width: 600px; + margin: 0 auto; + border: 1px solid #ddd; + border-radius: 8px; +} + +.messages { + flex: 1; + overflow-y: auto; + padding: 10px; + max-height: 600px; +} + +.user-message, +.bot-message { + padding: 8px 12px; + border-radius: 8px; + margin-bottom: 10px; + word-wrap: break-word; +} + +.user-message { + margin-left: auto; + align-self: flex-end; + width: fit-content; + background-color: #cce7ff; + text-align: right; +} + +.bot-message { + width: 80%; + align-self: flex-start; + background-color: #f0f0f0; + text-align: left; +} + +.input-form { + display: flex; + border-top: 1px solid #ddd; + padding: 10px; +} + +input, +textarea { + flex: 1; + padding: 12px; + font-size: 1rem; + border: none; + outline: none; + color: #ffffff; + background-color: #333333; + border-radius: 20px; +} + +/* Loader styles */ +.loader { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 24px; + height: 24px; + animation: spin 1s linear infinite; + margin: 10px auto; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.bot-message.positive { + background-color: #28a745; + width: fit-content; +} + +.bot-message.negative { + background-color: #dc3545; + width: fit-content; +} + +.bot-message.neutral { + background-color: #6c757d; + width: fit-content; +} diff --git a/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.ts b/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.ts new file mode 100644 index 0000000..f9fa506 --- /dev/null +++ b/Angular/src/app/pages/sentiment-analyzer/sentiment-analyzer.component.ts @@ -0,0 +1,74 @@ +import { Component, inject } from '@angular/core'; +import { SentimentResponse } from '../../model/sentiment.response'; +import { GeminiGoogleSentimentAiService } from '../../services/gemini-google-ai/gemini-google-sentiment-ai.service'; +import { FormsModule } from '@angular/forms'; +import { catchError, EMPTY, take } from 'rxjs'; +import { CommonModule } from '@angular/common'; + +@Component({ + standalone: true, + imports: [FormsModule, CommonModule], + selector: 'app-sentiment-analyzer', + templateUrl: './sentiment-analyzer.component.html', + styleUrls: ['./sentiment-analyzer.component.scss'], +}) +export class SentimentAnalyzerComponent { + messages: { + text?: string; + isUser: boolean; + response?: SentimentResponse; + error?: string; + }[] = []; + + userInput: string = ''; + isLoading: boolean = false; + + readonly #sentimentService: GeminiGoogleSentimentAiService = inject( + GeminiGoogleSentimentAiService, + ); + + onSubmit(): void { + if (!this.userInput.trim()) return; + + this.isLoading = true; + + this.messages.push({ text: this.userInput, isUser: true }); + + const userMessage = this.userInput; + this.userInput = ''; + + this.#sentimentService + .generateSentimentAnalysis(userMessage) + .pipe( + catchError((err) => { + this.isLoading = false; + this.messages.push({ + error: + 'Failed to analyze sentiment. Please ensure that you have added your API key and try again.', + isUser: false, + }); + return EMPTY; + }), + take(1), + ) + .subscribe((response) => { + this.isLoading = false; + this.messages.push({ response: response, isUser: false }); + }); + } + + // Helper method to get CSS class based on sentiment score + getSentimentClass(score: number | undefined): string { + if (score === undefined) { + return ''; + } + + if (score > 0) { + return 'positive'; + } else if (score < 0) { + return 'negative'; + } else { + return 'neutral'; + } + } +} diff --git a/Angular/src/app/services/gemini-google-ai/gemini-google-sentiment-ai.service.ts b/Angular/src/app/services/gemini-google-ai/gemini-google-sentiment-ai.service.ts new file mode 100644 index 0000000..3b1cbd4 --- /dev/null +++ b/Angular/src/app/services/gemini-google-ai/gemini-google-sentiment-ai.service.ts @@ -0,0 +1,28 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { SentimentResponse } from '../../model/sentiment.response'; +import { Observable, of } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class GeminiGoogleSentimentAiService { + readonly #httpClient = inject(HttpClient); + readonly #sentimentApiUrl = `https://language.googleapis.com/v1/documents:analyzeSentiment?key=${environment.sentimentKey}`; + + generateSentimentAnalysis(prompt: string): Observable { + const body = { + document: { + type: 'PLAIN_TEXT', + content: prompt, + }, + encodingType: 'UTF8', + }; + + return this.#httpClient.post( + this.#sentimentApiUrl, + body, + ); + } +} diff --git a/Angular/src/environments/environment.ts b/Angular/src/environments/environment.ts index f8fd994..2ac9a4c 100644 --- a/Angular/src/environments/environment.ts +++ b/Angular/src/environments/environment.ts @@ -1,4 +1,5 @@ export const environment = { production: false, googleAiKey: '', + sentimentKey: '', };