import { Component, computed, inject, OnInit, signal } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { AiOrManual } from './steps/ai-or-manual/ai-or-manual'; import { AIOrManualSubmitEvent } from './steps/ai-or-manual/AIOrManualSubmitEvent'; import { Infer } from './steps/infer/infer'; import { EnterRecipeData } from './steps/enter-recipe-data/enter-recipe-data'; import { RecipeUploadTrail } from './recipe-upload-trail/recipe-upload-trail'; import { ActivatedRoute, Router } from '@angular/router'; import { StepClickEvent } from './recipe-upload-trail/StepClickEvent'; import { RecipeUploadClientModel } from '../../shared/client-models/RecipeUploadClientModel'; import { RecipeDraftService } from '../../shared/services/RecipeDraftService'; import { RecipeUploadStep } from '../../shared/client-models/RecipeUploadStep'; import { FileUploadEvent } from '../../shared/components/file-upload/FileUploadEvent'; import { tryMaybeInt } from '../../shared/util'; 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', imports: [ReactiveFormsModule, AiOrManual, Infer, EnterRecipeData, RecipeUploadTrail, Review], templateUrl: './recipe-upload-page.html', styleUrl: './recipe-upload-page.css', }) export class RecipeUploadPage implements OnInit { protected readonly model = signal({ inProgressStep: RecipeUploadStep.START, }); protected readonly displayStep = signal(RecipeUploadStep.START); protected readonly inProgressStep = computed(() => this.model().inProgressStep); protected readonly includeInfer = signal(false); protected readonly sourceFile = computed(() => this.model().inputSourceFile ?? null); private readonly router = inject(Router); 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') { return step <= RecipeUploadStep.REVIEW; } else { return [0, 2, 3].includes(step); } } public ngOnInit(): void { this.activatedRoute.queryParamMap .pipe( map((paramMap) => { const stepParam: string | null = paramMap.get('step'); const step = tryMaybeInt(stepParam); return [paramMap.get('draftId'), step] as const; }), switchMap(([draftId, step]) => { if (draftId !== null) { return this.recipeDraftService.getRecipeUploadClientModel(draftId).pipe( tap(async (recipeUploadClientModel) => { await this.switchModel(recipeUploadClientModel); }), switchMap((updatedModel) => { if (step !== null && this.isValidStep(step)) { return from(this.changeDisplayStep(step)); } else { return from(this.changeDisplayStep(updatedModel.inProgressStep)); } }), ); } else if (step !== null && this.isValidStep(step)) { return from(this.changeDisplayStep(step)); } else { return from(this.changeDisplayStep(RecipeUploadStep.START)); } }), ) .subscribe(); } private async switchModel( model: RecipeUploadClientModel, switchStep: boolean | RecipeUploadStep = false, ): Promise { this.model.set(model); this.includeInfer.set(!!model.draft?.lastInference || model.inProgressStep === RecipeUploadStep.INFER); if (switchStep === true) { await this.changeDisplayStep(model.inProgressStep); } else if (typeof switchStep === 'number') { await this.changeDisplayStep(switchStep); } } private async changeDisplayStep(targetStep: number): Promise { this.displayStep.set(targetStep); await this.router.navigate([], { relativeTo: this.activatedRoute, queryParams: { draftId: this.model().draft?.id, step: targetStep, }, queryParamsHandling: 'replace', }); } protected async onStepClick(event: StepClickEvent): Promise { await this.changeDisplayStep(event.step); } protected onSourceFileChange(event: FileUploadEvent) { if (event._tag === 'file-add-event') { this.model.update((model) => ({ ...model, inputSourceFile: event.file, })); } else { this.model.update((model) => ({ ...model, inputSourceFile: null, })); } } protected async onAiOrManualSubmit(event: AIOrManualSubmitEvent): Promise { if (event.mode === 'manual') { const model = await this.recipeDraftService.createManualDraft(); await this.switchModel(model, true); } else { 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'); }, }); } } protected async onEnterRecipeDataSubmit(event: RecipeEditFormSubmitEvent): Promise { const model = await this.recipeDraftService.updateDraft(this.model().draft!.id, event); await this.switchModel(model, RecipeUploadStep.REVIEW); } protected async onDeleteDraft(): Promise { await this.recipeDraftService.deleteDraft(this.model().draft!.id); await this.queryClient.invalidateQueries({ queryKey: ['recipe-upload', 'in-progress-drafts'], }); await this.switchModel( { inProgressStep: RecipeUploadStep.START, }, RecipeUploadStep.START, ); } protected async onPublish(): Promise { const recipe = await this.recipeDraftService.publish(this.model().draft!.id); await this.router.navigate(['recipes', recipe.owner.username, recipe.slug]); } protected readonly RecipeUploadStep = RecipeUploadStep; }