109 }
110
111 if (uploadedFile.type.startsWith('image/')) {
112 endpoint = '/api/parse/image';
113 requestData = { type: 'image', content: parseInput };
114 } else if (uploadedFile.type === 'application/pdf') {
115 endpoint = '/api/parse/pdf';
150
151 // Validate file type
152 const isImage = file.type.startsWith('image/');
153 const isPDF = file.type === 'application/pdf';
154
155 if (!isImage && !isPDF) {
156 setError('Please select an image file (JPG, PNG, etc.) or a PDF file');
157 return;
158 }
274 ref={fileInputRef}
275 type="file"
276 accept="image/*,.pdf"
277 onChange={handleFileUpload}
278 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
279 />
280 <div className="mt-2 text-sm text-gray-600">
281 📷 Images (JPG, PNG, etc.) or 📄 PDF files supported
282 </div>
283 </div>
286 <span className="mr-2">✅</span>
287 <span>
288 {uploadedFile.type.startsWith('image/') ? '📷' : '📄'}
289 {uploadedFile.name} uploaded and ready to parse
290 </span>
19 const recipeResult = await sqlite.execute(`
20 INSERT INTO ${RECIPES_TABLE}
21 (title, description, servings, prep_time, cook_time, difficulty, tags, source, image_url, steps)
22 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
23 `, [
30 tags ? JSON.stringify(tags) : null,
31 recipeData.source || null,
32 recipeData.imageUrl || null,
33 JSON.stringify(steps)
34 ]);
108 tags: recipeRow.tags ? JSON.parse(recipeRow.tags as string) : undefined,
109 source: recipeRow.source as string || undefined,
110 imageUrl: recipeRow.image_url as string || undefined,
111 createdAt: recipeRow.created_at as string,
112 updatedAt: recipeRow.updated_at as string
168 const dbKey = key === 'prepTime' ? 'prep_time' :
169 key === 'cookTime' ? 'cook_time' :
170 key === 'imageUrl' ? 'image_url' : key;
171 setParts.push(`${dbKey} = ?`);
172 params.push(value);
68 tags: Array.isArray(recipe.tags) ? recipe.tags.filter(tag => typeof tag === 'string') : undefined,
69 source: recipe.source && typeof recipe.source === 'string' ? recipe.source.trim() : undefined,
70 imageUrl: recipe.imageUrl && typeof recipe.imageUrl === 'string' ? recipe.imageUrl.trim() : undefined
71 };
72}
122
123// System prompt for recipe parsing
124const RECIPE_PARSING_PROMPT = `You are a recipe parsing expert. Your task is to extract structured recipe information from various sources (text, images, PDFs).
125
126IMPORTANT: You must return ONLY a valid JSON object with no additional text, markdown formatting, or explanations.
232});
233
234app.post('/image', async (c) => {
235 try {
236 const { content: base64Image } = await c.req.json() as ParseRequest;
237
238 console.log('Processing image upload, base64 length:', base64Image?.length);
239
240 // Validate base64 image
241 if (!base64Image || base64Image.length < 100) {
242 return c.json({ success: false, error: 'Invalid or empty image data' } as ParseResponse);
243 }
244
245 // Use OpenAI Vision to parse recipe from image
246 const completion = await openai.chat.completions.create({
247 model: "gpt-4o-mini",
253 {
254 type: "text",
255 text: "Parse the recipe from this image. Extract all visible recipe information including title, ingredients with amounts, and cooking steps."
256 },
257 {
258 type: "image_url",
259 image_url: {
260 url: base64Image.startsWith('data:') ? base64Image : `data:image/jpeg;base64,${base64Image}`
261 }
262 }
300 const rawRecipe = JSON.parse(jsonText);
301 const recipe = validateAndCleanRecipe(rawRecipe);
302 recipe.source = 'Image upload';
303
304 return c.json({ success: true, recipe } as ParseResponse);
313
314 } catch (error) {
315 console.error('Image parsing error:', error);
316 return c.json({
317 success: false,
318 error: `Failed to parse recipe from image: ${error.message}`
319 } as ParseResponse);
320 }
332 return c.json({
333 success: false,
334 error: 'PDF parsing not yet implemented. Please copy and paste the recipe text or use an image instead.'
335 } as ParseResponse);
336
20 `,
21 {
22 headers: { "Content-Type": "image/svg+xml" },
23 },
24 );
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Recipe Parser - Capture, Parse & Store Recipes</title>
7 <meta name="description" content="Intelligent recipe parser that extracts recipes from URLs, PDFs, and images">
8
9 <!-- TailwindCSS -->
19 tags TEXT, -- JSON array of tags
20 source TEXT,
21 image_url TEXT,
22 steps TEXT NOT NULL, -- JSON array of steps
23 created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
21 tags?: string[];
22 source?: string; // URL or source description
23 imageUrl?: string;
24 createdAt?: string;
25 updatedAt?: string;
27
28export interface ParseRequest {
29 type: 'url' | 'pdf' | 'image';
30 content: string; // URL, base64 PDF, or base64 image
31 filename?: string;
32}
1# Recipe Parser App
2
3A mobile-friendly web application that captures, parses, and stores recipes from multiple sources including URLs, PDFs, and images.
4
5## Features
8 - Web URLs (recipe websites)
9 - PDF files
10 - Images (photos of recipes)
11- **Intelligent Parsing**: Automatically extracts:
12 - Dish title
23- **Database**: SQLite for recipe storage
24- **AI**: OpenAI GPT-4 for intelligent recipe parsing
25- **File Processing**: PDF parsing and image OCR capabilities
26
27## Project Structure
52- `POST /api/parse/url` - Parse recipe from URL
53- `POST /api/parse/pdf` - Parse recipe from PDF
54- `POST /api/parse/image` - Parse recipe from image
55- `GET /api/recipes` - Get all recipes
56- `POST /api/recipes` - Save a recipe
62
631. Open the app in your browser
642. Choose input method (URL, PDF, or image)
653. Submit your recipe source
664. Review and edit the parsed recipe
64 class="rounded-md w-[120px] h-[180px] object-cover flex-shrink-0"
65 height="180"
66 src="https://storage.googleapis.com/a1aa/image/da09cbc2-570f-460a-05bb-9836a9055c79.jpg"
67 width="120"
68 />
17 description?: string | null;
18 readme?: string;
19 imageUrl?: string | null;
20 links: {
21 self: string;