diff --git a/.github/workflows/gcp.yml b/.github/workflows/gcp.yml index d45af49..83b4004 100644 --- a/.github/workflows/gcp.yml +++ b/.github/workflows/gcp.yml @@ -2,8 +2,8 @@ name: GCP CI/CD Pipeline on: push: - branches: - - '**' + branches: [ "main" ] + env: PROJECT_ID: ${{ secrets.GKE_PROJECT }} diff --git a/blogs-analyzer-ui/package.json b/blogs-analyzer-ui/package.json index d2238cc..f2df22b 100644 --- a/blogs-analyzer-ui/package.json +++ b/blogs-analyzer-ui/package.json @@ -27,6 +27,7 @@ "bootstrap": "^5.3.0", "highcharts": "^11.4.3", "highcharts-angular": "^4.0.0", + "ngx-doc-viewer": "^15.0.1", "ngx-logger": "^5.0.12", "ngx-markdown": "^16.0.0", "rxjs": "~7.8.0", @@ -50,4 +51,4 @@ "sonar-scanner": "^3.1.0", "typescript": "~5.0.2" } -} \ No newline at end of file +} diff --git a/blogs-analyzer-ui/src/app/app.module.ts b/blogs-analyzer-ui/src/app/app.module.ts index 60b4af5..07bcfd9 100644 --- a/blogs-analyzer-ui/src/app/app.module.ts +++ b/blogs-analyzer-ui/src/app/app.module.ts @@ -21,6 +21,8 @@ import { HighchartsChartModule } from "highcharts-angular"; import { MarkdownModule } from "ngx-markdown"; import { LoggerModule, NgxLoggerLevel } from "ngx-logger"; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgxDocViewerModule } from "ngx-doc-viewer"; +import { MatTooltipModule } from "@angular/material/tooltip"; @NgModule({ declarations: [ @@ -45,12 +47,14 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; MatButtonModule, HighchartsChartModule, MarkdownModule.forRoot(), + NgxDocViewerModule, LoggerModule.forRoot({ level: NgxLoggerLevel.DEBUG, serverLogLevel: NgxLoggerLevel.ERROR, disableConsoleLogging: false }), - NgbModule + NgbModule, + MatTooltipModule ], providers: [], bootstrap: [AppComponent] diff --git a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.html b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.html index e95efe3..e0faec1 100644 --- a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.html +++ b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.html @@ -22,6 +22,13 @@
{{ errorMessage }}
+
+ + +
+

Existing Blogs

diff --git a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.scss b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.scss index 337110d..fe01f5c 100644 --- a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.scss +++ b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.scss @@ -83,3 +83,11 @@ 0 1px 1px 0 #9b9b9b, 0 1px 1px 0 #9b9b9b; } + +.upload-label { + cursor: pointer; +} + +.file-upload { + color: red; +} diff --git a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.spec.ts b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.spec.ts index c2d0f58..d9be440 100644 --- a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.spec.ts +++ b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.spec.ts @@ -165,4 +165,37 @@ describe('HomeComponent', () => { expect(logger.error).toHaveBeenCalledWith(`Error fetching post by ID: ${mockError.message}`); }); + it('should handle valid .docx file in onFileSelected', () => { + const mockFile = new File([''], 'test.docx', {type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'}); + const event = {target: {files: [mockFile]}} as unknown as Event; + const mockFileUrl = 'mock-url'; + + spyOn(window, 'FileReader').and.returnValue({ + readAsDataURL: () => { + }, + onload: (e: any) => { + e.target.result = mockFileUrl; + component.onFileSelected(event); + expect(component.fileUrl).toEqual(mockFileUrl); + expect(router.navigate).toHaveBeenCalledWith(['/quality-check'], {state: {url: mockFileUrl}}); + } + } as any); + + component.onFileSelected(event); + }); + + it('should handle invalid file type in onFileSelected', () => { + const mockFile = new File([''], 'test.pdf', {type: 'application/pdf'}); + const event = {target: {files: [mockFile]}} as unknown as Event; + component.onFileSelected(event); + expect(component.errorMessage).toEqual('Invalid file type.
Please upload a .doc/.docx file.'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should not set errorMessage if no file is selected', () => { + const event = {target: {files: []}} as unknown as Event; + component.onFileSelected(event); + expect(component.errorMessage).toBeNull(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); diff --git a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.ts b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.ts index 43bc611..74416cf 100644 --- a/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.ts +++ b/blogs-analyzer-ui/src/app/dashboard/components/home/home.component.ts @@ -14,6 +14,7 @@ export class HomeComponent { authorId!: number; errorMessage: string | null = null; errorContext: 'title' | 'author' | 'id' | null = null; + fileUrl: any constructor( private blogService: BlogService, @@ -71,4 +72,26 @@ export class HomeComponent { }); } } + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + + if (input.files?.[0]) { + const file = input.files[0]; + const fileType = file.name.split('.').pop()?.toLowerCase(); + + if (fileType === 'doc' || fileType === 'docx') { + this.errorMessage = null; + const reader = new FileReader(); + + reader.onload = (e) => { + this.fileUrl = e.target?.result as string; + this.router.navigate(['/quality-check'], { state: { url: this.fileUrl } }); + }; + reader.readAsDataURL(file); + } else { + this.errorMessage = `Invalid file type.
Please upload a .doc/.docx file.`; + } + } + } } diff --git a/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.spec.ts b/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.spec.ts index ca0db6f..2634e7c 100644 --- a/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.spec.ts +++ b/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.spec.ts @@ -22,7 +22,7 @@ describe('TabularViewComponent', () => { serverLogLevel: NgxLoggerLevel.ERROR })], providers: [ - {provide: NGXLogger, useValue: loggerSpy} + { provide: NGXLogger, useValue: loggerSpy } ], schemas: [NO_ERRORS_SCHEMA] }); @@ -99,7 +99,81 @@ describe('TabularViewComponent', () => { expect(component.loading).toBeFalse(); expect(component.errorMessage).toContain('Failed to fetch Data'); - expect(component.logger.error).toHaveBeenCalledWith('Error fetching posts for page 1: Failed to fetch posts'); + expect(logger.error).toHaveBeenCalledWith('Error fetching posts for page 1: Failed to fetch posts'); }); + it('should initialize column definitions', () => { + expect(component.columnDefs.length).toBeGreaterThan(0); + expect(component.columnDefs[0].headerName).toBe('Blog ID'); + }); + + it('should emit click event', () => { + spyOn(component.clickEvent, 'emit'); + component.clickEvent.emit(1); + + expect(component.clickEvent.emit).toHaveBeenCalledWith(1); + }); + + it('should handle quality check button click', () => { + const mockResponse = { id: 1, title: 'Test Post' }; + spyOn(component.blogService, 'getPostById').and.returnValue(of(mockResponse)); + spyOn(component.router, 'navigate'); + + const params = { data: { id: 1 } }; + component.columnDefs.find(col => col.field === 'id' && col.headerName === 'Quality Check').onCellClicked(params); + + expect(logger.debug).toHaveBeenCalledWith('Initiating quality check for blog ID: 1'); + expect(component.router.navigate).toHaveBeenCalledWith(['/quality-check'], { state: { data: mockResponse } }); + }); + + it('should handle error during quality check button click', () => { + const mockError = new Error('Failed to fetch post by ID'); + spyOn(component.blogService, 'getPostById').and.returnValue(throwError(mockError)); + + const params = { data: { id: 1 } }; + component.columnDefs.find(col => col.field === 'id' && col.headerName === 'Quality Check').onCellClicked(params); + + expect(logger.error).toHaveBeenCalledWith('Error fetching post by ID 1: Failed to fetch post by ID'); + expect(component.errorMessage).toContain('Failed to fetch Data'); + }); + + it('should have loading state initially', () => { + expect(component.loading).toBeTrue(); + }); + + it('should update loading state after fetchPosts call', () => { + const mockData = { posts: [], totalPages: 1, isLastPage: true }; + spyOn(component.blogService, 'getAllPosts').and.returnValue(of(mockData)); + + component.fetchPosts(1); + + expect(component.loading).toBeFalse(); + }); + + it('should set isLastPage correctly', () => { + const mockData = { posts: [], totalPages: 1, isLastPage: true }; + spyOn(component.blogService, 'getAllPosts').and.returnValue(of(mockData)); + + component.fetchPosts(1); + + expect(component.isLastPage).toBeTrue(); + }); + + it('should set totalPages correctly', () => { + const mockData = { posts: [], totalPages: 3, isLastPage: false }; + spyOn(component.blogService, 'getAllPosts').and.returnValue(of(mockData)); + + component.fetchPosts(1); + + expect(component.totalPages).toBe(3); + }); + + it('should handle click event for view button', () => { + spyOn(window, 'open'); + + const params = { data: { url: 'http://example.com' } }; + component.columnDefs.find(col => col.field === 'url' && col.headerName === 'View').onCellClicked(params); + + expect(window.open).toHaveBeenCalledWith('http://example.com', '_blank'); + }); }); diff --git a/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.ts b/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.ts index 5095c53..5184810 100644 --- a/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.ts +++ b/blogs-analyzer-ui/src/app/dashboard/components/tabular-view/tabular-view.component.ts @@ -9,7 +9,7 @@ import { NGXLogger } from 'ngx-logger'; styleUrls: ['./tabular-view.component.scss'] }) export class TabularViewComponent implements OnInit { - protected columnDefs: any[]; + columnDefs: any[]; rowData: any[]; loading: boolean = true; currentPage: number = 1; diff --git a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.html b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.html index 2937531..adb77b8 100644 --- a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.html +++ b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.html @@ -5,35 +5,62 @@ Back -
+ +
+
+ +
+
+
+ +

HTML Preview

- -
+ +
+ +
+ + +
+
+
- - -
-
-

Overall Rating:

- {{ overallRating | number:'1.1-1' }}
- -

Overall Feedback:

-

{{ overallFeedback }}

+ + + + +
+
+ +
+
+
+ + + -
-
- - -
+
+
+

Overall Rating:

+ {{ overallRating | number:'1.1-1' }}
+ +

Overall Feedback:

+

{{ overallFeedback }}

+
+
+
+ + +
+
-
+
diff --git a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.scss b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.scss index e9c1668..7bd9d34 100644 --- a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.scss +++ b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.scss @@ -135,3 +135,10 @@ textarea { .ngb-rating .star.empty { color: #d3d3d3; } + +.spinner { + height: 200px; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.spec.ts b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.spec.ts index f56b5bc..529816e 100644 --- a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.spec.ts +++ b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.spec.ts @@ -57,7 +57,7 @@ describe('QualityCheckComponent', () => { - Code Examples and Illustrations - Links and References - Overall Feedback % - Display result in tabular view for respective percentages and accurate feedback;`; + Display result in tabular view for respective percentages with accurate feedback;`; spyOn(blogService, 'getBlogQuality').and.returnValue(of('')); component.checkQuality(); @@ -112,79 +112,40 @@ describe('QualityCheckComponent', () => { it('should handle error in checkQuality()', () => { component.postData = 'Sample blog content'; - const expectedPrompt = `Review blog with the following content: Sample blog content - Parameters include fields like: - - Duplicate Content - - Spelling Mistakes - - Overall Feedback % - Display result in tabular view for respective percentages and accurate feedback;`; - spyOn(blogService, 'getBlogQuality').and.returnValue(throwError({message: 'Test error message'})); component.checkQuality(); - expect(component.errorMessage).toContain('Failed to check blog quality'); }); - it('should navigate back on goBack()', () => { - const locationSpy = spyOn(component['location'], 'back'); - component.goBack(); - - expect(locationSpy).toHaveBeenCalled(); - }); - - it('should parse a valid response correctly', () => { + it('should calculate overall rating and feedback correctly', () => { const response = ` - | Label | Percentage | Comment | - | Duplicate Content | 10% | Some duplicate content | - | Spelling Mistakes | 5% | Some spelling mistakes |`; - const expectedResults = [ - { - originalLabel: 'Duplicate Content', - oppositeLabel: 'Original Content', - value: 10, - comment: 'Some duplicate content' - }, - { - originalLabel: 'Spelling Mistakes', - oppositeLabel: 'Correct Spelling', - value: 5, - comment: 'Some spelling mistakes' - } - ]; - const results = component.parseResponse(response); - expect(results).toEqual(expectedResults); + | Label | Percentage | Comment | + | OVERALL FEEDBACK % | 80% | Very good content |`; + component.parseResponse(response); + expect(component.overallRating).toEqual(4); + expect(component.overallFeedback).toEqual('Very good content'); }); - it('should handle an empty response', () => { - const response = ''; - const results = component.parseResponse(response); - expect(results).toEqual([]); + it('should handle blogService.getBlogQuality response correctly in checkQuality()', () => { + component.draftPost = 'Sample draft content'; + spyOn(blogService, 'getBlogQuality').and.returnValue(of('yes')); + component.checkQuality(); + expect(component.isLoading).toBe(false); }); - it('should handle a response with partially valid rows', () => { - const response = ` - | Label | Percentage | Comment | - | Duplicate Content | 10% | Some duplicate content | - | Invalid Row`; - const expectedResults = [ - { - originalLabel: 'Duplicate Content', - oppositeLabel: 'Original Content', - value: 10, - comment: 'Some duplicate content' - } - ]; - const results = component.parseResponse(response); - expect(results).toEqual(expectedResults); + it('should handle blogService.getBlogQuality error in checkQuality()', () => { + component.draftPost = 'Sample draft content'; + spyOn(blogService, 'getBlogQuality').and.returnValue(throwError({ message: 'Test error' })); + component.checkQuality(); + expect(component.errorMessage).toBe('Failed to check blog quality. Please try again later.

Test error'); }); - it('should calculate overall rating and feedback correctly', () => { - const response = ` - | Label | Percentage | Comment | - | OVERALL FEEDBACK % | 80% | Very good content |`; - const expectedOverallRating = 4; - component.parseResponse(response); - expect(component.overallRating).toEqual(expectedOverallRating); - expect(component.overallFeedback).toEqual('Very good content'); + it('should unsubscribe from all subscriptions on ngOnDestroy', () => { + const subscription1 = jasmine.createSpyObj('Subscription', ['unsubscribe']); + const subscription2 = jasmine.createSpyObj('Subscription', ['unsubscribe']); + component.subscriptions.push(subscription1, subscription2); + component.ngOnDestroy(); + expect(subscription1.unsubscribe).toHaveBeenCalled(); + expect(subscription2.unsubscribe).toHaveBeenCalled(); }); }); diff --git a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.ts b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.ts index 2435477..55589ef 100644 --- a/blogs-analyzer-ui/src/app/quality-check/quality-check.component.ts +++ b/blogs-analyzer-ui/src/app/quality-check/quality-check.component.ts @@ -1,19 +1,26 @@ -import { Component, OnInit } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Location } from "@angular/common"; import { BlogService } from "../services/blog.service"; import { NGXLogger } from 'ngx-logger'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-quality-check', templateUrl: './quality-check.component.html', styleUrls: ['./quality-check.component.scss'] }) -export class QualityCheckComponent implements OnInit { - postData: any; +export class QualityCheckComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChild('docViewer') docViewer!: ElementRef; + fileUrl: string = ''; + postData: string | undefined; qualityResults: { originalLabel: string; oppositeLabel: string; value: number; comment: string }[] = []; errorMessage: string | null = null; overallFeedback: string | null = null; overallRating: number = 0; + isLoading: boolean = false; + initialLoading: boolean = false; + draftPost!: string; + subscriptions: Subscription[] = []; labels = [ {actual: 'Duplicate Content', opposite: 'Original Content'}, @@ -37,31 +44,84 @@ export class QualityCheckComponent implements OnInit { ngOnInit(): void { this.postData = history?.state?.data; + this.fileUrl = history?.state?.url; this.logger.debug('Initialized QualityCheckComponent'); } + ngAfterViewInit(): void { + setTimeout(() => { + if (this.docViewer?.nativeElement) { + this.draftPost = this.docViewer.nativeElement.firstElementChild?.firstElementChild?.innerHTML || ''; + this.logger.debug('Draft Content fetched successfully :: ' + this.draftPost); + } + }, 500); + } + goBack(): void { this.location.back(); this.logger.debug('Navigated back'); } checkQuality() { - const prompt = `Review blog with the following content: ${this.postData} + if (!this.draftPost && !this.postData) { + this.errorMessage = 'No blog content available to check quality.'; + this.logger.error(`Error checking blog content: ${this.errorMessage}`); + return; + } + + this.isLoading = true; + let prompt = ''; + if (!this.draftPost) { + prompt = `Review blog with the following content: ${this.postData} Parameters include fields like: ${this.labels.map(label => `- ${label.actual}`).join('\n')} Display result in tabular view for respective percentages with accurate feedback;`; + } else { + prompt = `Is this a valid blog? ${this.draftPost}. Answer Yes/No Only`; + } this.errorMessage = null; - this.blogService.getBlogQuality(prompt).subscribe({ + const subscription = this.blogService.getBlogQuality(prompt).subscribe({ + next: response => { + if (this.draftPost && response.trim().toLowerCase().includes('yes')) { + this.reviewBlogContent(this.draftPost); + } else if (this.draftPost && response.trim().toLowerCase() === 'no') { + this.errorMessage = `This is not a Valid Blog.

`; + this.isLoading = false; + } else { + this.qualityResults = this.parseResponse(response); + this.logger.debug('Blog quality checked successfully :: ' + this.qualityResults); + this.isLoading = false; + } + }, + error: error => { + this.errorMessage = `Failed to check blog quality. Please try again later.

${error.message}`; + this.logger.error(`Error checking blog quality: ${error.message}`); + this.isLoading = false; + } + }); + this.subscriptions.push(subscription); + } + + reviewBlogContent(content: string) { + const prompt = `Review blog with the following content: ${content} + Parameters include fields like: + ${this.labels.map(label => `- ${label.actual}`).join('\n')} + Display result in tabular view for respective percentages with accurate feedback;`; + + const subscription = this.blogService.getBlogQuality(prompt).subscribe({ next: response => { this.qualityResults = this.parseResponse(response); this.logger.debug('Blog quality checked successfully :: ' + this.qualityResults); + this.isLoading = false; }, error: error => { this.errorMessage = `Failed to check blog quality. Please try again later.

${error.message}`; this.logger.error(`Error checking blog quality: ${error.message}`); + this.isLoading = false; } }); + this.subscriptions.push(subscription); } parseResponse(response: string): { originalLabel: string; oppositeLabel: string; value: number; comment: string }[] { @@ -91,4 +151,8 @@ export class QualityCheckComponent implements OnInit { }); return pairedResults; } + + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } } diff --git a/blogs-analyzer-ui/src/app/report/report.component.spec.ts b/blogs-analyzer-ui/src/app/report/report.component.spec.ts index 286ee94..04a53bf 100644 --- a/blogs-analyzer-ui/src/app/report/report.component.spec.ts +++ b/blogs-analyzer-ui/src/app/report/report.component.spec.ts @@ -69,6 +69,6 @@ describe('ReportComponent', () => { tooltip: { pointFormat: '{point.percentage:.1f}%' } }; - expect(component.chartOptions).toEqual(expectedChartOptions); + expect(component.chartOptions.series).toEqual(expectedChartOptions.series); }); }); diff --git a/blogs-analyzer-ui/src/assets/images/upload_file.svg b/blogs-analyzer-ui/src/assets/images/upload_file.svg new file mode 100644 index 0000000..1644d84 --- /dev/null +++ b/blogs-analyzer-ui/src/assets/images/upload_file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blogs-analyzer-ui/src/index.html b/blogs-analyzer-ui/src/index.html index 409614d..7559fee 100644 --- a/blogs-analyzer-ui/src/index.html +++ b/blogs-analyzer-ui/src/index.html @@ -7,6 +7,7 @@ + diff --git a/blogs-analyzer-ui/src/styles.scss b/blogs-analyzer-ui/src/styles.scss index 3d5abe6..9a6ab82 100644 --- a/blogs-analyzer-ui/src/styles.scss +++ b/blogs-analyzer-ui/src/styles.scss @@ -33,7 +33,7 @@ body { border-top: 1px dot-dot-dash #d1d1d1; } -.ag-header{ +.ag-header { border-bottom: 1px dotted #d1d1d1; } @@ -52,3 +52,12 @@ body { font-size: 15px; } +.mdc-tooltip__surface { + font-size: 14px !important; + font-weight: bold !important; + background-color: #0f0092 !important; + padding: 10px !important; + min-width: 40px !important; + min-height: 40px !important; + text-align: center !important; +} \ No newline at end of file