added AI text import

This commit is contained in:
vabene1111
2025-04-11 15:49:11 +02:00
parent 604d18d594
commit 052219e141
16 changed files with 266 additions and 2032 deletions

View File

@@ -79,14 +79,15 @@ export function useFileApi() {
/**
* uploads the given file to the image recognition endpoint
* @param file file object to upload
* @param text text to import
*/
function convertImageToRecipe(file: File) {
function doAiImport(file: File|null, text: string = '') {
let formData = new FormData()
if (file != null) {
formData.append('image', file)
}
return fetch(getDjangoUrl(`api/image-to-recipe/`), {
formData.append('file', '')
formData.append('text', text)
return fetch(getDjangoUrl(`api/ai-import/`), {
method: 'POST',
headers: {'X-CSRFToken': getCookie('csrftoken')},
body: formData
@@ -127,5 +128,5 @@ export function useFileApi() {
})
}
return {fileApiLoading, createOrUpdateUserFile, updateRecipeImage, convertImageToRecipe, doAppImport}
return {fileApiLoading, createOrUpdateUserFile, updateRecipeImage, doAiImport, doAppImport}
}

View File

@@ -7,8 +7,6 @@ models/AccessToken.ts
models/AuthToken.ts
models/AutoMealPlan.ts
models/Automation.ts
models/AutomationTypeEnum.ts
models/BaseUnitEnum.ts
models/BookmarkletImport.ts
models/BookmarkletImportList.ts
models/ConnectorConfigConfig.ts
@@ -35,16 +33,6 @@ models/MealPlan.ts
models/MealType.ts
models/MethodEnum.ts
models/NutritionInformation.ts
models/OpenDataCategory.ts
models/OpenDataConversion.ts
models/OpenDataFood.ts
models/OpenDataFoodProperty.ts
models/OpenDataProperty.ts
models/OpenDataStore.ts
models/OpenDataStoreCategory.ts
models/OpenDataUnit.ts
models/OpenDataUnitTypeEnum.ts
models/OpenDataVersion.ts
models/PaginatedAutomationList.ts
models/PaginatedBookmarkletImportListList.ts
models/PaginatedCookLogList.ts
@@ -91,13 +79,6 @@ models/PatchedInviteLink.ts
models/PatchedKeyword.ts
models/PatchedMealPlan.ts
models/PatchedMealType.ts
models/PatchedOpenDataCategory.ts
models/PatchedOpenDataConversion.ts
models/PatchedOpenDataFood.ts
models/PatchedOpenDataProperty.ts
models/PatchedOpenDataStore.ts
models/PatchedOpenDataUnit.ts
models/PatchedOpenDataVersion.ts
models/PatchedProperty.ts
models/PatchedPropertyType.ts
models/PatchedRecipe.ts
@@ -157,6 +138,7 @@ models/SupermarketCategoryRelation.ts
models/Sync.ts
models/SyncLog.ts
models/ThemeEnum.ts
models/TypeEnum.ts
models/Unit.ts
models/UnitConversion.ts
models/User.ts

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,12 @@
*/
import { mapValues } from '../runtime';
import type { AutomationTypeEnum } from './AutomationTypeEnum';
import type { TypeEnum } from './TypeEnum';
import {
AutomationTypeEnumFromJSON,
AutomationTypeEnumFromJSONTyped,
AutomationTypeEnumToJSON,
} from './AutomationTypeEnum';
TypeEnumFromJSON,
TypeEnumFromJSONTyped,
TypeEnumToJSON,
} from './TypeEnum';
/**
*
@@ -34,10 +34,10 @@ export interface Automation {
id?: number;
/**
*
* @type {AutomationTypeEnum}
* @type {TypeEnum}
* @memberof Automation
*/
type: AutomationTypeEnum;
type: TypeEnum;
/**
*
* @type {string}
@@ -108,7 +108,7 @@ export function AutomationFromJSONTyped(json: any, ignoreDiscriminator: boolean)
return {
'id': json['id'] == null ? undefined : json['id'],
'type': AutomationTypeEnumFromJSON(json['type']),
'type': TypeEnumFromJSON(json['type']),
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'param1': json['param_1'] == null ? undefined : json['param_1'],
@@ -127,7 +127,7 @@ export function AutomationToJSON(value?: Omit<Automation, 'createdBy'> | null):
return {
'id': value['id'],
'type': AutomationTypeEnumToJSON(value['type']),
'type': TypeEnumToJSON(value['type']),
'name': value['name'],
'description': value['description'],
'param_1': value['param1'],

View File

@@ -64,7 +64,7 @@ export interface Group {
* @type {string}
* @memberof Group
*/
name: string;
readonly name: string;
}
/**
@@ -90,14 +90,13 @@ export function GroupFromJSONTyped(json: any, ignoreDiscriminator: boolean): Gro
};
}
export function GroupToJSON(value?: Group | null): any {
export function GroupToJSON(value?: Omit<Group, 'name'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
};
}

View File

@@ -13,12 +13,12 @@
*/
import { mapValues } from '../runtime';
import type { AutomationTypeEnum } from './AutomationTypeEnum';
import type { TypeEnum } from './TypeEnum';
import {
AutomationTypeEnumFromJSON,
AutomationTypeEnumFromJSONTyped,
AutomationTypeEnumToJSON,
} from './AutomationTypeEnum';
TypeEnumFromJSON,
TypeEnumFromJSONTyped,
TypeEnumToJSON,
} from './TypeEnum';
/**
*
@@ -34,10 +34,10 @@ export interface PatchedAutomation {
id?: number;
/**
*
* @type {AutomationTypeEnum}
* @type {TypeEnum}
* @memberof PatchedAutomation
*/
type?: AutomationTypeEnum;
type?: TypeEnum;
/**
*
* @type {string}
@@ -106,7 +106,7 @@ export function PatchedAutomationFromJSONTyped(json: any, ignoreDiscriminator: b
return {
'id': json['id'] == null ? undefined : json['id'],
'type': json['type'] == null ? undefined : AutomationTypeEnumFromJSON(json['type']),
'type': json['type'] == null ? undefined : TypeEnumFromJSON(json['type']),
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'param1': json['param_1'] == null ? undefined : json['param_1'],
@@ -125,7 +125,7 @@ export function PatchedAutomationToJSON(value?: Omit<PatchedAutomation, 'created
return {
'id': value['id'],
'type': AutomationTypeEnumToJSON(value['type']),
'type': TypeEnumToJSON(value['type']),
'name': value['name'],
'description': value['description'],
'param_1': value['param1'],

View File

@@ -54,19 +54,19 @@ export interface PatchedUser {
* @type {boolean}
* @memberof PatchedUser
*/
isStaff?: boolean;
readonly isStaff?: boolean;
/**
* Designates that this user has all permissions without explicitly assigning them.
* @type {boolean}
* @memberof PatchedUser
*/
isSuperuser?: boolean;
readonly isSuperuser?: boolean;
/**
* Designates whether this user should be treated as active. Unselect this instead of deleting accounts.
* @type {boolean}
* @memberof PatchedUser
*/
isActive?: boolean;
readonly isActive?: boolean;
}
/**
@@ -97,7 +97,7 @@ export function PatchedUserFromJSONTyped(json: any, ignoreDiscriminator: boolean
};
}
export function PatchedUserToJSON(value?: Omit<PatchedUser, 'username'|'displayName'> | null): any {
export function PatchedUserToJSON(value?: Omit<PatchedUser, 'username'|'displayName'|'isStaff'|'isSuperuser'|'isActive'> | null): any {
if (value == null) {
return value;
}
@@ -106,9 +106,6 @@ export function PatchedUserToJSON(value?: Omit<PatchedUser, 'username'|'displayN
'id': value['id'],
'first_name': value['firstName'],
'last_name': value['lastName'],
'is_staff': value['isStaff'],
'is_superuser': value['isSuperuser'],
'is_active': value['isActive'],
};
}

View File

@@ -42,7 +42,7 @@ export interface SourceImportKeyword {
* @type {boolean}
* @memberof SourceImportKeyword
*/
importKeyword: boolean;
importKeyword?: boolean;
}
/**
@@ -51,7 +51,6 @@ export interface SourceImportKeyword {
export function instanceOfSourceImportKeyword(value: object): value is SourceImportKeyword {
if (!('label' in value) || value['label'] === undefined) return false;
if (!('name' in value) || value['name'] === undefined) return false;
if (!('importKeyword' in value) || value['importKeyword'] === undefined) return false;
return true;
}
@@ -68,7 +67,7 @@ export function SourceImportKeywordFromJSONTyped(json: any, ignoreDiscriminator:
'id': json['id'] == null ? undefined : json['id'],
'label': json['label'],
'name': json['name'],
'importKeyword': json['import_keyword'],
'importKeyword': json['import_keyword'] == null ? undefined : json['import_keyword'],
};
}

View File

@@ -49,7 +49,7 @@ export interface SourceImportRecipe {
* @type {boolean}
* @memberof SourceImportRecipe
*/
internal: boolean;
internal?: boolean;
/**
*
* @type {string}
@@ -67,49 +67,49 @@ export interface SourceImportRecipe {
* @type {string}
* @memberof SourceImportRecipe
*/
description: string;
description?: string;
/**
*
* @type {number}
* @memberof SourceImportRecipe
*/
servings: number;
servings?: number;
/**
*
* @type {string}
* @memberof SourceImportRecipe
*/
servingsText: string;
servingsText?: string;
/**
*
* @type {number}
* @memberof SourceImportRecipe
*/
workingTime: number;
workingTime?: number;
/**
*
* @type {number}
* @memberof SourceImportRecipe
*/
waitingTime: number;
waitingTime?: number;
/**
*
* @type {string}
* @memberof SourceImportRecipe
*/
imageUrl: string;
imageUrl?: string;
/**
*
* @type {Array<SourceImportKeyword>}
* @memberof SourceImportRecipe
*/
keywords: Array<SourceImportKeyword>;
keywords?: Array<SourceImportKeyword>;
/**
*
* @type {Array<SourceImportProperty>}
* @memberof SourceImportRecipe
*/
properties: Array<SourceImportProperty>;
properties?: Array<SourceImportProperty>;
}
/**
@@ -117,17 +117,8 @@ export interface SourceImportRecipe {
*/
export function instanceOfSourceImportRecipe(value: object): value is SourceImportRecipe {
if (!('steps' in value) || value['steps'] === undefined) return false;
if (!('internal' in value) || value['internal'] === undefined) return false;
if (!('sourceUrl' in value) || value['sourceUrl'] === undefined) return false;
if (!('name' in value) || value['name'] === undefined) return false;
if (!('description' in value) || value['description'] === undefined) return false;
if (!('servings' in value) || value['servings'] === undefined) return false;
if (!('servingsText' in value) || value['servingsText'] === undefined) return false;
if (!('workingTime' in value) || value['workingTime'] === undefined) return false;
if (!('waitingTime' in value) || value['waitingTime'] === undefined) return false;
if (!('imageUrl' in value) || value['imageUrl'] === undefined) return false;
if (!('keywords' in value) || value['keywords'] === undefined) return false;
if (!('properties' in value) || value['properties'] === undefined) return false;
return true;
}
@@ -142,17 +133,17 @@ export function SourceImportRecipeFromJSONTyped(json: any, ignoreDiscriminator:
return {
'steps': ((json['steps'] as Array<any>).map(SourceImportStepFromJSON)),
'internal': json['internal'],
'internal': json['internal'] == null ? undefined : json['internal'],
'sourceUrl': json['source_url'],
'name': json['name'],
'description': json['description'],
'servings': json['servings'],
'servingsText': json['servings_text'],
'workingTime': json['working_time'],
'waitingTime': json['waiting_time'],
'imageUrl': json['image_url'],
'keywords': ((json['keywords'] as Array<any>).map(SourceImportKeywordFromJSON)),
'properties': ((json['properties'] as Array<any>).map(SourceImportPropertyFromJSON)),
'description': json['description'] == null ? undefined : json['description'],
'servings': json['servings'] == null ? undefined : json['servings'],
'servingsText': json['servings_text'] == null ? undefined : json['servings_text'],
'workingTime': json['working_time'] == null ? undefined : json['working_time'],
'waitingTime': json['waiting_time'] == null ? undefined : json['waiting_time'],
'imageUrl': json['image_url'] == null ? undefined : json['image_url'],
'keywords': json['keywords'] == null ? undefined : ((json['keywords'] as Array<any>).map(SourceImportKeywordFromJSON)),
'properties': json['properties'] == null ? undefined : ((json['properties'] as Array<any>).map(SourceImportPropertyFromJSON)),
};
}
@@ -172,8 +163,8 @@ export function SourceImportRecipeToJSON(value?: SourceImportRecipe | null): any
'working_time': value['workingTime'],
'waiting_time': value['waitingTime'],
'image_url': value['imageUrl'],
'keywords': ((value['keywords'] as Array<any>).map(SourceImportKeywordToJSON)),
'properties': ((value['properties'] as Array<any>).map(SourceImportPropertyToJSON)),
'keywords': value['keywords'] == null ? undefined : ((value['keywords'] as Array<any>).map(SourceImportKeywordToJSON)),
'properties': value['properties'] == null ? undefined : ((value['properties'] as Array<any>).map(SourceImportPropertyToJSON)),
};
}

