MME-6 Re-introduce ai infer for recipes upload.

This commit is contained in:
Jesse Brault 2026-02-18 13:02:50 -06:00
parent ef49f55598
commit 907c8c34ad
2 changed files with 66 additions and 22 deletions

View File

@ -16,6 +16,7 @@ import { from, map, switchMap, tap } from 'rxjs';
import { Review } from './steps/review/review'; import { Review } from './steps/review/review';
import { RecipeEditFormSubmitEvent } from '../../shared/components/recipe-edit-form/RecipeEditFormSubmitEvent'; import { RecipeEditFormSubmitEvent } from '../../shared/components/recipe-edit-form/RecipeEditFormSubmitEvent';
import { QueryClient } from '@tanstack/angular-query-experimental'; import { QueryClient } from '@tanstack/angular-query-experimental';
import { ToastrService } from 'ngx-toastr';
@Component({ @Component({
selector: 'app-recipe-upload-page', selector: 'app-recipe-upload-page',
@ -37,6 +38,7 @@ export class RecipeUploadPage implements OnInit {
private readonly activatedRoute = inject(ActivatedRoute); private readonly activatedRoute = inject(ActivatedRoute);
private readonly recipeDraftService = inject(RecipeDraftService); private readonly recipeDraftService = inject(RecipeDraftService);
private readonly queryClient = inject(QueryClient); private readonly queryClient = inject(QueryClient);
private readonly toastrService = inject(ToastrService);
private isValidStep(step: number): boolean { private isValidStep(step: number): boolean {
if (this.model().draft?.lastInference || this.model().draft?.state === 'INFER') { if (this.model().draft?.lastInference || this.model().draft?.state === 'INFER') {
@ -83,7 +85,7 @@ export class RecipeUploadPage implements OnInit {
switchStep: boolean | RecipeUploadStep = false, switchStep: boolean | RecipeUploadStep = false,
): Promise<void> { ): Promise<void> {
this.model.set(model); this.model.set(model);
this.includeInfer.set(!!model.draft?.lastInference); this.includeInfer.set(!!model.draft?.lastInference || model.inProgressStep === RecipeUploadStep.INFER);
if (switchStep === true) { if (switchStep === true) {
await this.changeDisplayStep(model.inProgressStep); await this.changeDisplayStep(model.inProgressStep);
} else if (typeof switchStep === 'number') { } else if (typeof switchStep === 'number') {
@ -126,16 +128,30 @@ export class RecipeUploadPage implements OnInit {
const model = await this.recipeDraftService.createManualDraft(); const model = await this.recipeDraftService.createManualDraft();
await this.switchModel(model, true); await this.switchModel(model, true);
} else { } else {
this.model.update((model) => ({ await this.switchModel(
...model, {
...this.model(),
inputSourceFile: this.sourceFile(), inputSourceFile: this.sourceFile(),
inProgressStep: RecipeUploadStep.INFER, inProgressStep: RecipeUploadStep.INFER,
})); },
await this.changeDisplayStep(RecipeUploadStep.INFER); true,
this.includeInfer.set(true); );
this.recipeDraftService.doInference(this.model()).subscribe((updatedModel) => { this.recipeDraftService
this.model.set(updatedModel); .startInference(this.model())
this.changeDisplayStep(RecipeUploadStep.ENTER_DATA); .pipe(
tap((updated) => {
this.switchModel(updated, true);
}),
switchMap((model) => this.recipeDraftService.pollUntilInferenceComplete(model)),
)
.subscribe({
next: (inferredModel) => {
this.switchModel(inferredModel, true);
},
error: (e) => {
console.error(e);
this.toastrService.error('Error while extracting recipe data');
},
}); });
} }
} }

View File

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { delay, firstValueFrom, map, Observable, of } from 'rxjs'; import { first, firstValueFrom, interval, map, Observable, switchMap, takeWhile } from 'rxjs';
import { RecipeUploadClientModel } from '../client-models/RecipeUploadClientModel'; import { RecipeUploadClientModel } from '../client-models/RecipeUploadClientModel';
import { RecipeUploadStep } from '../client-models/RecipeUploadStep'; import { RecipeUploadStep } from '../client-models/RecipeUploadStep';
import { RecipeDraftViewModel } from '../models/RecipeDraftView.model'; import { RecipeDraftViewModel } from '../models/RecipeDraftView.model';
@ -122,14 +122,42 @@ export class RecipeDraftService {
return firstValueFrom(this.http.delete<void>(this.endpointService.getUrl('recipeDrafts', [id]))); return firstValueFrom(this.http.delete<void>(this.endpointService.getUrl('recipeDrafts', [id])));
} }
public doInference(model: RecipeUploadClientModel): Observable<RecipeUploadClientModel> { public startInference(model: RecipeUploadClientModel): Observable<RecipeUploadClientModel> {
return of({ const formData = new FormData();
formData.set('sourceFile', model.inputSourceFile!);
formData.set('sourceFileName', model.inputSourceFile!.name);
return this.http
.post<WithStringDates<RecipeDraftViewModel>>(this.endpointService.getUrl('recipeDrafts', ['ai']), formData)
.pipe(
map((rawDraft) => this.hydrateView(rawDraft)),
map((draft) => ({
...model,
inProgressStep: RecipeUploadStep.INFER,
draft,
})),
);
}
public pollUntilInferenceComplete(
model: RecipeUploadClientModel,
pollInterval: number = 5000,
maxPollCount: number = 12,
): Observable<RecipeUploadClientModel> {
return interval(pollInterval).pipe(
takeWhile((pollCount) => pollCount < maxPollCount),
switchMap(() =>
this.http.get<WithStringDates<RecipeDraftViewModel>>(
this.endpointService.getUrl('recipeDrafts', [model.draft!.id]),
),
),
map((rawDraft) => this.hydrateView(rawDraft)),
first((draft) => draft.state === 'ENTER_DATA'),
map((draft) => ({
...model,
inProgressStep: RecipeUploadStep.ENTER_DATA, inProgressStep: RecipeUploadStep.ENTER_DATA,
id: 16, draft,
inferredTitle: 'Some recipe', })),
inferredSlug: 'some-recipe', );
inferredText: 'Some text.',
inferredIngredients: [],
}).pipe(delay(5_000));
} }
} }