Files
Recipe-AI/src/app/recipe/page.tsx
2025-10-28 14:33:24 -04:00

181 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from 'react';
type Analysis = {
platform: 'tiktok' | 'instagram' | 'unknown';
analysis: {
ingredients: Array<{ name: string; quantity: string | null; unit: string | null; notes?: string | null }>;
prep_steps: string[];
cooking_steps: string[];
};
description?: string;
transcript?: string;
thumbnailBase64?: string;
title?: string;
};
export default function RecipeAnalyzerPage() {
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<Analysis | null>(null);
const [saving, setSaving] = useState(false);
const canSave = !!result && !!url;
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch('/api/analyze', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url }),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || 'Failed');
setResult(data);
} catch (err: any) {
setError(err?.message || 'Something went wrong');
} finally {
setLoading(false);
}
}
async function onSave() {
if (!result) return;
setSaving(true);
try {
const res = await fetch('/api/recipes', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
url,
platform: result.platform,
title: result.title || '',
description: result.description || '',
transcript: result.transcript || '',
analysis: result.analysis,
thumbnailBase64: result.thumbnailBase64,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || 'Failed to save');
alert('Saved!');
} catch (err: any) {
alert(err?.message || 'Failed to save');
} finally {
setSaving(false);
}
}
return (
<main className="min-h-screen p-6 md:p-12 lg:p-16 flex flex-col gap-8 max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold">Recipe AI from Reels/TikToks</h1>
<p className="text-neutral-600 dark:text-neutral-300">
Paste a TikTok or Instagram share link. The AI analyzes only the video to extract an ingredient list, prep steps, and cooking steps.
</p>
<form onSubmit={onSubmit} className="flex flex-col gap-3">
<input
type="url"
required
placeholder="https://www.tiktok.com/@user/video/123... or https://www.instagram.com/reel/..."
value={url}
onChange={(e) => setUrl(e.target.value)}
className="w-full rounded border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 py-2"
/>
<button
type="submit"
disabled={loading}
className="inline-flex items-center justify-center rounded bg-black text-white dark:bg-white dark:text-black px-4 py-2 disabled:opacity-60"
>
{loading ? 'Analyzing…' : 'Analyze Video'}
</button>
{result && (
<button
type="button"
onClick={onSave}
disabled={saving}
className="inline-flex items-center justify-center rounded border border-neutral-300 dark:border-neutral-700 px-4 py-2 disabled:opacity-60"
>
{saving ? 'Saving…' : 'Save Recipe'}
</button>
)}
</form>
{error && (
<div className="rounded border border-red-300 bg-red-50 text-red-800 dark:bg-red-950 dark:text-red-200 p-3">
{error}
</div>
)}
{result && (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={result.title || ''}
onChange={(e) => setResult({ ...result, title: e.target.value })}
className="w-full rounded border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 py-2"
placeholder="Recipe title"
/>
</div>
{result.thumbnailBase64 && (
<img src={result.thumbnailBase64} alt="thumbnail" className="w-full h-auto rounded" />
)}
<div className="text-sm text-neutral-600 dark:text-neutral-300 break-words">
<div>
<span className="font-medium">Source:</span>{' '}
<a href={url} className="underline" target="_blank" rel="noreferrer">
{url}
</a>
</div>
{result.description && (
<div className="mt-1 whitespace-pre-wrap">
<span className="font-medium">Description:</span> {result.description}
</div>
)}
</div>
<div>
<h2 className="text-xl font-semibold">Ingredients</h2>
<ul className="list-disc pl-6 mt-2 space-y-1">
{result.analysis.ingredients.map((it, idx) => (
<li key={idx}>
<span className="font-medium">{it.name}</span>
{it.quantity ? ` ${it.quantity}` : ''}
{it.unit ? ` ${it.unit}` : ''}
{it.notes ? ` (${it.notes})` : ''}
</li>
))}
</ul>
</div>
<div>
<h2 className="text-xl font-semibold">Prep Steps</h2>
<ol className="list-decimal pl-6 mt-2 space-y-1">
{result.analysis.prep_steps.map((s, i) => (
<li key={i}>{s}</li>
))}
</ol>
</div>
<div>
<h2 className="text-xl font-semibold">Cooking Steps</h2>
<ol className="list-decimal pl-6 mt-2 space-y-1">
{result.analysis.cooking_steps.map((s, i) => (
<li key={i}>{s}</li>
))}
</ol>
</div>
</div>
)}
</main>
);
}