View File

@@ -43,7 +43,7 @@ export interface SourceImportStep {
* @type {boolean}
* @memberof SourceImportStep
*/
showIngredientsTable: boolean;
showIngredientsTable?: boolean;
}
/**
@@ -52,7 +52,6 @@ export interface SourceImportStep {
export function instanceOfSourceImportStep(value: object): value is SourceImportStep {
if (!('instruction' in value) || value['instruction'] === undefined) return false;
if (!('ingredients' in value) || value['ingredients'] === undefined) return false;
if (!('showIngredientsTable' in value) || value['showIngredientsTable'] === undefined) return false;
return true;
}
@@ -68,7 +67,7 @@ export function SourceImportStepFromJSONTyped(json: any, ignoreDiscriminator: bo
'instruction': json['instruction'],
'ingredients': ((json['ingredients'] as Array<any>).map(SourceImportIngredientFromJSON)),
'showIngredientsTable': json['show_ingredients_table'],
'showIngredientsTable': json['show_ingredients_table'] == null ? undefined : json['show_ingredients_table'],
};
}

View File

@@ -54,19 +54,19 @@ export interface User {
* @type {boolean}
* @memberof User
*/
isStaff?: boolean;
readonly isStaff: boolean;
/**
* Designates that this user has all permissions without explicitly assigning them.
* @type {boolean}
* @memberof User
*/
isSuperuser?: boolean;
readonly isSuperuser: boolean;
/**
* Designates whether this user should be treated as active. Unselect this instead of deleting accounts.
* @type {boolean}
* @memberof User
*/
isActive?: boolean;
readonly isActive: boolean;
}
/**
@@ -75,6 +75,9 @@ export interface User {
export function instanceOfUser(value: object): value is User {
if (!('username' in value) || value['username'] === undefined) return false;
if (!('displayName' in value) || value['displayName'] === undefined) return false;
if (!('isStaff' in value) || value['isStaff'] === undefined) return false;
if (!('isSuperuser' in value) || value['isSuperuser'] === undefined) return false;
if (!('isActive' in value) || value['isActive'] === undefined) return false;
return true;
}
@@ -93,13 +96,13 @@ export function UserFromJSONTyped(json: any, ignoreDiscriminator: boolean): User
'firstName': json['first_name'] == null ? undefined : json['first_name'],
'lastName': json['last_name'] == null ? undefined : json['last_name'],
'displayName': json['display_name'],
'isStaff': json['is_staff'] == null ? undefined : json['is_staff'],
'isSuperuser': json['is_superuser'] == null ? undefined : json['is_superuser'],
'isActive': json['is_active'] == null ? undefined : json['is_active'],
'isStaff': json['is_staff'],
'isSuperuser': json['is_superuser'],
'isActive': json['is_active'],
};
}
export function UserToJSON(value?: Omit<User, 'username'|'displayName'> | null): any {
export function UserToJSON(value?: Omit<User, 'username'|'displayName'|'isStaff'|'isSuperuser'|'isActive'> | null): any {
if (value == null) {
return value;
}
@@ -108,9 +111,6 @@ export function UserToJSON(value?: Omit<User, 'username'|'displayName'> | null):
'id': value['id'],
'first_name': value['firstName'],
'last_name': value['lastName'],
'is_staff': value['isStaff'],
'is_superuser': value['isSuperuser'],
'is_active': value['isActive'],
};
}

View File

@@ -4,8 +4,6 @@ export * from './AccessToken';
export * from './AuthToken';
export * from './AutoMealPlan';
export * from './Automation';
export * from './AutomationTypeEnum';
export * from './BaseUnitEnum';
export * from './BookmarkletImport';
export * from './BookmarkletImportList';
export * from './ConnectorConfigConfig';
@@ -32,16 +30,6 @@ export * from './MealPlan';
export * from './MealType';
export * from './MethodEnum';
export * from './NutritionInformation';
export * from './OpenDataCategory';
export * from './OpenDataConversion';
export * from './OpenDataFood';
export * from './OpenDataFoodProperty';
export * from './OpenDataProperty';
export * from './OpenDataStore';
export * from './OpenDataStoreCategory';
export * from './OpenDataUnit';
export * from './OpenDataUnitTypeEnum';
export * from './OpenDataVersion';
export * from './PaginatedAutomationList';
export * from './PaginatedBookmarkletImportListList';
export * from './PaginatedCookLogList';
@@ -88,13 +76,6 @@ export * from './PatchedInviteLink';
export * from './PatchedKeyword';
export * from './PatchedMealPlan';
export * from './PatchedMealType';
export * from './PatchedOpenDataCategory';
export * from './PatchedOpenDataConversion';
export * from './PatchedOpenDataFood';
export * from './PatchedOpenDataProperty';
export * from './PatchedOpenDataStore';
export * from './PatchedOpenDataUnit';
export * from './PatchedOpenDataVersion';
export * from './PatchedProperty';
export * from './PatchedPropertyType';
export * from './PatchedRecipe';
@@ -154,6 +135,7 @@ export * from './SupermarketCategoryRelation';
export * from './Sync';
export * from './SyncLog';
export * from './ThemeEnum';
export * from './TypeEnum';
export * from './Unit';
export * from './UnitConversion';
export * from './User';

View File

@@ -118,13 +118,23 @@
<v-text-field :label="$t('Website') + ' (https://...)'" v-model="importUrl" v-if="importType == 'url'" :loading="loading" autofocus
@keydown.enter="loadRecipeFromUrl({url: importUrl})"></v-text-field>
<v-file-upload v-model="image" v-if="importType == 'ai'" :loading="loading" clearable>
<template #icon>
<v-icon icon="fa-solid fa-file-pdf"></v-icon>
{{$t('or')}}
<v-icon icon="fa-solid fa-file-image"></v-icon>
</template>
</v-file-upload>
<div v-if="importType == 'ai'">
<v-btn-toggle v-model="aiMode">
<v-btn value="file">{{ $t('File') }}</v-btn>
<v-btn value="text">{{ $t('Text') }}</v-btn>
</v-btn-toggle>
<v-file-upload v-model="image" v-if="aiMode == 'file'" :loading="loading" clearable>
<template #icon>
<v-icon icon="fa-solid fa-file-pdf"></v-icon>
{{ $t('or') }}
<v-icon icon="fa-solid fa-file-image"></v-icon>
</template>
</v-file-upload>
<v-textarea v-model="sourceImportText" :loading="loading" autofocus v-if="aiMode == 'text'"
@keydown.enter="loadRecipeFromAiImport()"></v-textarea>
</div>
<v-textarea v-model="sourceImportText" label="JSON/HTML" :loading="loading" v-if="importType == 'source'" :hint="$t('SourceImportHelp')"
persistent-hint autofocus @keydown.enter="loadRecipeFromUrl({data: sourceImportText})"></v-textarea>
@@ -145,7 +155,7 @@
<v-btn @click="loadRecipeFromUrl({data: sourceImportText})" v-if="importType == 'source'" :disabled="sourceImportText == ''"
:loading="loading">{{ $t('Load') }}
</v-btn>
<v-btn @click="uploadAndConvertImage()" v-if="importType == 'ai'" :disabled="image == null" :loading="loading">{{ $t('Load') }}</v-btn>
<v-btn @click="loadRecipeFromAiImport()" v-if="importType == 'ai'" :disabled="image == null" :loading="loading">{{ $t('Load') }}</v-btn>
</template>
</v-stepper-actions>
</v-stepper-window-item>
@@ -460,7 +470,7 @@ import bookmarkletJs from '@/assets/bookmarklet_v3?url'
const params = useUrlSearchParams('history', {})
const {mobile} = useDisplay()
const router = useRouter()
const {updateRecipeImage, convertImageToRecipe, doAppImport, fileApiLoading} = useFileApi()
const {updateRecipeImage, doAiImport, doAppImport, fileApiLoading} = useFileApi()
const {getDjangoUrl} = useDjangoUrls()
const bookmarkletContent = computed(() => {
@@ -488,6 +498,7 @@ const appImportFiles = ref<File[]>([])
const appImportDuplicates = ref(false)
const appImportLog = ref<null | ImportLog>(null)
const image = ref<null | File>(null)
const aiMode = ref<'file' | 'text'>('file')
const bookmarkletToken = ref("")
@@ -533,7 +544,7 @@ function loadRecipeFromUrl(recipeFromSourceRequest: RecipeFromSource) {
if (importResponse.value.duplicates && importResponse.value.duplicates.length > 0) {
stepper.value = 'duplicates'
} else {
if(importResponse.value.images && importResponse.value.images.length > 0){
if (importResponse.value.images && importResponse.value.images.length > 0) {
stepper.value = 'image_chooser'
} else {
stepper.value = 'keywords_chooser'
@@ -555,15 +566,22 @@ function loadRecipeFromUrl(recipeFromSourceRequest: RecipeFromSource) {
/**
* upload file to conversion endpoint
*/
function uploadAndConvertImage() {
if (image.value != null) {
function loadRecipeFromAiImport() {
let request = null
if (image.value != null && aiMode.value == 'file') {
request = doAiImport(image.value)
} else if (sourceImportText.value != '' && aiMode.value == 'text') {
request = doAiImport(null, sourceImportText.value)
}
if (request != null) {
loading.value = true
convertImageToRecipe(image.value).then(r => {
request.then(r => {
loading.value = false
importResponse.value = r
if (!importResponse.value.error){
if(importResponse.value.images && importResponse.value.images.length > 0){
if (!importResponse.value.error) {
if (importResponse.value.images && importResponse.value.images.length > 0) {
stepper.value = 'image_chooser'
} else {
stepper.value = 'keywords_chooser'
@@ -573,6 +591,7 @@ function uploadAndConvertImage() {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
}
function appImport() {