LLM reccomendation service

This commit is contained in:
2026-04-08 19:07:09 -07:00
parent af4ec3f751
commit 41d0dc8d40
12 changed files with 524 additions and 169 deletions
@@ -1,4 +1,15 @@
<app-modal-shell [title]="modalTitle" (closed)="closed.emit()">
@if (!isEditMode) {
<div class="ai-suggest-row">
<button class="btn-ai" (click)="onGetRecommendation()" [disabled]="recommending() || saving()">
{{ recommending() ? 'Generating...' : '✦ Suggest with AI' }}
</button>
@if (recommendError()) {
<p class="recommend-error">{{ recommendError() }}</p>
}
</div>
}
<div class="field">
<label class="field-label">Benchmark</label>
<textarea class="field-input field-textarea" [(ngModel)]="benchmarkText"
@@ -16,7 +27,7 @@
<div class="modal-actions">
<button class="btn-secondary" (click)="closed.emit()">Cancel</button>
<button class="btn-primary" (click)="onSave()" [disabled]="saving() || !benchmarkText.trim()">
<button class="btn-primary" (click)="onSave()" [disabled]="saving() || recommending() || !benchmarkText.trim()">
{{ saving() ? 'Saving...' : submitLabel }}
</button>
</div>
@@ -1 +1,32 @@
/* Inherits all styles from modal-shell via ::ng-deep */
:host ::ng-deep {
.ai-suggest-row {
margin-bottom: 14px;
}
.btn-ai {
padding: 7px 14px;
border-radius: var(--radius-md);
border: 1px solid #c4b5fd;
background: #f5f3ff;
color: #6d28d9;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
&:hover:not(:disabled) {
background: #ede9fe;
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
}
.recommend-error {
font-size: 12px;
color: #dc2626;
margin: 6px 0 0;
}
}
@@ -22,7 +22,9 @@ export class EditBenchmarkModal {
readonly closed = output<void>();
protected readonly saving = signal(false);
protected readonly recommending = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected readonly recommendError = signal<string | null>(null);
protected shortName = '';
protected benchmarkText = '';
@@ -47,6 +49,22 @@ export class EditBenchmarkModal {
}
}
async onGetRecommendation() {
this.recommending.set(true);
this.recommendError.set(null);
const result = await this.studentService.getBenchmarkRecommendation(this.studentId(), this.goalId());
this.recommending.set(false);
if (result.success && result.payload) {
this.benchmarkText = result.payload.benchmark;
this.shortName = result.payload.shortName;
} else {
this.recommendError.set(result.message);
}
}
async onSave() {
if (!this.benchmarkText.trim()) return;
this.saving.set(true);
@@ -1,3 +1,8 @@
export interface BenchmarkRecommendationDto {
benchmark: string;
shortName: string;
}
export interface StudentBenchmarkSummary {
studentIdentifier: string;
benchmarks: BenchmarkDto[];
@@ -24,7 +24,7 @@ export function describeHttpError(error: HttpErrorResponse): string {
case 500:
return 'Server error (500). The API encountered an internal failure (possibly a database issue).';
case 503:
return 'Server unavailable (503). The API may be starting up or overwhelmed.';
return serverMessage ?? 'Service unavailable (503). The API or a required provider may be starting up or unreachable.';
default:
return `Unexpected error (${error.status}).`;
}
@@ -9,7 +9,7 @@ import { CreateStudentDto } from '../classes/create-student.dto';
import { CreateGoalDto } from '../classes/create-goal.dto';
import { StudentCardDto } from '../classes/student-card.dto';
import { StudentGoalSummary, StudentGoalItem } from '../classes/student-goal';
import { StudentBenchmarkSummary } from '../classes/benchmark.dto';
import { BenchmarkRecommendationDto, StudentBenchmarkSummary } from '../classes/benchmark.dto';
import { StudentProgressReportDto } from '../classes/student-progress-report.dto';
import { StudentFullProfileDto } from '../classes/student-full-profile.dto';
@@ -269,6 +269,24 @@ export class StudentService {
}
}
// *****************************************************************
// Requests an AI-generated benchmark recommendation for a goal.
// *****************************************************************
async getBenchmarkRecommendation(studentId: string, goalId: string): Promise<ApiResult<BenchmarkRecommendationDto>> {
try {
const result = await firstValueFrom(
this.http.get<ResponseResult<BenchmarkRecommendationDto>>(
`${this.base}/api/Student/${studentId}/goals/${goalId}/benchmark-recommendation`
)
);
return result.success && result.data
? ApiResult.ok(result.data)
: ApiResult.fail(result.message);
} catch (error) {
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
}
}
// *****************************************************************
// Updates a goal's description, category, and baseline.
// *****************************************************************