All files / src recipeGeneration.tsx

91.48% Statements 43/47
75% Branches 12/16
90% Functions 9/10
97.5% Lines 39/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 99                                                    3x     22x 22x 22x               64x 64x 64x   64x 64x 64x 64x 64x   64x 20x     64x 4x 4x 4x 4x 4x 4x   4x 4x 4x 4x 4x 4x         4x 3x 3x 3x   1x 1x   4x         64x     64x         63x 63x 63x        
import type {Dispatch, ReactNode, SetStateAction} from 'react';
import {createContext, useCallback, useContext, useEffect, useState} from 'react';
import {useNavigate} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import type {components} from './api';
import {tagsById} from './recipeFormat';
import {errorMessage} from './apiError';
import {SessionExpiredError, useApi} from './useApi';
import {currentLanguage} from './i18n';
 
// id is stored on the recipe once it is saved
type Recipe = components['schemas']['RecipeInput'] & { id?: number }
type RecipeRequest = components['schemas']['RecipeRequest']
 
export interface RecipeGenerationContextValue {
	prompt: string;
	setPrompt: Dispatch<SetStateAction<string>>;
	selectedTags: string[];
	setSelectedTags: Dispatch<SetStateAction<string[]>>;
	recipes: Recipe[];
	setRecipes: Dispatch<SetStateAction<Recipe[]>>;
	status: string | null;
	loading: boolean;
	generate: () => void;
}
 
const RecipeGenerationContext = createContext<RecipeGenerationContextValue | null>(null);
 
function safeGetArrayFromStorage(key: string) {
	try {
		const stored = sessionStorage.getItem(key);
		return stored ? (JSON.parse(stored)) : [];
	} catch {
		return [];
	}
}
 
// Lives above the tab layout so an ongoing generation survives navigating to another tab
export function RecipeGenerationProvider({children}: { children: ReactNode }) {
	const {t} = useTranslation();
	const apiFetch = useApi();
	const navigate = useNavigate();
 
	const [prompt, setPrompt] = useState(() => sessionStorage.getItem('recipe_prompt') ?? '');
	const [selectedTags, setSelectedTags] = useState<string[]>(() => safeGetArrayFromStorage('recipe_tags'));
	const [recipes, setRecipes] = useState<Recipe[]>(() => safeGetArrayFromStorage('generated_recipes'));
	const [status, setStatus] = useState<string | null>(null);
	const [loading, setLoading] = useState(false);
 
	useEffect(() => {
		sessionStorage.setItem('generated_recipes', JSON.stringify(recipes));
	}, [recipes]);
 
	const generate = useCallback(() => {
		setLoading(true);
		setStatus(t('generate.generatingStatus'));
		setRecipes([]);
		sessionStorage.setItem('recipe_prompt', prompt);
		sessionStorage.setItem('recipe_tags', JSON.stringify(selectedTags));
		navigate('/generate/results');
 
		void (async () => {
			try {
				const tagLabels = selectedTags.map((id) => tagsById.get(id)?.label).filter(Boolean);
				const fullPrompt = tagLabels.length > 0 ? `${prompt}\n\nPreferences: ${tagLabels.join(', ')}` : prompt;
				const body: RecipeRequest = {prompt: fullPrompt, language: currentLanguage()};
				const response = await apiFetch('/ai/recipes', {
					method: 'POST',
					headers: {'content-type': 'application/json'},
					body: JSON.stringify(body),
				});
				if (!response.ok) throw new Error(await errorMessage(response));
				const data = (await response.json()) as Recipe[];
				setRecipes(data);
				setStatus(data.length === 0 ? t('generate.noRecipes') : null);
			} catch (e) {
				Iif (e instanceof SessionExpiredError) return;
				setStatus(t('common.error', {message: e instanceof Error ? e.message : String(e)}));
			} finally {
				setLoading(false);
			}
		})();
	}, [apiFetch, navigate, prompt, selectedTags, t]);
 
	const value: RecipeGenerationContextValue = {
		prompt, setPrompt, selectedTags, setSelectedTags, recipes, setRecipes, status, loading, generate,
	};
	return <RecipeGenerationContext.Provider value={value}>{children}</RecipeGenerationContext.Provider>;
}
 
// eslint-disable-next-line react-refresh/only-export-components
export function useRecipeGeneration(): RecipeGenerationContextValue {
	const ctx = useContext(RecipeGenerationContext);
	Iif (!ctx) throw new Error('useRecipeGeneration must be used within a RecipeGenerationProvider');
	return ctx;
}
 
export type {Recipe};