From 907c8c34adf939f81751499a30b87f05bb49f81e Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Wed, 18 Feb 2026 13:02:50 -0600 Subject: [PATCH] MME-6 Re-introduce ai infer for recipes upload. --- .../recipe-upload-page/recipe-upload-page.ts | 40 +++++++++++----- src/app/shared/services/RecipeDraftService.ts | 48 +++++++++++++++---- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/app/pages/recipe-upload-page/recipe-upload-page.ts b/src/app/pages/recipe-upload-page/recipe-upload-page.ts index 7b5bdf9..6a5b58b 100644 --- a/src/app/pages/recipe-upload-page/recipe-upload-page.ts +++ b/src/app/pages/recipe-upload-page/recipe-upload-page.ts @@ -16,6 +16,7 @@ import { from, map, switchMap, tap } from 'rxjs'; import { Review } from './steps/review/review'; import { RecipeEditFormSubmitEvent } from '../../shared/components/recipe-edit-form/RecipeEditFormSubmitEvent'; import { QueryClient } from '@tanstack/angular-query-experimental'; +import { ToastrService } from 'ngx-toastr'; @Component({ selector: 'app-recipe-upload-page', @@ -37,6 +38,7 @@ export class RecipeUploadPage implements OnInit { private readonly activatedRoute = inject(ActivatedRoute); private readonly recipeDraftService = inject(RecipeDraftService); private readonly queryClient = inject(QueryClient); + private readonly toastrService = inject(ToastrService); private isValidStep(step: number): boolean { if (this.model().draft?.lastInference || this.model().draft?.state === 'INFER') { @@ -83,7 +85,7 @@ export class RecipeUploadPage implements OnInit { switchStep: boolean | RecipeUploadStep = false, ): Promise { this.model.set(model); - this.includeInfer.set(!!model.draft?.lastInference); + this.includeInfer.set(!!model.draft?.lastInference || model.inProgressStep === RecipeUploadStep.INFER); if (switchStep === true) { await this.changeDisplayStep(model.inProgressStep); } else if (typeof switchStep === 'number') { @@ -126,17 +128,31 @@ export class RecipeUploadPage implements OnInit { const model = await this.recipeDraftService.createManualDraft(); await this.switchModel(model, true); } else { - this.model.update((model) => ({ - ...model, - inputSourceFile: this.sourceFile(), - inProgressStep: RecipeUploadStep.INFER, - })); - await this.changeDisplayStep(RecipeUploadStep.INFER); - this.includeInfer.set(true); - this.recipeDraftService.doInference(this.model()).subscribe((updatedModel) => { - this.model.set(updatedModel); - this.changeDisplayStep(RecipeUploadStep.ENTER_DATA); - }); + await this.switchModel( + { + ...this.model(), + inputSourceFile: this.sourceFile(), + inProgressStep: RecipeUploadStep.INFER, + }, + true, + ); + this.recipeDraftService + .startInference(this.model()) + .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'); + }, + }); } } diff --git a/src/app/shared/services/RecipeDraftService.ts b/src/app/shared/services/RecipeDraftService.ts index 8477e05..8e1ad71 100644 --- a/src/app/shared/services/RecipeDraftService.ts +++ b/src/app/shared/services/RecipeDraftService.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; 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 { RecipeUploadStep } from '../client-models/RecipeUploadStep'; import { RecipeDraftViewModel } from '../models/RecipeDraftView.model'; @@ -122,14 +122,42 @@ export class RecipeDraftService { return firstValueFrom(this.http.delete(this.endpointService.getUrl('recipeDrafts', [id]))); } - public doInference(model: RecipeUploadClientModel): Observable { - return of({ - inProgressStep: RecipeUploadStep.ENTER_DATA, - id: 16, - inferredTitle: 'Some recipe', - inferredSlug: 'some-recipe', - inferredText: 'Some text.', - inferredIngredients: [], - }).pipe(delay(5_000)); + public startInference(model: RecipeUploadClientModel): Observable { + const formData = new FormData(); + formData.set('sourceFile', model.inputSourceFile!); + formData.set('sourceFileName', model.inputSourceFile!.name); + + return this.http + .post>(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 { + return interval(pollInterval).pipe( + takeWhile((pollCount) => pollCount < maxPollCount), + switchMap(() => + this.http.get>( + 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, + draft, + })), + ); } }