diff --git a/src/app/models/open-ai.model.ts b/src/app/models/open-ai.model.ts index fd4d37a..9875261 100644 --- a/src/app/models/open-ai.model.ts +++ b/src/app/models/open-ai.model.ts @@ -34,3 +34,15 @@ export interface OpenAiAnalysis { bearer: string; report: OpenAiReport; } + +export interface OpenAiChatChoise { + message: { + role: string; + content: string; + }; +} + +export interface OpenAiChatResult { + isSuccess: boolean; + choises: Array; +} diff --git a/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.html b/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.html index 5a377b9..5ac9212 100644 --- a/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.html +++ b/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.html @@ -59,6 +59,7 @@ Slug Создана Обновлена + Действия @@ -77,6 +78,15 @@ {{ item.slug }} {{ item.createdAt | date: "yyyy-MM-dd HH:mm" }} {{ item.updatedAt | date: "yyyy-MM-dd HH:mm" }} + + + @@ -150,3 +160,32 @@ + + +
+
+ +
+
+ + +
+
+
diff --git a/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.spec.ts b/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.spec.ts index 0d8d10f..9a721a5 100644 --- a/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.spec.ts +++ b/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.spec.ts @@ -9,10 +9,14 @@ import { import { CompaniesAdminPageComponent } from "./companies-admin-page.component"; import { CompaniesService } from "@services/companies.service"; +import { of } from "rxjs"; +import { Company } from "@models/companies.model"; +import { OpenAiChatResult } from "@models/open-ai.model"; describe("CompaniesAdminPageComponent", () => { let component: CompaniesAdminPageComponent; let fixture: ComponentFixture; + let companiesService: CompaniesService; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -26,6 +30,7 @@ describe("CompaniesAdminPageComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(CompaniesAdminPageComponent); component = fixture.componentInstance; + companiesService = TestBed.inject(CompaniesService); fixture.detectChanges(); }); @@ -63,4 +68,62 @@ describe("CompaniesAdminPageComponent", () => { expect(component.search).toHaveBeenCalled(); }); + + it("should show AI analysis modal when showAiAnalysis is called", () => { + const mockCompany: Company = { + id: "1", + name: "Test Company", + slug: "test-company", + } as Company; + + const mockAiResult: OpenAiChatResult = { + isSuccess: true, + choises: [ + { + message: { + role: "assistant", + content: "Test analysis", + }, + }, + ], + }; + + spyOn(companiesService, "getOpenAiAnalysis").and.returnValue( + of(mockAiResult), + ); + + component.showAiAnalysis(mockCompany); + + expect(companiesService.getOpenAiAnalysis).toHaveBeenCalledWith( + "test-company", + ); + expect(component.aiAnalysisData).toEqual(mockAiResult); + expect(component.aiAnalysisJsonContent).toContain("Test analysis"); + }); + + it("should close AI analysis modal", () => { + component.aiAnalysisData = {} as any; + component.aiAnalysisJsonContent = "test content"; + + component.onAiAnalysisModalClose(); + + expect(component.aiAnalysisData).toBeNull(); + expect(component.aiAnalysisJsonContent).toBe(""); + }); + + it("should copy AI analysis content", () => { + const mockInputElement = { + select: jasmine.createSpy("select"), + setSelectionRange: jasmine.createSpy("setSelectionRange"), + }; + + spyOn(document, "execCommand"); + + component.copyAiAnalysis(mockInputElement); + + expect(mockInputElement.select).toHaveBeenCalled(); + expect(document.execCommand).toHaveBeenCalledWith("copy"); + expect(mockInputElement.setSelectionRange).toHaveBeenCalledWith(0, 0); + expect(component.copyBtnTitle).toBe("Скопировано"); + }); }); diff --git a/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.ts b/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.ts index 13b5273..0c260e3 100644 --- a/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.ts +++ b/src/app/modules/admin/components/companies/companies-admin-page/companies-admin-page.component.ts @@ -13,6 +13,7 @@ import { EditCompanyForm } from "../shared/edit-company-form"; import { AlertService } from "@shared/components/alert/services/alert.service"; import { DialogMessage } from "@shared/components/dialogs/models/dialog-message"; import { ConfirmMsg } from "@shared/components/dialogs/models/confirm-msg"; +import { OpenAiChatResult } from "@models/open-ai.model"; @Component({ templateUrl: "./companies-admin-page.component.html", @@ -31,6 +32,19 @@ export class CompaniesAdminPageComponent implements OnInit, OnDestroy { itemToEdit: Company | null = null; confirmDeletionMessage: DialogMessage | null = null; + // AI Analysis modal properties + aiAnalysisData: OpenAiChatResult | null = null; + aiAnalysisJsonContent: string = ""; + + // Copy button properties (similar to interview markdown modal) + private readonly copyBtnDefaultTitle = "Копировать"; + private readonly copyBtnDefaultIcon = "bi bi-clipboard2-check"; + private readonly copiedBtnTitle = "Скопировано"; + private readonly copiedBtnIcon = "bi bi-check2"; + + copyBtnTitle = this.copyBtnDefaultTitle; + copyBtnIcon = this.copyBtnDefaultIcon; + get searchButtonShouldBeEnabled(): boolean { return ( this.searchQuery != null && @@ -161,4 +175,40 @@ export class CompaniesAdminPageComponent implements OnInit, OnDestroy { ), ); } + + showAiAnalysis(company: Company): void { + this.service + .getOpenAiAnalysis(company.slug) + .pipe(untilDestroyed(this)) + .subscribe({ + next: (result) => { + this.aiAnalysisData = result; + this.aiAnalysisJsonContent = JSON.stringify(result, null, 2); + }, + error: (error) => { + this.alert.error("Ошибка при получении AI анализа: " + error.message); + }, + }); + } + + onAiAnalysisModalClose(): void { + this.aiAnalysisData = null; + this.aiAnalysisJsonContent = ""; + this.copyBtnTitle = this.copyBtnDefaultTitle; + this.copyBtnIcon = this.copyBtnDefaultIcon; + } + + copyAiAnalysis(inputElement: any): void { + inputElement.select(); + document.execCommand("copy"); + inputElement.setSelectionRange(0, 0); + + this.copyBtnTitle = this.copiedBtnTitle; + this.copyBtnIcon = this.copiedBtnIcon; + + setTimeout(() => { + this.copyBtnTitle = this.copyBtnDefaultTitle; + this.copyBtnIcon = this.copyBtnDefaultIcon; + }, 1000); + } } diff --git a/src/app/modules/home/components/telegram-bot/telegram-bot.component.spec.ts b/src/app/modules/home/components/telegram-bot/telegram-bot.component.spec.ts index 2079c9c..ca70733 100644 --- a/src/app/modules/home/components/telegram-bot/telegram-bot.component.spec.ts +++ b/src/app/modules/home/components/telegram-bot/telegram-bot.component.spec.ts @@ -18,15 +18,15 @@ describe("TelegramBotABoutComponent", () => { let mockViewportScroller: jasmine.SpyObj; beforeEach(async () => { - const viewportScrollerSpy = jasmine.createSpyObj('ViewportScroller', [ - 'scrollToAnchor', + const viewportScrollerSpy = jasmine.createSpyObj("ViewportScroller", [ + "scrollToAnchor", ]); - + mockActivatedRoute = { fragment: of(null), paramMap: of(new Map()), queryParams: of({}), - snapshot: { fragment: "" } + snapshot: { fragment: "" }, }; await TestBed.configureTestingModule({ @@ -34,14 +34,18 @@ describe("TelegramBotABoutComponent", () => { schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [...mostUsedImports], providers: [ - ...testUtilStubs.filter(provider => provider.provide !== ActivatedRoute), + ...testUtilStubs.filter( + (provider) => provider.provide !== ActivatedRoute, + ), ...mostUsedServices, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: ViewportScroller, useValue: viewportScrollerSpy }, ], }).compileComponents(); - mockViewportScroller = TestBed.inject(ViewportScroller) as jasmine.SpyObj; + mockViewportScroller = TestBed.inject( + ViewportScroller, + ) as jasmine.SpyObj; }); beforeEach(() => { @@ -55,22 +59,24 @@ describe("TelegramBotABoutComponent", () => { }); it("should scroll to anchor when fragment is provided", (done) => { - const testFragment = 'techinterview-salary-assistant-header'; + const testFragment = "techinterview-salary-assistant-header"; mockActivatedRoute.fragment = of(testFragment); - + component.ngOnInit(); - + setTimeout(() => { - expect(mockViewportScroller.scrollToAnchor).toHaveBeenCalledWith(testFragment); + expect(mockViewportScroller.scrollToAnchor).toHaveBeenCalledWith( + testFragment, + ); done(); }, 150); }); it("should not scroll when no fragment is provided", () => { mockActivatedRoute.fragment = of(null); - + component.ngOnInit(); - + expect(mockViewportScroller.scrollToAnchor).not.toHaveBeenCalled(); }); }); diff --git a/src/app/modules/home/components/telegram-bot/telegram-bot.component.ts b/src/app/modules/home/components/telegram-bot/telegram-bot.component.ts index 7344cbd..6837360 100644 --- a/src/app/modules/home/components/telegram-bot/telegram-bot.component.ts +++ b/src/app/modules/home/components/telegram-bot/telegram-bot.component.ts @@ -15,7 +15,7 @@ export class TelegramBotABoutComponent implements OnInit, OnDestroy { constructor( private readonly titleService: TitleService, private readonly route: ActivatedRoute, - private readonly viewportScroller: ViewportScroller + private readonly viewportScroller: ViewportScroller, ) { this.titleService.setTitle("О ботах в Telegram"); } diff --git a/src/app/services/companies.service.ts b/src/app/services/companies.service.ts index b6607b5..9452173 100644 --- a/src/app/services/companies.service.ts +++ b/src/app/services/companies.service.ts @@ -9,6 +9,7 @@ import { import { PageParams } from "@models/page-params"; import { ConvertObjectToHttpParams } from "@shared/value-objects/convert-object-to-http"; import { PaginatedList } from "@models/paginated-list"; +import { OpenAiChatResult } from "@models/open-ai.model"; export interface CompaniesSearchParams extends PageParams { searchQuery: string | null; @@ -146,4 +147,10 @@ export class CompaniesService { {}, ); } + + getOpenAiAnalysis(companyIdentifier: string): Observable { + return this.api.get( + this.apiUrl + companyIdentifier + "/open-ai-analysis", + ); + } }