added basic AI import and improved display for external recipes

This commit is contained in:
vabene1111
2025-08-16 15:08:25 +02:00
parent 4bd3da451d
commit 86fc4aa2d0
39 changed files with 181 additions and 49 deletions

View File

@@ -1768,7 +1768,7 @@ class RecipeFromSourceResponseSerializer(serializers.Serializer):
class AiImportSerializer(serializers.Serializer):
file = serializers.FileField(allow_null=True)
text = serializers.CharField(allow_null=True, allow_blank=True)
recipe_id = serializers.CharField(allow_null=True, allow_blank=True)
class ExportRequestSerializer(serializers.Serializer):
type = serializers.CharField()

View File

@@ -1891,6 +1891,12 @@ class AiImportView(APIView):
messages = []
uploaded_file = serializer.validated_data['file']
if serializer.validated_data['recipe_id']:
if recipe := Recipe.objects.filter(id=serializer.validated_data['recipe_id']).first():
if recipe.file_path:
uploaded_file = get_recipe_provider(recipe).get_file(recipe)
if uploaded_file:
base64type = None
try:

View File

@@ -1,18 +1,24 @@
<template>
<v-expansion-panels v-model="panelState">
<v-expansion-panel value="show">
<v-expansion-panel-title>{{ $t('ExternalRecipe') }}</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card class="mt-1 h-100">
<iframe width="100%" height="700px" :src="externalUrl" v-if="isPdf"></iframe>
<v-img :src="externalUrl" v-if="isImage"></v-img>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script setup lang="ts">
import {computed, PropType} from "vue";
import {computed, onMounted, PropType, ref} from "vue";
import {Recipe} from "@/openapi";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import {useUrlSearchParams} from "@vueuse/core";
const props = defineProps({
recipe: {type: {} as PropType<Recipe>, required: true}
})
@@ -20,6 +26,15 @@ const props = defineProps({
const params = useUrlSearchParams('history')
const {getDjangoUrl} = useDjangoUrls()
const panelState = ref('')
onMounted(() => {
// open panel by default if recipe has not been converted to internal yet
if (!props.recipe.internal) {
panelState.value = 'show'
}
})
/**
* determines if the file is a PDF based on the path
*/

View File

@@ -17,8 +17,7 @@
<recipe-image
max-height="25vh"
:recipe="recipe"
v-if="recipe.internal"
>
v-if="recipe.image != undefined">
</recipe-image>
<v-card>
@@ -36,8 +35,8 @@
</v-card>
</v-card>
<template v-if="recipe.internal">
<v-card class="mt-1">
<!-- only display values if not all are default (e.g. for external recipes) -->
<v-card class="mt-1" v-if="recipe.workingTime != 0 || recipe.waitingTime != 0 || recipe.servings != 1">
<v-container>
<v-row class="text-center text-body-2">
<v-col class="pt-1 pb-1">
@@ -61,7 +60,6 @@
</v-container>
</v-card>
</template>
</template>
<!-- Desktop horizontal layout -->
<template class="d-none d-lg-block">
<v-row dense>
@@ -69,8 +67,7 @@
<recipe-image
:rounded="true"
max-height="40vh"
:recipe="recipe"
v-if="recipe.internal">
:recipe="recipe">
</recipe-image>
</v-col>
<v-col cols="4">
@@ -78,7 +75,8 @@
<v-card-text class="flex-grow-1">
<div class="d-flex">
<h1 class="flex-column flex-grow-1">{{ recipe.name }}</h1>
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated" class="flex-column mb-auto mt-2 float-right"></recipe-context-menu>
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated"
class="flex-column mb-auto mt-2 float-right"></recipe-context-menu>
</div>
<p>
{{ $t('created_by') }} {{ recipe.createdBy.displayName }} ({{ DateTime.fromJSDate(recipe.createdAt).toLocaleString(DateTime.DATE_SHORT) }})
@@ -118,10 +116,18 @@
</v-row>
</template>
<template v-if="!recipe.internal">
<external-recipe-viewer :recipe="recipe"></external-recipe-viewer>
<template v-if="recipe.filePath">
<external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer>
<v-card :title="$t('AI')" prepend-icon="$ai" @click="aiConvertRecipe()" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading"
v-if="!recipe.internal">
<v-card-text>
Convert the recipe using AI
</v-card-text>
</v-card>
</template>
<template v-else>
<v-card class="mt-1" v-if="recipe.steps.length > 1 && recipe.showIngredientOverview">
<steps-overview :steps="recipe.steps" :ingredient-factor="ingredientFactor"></steps-overview>
</v-card>
@@ -129,9 +135,8 @@
<v-card class="mt-1" v-for="(step, index) in recipe.steps" :key="step.id">
<step-view v-model="recipe.steps[index]" :step-number="index+1" :ingredientFactor="ingredientFactor"></step-view>
</v-card>
</template>
<property-view v-model="recipe" :servings="servings" v-if="recipe.internal"></property-view>
<property-view v-model="recipe" :servings="servings"></property-view>
<v-card class="mt-2">
<v-card-text>
@@ -181,7 +186,7 @@
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {Recipe} from "@/openapi"
import {ApiApi, Recipe} from "@/openapi"
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"
import StepsOverview from "@/components/display/StepsOverview.vue";
import RecipeActivity from "@/components/display/RecipeActivity.vue";
@@ -189,23 +194,33 @@ import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
import KeywordsComponent from "@/components/display/KeywordsBar.vue";
import RecipeImage from "@/components/display/RecipeImage.vue";
import ExternalRecipeViewer from "@/components/display/ExternalRecipeViewer.vue";
import {useMediaQuery, useWakeLock} from "@vueuse/core";
import {useWakeLock} from "@vueuse/core";
import StepView from "@/components/display/StepView.vue";
import {DateTime} from "luxon";
import PropertyView from "@/components/display/PropertyView.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useFileApi} from "@/composables/useFileApi.ts";
const {request, release} = useWakeLock()
const {doAiImport, fileApiLoading} = useFileApi()
const loading = ref(false)
const recipe = defineModel<Recipe>({required: true})
const servings = ref(1)
const showFullRecipeName = ref(false)
/**
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
*/
const ingredientFactor = computed(() => {
return servings.value / ((recipe.value.servings != undefined) ? recipe.value.servings : 1)
})
/**
* change servings when recipe servings are changed
*/
watch(() => recipe.value.servings, () => {
if (recipe.value.servings) {
servings.value = recipe.value.servings
@@ -222,6 +237,42 @@ onBeforeUnmount(() => {
release()
})
/**
* converts the recipe into an internal recipe using AI
*/
function aiConvertRecipe() {
let api = new ApiApi()
doAiImport(null, '', recipe.value.id!).then(r => {
if (r.recipe) {
recipe.value.internal = true
recipe.value.steps = r.recipe.steps
recipe.value.keywords = r.recipe.keywords
recipe.value.servings = r.recipe.servings
recipe.value.servingsText = r.recipe.servingsText
recipe.value.workingTime = r.recipe.workingTime
recipe.value.waitingTime = r.recipe.waitingTime
loading.value = true
api.apiRecipeUpdate({id: recipe.value.id!, recipe: recipe.value}).then(r => {
recipe.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
loading.value = false
})
} else {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
</script>
<style scoped>

View File

@@ -84,8 +84,9 @@ export function useFileApi() {
* uploads the given file to the image recognition endpoint
* @param file file object to upload
* @param text text to import
* @param recipeId id of a recipe to use as import base (for external recipes
*/
function doAiImport(file: File | null, text: string = '') {
function doAiImport(file: File | null, text: string = '', recipeId: string = '') {
let formData = new FormData()
if (file != null) {
@@ -94,6 +95,8 @@ export function useFileApi() {
formData.append('file', '')
}
formData.append('text', text)
formData.append('recipe_id', recipeId)
fileApiLoading.value = true
return fetch(getDjangoUrl(`api/ai-import/`), {
method: 'POST',

View File

@@ -80,6 +80,7 @@
"Export_Supported": "",
"Export_To_ICal": "",
"External": "",
"ExternalRecipe": "",
"External_Recipe_Image": "",
"FETCH_ERROR": "",
"Failure": "",

View File

@@ -77,6 +77,7 @@
"Export_Supported": "Поддържа се експорт",
"Export_To_ICal": "Експортиране на .ics",
"External": "Външен",
"ExternalRecipe": "",
"External_Recipe_Image": "Външно изображение на рецептата",
"FETCH_ERROR": "",
"Failure": "Неуспешно",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "Exportació suportada",
"Export_To_ICal": "Exportar .ics",
"External": "Extern",
"ExternalRecipe": "",
"External_Recipe_Image": "Imatge externa de la recepta",
"FDC_ID": "FDC ID",
"FDC_ID_help": "Base de dades FDC ID",

View File

@@ -115,6 +115,7 @@
"Export_Supported": "Export podporován",
"Export_To_ICal": "Export ovat .ics",
"External": "Externí",
"ExternalRecipe": "",
"External_Recipe_Image": "Externí obrázek receptu",
"FDC_ID": "FDC ID",
"FDC_ID_help": "ID v databázi FDC",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "Eksport understøttet",
"Export_To_ICal": "Eksporter .ics",
"External": "Ekstern",
"ExternalRecipe": "",
"External_Recipe_Image": "Eksternt billede af opskrift",
"FDC_ID": "FDC ID",
"FDC_ID_help": "FDC database ID",

View File

@@ -170,6 +170,7 @@
"Export_Supported": "Exportieren wird unterstützt",
"Export_To_ICal": "Export als .ics",
"External": "Extern",
"ExternalRecipe": "Externes Rezept",
"ExternalRecipeImport": "Externer Rezeptimport",
"ExternalRecipeImportHelp": "Dateien die in überwachten Ordnern auf externen Speichern gefunden werden, werden nicht sofort als Rezept importiert, sondern zunächst als Rezeptimport zwischengespeichert. Hier können gefundene Rezepte schnell und einfach editiert werden, bevor Sie in die Sammlung aufgenommen werden ",
"ExternalStorage": "Externer Speicher",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "Υποστηρίζεται εξαγωγή",
"Export_To_ICal": "Εξαγωγή .ics",
"External": "Εξωτερική",
"ExternalRecipe": "",
"External_Recipe_Image": "Εξωτερική εικόνα συνταγής",
"FDC_ID": "Ταυτότητα FDC",
"FDC_ID_help": "Ταυτότητα βάσης δεδομένων FDC",

View File

@@ -168,6 +168,7 @@
"Export_Supported": "Export supported",
"Export_To_ICal": "Export .ics",
"External": "External",
"ExternalRecipe": "External Recipe",
"ExternalRecipeImport": "External recipe import",
"ExternalRecipeImportHelp": "Files in synced folders on external storages are not imported directly but temporarily saved as external import recipes. Here you can quickly view and edit newly found files before they are moved to the main collection. ",
"ExternalStorage": "External storage",

View File

@@ -166,6 +166,7 @@
"Export_Supported": "Exportación soportada",
"Export_To_ICal": "Exportar .ics",
"External": "Externo",
"ExternalRecipe": "",
"ExternalRecipeImport": "Importación externa de recetas",
"ExternalRecipeImportHelp": "Los archivos en carpetas sincronizadas en almacenamientos externos no se importan directamente, en su lugar son guardados temporalmente como recetas de importación externa. Aquí puedes ver y editar rápidamente los archivos recién encontrados antes de que se muevan a la colección principal. ",
"ExternalStorage": "Almacenamiento externo",

View File

@@ -113,6 +113,7 @@
"Export_Supported": "Vienti tuettu",
"Export_To_ICal": "Vie .ics",
"External": "Ulkoinen",
"ExternalRecipe": "",
"External_Recipe_Image": "Ulkoinen reseptin kuva",
"FDC_ID": "FDC -tunnus",
"FDC_ID_help": "FDC tietokanta tunnus",

View File

@@ -169,6 +169,7 @@
"Export_Supported": "Exportation prise en charge",
"Export_To_ICal": "Exporter .ics",
"External": "Externe",
"ExternalRecipe": "",
"ExternalRecipeImport": "Importation d'une recette externe",
"ExternalRecipeImportHelp": "Les fichiers des dossiers synchronisés sur des stockages externes ne sont pas importés directement, mais enregistrés temporairement comme recettes d'importation externe. Vous pouvez ainsi visualiser et modifier rapidement les fichiers nouvellement trouvés avant leur transfert vers la collection principale. ",
"ExternalStorage": "Stockage externe",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "ייצוא נתמך",
"Export_To_ICal": "ייצא .ics",
"External": "חיצוני",
"ExternalRecipe": "",
"External_Recipe_Image": "תמונת מתכון חיצונית",
"FDC_ID": "מספר FDC",
"FDC_ID_help": "מספר FDC",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "Izvoz podržan",
"Export_To_ICal": "Izvoz .ics",
"External": "Vanjski",
"ExternalRecipe": "",
"External_Recipe_Image": "Slika vanjskog recepta",
"FDC_ID": "FDC ID",
"FDC_ID_help": "FDC ID baze podataka",

View File

@@ -102,6 +102,7 @@
"Export_Supported": "",
"Export_To_ICal": "",
"External": "Külső",
"ExternalRecipe": "",
"External_Recipe_Image": "Külső receptkép",
"FETCH_ERROR": "",
"Failure": "Hiba",

View File

@@ -35,6 +35,7 @@
"Energy": "",
"Export": "",
"External": "",
"ExternalRecipe": "",
"External_Recipe_Image": "",
"FETCH_ERROR": "",
"Fats": "",

View File

@@ -91,6 +91,7 @@
"Export_Supported": "",
"Export_To_ICal": "",
"External": "Luar",
"ExternalRecipe": "",
"External_Recipe_Image": "Gambar Resep Eksternal",
"FETCH_ERROR": "",
"Failure": "Kegagalan",

View File

@@ -115,6 +115,7 @@
"Export_Supported": "",
"Export_To_ICal": "",
"External": "",
"ExternalRecipe": "",
"External_Recipe_Image": "",
"FDC_ID": "",
"FDC_ID_help": "",

View File

@@ -169,6 +169,7 @@
"Export_Supported": "Esportazione supportata",
"Export_To_ICal": "Esporta .ics",
"External": "Esterna",
"ExternalRecipe": "",
"ExternalRecipeImport": "Importa ricetta esterna",
"ExternalRecipeImportHelp": "I file nelle cartelle sincronizzate su dispositivi di archiviazione esterni non vengono importati direttamente, ma salvati temporaneamente come ricette di importazione esterne. Qui è possibile visualizzare e modificare rapidamente i file appena trovati prima che vengano spostati nella raccolta principale. ",
"ExternalStorage": "Archiviazione esterna",

View File

@@ -104,6 +104,7 @@
"Export_Supported": "",
"Export_To_ICal": "",
"External": "",
"ExternalRecipe": "",
"External_Recipe_Image": "Išorinis recepto vaizdas",
"FETCH_ERROR": "",
"Failure": "",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "",
"Export_To_ICal": "",
"External": "",
"ExternalRecipe": "",
"External_Recipe_Image": "",
"FDC_ID": "",
"FDC_ID_help": "",

View File

@@ -110,6 +110,7 @@
"Export_Supported": "",
"Export_To_ICal": "Eksporter .ics",
"External": "Ekstern",
"ExternalRecipe": "",
"External_Recipe_Image": "Bilde av ekstern oppskrift",
"FDC_ID_help": "FDC database-ID",
"FETCH_ERROR": "",

View File

@@ -170,6 +170,7 @@
"Export_Supported": "Export ondersteund",
"Export_To_ICal": "Exporteer .ics",
"External": "Externe",
"ExternalRecipe": "",
"ExternalRecipeImport": "Externe receptimport",
"ExternalRecipeImportHelp": "Bestanden in gesynchroniseerde mappen op externe opslag worden niet direct geïmporteerd, maar tijdelijk opgeslagen als externe receptimport. Hier kun je snel nieuw gevonden bestanden bekijken en bewerken voordat ze naar de hoofdcollectie worden verplaatst. ",
"ExternalStorage": "Externe opslag",

View File

@@ -142,6 +142,7 @@
"Export_Supported": "Eksportowanie wspierane",
"Export_To_ICal": "Eksportuj .ics",
"External": "Zewnętrzny",
"ExternalRecipe": "",
"External_Recipe_Image": "Zewnętrzny obraz dla przepisu",
"FDC_ID": "Identyfikator FDC",
"FDC_ID_help": "Identyfikator bazy FDC",

View File

@@ -89,6 +89,7 @@
"Export_As_ICal": "Exportar período atual para o formato ICal",
"Export_To_ICal": "Exportar .ics",
"External": "Externo",
"ExternalRecipe": "",
"External_Recipe_Image": "Imagem da receita externa",
"FDC_ID": "ID FDC",
"FDC_ID_help": "ID database FDC",

View File

@@ -168,6 +168,7 @@
"Export_Supported": "Exportação suportada",
"Export_To_ICal": "Exportar .ics",
"External": "Externo",
"ExternalRecipe": "",
"ExternalRecipeImport": "Importar receita externa",
"ExternalRecipeImportHelp": "Arquivos em pastas sincronizadas em armazenamentos externos não são importados diretamente, mas salvos temporariamente como receitas de importação externa. Aqui, você pode visualizar e editar rapidamente os arquivos recém-encontrados antes que eles sejam movidos para a coleção principal. ",
"ExternalStorage": "Armazenamento externo",

View File

@@ -98,6 +98,7 @@
"Export_Supported": "Export compatibil",
"Export_To_ICal": "Exportă .ics",
"External": "Extern",
"ExternalRecipe": "",
"External_Recipe_Image": "Imagine rețetă externă",
"FETCH_ERROR": "",
"Failure": "Eșec",

View File

@@ -169,6 +169,7 @@
"Export_Supported": "Экспорт поддерживается",
"Export_To_ICal": "Экспортировать .ics",
"External": "Внешний",
"ExternalRecipe": "",
"ExternalRecipeImport": "Импорт внешних рецептов",
"ExternalRecipeImportHelp": "Файлы в синхронизируемых папках на внешних хранилищах не импортируются напрямую, а временно сохраняются как рецепты внешнего импорта. Здесь вы можете быстро просмотреть и отредактировать найденные файлы перед тем, как они будут перемещены в основную коллекцию. ",
"ExternalStorage": "Внешнее хранилище",

View File

@@ -169,6 +169,7 @@
"Export_Supported": "Izvoz podprt",
"Export_To_ICal": "Izvoz.ics",
"External": "Zunanje",
"ExternalRecipe": "",
"ExternalRecipeImport": "Uvoz zunanjih receptov",
"ExternalRecipeImportHelp": "Datoteke v sinhroniziranih mapah na zunanjih shrambah se ne uvozijo neposredno, temveč se začasno shranijo kot recepti za zunanji uvoz. Tukaj si lahko hitro ogledate in uredite novo najdene datoteke, preden jih premaknete v glavno zbirko. ",
"ExternalStorage": "Zunanji pomnilnik",

View File

@@ -153,6 +153,7 @@
"Export_Supported": "Export stöds",
"Export_To_ICal": "Exportera .ics",
"External": "Extern",
"ExternalRecipe": "",
"External_Recipe_Image": "Extern receptbild",
"FDC_ID": "FDC ID",
"FDC_ID_help": "FDC databas ID",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "Desteklenen Dışa Aktarma",
"Export_To_ICal": ".ics olarak dışa aktar",
"External": "Harici",
"ExternalRecipe": "",
"External_Recipe_Image": "Harici Tarif Resim",
"FDC_ID": "FDC Kimlik",
"FDC_ID_help": "FDC veritabanı Kimlik",

View File

@@ -99,6 +99,7 @@
"Export_Supported": "",
"Export_To_ICal": "Експортувати .ics",
"External": "Зовнішній",
"ExternalRecipe": "",
"External_Recipe_Image": "Зображення Зовнішнього Рецепту",
"FDC_ID": "FDC ID",
"FDC_ID_help": "Ідентифікатор Бази FDC",

View File

@@ -116,6 +116,7 @@
"Export_Supported": "导出支持",
"Export_To_ICal": "导出 .ics",
"External": "外部",
"ExternalRecipe": "",
"External_Recipe_Image": "外部食谱图像",
"FDC_ID": "FDC ID",
"FDC_ID_help": "FDC数据库ID",

View File

@@ -168,6 +168,7 @@
"Export_Supported": "支援匯出",
"Export_To_ICal": "匯出到 iCal",
"External": "外部",
"ExternalRecipe": "",
"ExternalRecipeImport": "外部食譜匯入",
"ExternalRecipeImportHelp": "外部儲存同步資料夾中的檔案不會直接匯入,而是暫時儲存為外部匯入食譜。在這裡您可以快速檢視和編輯新發現的檔案,然後再將它們移至主要收藏。 ",
"ExternalStorage": "外部儲存",

View File

@@ -479,6 +479,7 @@ export interface ApiAccessTokenUpdateRequest {
export interface ApiAiImportCreateRequest {
file: string | null;
text: string | null;
recipeId: string | null;
}
export interface ApiAutoPlanCreateRequest {
@@ -741,6 +742,7 @@ export interface ApiGroupRetrieveRequest {
export interface ApiImportCreateRequest {
file: string | null;
text: string | null;
recipeId: string | null;
}
export interface ApiImportLogCreateRequest {
@@ -2087,6 +2089,13 @@ export class ApiApi extends runtime.BaseAPI {
);
}
if (requestParameters['recipeId'] == null) {
throw new runtime.RequiredError(
'recipeId',
'Required parameter "recipeId" was null or undefined when calling apiAiImportCreate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
@@ -2117,6 +2126,10 @@ export class ApiApi extends runtime.BaseAPI {
formParams.append('text', requestParameters['text'] as any);
}
if (requestParameters['recipeId'] != null) {
formParams.append('recipe_id', requestParameters['recipeId'] as any);
}
const response = await this.request({
path: `/api/ai-import/`,
method: 'POST',
@@ -4425,6 +4438,13 @@ export class ApiApi extends runtime.BaseAPI {
);
}
if (requestParameters['recipeId'] == null) {
throw new runtime.RequiredError(
'recipeId',
'Required parameter "recipeId" was null or undefined when calling apiImportCreate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
@@ -4455,6 +4475,10 @@ export class ApiApi extends runtime.BaseAPI {
formParams.append('text', requestParameters['text'] as any);
}
if (requestParameters['recipeId'] != null) {
formParams.append('recipe_id', requestParameters['recipeId'] as any);
}
const response = await this.request({
path: `/api/import/`,
method: 'POST',