-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerator.component.ts
More file actions
98 lines (88 loc) · 3.38 KB
/
Copy pathgenerator.component.ts
File metadata and controls
98 lines (88 loc) · 3.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RecipeViewBase } from '../shared/recipe-view.base';
import type { Recipe } from '../../recipe.types';
@Component({
selector: 'app-generator',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './generator.component.html',
})
export class GeneratorComponent extends RecipeViewBase {
prompt = signal('');
isRecipeLoading = signal(false);
error = signal<string | null>(null);
/**
* KAN-256 — the generator resets on ROUTE ENTRY, not on submit.
*
* `clearRecipe()` used to fire only inside `onGenerate()`, which is
* submit-time. The generated recipe lives on `RecipeStateService`, a
* root-scoped singleton that outlives this component, so navigating away and
* back re-rendered the *previous* recipe under an empty prompt box — the
* "why is my old recipe still here" report.
*
* The constructor is the right hook because route entry is exactly when
* Angular creates this component: navigating to `/` from another route
* destroys and recreates it, while staying on `/` reuses the instance and so
* does not clear a result the user is still reading.
*
* This deliberately does NOT cancel an in-flight image generation. That is
* tracked on the service by recipe id, so the spinner keeps running on
* recipe-detail and the KAN-255 metadata reconcile still lands — the
* generator just is not the surface showing it any more.
*/
constructor() {
super();
this.recipeState.clearRecipe();
}
/**
* A guest activating the publish toggle gets the sign-in modal here, where
* recipe-detail stays silent (its template renders a dedicated "Sign in to
* publish" button instead — #3211).
*/
protected override onPublishDenied(): void {
this.modalService.openAuth();
}
async onGenerate() {
if (!this.prompt().trim()) return;
this.authService.ensureGuestSession();
this.isRecipeLoading.set(true);
this.error.set(null);
this.recipeState.clearRecipe();
this.servingsMultiplier.set(1);
try {
const generatedRecipe: Recipe = {
...(await this.geminiService.generateRecipe(this.prompt())),
// KAN-140: provenance label; lets the server distinguish
// AI-mediated content from manual entry.
origin: 'generated',
};
this.recipe.set(generatedRecipe);
this.isSaved.set(true);
await this.persistenceService.saveRecipe(generatedRecipe);
// Fire-and-forget: the image takes far longer than the recipe text, and
// the user must be able to read (and leave) the recipe while it renders.
void this.runImageGeneration(generatedRecipe.id, { regenerate: false });
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : 'Failed to generate recipe. Please try again.';
this.error.set(message);
} finally {
this.isRecipeLoading.set(false);
}
}
async onSaveRecipe() {
const currentRecipe = this.recipe();
if (!currentRecipe) return;
await this.persistenceService.saveRecipe(currentRecipe);
this.isSaved.set(true);
}
openAddToCookbookModal() {
const r = this.recipe();
if (r) this.modalService.openAddToCookbook(r);
}
isString(val: unknown): boolean {
return typeof val === 'string';
}
}