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
12 changes: 12 additions & 0 deletions src/app/models/open-ai.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenAiChatChoise>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<th>Slug</th>
<th>Создана</th>
<th>Обновлена</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
Expand All @@ -77,6 +78,15 @@
<td>{{ item.slug }}</td>
<td>{{ item.createdAt | date: "yyyy-MM-dd HH:mm" }}</td>
<td>{{ item.updatedAt | date: "yyyy-MM-dd HH:mm" }}</td>
<td>
<button
type="button"
class="btn-link-sm"
(click)="showAiAnalysis(item)"
>
AI Анализ
</button>
</td>
</tr>
</tbody>
</table>
Expand Down Expand Up @@ -150,3 +160,32 @@
</div>
</form>
</app-dialog>

<app-dialog
[additionalCss]="'modal-lg'"
[show]="!!aiAnalysisData"
(closed)="onAiAnalysisModalClose()"
[header]="'AI Анализ компании'"
>
<div *ngIf="aiAnalysisData">
<div class="p-2">
<textarea #aiAnalysisInput class="form-control" rows="30" readonly>{{
aiAnalysisJsonContent
}}</textarea>
</div>
<div class="p-1 d-flex justify-content-between">
<button
class="btn btn-outline-secondary"
(click)="onAiAnalysisModalClose()"
>
Закрыть
</button>
<button
class="btn btn-outline-dark"
(click)="copyAiAnalysis(aiAnalysisInput)"
>
<i class="{{ copyBtnIcon }}"></i> {{ copyBtnTitle }}
</button>
</div>
</div>
</app-dialog>
Original file line number Diff line number Diff line change
Expand Up @@ -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<CompaniesAdminPageComponent>;
let companiesService: CompaniesService;

beforeEach(async () => {
await TestBed.configureTestingModule({
Expand All @@ -26,6 +30,7 @@ describe("CompaniesAdminPageComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(CompaniesAdminPageComponent);
component = fixture.componentInstance;
companiesService = TestBed.inject(CompaniesService);
fixture.detectChanges();
});

Expand Down Expand Up @@ -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("Скопировано");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -31,6 +32,19 @@ export class CompaniesAdminPageComponent implements OnInit, OnDestroy {
itemToEdit: Company | null = null;
confirmDeletionMessage: DialogMessage<ConfirmMsg> | 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 &&
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,34 @@ describe("TelegramBotABoutComponent", () => {
let mockViewportScroller: jasmine.SpyObj<ViewportScroller>;

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({
declarations: [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<ViewportScroller>;
mockViewportScroller = TestBed.inject(
ViewportScroller,
) as jasmine.SpyObj<ViewportScroller>;
});

beforeEach(() => {
Expand All @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
7 changes: 7 additions & 0 deletions src/app/services/companies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -146,4 +147,10 @@ export class CompaniesService {
{},
);
}

getOpenAiAnalysis(companyIdentifier: string): Observable<OpenAiChatResult> {
return this.api.get<OpenAiChatResult>(
this.apiUrl + companyIdentifier + "/open-ai-analysis",
);
}
}