feat: auto-classify AI recipes as dish/drink (v0.33.0)
generate, generate-from-idea, URL import, and photo import all now have the AI set recipeType directly (defaulting to "dish" whenever ambiguous, per instruction), with cookMins force-cleared for drinks as a backstop in case the model doesn't follow the prompt guidance. 1-serving-by-default for unspecified drink quantities is left to the model's own judgment of the request text, since there's no reliable way to detect "was a serving count stated" without re-parsing intent. Drink recipes get a distinct default icon (no cover photo case), and Explore gains the same dish/drink Type filter My Recipes already had. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ type Recipe = {
|
||||
updatedAt: Date;
|
||||
photos?: Array<{ storageKey: string; isCover: boolean }>;
|
||||
sourceUrl?: string | null;
|
||||
recipeType?: "dish" | "drink";
|
||||
};
|
||||
|
||||
const VISIBILITY_ICON = {
|
||||
@@ -56,7 +57,7 @@ export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="aspect-video bg-gradient-to-br from-muted to-muted/50 flex items-center justify-center text-4xl">
|
||||
🍽️
|
||||
{recipe.recipeType === "drink" ? "🍹" : "🍽️"}
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className="pb-2">
|
||||
|
||||
@@ -49,6 +49,7 @@ type Recipe = {
|
||||
isBatchCook?: boolean;
|
||||
dishCount?: number;
|
||||
sourceUrl?: string | null;
|
||||
recipeType?: "dish" | "drink";
|
||||
};
|
||||
|
||||
/** Stops the click from bubbling to the card's own Link/select handler. */
|
||||
@@ -92,7 +93,7 @@ function RecipeThumb({ recipe, selectMode, selected, className }: { recipe: Reci
|
||||
/>
|
||||
) : (
|
||||
<div className={cn("w-full h-full flex items-center justify-center text-2xl", selectMode && !selected && "opacity-60")}>
|
||||
🍽️
|
||||
{recipe.recipeType === "drink" ? "🍹" : "🍽️"}
|
||||
</div>
|
||||
)}
|
||||
{selectMode && selected && <div className="absolute inset-0 bg-primary/20" />}
|
||||
|
||||
@@ -95,6 +95,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
const [inputValue, setInputValue] = useState(initialQuery);
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [difficulty, setDifficulty] = useState("any");
|
||||
const [recipeType, setRecipeType] = useState("any");
|
||||
const [maxMins, setMaxMins] = useState("");
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
const [results, setResults] = useState<SearchRecipeResult[]>([]);
|
||||
@@ -115,7 +116,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const fetchResults = useCallback(
|
||||
async (q: string, diff: string, maxM: string, off: number, tags: string[], append = false) => {
|
||||
async (q: string, diff: string, maxM: string, off: number, tags: string[], type = "any", append = false) => {
|
||||
if (!q.trim()) {
|
||||
setResults([]);
|
||||
setTotal(0);
|
||||
@@ -127,6 +128,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
try {
|
||||
const params = new URLSearchParams({ q, limit: "20", offset: String(off) });
|
||||
if (diff && diff !== "any") params.set("difficulty", diff);
|
||||
if (type && type !== "any") params.set("recipeType", type);
|
||||
if (maxM) params.set("maxMins", maxM);
|
||||
if (tags.length > 0) params.set("tags", tags.join(","));
|
||||
const res = await fetch(`/api/v1/search?${params}`);
|
||||
@@ -162,7 +164,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
|
||||
useEffect(() => {
|
||||
if (initialQuery) {
|
||||
fetchResults(initialQuery, "any", "", 0, []);
|
||||
fetchResults(initialQuery, "any", "", 0, [], "any");
|
||||
fetchPeople(initialQuery);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -218,7 +220,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setQuery(value);
|
||||
updateUrl(value);
|
||||
fetchResults(value, difficulty, maxMins, 0, [...selectedTags]);
|
||||
fetchResults(value, difficulty, maxMins, 0, [...selectedTags], recipeType);
|
||||
fetchPeople(value);
|
||||
}, 300);
|
||||
};
|
||||
@@ -228,21 +230,27 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
setQuery(inputValue);
|
||||
updateUrl(inputValue);
|
||||
fetchResults(inputValue, difficulty, maxMins, 0, [...selectedTags]);
|
||||
fetchResults(inputValue, difficulty, maxMins, 0, [...selectedTags], recipeType);
|
||||
fetchPeople(inputValue);
|
||||
};
|
||||
|
||||
const handleDifficultyChange = (value: string | null) => {
|
||||
const v = value ?? "any";
|
||||
setDifficulty(v);
|
||||
fetchResults(query, v, maxMins, 0, [...selectedTags]);
|
||||
fetchResults(query, v, maxMins, 0, [...selectedTags], recipeType);
|
||||
};
|
||||
|
||||
const handleRecipeTypeChange = (value: string | null) => {
|
||||
const v = value ?? "any";
|
||||
setRecipeType(v);
|
||||
fetchResults(query, difficulty, maxMins, 0, [...selectedTags], v);
|
||||
};
|
||||
|
||||
const handleMaxMinsChange = (value: string) => {
|
||||
setMaxMins(value);
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
fetchResults(query, difficulty, value, 0, [...selectedTags]);
|
||||
fetchResults(query, difficulty, value, 0, [...selectedTags], recipeType);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
@@ -251,7 +259,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
const next = new Set(prev);
|
||||
if (next.has(tag)) next.delete(tag);
|
||||
else next.add(tag);
|
||||
fetchResults(query, difficulty, maxMins, 0, [...next]);
|
||||
fetchResults(query, difficulty, maxMins, 0, [...next], recipeType);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -294,6 +302,17 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
<SelectItem value="hard">{tRecipe("difficulty.hard")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={recipeType} onValueChange={handleRecipeTypeChange}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder={t("anyType")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="any">{t("anyType")}</SelectItem>
|
||||
<SelectItem value="dish">{tRecipe("recipeType.dish")}</SelectItem>
|
||||
<SelectItem value="drink">{tRecipe("recipeType.drink")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -392,7 +411,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => fetchResults(query, difficulty, maxMins, offset, [...selectedTags], true)}
|
||||
onClick={() => fetchResults(query, difficulty, maxMins, offset, [...selectedTags], recipeType, true)}
|
||||
disabled={loadingMore}
|
||||
className="min-w-32"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user