Autosave: 20260126-195712
This commit is contained in:
parent
4ea0efca37
commit
05f4c9c979
69
api/scan_recipe.php
Normal file
69
api/scan_recipe.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No image uploaded or upload error']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imagePath = $_FILES['image']['tmp_name'];
|
||||||
|
$imageData = base64_encode(file_get_contents($imagePath));
|
||||||
|
$imageType = $_FILES['image']['type'];
|
||||||
|
|
||||||
|
$prompt = "Analyze this image. If it's a photo of a dish, identify what it is and generate a possible recipe for it. If it's a photo of a written recipe, extract the information.
|
||||||
|
Extract or generate the following information in JSON format:
|
||||||
|
- name: The name of the dish/recipe
|
||||||
|
- category: One of 'Drinks', 'Breakfast', 'Dinner', 'Appetizers'
|
||||||
|
- ingredients: An array of objects, each with:
|
||||||
|
- name: Name of the ingredient
|
||||||
|
- quantity: Numeric quantity (float)
|
||||||
|
- unit: Unit of measurement (e.g., 'g', 'kg', 'ml', 'l', 'piece', 'pack')
|
||||||
|
- guests: The default number of guests/servings the recipe is for (integer)
|
||||||
|
|
||||||
|
Important:
|
||||||
|
1. The quantities should be for 1 person if possible, or for the number of guests specified. If you're generating a recipe for a dish, default to 1 or 2 guests.
|
||||||
|
2. Return ONLY the JSON object.
|
||||||
|
3. If you can't determine something, provide a best guess or null.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = LocalAIApi::createResponse([
|
||||||
|
'input' => [
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
[
|
||||||
|
'type' => 'text',
|
||||||
|
'text' => $prompt
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'image_url',
|
||||||
|
'image_url' => [
|
||||||
|
'url' => "data:$imageType;base64,$imageData"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$response['success']) {
|
||||||
|
throw new Exception($response['error'] ?? 'AI request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$recipeData = LocalAIApi::decodeJsonFromResponse($response);
|
||||||
|
|
||||||
|
if (!$recipeData) {
|
||||||
|
throw new Exception('Failed to parse AI response as JSON. Raw response: ' . LocalAIApi::extractText($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $recipeData]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@ -432,6 +432,54 @@ const app = {
|
|||||||
attachEventListeners() {
|
attachEventListeners() {
|
||||||
app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow());
|
app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow());
|
||||||
|
|
||||||
|
app.dom.aiScanBtn.addEventListener('click', async function() {
|
||||||
|
const file = app.dom.recipeImage.files[0];
|
||||||
|
if (!file) {
|
||||||
|
alert('Please select an image first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.dom.aiScanBtn.disabled = true;
|
||||||
|
app.dom.aiScanLoading.classList.remove('d-none');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/scan_recipe.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
if (data.name) app.dom.recipeNameInput.value = data.name;
|
||||||
|
if (data.category) app.dom.recipeCategoryInput.value = data.category;
|
||||||
|
if (data.guests) app.dom.guestCountInput.value = data.guests;
|
||||||
|
|
||||||
|
if (data.ingredients && Array.isArray(data.ingredients)) {
|
||||||
|
app.dom.ingredientsContainer.innerHTML = '';
|
||||||
|
data.ingredients.forEach(ing => {
|
||||||
|
app.ui.addIngredientRow({
|
||||||
|
name: ing.name || '',
|
||||||
|
quantity: ing.quantity || '',
|
||||||
|
unit: ing.unit || 'g'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('AI Scan failed: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during AI scan:', error);
|
||||||
|
alert('An error occurred during AI scanning.');
|
||||||
|
} finally {
|
||||||
|
app.dom.aiScanBtn.disabled = false;
|
||||||
|
app.dom.aiScanLoading.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.dom.ingredientsContainer.addEventListener('click', function(e) {
|
app.dom.ingredientsContainer.addEventListener('click', function(e) {
|
||||||
if (e.target.classList.contains('remove-ingredient')) {
|
if (e.target.classList.contains('remove-ingredient')) {
|
||||||
e.target.closest('.ingredient-row').remove();
|
e.target.closest('.ingredient-row').remove();
|
||||||
@ -759,6 +807,8 @@ const app = {
|
|||||||
recipeIdInput: document.getElementById('recipeId'),
|
recipeIdInput: document.getElementById('recipeId'),
|
||||||
recipeCategoryInput: document.getElementById('recipeCategory'),
|
recipeCategoryInput: document.getElementById('recipeCategory'),
|
||||||
recipeImage: document.getElementById('recipeImage'),
|
recipeImage: document.getElementById('recipeImage'),
|
||||||
|
aiScanBtn: document.getElementById('ai-scan-btn'),
|
||||||
|
aiScanLoading: document.getElementById('ai-scan-loading'),
|
||||||
recipeSearchInput: document.getElementById('recipe-search'),
|
recipeSearchInput: document.getElementById('recipe-search'),
|
||||||
categoryFilters: document.getElementById('category-filters'),
|
categoryFilters: document.getElementById('category-filters'),
|
||||||
addProductBtn: document.getElementById('add-product-btn'),
|
addProductBtn: document.getElementById('add-product-btn'),
|
||||||
|
|||||||
11
index.php
11
index.php
@ -123,8 +123,17 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="recipeImage" class="form-label">Recipe Image</label>
|
<label for="recipeImage" class="form-label d-flex justify-content-between">
|
||||||
|
Recipe Image
|
||||||
|
<button type="button" id="ai-scan-btn" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="bi bi-magic"></i> AI Scan
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
<input type="file" class="form-control" id="recipeImage">
|
<input type="file" class="form-control" id="recipeImage">
|
||||||
|
<div id="ai-scan-loading" class="text-primary mt-2 d-none">
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
AI is identifying the dish and recipe...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4 border-secondary">
|
<hr class="my-4 border-secondary">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user