Compare commits

..

30 Commits
2.1.2 ... 2.2.0

Author SHA1 Message Date
vabene1111
ed8f97e9e0 Merge branch 'develop'
# Conflicts:
#	vue3/src/locales/de.json
2025-09-10 20:36:33 +02:00
vabene1111
034f68fc28 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-09-10 20:34:55 +02:00
vabene1111
0158087a0b fixed ai test 2025-09-10 20:34:51 +02:00
vabene1111
cb6bfd741d Merge pull request #4023 from TandoorRecipes/dependabot/pip/django-4.2.24
Bump django from 4.2.22 to 4.2.24
2025-09-10 20:34:21 +02:00
vabene1111
afeee5f7cb fixed link for subpath setups 2025-09-10 20:16:22 +02:00
vabene1111
b43d6e08d4 food batch editor implementation 2025-09-10 16:52:35 +02:00
vabene1111
1188624376 food batch update dialog and first api functions 2025-09-10 07:54:42 +02:00
dependabot[bot]
9ac837c969 Bump django from 4.2.22 to 4.2.24
Bumps [django](https://github.com/django/django) from 4.2.22 to 4.2.24.
- [Commits](https://github.com/django/django/compare/4.2.22...4.2.24)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 4.2.24
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 02:19:20 +00:00
vabene1111
fc4b017d30 food batch update endpoint 2025-09-09 16:54:29 +02:00
vabene1111
4636ac28f9 ai system improvements 2025-09-09 16:30:54 +02:00
vabene1111
397912e87f WIP AI system 2025-09-09 14:58:32 +02:00
vabene1111
d0b860e623 shorter create message for model select 2025-09-09 13:36:05 +02:00
vabene1111
8a90ed1274 fixed ingredient parser error 2025-09-09 13:33:18 +02:00
vabene1111
286d707347 lots of AI provider stuff 2025-09-09 07:54:59 +02:00
vabene1111
98d308aee9 fixed space overview 2025-09-09 07:54:45 +02:00
vabene1111
a7c5240227 ai provider system 2025-09-08 22:15:57 +02:00
vabene1111
75fcff8e70 font show sponsor link on hosted edition 2025-09-08 21:15:20 +02:00
vabene1111
2f27cf4deb fixed meal plan loading 2025-09-08 21:13:33 +02:00
vabene1111
686b595f45 fixed demo auto login 2025-09-08 20:55:20 +02:00
vabene1111
0f9f9e8f7c Merge pull request #4013 from tomtjes/fix-logo-font
Make logo consistent across end user systems
2025-09-08 20:15:19 +02:00
vabene1111
e6abdf8cd4 fixed vite config 2025-09-08 08:38:46 +02:00
vabene1111
741e9eb370 plugin and hosted fixes 2025-09-08 08:31:01 +02:00
tomtjes
7db523d8c4 convert text to path 2025-09-07 19:34:11 +00:00
vabene1111
41f0060c43 basics of AI provider system 2025-09-05 21:36:43 +02:00
vabene1111
5572833f64 dont display 0 amount in ingredient table 2025-09-05 18:02:09 +02:00
vabene1111
780e441a3b fixed migration tree 2025-09-05 17:50:02 +02:00
vabene1111
c4fd2d0b4e fixed timer localization 2025-09-05 17:46:18 +02:00
vabene1111
1c6618f452 Merge pull request #3999 from icedieler/patch-1
Update nginx configuration for manual setup
2025-09-05 17:30:00 +02:00
vabene1111
8c96a75a1e basics of ai database 2025-09-05 14:50:10 +02:00
Matthias Lange
f099e2e5d3 Update nginx configuration for manual setup
Since v2 tandoor added user session tracking which requires the reverse proxy to add an extra header.

This change adds the `X-Forwarded-For` header to the example nginx configuration. This header fixes the issue described in #3943.
2025-09-03 10:22:49 +02:00
85 changed files with 3269 additions and 261 deletions

View File

@@ -15,14 +15,14 @@
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
<a href="https://app.tandoor.dev/e/demo-auto-login/" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
</p>
<p align="center">
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://app.tandoor.dev/e/demo-auto-login/" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://community.tandoor.dev" target="_blank" rel="noopener noreferrer">Community</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>

View File

@@ -17,7 +17,7 @@ from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, Im
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, ConnectorConfig)
ViewLog, ConnectorConfig, AiProvider, AiLog)
admin.site.login = secure_admin_login(admin.site.login)
@@ -90,6 +90,20 @@ class SearchPreferenceAdmin(admin.ModelAdmin):
admin.site.register(SearchPreference, SearchPreferenceAdmin)
class AiProviderAdmin(admin.ModelAdmin):
list_display = ('name', 'space', 'model',)
search_fields = ('name', 'space', 'model',)
admin.site.register(AiProvider, AiProviderAdmin)
class AiLogAdmin(admin.ModelAdmin):
list_display = ('ai_provider', 'function', 'credit_cost', 'created_by', 'created_at',)
admin.site.register(AiLog, AiLogAdmin)
class StorageAdmin(admin.ModelAdmin):
list_display = ('name', 'method')
search_fields = ('name',)

View File

@@ -0,0 +1,80 @@
from decimal import Decimal
from django.utils import timezone
from django.db.models import Sum
from litellm import CustomLogger
from cookbook.models import AiLog
def get_monthly_token_usage(space):
"""
returns the number of credits the space has used in the current month
"""
token_usage = AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum']
if token_usage is None:
token_usage = 0
return token_usage
def has_monthly_token(space):
"""
checks if the monthly credit limit has been exceeded
"""
return get_monthly_token_usage(space) < space.ai_credits_monthly
def can_perform_ai_request(space):
return (has_monthly_token(space) or space.ai_credits_balance > 0) and space.ai_enabled
class AiCallbackHandler(CustomLogger):
space = None
user = None
ai_provider = None
def __init__(self, space, user, ai_provider):
super().__init__()
self.space = space
self.user = user
self.ai_provider = ai_provider
def log_pre_api_call(self, model, messages, kwargs):
pass
def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
pass
def log_success_event(self, kwargs, response_obj, start_time, end_time):
self.create_ai_log(kwargs, response_obj, start_time, end_time)
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
self.create_ai_log(kwargs, response_obj, start_time, end_time)
def create_ai_log(self, kwargs, response_obj, start_time, end_time):
credit_cost = 0
credits_from_balance = False
if self.ai_provider.log_credit_cost:
credit_cost = kwargs.get("response_cost", 0) * 100
if (not has_monthly_token(self.space)) and self.space.ai_credits_balance > 0:
remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
if remaining_balance < 0:
remaining_balance = 0
self.space.ai_credits_balance = remaining_balance
credits_from_balance = True
self.space.save()
AiLog.objects.create(
created_by=self.user,
space=self.space,
ai_provider=self.ai_provider,
start_time=start_time,
end_time=end_time,
input_tokens=response_obj['usage']['prompt_tokens'],
output_tokens=response_obj['usage']['completion_tokens'],
function=AiLog.F_FILE_IMPORT,
credit_cost=credit_cost,
credits_from_balance=credits_from_balance,
)

View File

@@ -0,0 +1,22 @@
def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
"""
given a model, the base and related field and the base and related ids, bulk create relation objects
"""
relation_objects = []
for b in base_ids:
for r in related_ids:
relation_objects.append(relation_model(**{base_field_name: b, related_field_name: r}))
relation_model.objects.bulk_create(relation_objects, ignore_conflicts=True, unique_fields=(base_field_name, related_field_name,))
def remove_from_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids, f'{related_field_name}__in': related_ids}).delete()
def remove_all_from_relation(relation_model, base_field_name, base_ids):
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids}).delete()
def set_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
remove_all_from_relation(relation_model, base_field_name, base_ids)
add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids)

View File

@@ -330,6 +330,24 @@ class CustomRecipePermission(permissions.BasePermission):
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS)
or has_group_permission(request.user, ['user'])) and obj.space == request.space
class CustomAiProviderPermission(permissions.BasePermission):
"""
Custom permission class for the AiProvider api endpoint
users: can read all
admins: can read and write
superusers: can read and write + write providers without a space
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view): # user is either at least a user and the request is safe
return (has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS) or (has_group_permission(request.user, ['admin']) or request.user.is_superuser)
# editing of global providers allowed for superusers, space providers by admins and users can read only access
def has_object_permission(self, request, view, obj):
return ((obj.space is None and request.user.is_superuser)
or (obj.space == request.space and has_group_permission(request.user, ['admin']))
or (obj.space == request.space and has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS))
class CustomUserPermission(permissions.BasePermission):
"""

View File

@@ -0,0 +1,60 @@
# Generated by Django 4.2.22 on 2025-09-05 06:51
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0223_auto_20250831_1111'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_credits_balance',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='space',
name='ai_credits_monthly',
field=models.IntegerField(default=100),
),
migrations.CreateModel(
name='AiProvider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('description', models.TextField(blank=True)),
('api_key', models.CharField(max_length=2048)),
('model_name', models.CharField(max_length=256)),
('url', models.CharField(blank=True, max_length=2048, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('space', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
),
migrations.CreateModel(
name='AiLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('function', models.CharField(max_length=64)),
('credit_cost', models.DecimalField(decimal_places=4, max_digits=16)),
('credits_from_balance', models.BooleanField(default=False)),
('input_tokens', models.IntegerField(default=0)),
('output_tokens', models.IntegerField(default=0)),
('start_time', models.DateTimeField(null=True)),
('end_time', models.DateTimeField(null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('ai_provider', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.aiprovider')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.22 on 2025-09-08 19:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0224_space_ai_credits_balance_space_ai_credits_monthly_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_enabled',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.22 on 2025-09-08 20:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0225_space_ai_enabled'),
]
operations = [
migrations.AddField(
model_name='aiprovider',
name='log_credit_cost',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='space',
name='ai_credits_monthly',
field=models.IntegerField(default=10000),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.2.22 on 2025-09-09 11:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0226_aiprovider_log_credit_cost_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_default_provider',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_ai_default_provider', to='cookbook.aiprovider'),
),
migrations.AlterField(
model_name='space',
name='ai_credits_balance',
field=models.DecimalField(decimal_places=4, default=0, max_digits=16),
),
]

View File

@@ -329,6 +329,11 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
ai_enabled = models.BooleanField(default=True)
ai_credits_monthly = models.IntegerField(default=100)
ai_credits_balance = models.DecimalField(default=0, max_digits=16, decimal_places=4)
ai_default_provider = models.ForeignKey("AiProvider", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_ai_default_provider')
internal_note = models.TextField(blank=True, null=True)
def safe_delete(self):
@@ -341,6 +346,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
BookmarkletImport.objects.filter(space=self).delete()
CustomFilter.objects.filter(space=self).delete()
AiLog.objects.filter(space=self).delete()
AiProvider.objects.filter(space=self).delete()
Property.objects.filter(space=self).delete()
PropertyType.objects.filter(space=self).delete()
@@ -393,6 +401,41 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
return self.name
class AiProvider(models.Model):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
# AiProviders can be global, so space=null is allowed (configurable by superusers)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
api_key = models.CharField(max_length=2048)
model_name = models.CharField(max_length=256)
url = models.CharField(max_length=2048, blank=True, null=True)
log_credit_cost = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class AiLog(models.Model, PermissionModelMixin):
F_FILE_IMPORT = 'FILE_IMPORT'
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
function = models.CharField(max_length=64)
credit_cost = models.DecimalField(max_digits=16, decimal_places=4)
# if credits from balance were used, else its from monthly quota
credits_from_balance = models.BooleanField(default=False)
input_tokens = models.IntegerField(default=0)
output_tokens = models.IntegerField(default=0)
start_time = models.DateTimeField(null=True)
end_time = models.DateTimeField(null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class ConnectorConfig(models.Model, PermissionModelMixin):
HOMEASSISTANT = 'HomeAssistant'
CONNECTER_TYPE = ((HOMEASSISTANT, 'HomeAssistant'),)

View File

@@ -24,6 +24,7 @@ from rest_framework.fields import IntegerField
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.ai_helper import get_monthly_token_usage
from cookbook.helper.image_processing import is_file_type_allowed
from cookbook.helper.permission_helper import above_space_limit
from cookbook.helper.property_helper import FoodPropertyHelper
@@ -36,7 +37,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields)
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
@@ -325,11 +326,52 @@ class UserFileViewSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'file', 'file_download', 'file_size_kb', 'preview', 'created_by', 'created_at')
class AiProviderSerializer(serializers.ModelSerializer):
api_key = serializers.CharField(required=False, write_only=True)
def create(self, validated_data):
validated_data = self.handle_global_space_logic(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
validated_data = self.handle_global_space_logic(validated_data)
return super().update(instance, validated_data)
def handle_global_space_logic(self, validated_data):
"""
allow superusers to create AI providers without a space but make sure everyone else only uses their own space
"""
if ('space' not in validated_data or not validated_data['space']) and self.context['request'].user.is_superuser:
validated_data['space'] = None
else:
validated_data['space'] = self.context['request'].space
return validated_data
class Meta:
model = AiProvider
fields = ('id', 'name', 'description', 'api_key', 'model_name', 'url', 'log_credit_cost', 'space', 'created_at', 'updated_at')
read_only_fields = ('created_at', 'updated_at',)
class AiLogSerializer(serializers.ModelSerializer):
ai_provider = AiProviderSerializer(read_only=True)
class Meta:
model = AiLog
fields = ('id', 'ai_provider', 'function', 'credit_cost', 'credits_from_balance', 'input_tokens', 'output_tokens', 'start_time', 'end_time', 'created_by', 'created_at',
'updated_at')
read_only_fields = ('__all__',)
class SpaceSerializer(WritableNestedModelSerializer):
created_by = UserSerializer(read_only=True)
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used')
ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
food_inherit = FoodInheritFieldSerializer(many=True)
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
@@ -350,6 +392,10 @@ class SpaceSerializer(WritableNestedModelSerializer):
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
@extend_schema_field(int)
def get_ai_monthly_credits_used(self, obj):
return get_monthly_token_usage(obj)
@extend_schema_field(float)
def get_file_size_mb(self, obj):
try:
@@ -360,16 +406,29 @@ class SpaceSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
def update(self, instance, validated_data):
if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_enabled']
if 'ai_credits_monthly' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_credits_monthly']
if 'ai_credits_balance' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_credits_balance']
return super().update(instance, validated_data)
class Meta:
model = Space
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color',
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg',)
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg', 'ai_credits_monthly',
'ai_credits_balance', 'ai_monthly_credits_used', 'ai_enabled', 'ai_default_provider')
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo',)
'demo', 'ai_monthly_credits_used')
class UserSpaceSerializer(WritableNestedModelSerializer):
@@ -1038,7 +1097,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'private','servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
'internal', 'private', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
# TODO having these readonly fields makes "RecipeOverview.ts" (API Client) not generate the RecipeOverviewToJSON second else block which leads to errors when using the api
# TODO find a solution (custom schema?) to have these fields readonly (to save performance) and generate a proper client (two serializers would probably do the trick)
@@ -1134,6 +1193,35 @@ class RecipeBatchUpdateSerializer(serializers.Serializer):
clear_description = serializers.BooleanField(required=False, allow_null=True)
class FoodBatchUpdateSerializer(serializers.Serializer):
foods = serializers.ListField(child=serializers.IntegerField())
category = serializers.IntegerField(required=False, allow_null=True)
substitute_add = serializers.ListField(child=serializers.IntegerField())
substitute_remove = serializers.ListField(child=serializers.IntegerField())
substitute_set = serializers.ListField(child=serializers.IntegerField())
substitute_remove_all = serializers.BooleanField(default=False)
inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
inherit_fields_remove_all = serializers.BooleanField(default=False)
child_inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_remove_all = serializers.BooleanField(default=False)
substitute_children = serializers.BooleanField(required=False, allow_null=True)
substitute_siblings = serializers.BooleanField(required=False, allow_null=True)
ignore_shopping = serializers.BooleanField(required=False, allow_null=True)
on_hand = serializers.BooleanField(required=False, allow_null=True)
parent_remove = serializers.BooleanField(required=False, allow_null=True)
parent_set = serializers.IntegerField(required=False, allow_null=True)
class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserSerializer(many=True, required=False)
@@ -1564,7 +1652,6 @@ class ServerSettingsSerializer(serializers.Serializer):
# TODO add all other relevant settings including path/url related ones?
shopping_min_autosync_interval = serializers.CharField()
enable_pdf_export = serializers.BooleanField()
enable_ai_import = serializers.BooleanField()
disable_external_connectors = serializers.BooleanField()
terms_url = serializers.CharField()
privacy_url = serializers.CharField()
@@ -1788,6 +1875,7 @@ class RecipeFromSourceResponseSerializer(serializers.Serializer):
class AiImportSerializer(serializers.Serializer):
ai_provider_id = serializers.IntegerField()
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)

View File

@@ -0,0 +1,162 @@
import json
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import MealType, PropertyType, AiProvider
LIST_URL = 'api:aiprovider-list'
DETAIL_URL = 'api:aiprovider-detail'
@pytest.fixture()
def obj_1(space_1, a1_s1):
return AiProvider.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture
def obj_2(space_1, a1_s1):
return AiProvider.objects.get_or_create(name='test_2', space=None)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
obj_1.space = space_2
obj_1.save()
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 403],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 403],
['g1_s2', 403],
['u1_s2', 403],
['a1_s2', 403],
['s1_s1', 200],
])
def test_update_global(arg, request, obj_2):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_2.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test', 'api_key': 'test', 'model_name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(a1_s1, a1_s2, obj_1):
# admins cannot delete foreign space providers
r = a1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
# admins can delete their space providers
r = a1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert AiProvider.objects.count() == 0
def test_delete_global(a1_s1, s1_s1, obj_2):
# admins cant delete global providers
r = a1_s1.delete(
reverse(
DETAIL_URL,
args={obj_2.id}
)
)
assert r.status_code == 403
# superusers can delete global providers
r = s1_s1.delete(
reverse(
DETAIL_URL,
args={obj_2.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert AiProvider.objects.count() == 0

View File

@@ -298,3 +298,11 @@ def a1_s2(client, space_2):
@pytest.fixture()
def a2_s2(client, space_2):
return create_user(client, space_2, group='admin')
@pytest.fixture()
def s1_s1(client, space_1):
client = create_user(client, space_1, group='admin')
user = auth.get_user(client)
user.is_superuser = True
user.save()
return client

View File

@@ -61,6 +61,8 @@ router.register(r'search-preference', api.SearchPreferenceViewSet)
router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
router.register(r'access-token', api.AccessTokenViewSet)
router.register(r'ai-provider', api.AiProviderViewSet)
router.register(r'ai-log', api.AiLogViewSet)
router.register(r'localization', api.LocalizationViewSet, basename='localization')
router.register(r'server-settings', api.ServerSettingsViewSet, basename='server-settings')

View File

@@ -65,6 +65,8 @@ from cookbook.connectors.connector_manager import ConnectorManager, ActionType
from cookbook.forms import ImportForm, ImportExportBase
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.HelperFunctions import str2bool, validate_import_url
from cookbook.helper.ai_helper import has_monthly_token, can_perform_ai_request, AiCallbackHandler
from cookbook.helper.batch_edit_helper import add_to_relation, remove_from_relation, remove_all_from_relation, set_relation
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.open_data_importer import OpenDataImporter
@@ -74,7 +76,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, Cus
CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF,
above_space_limit,
group_required, has_group_permission, is_space_owner,
switch_user_active_space
switch_user_active_space, CustomAiProviderPermission
)
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
@@ -85,7 +87,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo
RecipeBookEntry, ShareLink, ShoppingListEntry,
ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider
)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
@@ -110,12 +112,13 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
UserSerializer, UserSpaceSerializer, ViewLogSerializer,
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer,
AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer,
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer,
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer
)
from cookbook.version_info import TANDOOR_VERSION
from cookbook.views.import_export import get_integration
from recipes import settings
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT, AI_API_KEY, AI_MODEL_NAME
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT
DateExample = OpenApiExample('Date Format', value='1972-12-05', request_only=True)
BeforeDateExample = OpenApiExample('Before Date Format', value='-1972-12-05', request_only=True)
@@ -617,6 +620,29 @@ class SearchPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
return self.queryset.filter(user=self.request.user)
class AiProviderViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AiProvider.objects
serializer_class = AiProviderSerializer
permission_classes = [CustomAiProviderPermission & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
def get_queryset(self):
# read only access to all space and global AiProviders
with scopes_disabled():
return self.queryset.filter(Q(space=self.request.space) | Q(space__isnull=True))
class AiLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AiLog.objects
serializer_class = AiLogSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
http_method_names = ['get']
pagination_class = DefaultPagination
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class StorageViewSet(LoggingMixin, viewsets.ModelViewSet):
# TODO handle delete protect error and adjust test
queryset = Storage.objects
@@ -915,6 +941,94 @@ class FoodViewSet(LoggingMixin, TreeMixin):
content = {'error': True, 'msg': e.args[0]}
return Response(content, status=status.HTTP_403_FORBIDDEN)
@decorators.action(detail=False, methods=['PUT'], serializer_class=FoodBatchUpdateSerializer)
def batch_update(self, request):
serializer = self.serializer_class(data=request.data, partial=True)
if serializer.is_valid():
foods = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space)
safe_food_ids = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space).values_list('id', flat=True)
if 'category' in serializer.validated_data:
foods.update(supermarket_category_id=serializer.validated_data['category'])
if 'ignore_shopping' in serializer.validated_data and serializer.validated_data['ignore_shopping'] is not None:
foods.update(ignore_shopping=serializer.validated_data['ignore_shopping'])
if 'on_hand' in serializer.validated_data and serializer.validated_data['on_hand'] is not None:
if serializer.validated_data['on_hand']:
user_relation = []
for f in safe_food_ids:
user_relation.append(Food.onhand_users.through(food_id=f, user_id=request.user.id))
Food.onhand_users.through.objects.bulk_create(user_relation, ignore_conflicts=True, unique_fields=('food_id', 'user_id',))
else:
Food.onhand_users.through.objects.filter(food_id__in=safe_food_ids, user_id=request.user.id).delete()
if 'substitute_children' in serializer.validated_data and serializer.validated_data['substitute_children'] is not None:
foods.update(substitute_children=serializer.validated_data['substitute_children'])
if 'substitute_siblings' in serializer.validated_data and serializer.validated_data['substitute_siblings'] is not None:
foods.update(substitute_siblings=serializer.validated_data['substitute_siblings'])
# ---------- substitutes -------------
if 'substitute_add' in serializer.validated_data:
add_to_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_add'])
if 'substitute_remove' in serializer.validated_data:
remove_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_remove'])
if 'substitute_set' in serializer.validated_data and len(serializer.validated_data['substitute_set']) > 0:
set_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_set'])
if 'substitute_remove_all' in serializer.validated_data and serializer.validated_data['substitute_remove_all']:
remove_all_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids)
# ---------- inherit fields -------------
if 'inherit_fields_add' in serializer.validated_data:
add_to_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_add'])
if 'inherit_fields_remove' in serializer.validated_data:
remove_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_remove'])
if 'inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['inherit_fields_set']) > 0:
set_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_set'])
if 'inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['inherit_fields_remove_all']:
remove_all_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids)
# ---------- child inherit fields -------------
if 'child_inherit_fields_add' in serializer.validated_data:
add_to_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_add'])
if 'child_inherit_fields_remove' in serializer.validated_data:
remove_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_remove'])
if 'child_inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['child_inherit_fields_set']) > 0:
set_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_set'])
if 'child_inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['child_inherit_fields_remove_all']:
remove_all_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids)
# ------- parent --------
if self.model.node_order_by:
node_location = 'sorted'
else:
node_location = 'last'
if 'parent_remove' in serializer.validated_data and serializer.validated_data['parent_remove']:
for f in foods:
f.move(Food.get_first_root_node(), f'{node_location}-sibling')
if 'parent_set' in serializer.validated_data:
parent_food = Food.objects.filter(space=request.space, id=serializer.validated_data['parent_set']).first()
if parent_food:
for f in foods:
f.move(parent_food, f'{node_location}-child')
return Response({}, 200)
return Response(serializer.errors, 400)
@extend_schema_view(list=extend_schema(parameters=[
OpenApiParameter(name='order_field', description='Field to order recipe books on', type=str,
@@ -2000,6 +2114,24 @@ class AiImportView(APIView):
if serializer.is_valid():
# TODO max file size check
if 'ai_provider_id' not in serializer.validated_data:
response = {
'error': True,
'msg': _('You must select an AI provider to perform your request.'),
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
if not can_perform_ai_request(request.space):
response = {
'error': True,
'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."),
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first()
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider)]
messages = []
uploaded_file = serializer.validated_data['file']
@@ -2068,7 +2200,15 @@ class AiImportView(APIView):
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
try:
ai_response = completion(api_key=AI_API_KEY, model=AI_MODEL_NAME, response_format={"type": "json_object"}, messages=messages, )
ai_request = {
'api_key': ai_provider.api_key,
'model': ai_provider.model_name,
'response_format': {"type": "json_object"},
'messages': messages,
}
if ai_provider.url:
ai_request['api_base'] = ai_provider.url
ai_response = completion(**ai_request)
except BadRequestError as err:
response = {
'error': True,
@@ -2373,7 +2513,6 @@ class ServerSettingsViewSet(viewsets.GenericViewSet):
# Attention: No login required, do not return sensitive data
s['shopping_min_autosync_interval'] = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
s['enable_pdf_export'] = settings.ENABLE_PDF_EXPORT
s['enable_ai_import'] = settings.AI_API_KEY != ''
s['disable_external_connectors'] = settings.DISABLE_EXTERNAL_CONNECTORS
s['terms_url'] = settings.TERMS_URL
s['privacy_url'] = settings.PRIVACY_URL
@@ -2546,10 +2685,9 @@ def ingredient_from_string(request):
if unit:
if unit_obj := Unit.objects.filter(space=request.space).filter(Q(name=unit) | Q(plural_name=unit)).first():
ingredient['food'] = {'name': unit_obj.name, 'id': unit_obj.id}
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
else:
unit_obj = Unit.objects.create(space=request.space, name=unit)
ingredient['food'] = {'name': unit_obj.name, 'id': unit_obj.id}
ingredient['unit'] = {'name': unit.name, 'id': unit.id}
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
return JsonResponse(ingredient, status=200)

View File

@@ -97,7 +97,8 @@ def space_overview(request):
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
)
ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,)
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get())

18
docs/features/ai.md Normal file
View File

@@ -0,0 +1,18 @@
Tandoor has several AI based features. To allow maximum flexibility, you can configure different AI providers and select them based on the task you want to perform.
To prevent accidental cost escalation Tandoor has a robust system of tracking and limiting AI costs.
## Default Configuration
By default the AI features are enabled for every space. Each space has a spending limit of roughly 1 USD per month.
This can be changed using the [configuration variables](https://docs.tandoor.dev/system/configuration/#ai-integration)
You can change these settings any time using the django admin. If you do not care about AI cost you can enter a very high limit or disable cost tracking for your providers.
The limit resets on the first of every month.
## Configure AI Providers
When AI support is enabled for a space every user in a space can configure AI providers.
The models shown in the editor have been tested and work with Tandoor. Most other models that can parse images/files and return text should also work.
Superusers also have the ability to configure global AI providers that every space can use.
## AI Log
The AI Log allows you to track the usage of AI calls. Here you can also see the usage.

View File

@@ -196,6 +196,7 @@ server {
proxy_set_header Host $http_host;
proxy_pass http://unix:/var/www/recipes/recipes.sock;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
```

View File

@@ -472,15 +472,20 @@ S3_CUSTOM_DOMAIN= # when using a CDN/proxy to S3 (see https://github.com/Tandoor
#### AI Integration
To use AI to perform different tasks you need to configure an API key and the AI provider. [LiteLLM](https://www.litellm.ai/) is used
to make a standardized request to different AI providers of your liking.
Configuring this via environment parameters is a temporary solution. In the future I plan on adding support for multiple AI providers per Tandoor instance
with the option to select them for various tasks. For now only gemini 2.0 flash has been tested but feel free to try out other models.
Most AI features are configured trough the AI Provider settings in the Tandoor web interface. Some defaults can be set for new spaces on your instance.
Enables AI features for spaces by default
```
SPACE_AI_ENABLED=1
```
Sets the monthly default credit limit for AI usage
```
SPACE_AI_CREDITS_MONTHLY=100
```
Ratelimit for AI API
```
AI_API_KEY=
AI_MODEL_NAME=gemini/gemini-2.0-flash
AI_RATELIMIT=60/hour
```

View File

@@ -59,6 +59,8 @@ SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
SPACE_DEFAULT_ALLOW_SHARING = extract_bool('SPACE_DEFAULT_ALLOW_SHARING', True)
SPACE_AI_ENABLED = extract_bool('SPACE_AI_ENABLED', True)
SPACE_AI_CREDITS_MONTHLY = int(os.getenv('SPACE_AI_CREDITS_MONTHLY', 10000))
INTERNAL_IPS = extract_comma_list('INTERNAL_IPS', '127.0.0.1')
@@ -137,8 +139,6 @@ HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
AI_API_KEY = os.getenv('AI_API_KEY', '')
AI_MODEL_NAME = os.getenv('AI_MODEL_NAME', 'gemini/gemini-2.0-flash')
AI_RATELIMIT = os.getenv('AI_RATELIMIT', '60/hour')
SHARING_ABUSE = extract_bool('SHARING_ABUSE', False)

View File

@@ -1,4 +1,4 @@
Django==4.2.22
Django==4.2.24
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,186 @@
<template>
<v-dialog max-width="1200px" :activator="props.activator" v-model="dialog">
<v-card :loading="loading">
<v-closable-card-title
:title="$t('BatchEdit')"
:sub-title="$t('BatchEditUpdatingItemsCount', {type: $t('Foods'), count: updateItems.length})"
:icon="TFood.icon"
v-model="dialog"
></v-closable-card-title>
<v-divider></v-divider>
<v-card-text>
<v-form>
<v-row>
<v-col cols="12" md="6">
<v-card :title="$t('Miscellaneous')" prepend-icon="fa-solid fa-list" variant="flat">
<v-card-text>
<model-select model="SupermarketCategory" v-model="batchUpdateRequest.foodBatchUpdate.category" :object="false" allow-create mode="single">
</model-select>
<v-select :items="boolUpdateOptions" :label="$t('Ignore_Shopping')" clearable v-model="batchUpdateRequest.foodBatchUpdate.ignoreShopping"></v-select>
<v-select :items="boolUpdateOptions" :label="$t('OnHand')" clearable v-model="batchUpdateRequest.foodBatchUpdate.onHand"></v-select>
<v-spacer></v-spacer>
<v-label :text="$t('Substitutes')"></v-label>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('Substitutes')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.substituteRemoveAll"></v-checkbox>
<v-select :items="boolUpdateOptions" :label="$t('substitute_siblings')" clearable v-model="batchUpdateRequest.foodBatchUpdate.substituteChildren"></v-select>
<v-select :items="boolUpdateOptions" :label="$t('substitute_children')" clearable v-model="batchUpdateRequest.foodBatchUpdate.substituteSiblings"></v-select>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :title="$t('Hierarchy')" prepend-icon="fa-solid fa-folder-tree" variant="flat">
<v-card-text>
<model-select model="Food" :label="$t('Parent')" :object="false" allow-create clearable v-model="batchUpdateRequest.foodBatchUpdate.parentSet">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-select :items="boolUpdateOptions" :label="$t('RemoveParent')" clearable v-model="batchUpdateRequest.foodBatchUpdate.parentRemove"></v-select>
<v-spacer></v-spacer>
<v-label :text="$t('InheritFields')"></v-label>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('InheritFields')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsRemoveAll"></v-checkbox>
<v-spacer></v-spacer>
<v-label :text="$t('ChildInheritFields')"></v-label>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('ChildInheritFields')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsRemoveAll"></v-checkbox>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn :disabled="loading" @click="dialog = false">{{ $t('Cancel') }}</v-btn>
<v-btn color="warning" :loading="loading" @click="batchUpdateFoods()" :disabled="updateItems.length < 1">{{ $t('Update') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {EditorSupportedModels, EditorSupportedTypes, getGenericModelFromString, TFood, TKeyword, TRecipe} from "@/types/Models.ts";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {useI18n} from "vue-i18n";
import {ApiApi, ApiFoodBatchUpdateUpdateRequest, ApiRecipeBatchUpdateUpdateRequest, Food, Recipe, RecipeOverview} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const emit = defineEmits(['change'])
const props = defineProps({
items: {type: Array as PropType<Array<Food>>, required: true},
activator: {type: String, default: 'parent'},
})
const {t} = useI18n()
const dialog = defineModel<boolean>({default: false})
const loading = ref(false)
const updateItems = ref([] as Food[])
const batchUpdateRequest = ref({foodBatchUpdate: {}} as ApiFoodBatchUpdateUpdateRequest)
const boolUpdateOptions = ref([
{value: true, title: t('Yes')},
{value: false, title: t('No')},
])
/**
* copy prop when dialog opens so that items remain when parent is updated after change is emitted
*/
watch(dialog, (newValue, oldValue) => {
if (!oldValue && newValue && props.items != undefined) {
batchUpdateRequest.value.foodBatchUpdate.foods = props.items.flatMap(r => r.id!)
updateItems.value = JSON.parse(JSON.stringify(props.items))
}
})
/**
* perform batch request to update recipes
*/
function batchUpdateFoods() {
let api = new ApiApi()
loading.value = true
api.apiFoodBatchUpdateUpdate(batchUpdateRequest.value).then(r => {
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
emit('change')
loading.value = false
})
}
</script>
<style scoped>
</style>

View File

@@ -14,6 +14,7 @@
<v-list-item link title="Space" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Recipes')" @click="window = 'recipes'" prepend-icon="$recipes"></v-list-item>
<v-list-item link :title="$t('Import')" @click="window = 'import'" prepend-icon="$import"></v-list-item>
<v-list-item link :title="$t('AI')" @click="window = 'ai'" prepend-icon="$ai"></v-list-item>
<v-list-item link :title="$t('Unit')" @click="window = 'unit'" prepend-icon="fa-solid fa-scale-balanced"></v-list-item>
<v-list-item link :title="$t('Food')" @click="window = 'food'" prepend-icon="fa-solid fa-carrot"></v-list-item>
<v-list-item link :title="$t('Keyword')" @click="window = 'keyword'" prepend-icon="fa-solid fa-tags"></v-list-item>
@@ -45,7 +46,7 @@
<v-btn class="mt-2 ms-2" color="info" href="https://github.com/TandoorRecipes/recipes" target="_blank" prepend-icon="fa-solid fa-code-branch">GitHub
</v-btn>
<v-alert class="mt-3" border="start" variant="tonal" color="success">
<v-alert class="mt-3" border="start" variant="tonal" color="success" v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)">
<v-alert-title>Did you know?</v-alert-title>
Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend
making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort.
@@ -105,6 +106,35 @@
<v-btn color="primary" variant="tonal" prepend-icon="$import" class="me-2" :to="{name: 'RecipeImportPage'}">{{ $t('Import') }}</v-btn>
</v-window-item>
<v-window-item value="ai">
<p class="mt-3">Tandoor has several functions that allow you to use AI to automatically perform certain tasks like importing recipes from a PDFs or images.
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To use AI you must first configure an AI Provider. This can also be done globally for all spaces by the person operating your Tandoor Server.
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Some AI Providers are available globally for every space to use. You can also configure additional AI Providers for your space only.
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space (either monthly or using a balance).
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Depending on your subscription you will have different AI Credits available for your space every month. Additionally you might have a Credit balance
that will be used once your monthly limit is reached.
</p>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'ModelListPage', params: {model: 'AiProvider'}}">
{{ $t('AiProvider') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
{{ $t('AiLog') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$import" class="me-2" :to="{name: 'RecipeImportPage'}">{{ $t('Import') }}</v-btn>
</v-window-item>
<v-window-item value="unit">
<p class="mt-3">Units allow you to measure how much of something you need in a recipe or on a shopping list.
@@ -337,6 +367,7 @@
<script setup lang="ts">
import {ref} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const drawer = defineModel()
const window = ref('start')

View File

@@ -36,11 +36,16 @@
<td style="width: 1%; text-wrap: nowrap" class="pa-0 d-print-none" v-if="showCheckbox">
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1"
v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)" v-if="!i.noAmount"></td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-if="i.noAmount"></td>
<!-- display calculated food amount or empty cell -->
<td style="width: 1%; text-wrap: nowrap"
class="pr-1"
v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"
v-if="!i.noAmount && i.amount != 0">
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-else></td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1">
<template v-if="i.unit && !i.noAmount"> {{ ingredientToUnitString(i, ingredientFactor) }}</template>
<template v-if="i.unit && !i.noAmount && i.amount != 0"> {{ ingredientToUnitString(i, ingredientFactor) }}</template>
</td>
<td>
<template v-if="i.food">

View File

@@ -121,11 +121,16 @@
<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-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled"
v-if="!recipe.internal">
<v-card-text>
Convert the recipe using AI
{{$t('ConvertUsingAI')}}
<model-select model="AiProvider" v-model="selectedAiProvider">
<template #append>
<v-btn @click="aiConvertRecipe()" icon="fa-solid fa-person-running" color="success"></v-btn>
</template>
</model-select>
</v-card-text>
</v-card>
</template>
@@ -191,7 +196,7 @@
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {ApiApi, Recipe} from "@/openapi"
import {AiProvider, 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";
@@ -207,6 +212,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useFileApi} from "@/composables/useFileApi.ts";
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const {request, release} = useWakeLock()
const {doAiImport, fileApiLoading} = useFileApi()
@@ -217,6 +223,8 @@ const recipe = defineModel<Recipe>({required: true})
const servings = ref(1)
const showFullRecipeName = ref(false)
const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
/**
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
*/
@@ -249,7 +257,7 @@ onBeforeUnmount(() => {
function aiConvertRecipe() {
let api = new ApiApi()
doAiImport(null, '', recipe.value.id!).then(r => {
doAiImport(selectedAiProvider.value.id!,null, '', recipe.value.id!).then(r => {
if (r.recipe) {
recipe.value.internal = true
recipe.value.steps = r.recipe.steps

View File

@@ -15,6 +15,8 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
const emit = defineEmits(['stop'])
import {computed, onMounted, ref} from "vue";
@@ -24,6 +26,8 @@ const props = defineProps({
seconds: {type: Number, required: true}
})
const {t} = useI18n()
const initialDurationSeconds = ref(props.seconds)
const durationSeconds = ref(initialDurationSeconds.value)
const timerRunning = ref(true)

View File

@@ -58,7 +58,6 @@
<template v-if="hasMoreItems && !loading" #afterlist>
<span class="text-disabled font-italic text-caption ms-3">{{ $t('ModelSelectResultsHelp') }}</span>
</template>
</Multiselect>
<template #append v-if="$slots.append">
@@ -73,7 +72,7 @@
import {computed, onBeforeMount, onMounted, PropType, ref, useTemplateRef} from "vue"
import {EditorSupportedModels, GenericModel, getGenericModelFromString} from "@/types/Models"
import Multiselect from '@vueform/multiselect'
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
import {ErrorMessageType, MessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
@@ -171,7 +170,7 @@ function search(query: string) {
*/
async function createObject(object: any, select$: Multiselect) {
return await modelClass.value.create({name: object[itemLabel.value]}).then((createdObj: any) => {
useMessageStore().addMessage(MessageType.SUCCESS, 'Created', 5000, createdObj)
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS, createdObj)
emit('create', object)
return createdObj
}).catch((err: any) => {

View File

@@ -0,0 +1,105 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<v-text-field :label="$t('APIKey')" v-model="editingObj.apiKey"></v-text-field>
<v-combobox :label="$t('Model')" :items="aiModels" v-model="editingObj.modelName" hide-details>
</v-combobox>
<p class="mt-2 mb-2">{{ $t('AiModelHelp') }} <a href="https://docs.litellm.ai/docs/providers" target="_blank">LiteLLM</a></p>
<v-checkbox :label="$t('LogCredits')" :hint="$t('LogCreditsHelp')" v-model="editingObj.logCreditCost" v-if="useUserPreferenceStore().userSettings.user.isSuperuser" persistent-hint
class="mb-2"></v-checkbox>
<v-text-field :label="$t('Url')" v-model="editingObj.url"></v-text-field>
<v-checkbox :label="$t('Global')" :hint="$t('GlobalHelp')" v-model="globalProvider" v-if="useUserPreferenceStore().userSettings.user.isSuperuser" persistent-hint
class="mb-2"></v-checkbox>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {AiProvider} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import editor from "mavon-editor";
const props = defineProps({
item: {type: {} as PropType<AiProvider>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<AiProvider>, required: false, default: {} as AiProvider},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<AiProvider>('AiProvider', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
const aiModels = ref(['gemini/gemini-2.5-pro', 'gemini/gemini-2.5-flash', 'gemini/gemini-2.5-flash-lite', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'])
const globalProvider = ref(false)
watch(() => globalProvider.value, () => {
if (globalProvider.value) {
editingObj.value.space = undefined
} else {
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
}
})
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {
itemDefaults: props.itemDefaults,
newItemFunction: () => {
editingObj.value.logCreditCost = true
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
},
}).then(() => {
globalProvider.value = editingObj.value.space == undefined
})
}
</script>
<style scoped>
</style>

View File

@@ -54,7 +54,7 @@
<properties-editor v-model="editingObj.properties" :amount-for="propertiesAmountFor"></properties-editor>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 60px;"></v-spacer>
<v-spacer style="margin-top: 80px;"></v-spacer>
</v-form>
</v-tabs-window-item>
@@ -106,7 +106,7 @@
</v-card>
</v-form>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 60px;"></v-spacer>
<v-spacer style="margin-top: 80px;"></v-spacer>
</v-tabs-window-item>
<v-tabs-window-item value="hierarchy">
@@ -119,6 +119,9 @@
mode="tags"></ModelSelect>
<ModelSelect model="FoodInheritField" v-model="editingObj.childInheritFields" :label="$t('ChildInheritFields')" :hint="$t('ChildInheritFields_help')"
mode="tags"></ModelSelect>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 100px;"></v-spacer>
</v-tabs-window-item>
<v-tabs-window-item value="misc">

View File

@@ -16,7 +16,7 @@
<v-date-input :label="$t('Valid Until')" v-model="editingObj.validUntil"></v-date-input>
<v-textarea :label="$t('Note')" v-model="editingObj.internalNote"></v-textarea>
<v-checkbox :label="$t('Reusable')" v-model="editingObj.reusable"></v-checkbox>
<v-text-field :label="$t('Link')" readonly :model-value="inviteLinkUrl(editingObj)">
<v-text-field :label="$t('Link')" readonly :model-value="inviteLinkUrl(editingObj)" v-if="isUpdate()">
<template #append-inner>
<btn-copy variant="plain" color="undefined" :copy-value="inviteLinkUrl(editingObj)"></btn-copy>
</template>
@@ -37,6 +37,7 @@ import {DateTime} from "luxon";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
const props = defineProps({
@@ -91,7 +92,7 @@ function initializeEditor(){
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}

View File

@@ -20,9 +20,9 @@
<v-text-field :label="$t('Username')" v-model="editingObj.username" v-if="editingObj.method == 'NEXTCLOUD' || editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Password')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.password" v-if="editingObj.method == 'NEXTCLOUD'"></v-text-field>
<v-text-field :label="$t('Access_Token')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.token" v-if="editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Access_Token')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.token" v-if="editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Path')" v-model="editingObj.path"></v-text-field>
<v-text-field :label="$t('Path')" v-model="editingObj.path"></v-text-field>
</v-form>
</v-card-text>
@@ -33,7 +33,7 @@
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import { Storage } from "@/openapi";
import {Storage} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
@@ -64,7 +64,7 @@ onMounted(() => {
/**
* component specific state setup logic
*/
function initializeEditor(){
function initializeEditor() {
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}

View File

@@ -54,6 +54,7 @@ import {useI18n} from "vue-i18n";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
const {t} = useI18n()
@@ -111,7 +112,7 @@ function deleteInviteLink(inviteLink: InviteLink) {
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}
</script>

View File

@@ -78,19 +78,38 @@
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<!-- <model-select v-model="space.foodInherit" model="FoodInheritField" mode="tags"></model-select>-->
<p class="text-h6 mt-2">{{ $t('AI') }}</p>
<v-divider class="mb-2"></v-divider>
<p class="text-disabled font-italic text-body-2">
<span v-if="useUserPreferenceStore().serverSettings.hosted">
{{ $t('AISettingsHostedHelp') }}
</span>
<span v-else>
{{ $t('SettingsOnlySuperuser') }}
</span>
</p>
<v-checkbox v-model="space.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
<template v-if="space.aiEnabled">
<model-select model="AiProvider" :label="$t('Default')" v-model="space.aiDefaultProvider"></model-select>
<v-number-input v-model="space.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
<v-number-input v-model="space.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
</template>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<v-divider class="mt-4 mb-2"></v-divider>
<h2>{{$t('Cosmetic')}}</h2>
<span>{{$t('Space_Cosmetic_Settings')}}</span>
<h2>{{ $t('Cosmetic') }}</h2>
<span>{{ $t('Space_Cosmetic_Settings') }}</span>
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
<v-color-picker v-model="space.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
<v-btn class="mb-4" @click="space.navBgColor = ''">{{$t('Reset')}}</v-btn>
<v-btn class="mb-4" @click="space.navBgColor = ''">{{ $t('Reset') }}</v-btn>
<user-file-field v-model="space.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>

View File

@@ -1,7 +1,7 @@
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import {ref} from "vue";
import {getCookie} from "@/utils/cookie";
import {RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
/**
@@ -86,7 +86,7 @@ export function useFileApi() {
* @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 = '', recipeId: string = '') {
function doAiImport(providerId: number, file: File | null, text: string = '', recipeId: string = '') {
let formData = new FormData()
if (file != null) {
@@ -96,6 +96,7 @@ export function useFileApi() {
}
formData.append('text', text)
formData.append('recipe_id', recipeId)
formData.append('ai_provider_id', providerId)
fileApiLoading.value = true
return fetch(getDjangoUrl(`api/ai-import/`), {

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "",
@@ -14,6 +15,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",
@@ -44,6 +51,7 @@
"Color": "",
"Coming_Soon": "",
"Completed": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -58,6 +66,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -102,10 +111,13 @@
"FoodOnHand": "",
"Food_Alias": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -148,6 +160,8 @@
"Keywords": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Make_Header": "",
@@ -165,6 +179,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -256,6 +272,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Categories": "",
"Shopping_Category": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "Добави",
@@ -14,6 +15,12 @@
"Added_by": "Добавено от",
"Added_on": "Добавено",
"Advanced": "Разширено",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "Приложение",
"Apply": "",
"Are_You_Sure": "Сигурен ли си?",
@@ -44,6 +51,7 @@
"Color": "Цвят",
"Coming_Soon": "Очаквайте скоро",
"Completed": "Завършено",
"ConvertUsingAI": "",
"Copy": "Копиране",
"Copy_template_reference": "Копирайте препратка към шаблона",
"CountMore": "...+{count} още",
@@ -55,6 +63,7 @@
"Create_New_Meal_Type": "Добавете нов тип хранене",
"Create_New_Shopping Category": "Създайте нова категория за пазаруване",
"Create_New_Unit": "Добавяне на нова единица",
"Credits": "",
"Current_Period": "Текущ период",
"Custom Filter": "Персонализиран филтър",
"DELETE_ERROR": "",
@@ -99,10 +108,13 @@
"FoodOnHand": "Имате {храна} под ръка.",
"Food_Alias": "Псевдоним на храната",
"Foods": "Храни",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Групирай по",
"Hide_Food": "Скриване на храна",
"Hide_Keyword": "Скриване на ключови думи",
@@ -143,6 +155,8 @@
"Keywords": "Ключови думи",
"Link": "Връзка",
"Load_More": "Зареди още",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Дневник на Готвене",
"Log_Recipe_Cooking": "Дневник на Рецепта за готвене",
"Make_Header": "Направете заглавие",
@@ -159,6 +173,8 @@
"Merge_Keyword": "Обединяване на ключова дума",
"MissingProperties": "",
"Month": "Месец",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Премести",
"MoveCategory": "Премести към: ",
"Move_Down": "Премести надолу",
@@ -249,6 +265,7 @@
"Selected": "Избрано",
"Servings": "Порции",
"Settings": "Настройки",
"SettingsOnlySuperuser": "",
"Share": "Споделяне",
"Shopping_Categories": "Категории за пазаруване",
"Shopping_Category": "Категория за пазаруване",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Afegit per",
"Added_on": "Afegit el",
"Advanced": "Avançat",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineació",
"Amount": "Quantitat",
"App": "Aplicació",
@@ -57,6 +64,7 @@
"Comments_setting": "Mostrar comentaris",
"Completed": "Completat",
"Conversion": "Conversió",
"ConvertUsingAI": "",
"Copy": "Copiar",
"Copy Link": "Copiar Enllaç",
"Copy Token": "Copiar Token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Afegir nova Categoria de Compres",
"Create_New_Unit": "Afegir nova unitat",
"Created": "Creada",
"Credits": "",
"Current_Period": "Període Actual",
"Custom Filter": "Filtre Personalitzat",
"CustomImageHelp": "Carregar una imatge per mostrar a la vista general de lespai.",
@@ -143,10 +152,13 @@
"Food_Alias": "Àlies per l'aliment",
"Food_Replace": "Aliment equivalent",
"Foods": "Aliments",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupat per",
"Hide_Food": "Amagar Aliment",
"Hide_Keyword": "Amaga les paraules clau",
@@ -197,6 +209,8 @@
"Learn_More": "Saber-me més",
"Link": "Enllaç",
"Load_More": "Carregueu-ne més",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registreu el que s'ha cuinat",
"Log_Recipe_Cooking": "Registre de receptes",
"Logo": "Logotip",
@@ -216,6 +230,8 @@
"Message": "Missatge",
"MissingProperties": "",
"Month": "Mes",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Moure",
"MoveCategory": "Moure a: ",
"Move_Down": "Moveu avall",
@@ -328,6 +344,7 @@
"Selected": "Seleccionat",
"Servings": "Racions",
"Settings": "Opcions",
"SettingsOnlySuperuser": "",
"Share": "Compartir",
"ShoppingBackgroundSyncWarning": "Error de la connexió, esperant per sincronitzar ...",
"Shopping_Categories": "Categoria de compres",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Přidáno uživatelem",
"Added_on": "Přidáno v",
"Advanced": "Rozšířené",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Zarovnání",
"Amount": "Množství",
"App": "Aplikace",
@@ -57,6 +64,7 @@
"Comments_setting": "Zobrazit komentáře",
"Completed": "Dokončeno",
"Conversion": "Převod",
"ConvertUsingAI": "",
"Copy": "Kopírovat",
"Copy Link": "Kopírovat odkaz",
"Copy Token": "Kopírovat token",
@@ -73,6 +81,7 @@
"Create_New_Shopping Category": "Vytvořit novou nákupní kategorii",
"Create_New_Shopping_Category": "Přidat novou nákupní kategorii",
"Create_New_Unit": "Přidat novou jednotku",
"Credits": "",
"Current_Period": "Současné období",
"Custom Filter": "Uživatelský filtr",
"CustomImageHelp": "Nahrajte obrázek, který se zobrazí v přehledu prostoru.",
@@ -142,10 +151,13 @@
"Food_Alias": "Přezdívka potraviny",
"Food_Replace": "Nahrazení v potravině",
"Foods": "Potraviny",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Seskupit podle",
"Hide_Food": "Skrýt potravinu",
"Hide_Keyword": "Skrýt štítky",
@@ -195,6 +207,8 @@
"Learn_More": "Zjistit víc",
"Link": "Odkaz",
"Load_More": "Načíst další",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zaznamenat vaření",
"Log_Recipe_Cooking": "Záznam vaření receptu",
"Logo": "Logo",
@@ -214,6 +228,8 @@
"Message": "Zpráva",
"MissingProperties": "",
"Month": "Měsíc",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Přesunout",
"MoveCategory": "Přesunout do: ",
"Move_Down": "Dolů",
@@ -325,6 +341,7 @@
"Selected": "Vybrané",
"Servings": "Porce",
"Settings": "Nastavení",
"SettingsOnlySuperuser": "",
"Share": "Sdílet",
"Shopping_Categories": "Kategorie nákupního seznamu",
"Shopping_Category": "Kategorie nákupního seznamu",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Tilføjet af",
"Added_on": "Tilføjet den",
"Advanced": "Avanceret",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"Amount": "Mængde",
"App": "App",
@@ -57,6 +64,7 @@
"Comments_setting": "Vis kommentarer",
"Completed": "Afsluttet",
"Conversion": "Konversion",
"ConvertUsingAI": "",
"Copy": "Kopier",
"Copy Link": "Kopier link",
"Copy Token": "Kopier token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Opret ny indkøbskategori",
"Create_New_Unit": "Tilføj ny enhed",
"Created": "Skabt",
"Credits": "",
"Current_Period": "Nuværende periode",
"Custom Filter": "Tilpasset filter",
"CustomImageHelp": "Upload et billede for at vise dets plade i område-oversigten.",
@@ -143,10 +152,13 @@
"Food_Alias": "Alternativt navn til mad",
"Food_Replace": "Erstat ingrediens",
"Foods": "Mad",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupper efter",
"Hide_Food": "Skjul mad",
"Hide_Keyword": "Skjul nøgleord",
@@ -197,6 +209,8 @@
"Learn_More": "Lær mere",
"Link": "Link",
"Load_More": "Indlæs mere",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Noter tilberedning",
"Log_Recipe_Cooking": "Noter tilberedning af opskrift",
"Logo": "Logo",
@@ -216,6 +230,8 @@
"Message": "Besked",
"MissingProperties": "",
"Month": "Måned",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flyt",
"MoveCategory": "Flyt til: ",
"Move_Down": "Flyt ned",
@@ -328,6 +344,7 @@
"Selected": "Valgt",
"Servings": "Serveringer",
"Settings": "Indstillinger",
"SettingsOnlySuperuser": "",
"Share": "Del",
"ShoppingBackgroundSyncWarning": "Dårligt netværk, afventer synkronisering ...",
"Shopping_Categories": "Indkøbskategorier",

View File

@@ -1,10 +1,9 @@
{
"AI": "AI",
"AIImportSubtitle": "Verwende AI um Fotos von Rezepten zu importieren.",
"AISettingsHostedHelp": "AI Verfügbarkeit und Credit Limits können über die Tarifverwaltung geändert werden. ",
"API": "API",
"APIKey": "API Schlüssel",
"Summary": "Zusammenfassung",
"Structured": "Strukturiert",
"API_Browser": "API Browser",
"API_Documentation": "API Dokumentation",
"AccessTokenHelp": "Zugriffsschlüssel für die REST Schnittstelle.",
@@ -31,6 +30,12 @@
"Admin": "Admin",
"Advanced": "Erweitert",
"Advanced Search Settings": "Erweiterte Sucheinstellungen",
"AiCreditsBalance": "Credit Guthaben",
"AiLog": "AI Protokoll",
"AiLogHelp": "Eine Übersicht der AI Anfragen.",
"AiModelHelp": "Die Liste enthält Modelle die offiziell Unterstützt und getestet wurden. Weitere modelle können manuell eingetragen werden.",
"AiProvider": "AI Anbieter",
"AiProviderHelp": "Je nach Präferenz können verschiedene AI Anbieter angelegt werden. Diese können auch Space übergreifend sein.",
"Alignment": "Ausrichtung",
"AllRecipes": "Alle Rezepte",
"Amount": "Menge",
@@ -92,6 +97,7 @@
"Continue": "Weiter",
"Conversion": "Umrechnung",
"ConversionsHelp": "Mit Umrechnungen kann die Menge eines Lebensmittels in verschiedenen Einheiten ausgerechnet werden. Aktuell wird dies nur zur berechnung von Eigenschaften verwendet, später jedoch sollen auch andere Funktionen von Tandoor davon profitieren. ",
"ConvertUsingAI": "Mithilfe von AI Umwandeln",
"CookLog": "Kochprotokoll",
"CookLogHelp": "Einträge im Kochprotokoll für Rezepte. ",
"Cooked": "Gekocht",
@@ -114,6 +120,7 @@
"Create_New_Unit": "Neue Einheit hinzufügen",
"Created": "Erstellt",
"CreatedBy": "Erstellt von",
"Credits": "Credits",
"Ctrl+K": "Strg+K",
"Current_Period": "Aktueller Zeitraum",
"Custom Filter": "Benutzerdefinierter Filter",
@@ -207,11 +214,14 @@
"Food_Replace": "Essen Ersetzen",
"Foods": "Lebensmittel",
"Friday": "Freitag",
"FromBalance": "Guthaben verwendet",
"Fulltext": "Volltext",
"FulltextHelp": "Felder welche im Volltext durchsucht werden sollen. Tipp: Die Suchtypen 'web', 'raw' und 'phrase' funktionieren nur mit Volltext-Feldern.",
"Fuzzy": "Unscharf",
"FuzzySearchHelp": "Verwende unscharfe Suche um Einträge auch bei Unterschieden in der Schreibweise zu finden.",
"GettingStarted": "Erste Schritte",
"Global": "Global",
"GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.",
"GroupBy": "Gruppieren nach",
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
"Headline": "Überschrift",
@@ -279,6 +289,8 @@
"Link": "Link",
"Load": "Laden",
"Load_More": "Weitere laden",
"LogCredits": "Credits Protokollieren",
"LogCreditsHelp": "Protokolliere die Credit Kosten der AI Anfragen. Ohne diese Protokollierung können Nutzer unbgerenzt viele Anfragen stellen.",
"Log_Cooking": "Kochen protokollieren",
"Log_Recipe_Cooking": "Kochen protokollieren",
"Logo": "Logo",
@@ -308,6 +320,8 @@
"ModelSelectResultsHelp": "Für mehr Ergebnisse suchen",
"Monday": "Montag",
"Month": "Monat",
"MonthlyCredits": "Monatliche Credits",
"MonthlyCreditsUsed": "Monatliche Credits verwendet",
"More": "Mehr",
"Move": "Verschieben",
"MoveCategory": "Verschieben nach: ",
@@ -461,6 +475,7 @@
"Servings": "Portionen",
"ServingsText": "Portionstext",
"Settings": "Einstellungen",
"SettingsOnlySuperuser": "Einige Einstellungen können nur vom Server Administrator verändert werden.",
"Share": "Teilen",
"ShopLater": "Später kaufen",
"ShopNow": "Jetzt kaufen",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Προστέθηκε από",
"Added_on": "Προστέθηκε στις",
"Advanced": "Για προχωρημένους",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Ευθυγράμμιση",
"Amount": "Ποσότητα",
"App": "Εφαρμογή",
@@ -57,6 +64,7 @@
"Comments_setting": "Εμφάνιση σχολίων",
"Completed": "Ολοκληρωμένο",
"Conversion": "Μετατροπή",
"ConvertUsingAI": "",
"Copy": "Αντιγραφή",
"Copy Link": "Αντιγραφή συνδέσμου",
"Copy Token": "Αντιγραφή token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Προσθήκη νέας κατηγορίας αγορών",
"Create_New_Unit": "Προσθήκη νέας μονάδας μέτρησης",
"Created": "Δημιουργήθηκε",
"Credits": "",
"Current_Period": "Τρέχουσα περίοδος",
"Custom Filter": "Προσαρμοσμένο φίλτρο",
"CustomImageHelp": "Ανεβάστε μια εικόνα για να εμφανίζεται στην επισκόπηση χώρου",
@@ -143,10 +152,13 @@
"Food_Alias": "Ψευδώνυμο φαγητού",
"Food_Replace": "Αντικατάσταση Φαγητού",
"Foods": "Φαγητά",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Ομαδοποίηση κατά",
"Hide_Food": "Απόκρυψη φαγητού",
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
@@ -197,6 +209,8 @@
"Learn_More": "Μάθετε περισσότερα",
"Link": "Σύνδεσμος",
"Load_More": "Φόρτωση περισσότερων",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Καταγραφή μαγειρέματος",
"Log_Recipe_Cooking": "Καταγραφή εκτέλεσης συνταγής",
"Logo": "Λογότυπο",
@@ -216,6 +230,8 @@
"Message": "Μήνυμα",
"MissingProperties": "",
"Month": "Μήνας",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Μετακίνηση",
"MoveCategory": "Μετακίνηση σε: ",
"Move_Down": "Μετακίνηση κάτω",
@@ -328,6 +344,7 @@
"Selected": "Επιλεγμένο",
"Servings": "Μερίδες",
"Settings": "Ρυθμίσεις",
"SettingsOnlySuperuser": "",
"Share": "Κοινοποίηση",
"ShoppingBackgroundSyncWarning": "Κακό δίκτυο, αναμονή συγχρονισμού...",
"Shopping_Categories": "Κατηγορίες αγορών",

View File

@@ -1,7 +1,9 @@
{
"AI": "AI",
"AIImportSubtitle": "Use AI to import images of recipes.",
"AISettingsHostedHelp": "You can enable AI features or change available credits by managing your subscription.",
"API": "API",
"APIKey": "API key",
"API_Browser": "API Browser",
"API_Documentation": "API Docs",
"AccessTokenHelp": "Access keys for the REST API.",
@@ -26,6 +28,12 @@
"Added_on": "Added On",
"Admin": "Admin",
"Advanced": "Advanced",
"AiCreditsBalance": "Credit Balance",
"AiLog": "AI Log",
"AiLogHelp": "Overview of your spaces AI requests. ",
"AiModelHelp": "The list contains model that are offically tested and supported. You can add additional models if you want.",
"AiProvider": "AI Provider",
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
"Alignment": "Alignment",
"AllRecipes": "All Recipes",
"Amount": "Amount",
@@ -87,6 +95,7 @@
"Continue": "Continue",
"Conversion": "Conversion",
"ConversionsHelp": "With conversions you can calculate the amount of a food in different units. Currently this is only used for property calculation, later it might also be used in other parts of tandoor. ",
"ConvertUsingAI": "Convert using AI",
"CookLog": "Cook Log",
"CookLogHelp": "Entries in the cook log for recipes. ",
"Cooked": "Cooked",
@@ -109,6 +118,7 @@
"Create_New_Unit": "Add New Unit",
"Created": "Created",
"CreatedBy": "Created by",
"Credits": "Credits",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Current Period",
"Custom Filter": "Custom Filter",
@@ -202,11 +212,14 @@
"Food_Replace": "Food Replace",
"Foods": "Foods",
"Friday": "Friday",
"FromBalance": "From Balance",
"Fulltext": "Fulltext",
"FulltextHelp": "Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Use fuzzy search to find entries even when there are differences in how the word is written.",
"GettingStarted": "Getting Started",
"Global": "Global",
"GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ",
"GroupBy": "Group By",
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
"Headline": "Headline",
@@ -274,6 +287,8 @@
"Link": "Link",
"Load": "Load",
"Load_More": "Load More",
"LogCredits": "Log Credits.",
"LogCreditsHelp": "Log credit cost of AI requests. Without this users can perform as many AI requests as they want. ",
"Log_Cooking": "Log Cooking",
"Log_Recipe_Cooking": "Log Recipe Cooking",
"Logo": "Logo",
@@ -303,6 +318,8 @@
"ModelSelectResultsHelp": "Search for more results",
"Monday": "Monday",
"Month": "Month",
"MonthlyCredits": "Monthly Credits",
"MonthlyCreditsUsed": "Monthly credits used",
"More": "More",
"Move": "Move",
"MoveCategory": "Move To: ",
@@ -456,6 +473,7 @@
"Servings": "Servings",
"ServingsText": "Servings Text",
"Settings": "Settings",
"SettingsOnlySuperuser": "Some Settings can only be changed by the Server Administrator.",
"Share": "Share",
"ShopLater": "Shop later",
"ShopNow": "Shop now",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Usar IA para importar imágenes de recetas.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "Añadido el",
"Admin": "Administrador",
"Advanced": "Avanzado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineación",
"AllRecipes": "Todas las recetas",
"Amount": "Cantidad",
@@ -85,6 +92,7 @@
"Continue": "Continuar",
"Conversion": "Conversión",
"ConversionsHelp": "Con las conversiones puedes calcular la cantidad de un alimento en diferentes unidades. Actualmente esto solo se usa para el cálculo de propiedades, en un futuro podría ser usado en otras partes de Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Historial de cocina",
"CookLogHelp": "Entradas en el historial de cocina para recetas. ",
"Cooked": "Cocinado",
@@ -107,6 +115,7 @@
"Create_New_Unit": "Añadir nueva unidad",
"Created": "Creada",
"CreatedBy": "Creado por",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Periodo actual",
"Custom Filter": "Filtro personalizado",
@@ -200,7 +209,10 @@
"Food_Replace": "Sustituir Alimento",
"Foods": "Alimentos",
"Friday": "Viernes",
"FromBalance": "",
"GettingStarted": "Primeros pasos",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupar por",
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
"Headline": "Encabezado",
@@ -266,6 +278,8 @@
"Link": "Enlace",
"Load": "Cargar",
"Load_More": "Cargar más",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registrar cocinada",
"Log_Recipe_Cooking": "Registro de recetas",
"Logo": "Logotipo",
@@ -294,6 +308,8 @@
"ModelSelectResultsHelp": "Buscar más resultados",
"Monday": "Lunes",
"Month": "Mes",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Más",
"Move": "Mover",
"MoveCategory": "Mover a: ",
@@ -441,6 +457,7 @@
"Servings": "Raciones",
"ServingsText": "Texto de la porción",
"Settings": "Opciones",
"SettingsOnlySuperuser": "",
"Share": "Compartir",
"ShopLater": "Comprar después",
"ShopNow": "Comprar ahora",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -17,6 +18,12 @@
"Added_on": "Lisätty",
"Advanced": "Edistynyt",
"Advanced Search Settings": "Tarkennetun Haun Asetukset",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Tasaus",
"Amount": "Määrä",
"App": "Applikaatio",
@@ -55,6 +62,7 @@
"Comments_setting": "Näytä Kommentit",
"Completed": "Valmis",
"Conversion": "Muuntaminen",
"ConvertUsingAI": "",
"Copy": "Kopioi",
"Copy Link": "Kopioi Linkki",
"Copy Token": "Kopioi Token",
@@ -71,6 +79,7 @@
"Create_New_Shopping_Category": "Lisää uusi ostoskategoria",
"Create_New_Unit": "Lisää Uusi Yksikkö",
"Created": "Luotu",
"Credits": "",
"Current_Period": "Nykyinen Jakso",
"Custom Filter": "Mukautettu Suodatin",
"CustomImageHelp": "Lataa kuva näytettäväksi tilan yleiskatsauksessa.",
@@ -140,10 +149,13 @@
"Food_Alias": "Ruoan nimimerkki",
"Food_Replace": "Korvaa Ruoka",
"Foods": "Ruuat",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Ryhmittely peruste",
"Hide_Food": "Piilota Ruoka",
"Hide_Keyword": "Piilota avainsana",
@@ -191,6 +203,8 @@
"Learn_More": "Lisätietoja",
"Link": "Linkki",
"Load_More": "Lataa Lisää",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Kirjaa kokkaus",
"Log_Recipe_Cooking": "Kirjaa Reseptin valmistus",
"Logo": "Logo",
@@ -210,6 +224,8 @@
"Message": "Viesti",
"MissingProperties": "",
"Month": "Kuukausi",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Siirry",
"MoveCategory": "Siirrä paikkaan: ",
"Move_Down": "Siirry alas",
@@ -317,6 +333,7 @@
"Selected": "Valittu",
"Servings": "Annokset",
"Settings": "Asetukset",
"SettingsOnlySuperuser": "",
"Share": "Jaa",
"ShoppingBackgroundSyncWarning": "Huono verkkoyhteys, odotetaan synkronointia ...",
"Shopping_Categories": "Ostoskategoriat",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Utiliser l'IA pour importer des images de recettes.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Admin",
"Advanced": "Avancé",
"Advanced Search Settings": "Paramètres de recherche avancée",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alignement",
"AllRecipes": "Toutes les recettes",
"Amount": "Quantité",
@@ -88,6 +95,7 @@
"Continue": "Continuer",
"Conversion": "Conversion",
"ConversionsHelp": "Avec les conversions, vous pouvez calculer une quantité dans différentes unités. Actuellement, c'est utilisé uniquement pour le calcul des propriétés, mais ça pourrait être utilisé dans d'autres parties de Tandoor dans le futur. ",
"ConvertUsingAI": "",
"CookLog": "Journal de cuisine",
"CookLogHelp": "Entrées dans le journal de cuisine pour les recettes. ",
"Cooked": "Cuit",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Ajouter une nouvelle unité",
"Created": "Créé",
"CreatedBy": "Créé par",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Période actuelle",
"Custom Filter": "Filtre personnalisé",
@@ -203,11 +212,14 @@
"Food_Replace": "Remplacer l'aliment",
"Foods": "Aliments",
"Friday": "Vendredi",
"FromBalance": "",
"Fulltext": "Texte intégral",
"FulltextHelp": "Champs de recherche en texte intégral. Remarque : les méthodes de recherche \"web\", \"phrase\" et \"raw\" ne fonctionnent qu'avec des champs en texte intégral.",
"Fuzzy": "Approximatif",
"FuzzySearchHelp": "Utilisez la recherche approximative pour trouver des entrées même lorsqu'il existe des différences dans la façon dont le mot est écrit.",
"GettingStarted": "Commencer",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grouper par",
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
"Headline": "En-tête",
@@ -275,6 +287,8 @@
"Link": "Lien",
"Load": "Chargement",
"Load_More": "Charger plus",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Marquer comme cuisiné",
"Log_Recipe_Cooking": "Marquer la recette comme cuisinée",
"Logo": "Logo",
@@ -301,6 +315,8 @@
"ModelSelectResultsHelp": "Chercher plus de résultats",
"Monday": "Lundi",
"Month": "Mois",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Plus",
"Move": "Déplacer",
"MoveCategory": "Déplacer vers : ",
@@ -454,6 +470,7 @@
"Servings": "Portions",
"ServingsText": "Texte des portions",
"Settings": "Paramètres",
"SettingsOnlySuperuser": "",
"Share": "Partager",
"ShopLater": "Acheter plus tard",
"ShopNow": "Acheter maintenant",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "נוסף ע\"י",
"Added_on": "נוסף ב",
"Advanced": "מתקדם",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "יישור",
"Amount": "כמות",
"App": "אפליקציה",
@@ -57,6 +64,7 @@
"Comments_setting": "הצג תגובות",
"Completed": "הושלם",
"Conversion": "עברית",
"ConvertUsingAI": "",
"Copy": "העתקה",
"Copy Link": "העתק קישור",
"Copy Token": "העתק טוקן",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "הוסף קטגוריות קניות חדשה",
"Create_New_Unit": "הוסף יחידה",
"Created": "נוצר",
"Credits": "",
"Current_Period": "תקופה נוכחית",
"Custom Filter": "פילטר מותאם",
"CustomImageHelp": "העלאת תמונה שתראה באזור הסקירה.",
@@ -143,10 +152,13 @@
"Food_Alias": "שם כינוי לאוכל",
"Food_Replace": "החלף אוכל",
"Foods": "מאכלים",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "אסוף לפי",
"Hide_Food": "הסתר אוכל",
"Hide_Keyword": "הסתר מילות מפתח",
@@ -197,6 +209,8 @@
"Learn_More": "למד עוד",
"Link": "קישור",
"Load_More": "טען עוד",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "רשום הכנת מתכון",
"Log_Recipe_Cooking": "רשום בישול מתכון",
"Logo": "לוגו",
@@ -216,6 +230,8 @@
"Message": "הודעה",
"MissingProperties": "",
"Month": "חודש",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "העברה",
"MoveCategory": "העבר אל: ",
"Move_Down": "העברה למטה",
@@ -328,6 +344,7 @@
"Selected": "נבחר",
"Servings": "מנות",
"Settings": "הגדרות",
"SettingsOnlySuperuser": "",
"Share": "שיתוף",
"ShoppingBackgroundSyncWarning": "בעיית תקשורת, מחכה לסנכון...",
"Shopping_Categories": "קטגוריות קניות",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Dodao",
"Added_on": "Dodano",
"Advanced": "Napredno",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnanje",
"Amount": "Količina",
"App": "Aplikacija",
@@ -57,6 +64,7 @@
"Comments_setting": "Prikaži komentare",
"Completed": "Završeno",
"Conversion": "Konverzija",
"ConvertUsingAI": "",
"Copy": "Kopiraj",
"Copy Link": "Kopiraj vezu",
"Copy Token": "Kopiraj token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Dodaj novu kategoriju za kupovinu",
"Create_New_Unit": "Dodaj novu jedinicu",
"Created": "Stvoreno",
"Credits": "",
"Current_Period": "Trenutno razdoblje",
"Custom Filter": "Prilagođeni filtar",
"CustomImageHelp": "Učitaj sliku za prikaz u pregledu prostora.",
@@ -143,10 +152,13 @@
"Food_Alias": "Nadimci namirnice",
"Food_Replace": "Zamjena namirnica",
"Foods": "Namirnice",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupiraj po",
"Hide_Food": "Sakrij namirnicu",
"Hide_Keyword": "Sakrij ključne riječi",
@@ -197,6 +209,8 @@
"Learn_More": "Saznajte više",
"Link": "Poveznica",
"Load_More": "Učitaj više",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zapis kuhanja",
"Log_Recipe_Cooking": "Dnevnik recepata kuhanja",
"Logo": "Logotip",
@@ -216,6 +230,8 @@
"Message": "Poruka",
"MissingProperties": "",
"Month": "Mjesec",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Premjesti",
"MoveCategory": "Premjesti u: ",
"Move_Down": "Premjesti dolje",
@@ -328,6 +344,7 @@
"Selected": "Odabrano",
"Servings": "Porcije",
"Settings": "Postavke",
"SettingsOnlySuperuser": "",
"Share": "Podijeli",
"ShoppingBackgroundSyncWarning": "Loša mreža, čeka se sinkronizacija...",
"Shopping_Categories": "Kategorije Kupovine",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Hozzádta",
"Added_on": "Hozzáadva",
"Advanced": "Haladó",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Igazítás",
"Amount": "Összeg",
"App": "Applikáció",
@@ -56,6 +63,7 @@
"Comments_setting": "Hozzászólások megjelenítése",
"Completed": "Kész",
"Conversion": "Konverzió",
"ConvertUsingAI": "",
"Copy": "Másolás",
"Copy Link": "Link másolása",
"Copy Token": "Token másolása",
@@ -71,6 +79,7 @@
"Create_New_Shopping Category": "Új vásárlási kategória létrehozása",
"Create_New_Shopping_Category": "Új vásárlási kategória hozzáadása",
"Create_New_Unit": "Új mértékegység hozzáadása",
"Credits": "",
"Current_Period": "Jelenlegi periódus",
"Custom Filter": "Egyéni szűrő",
"DELETE_ERROR": "",
@@ -126,10 +135,13 @@
"Food_Alias": "",
"Food_Replace": "Étel cseréje",
"Foods": "Alapanyagok",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Csoportosítva",
"Hide_Food": "Alapanyag elrejtése",
"Hide_Keyword": "Kulcsszavak elrejtése",
@@ -179,6 +191,8 @@
"Learn_More": "Tudjon meg többet",
"Link": "Link",
"Load_More": "Továbbiak betöltése",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Főzés naplózása",
"Log_Recipe_Cooking": "Főzés naplózása",
"Make_Header": "Átalakítás címsorra",
@@ -197,6 +211,8 @@
"Message": "Üzenet",
"MissingProperties": "",
"Month": "Hónap",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mozgatás",
"MoveCategory": "Áthelyezés ide: ",
"Move_Down": "Lefelé mozgatás",
@@ -301,6 +317,7 @@
"Selected": "Kiválasztott",
"Servings": "Adag",
"Settings": "Beállítások",
"SettingsOnlySuperuser": "",
"Share": "Megosztás",
"Shopping_Categories": "Vásárlási kategóriák",
"Shopping_Category": "Vásárlási kategória",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "",
@@ -8,6 +9,12 @@
"Add_to_Plan": "Ավելացնել պլանին",
"Add_to_Shopping": "Ավելացնել գնումներին",
"Advanced Search Settings": "Ընդլայնված փնտրման կարգավորումներ",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Apply": "",
"Automate": "Ավտոմատացնել",
"BatchDeleteConfirm": "",
@@ -22,11 +29,13 @@
"Categories": "",
"Category": "",
"Close": "",
"ConvertUsingAI": "",
"Copy": "",
"Create": "Ստեղծել",
"Create_New_Food": "Ավելացնել նոր սննդամթերք",
"Create_New_Keyword": "Ավելացնել նոր բանալի բառ",
"Create_New_Shopping Category": "Ստեղծել գնումների նոր կատեգորիա",
"Credits": "",
"DELETE_ERROR": "",
"Date": "",
"Delete": "",
@@ -51,10 +60,13 @@
"File": "",
"Files": "",
"Food": "Սննդամթերք",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Hide_Food": "Թաքցնել սննդամթերքը",
"Hide_Keywords": "Թաքցնել բանալի բառը",
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
@@ -69,6 +81,8 @@
"Keywords": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Գրանցել եփելը",
"Log_Recipe_Cooking": "Գրանցել բաղադրատոմսի օգտագործում",
"ManageSubscription": "",
@@ -78,6 +92,8 @@
"MergeAutomateHelp": "",
"Merge_Keyword": "Միացնել բանալի բառը",
"MissingProperties": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Տեղափոխել",
"Move_Food": "Տեղափոխել սննդամթերքը",
"Move_Keyword": "Տեղափոխել բանալի բառը",
@@ -124,6 +140,7 @@
"Selected": "",
"Servings": "",
"Settings": "Կարգավորումներ",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Category": "Գնումների կատեգորիա",
"Shopping_list": "Գնումների ցուցակ",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",
@@ -48,6 +55,7 @@
"Coming_Soon": "",
"Comments_setting": "",
"Completed": "",
"ConvertUsingAI": "",
"Copy": "Salin",
"Copy Link": "Salin Tautan",
"Copy Token": "Salin Token",
@@ -63,6 +71,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -114,10 +123,13 @@
"FoodOnHand": "",
"Food_Alias": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -164,6 +176,8 @@
"Last_name": "",
"Link": "Link",
"Load_More": "Muat lebih banyak",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Log Memasak",
"Log_Recipe_Cooking": "Log Resep Memasak",
"Make_Header": "Buat Header",
@@ -182,6 +196,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Bergerak",
"MoveCategory": "",
"Move_Down": "Pindahkan kebawah",
@@ -277,6 +293,7 @@
"Selected": "Terpilih",
"Servings": "Porsi",
"Settings": "Pengaturan",
"SettingsOnlySuperuser": "",
"Share": "Bagikan",
"Shopping_Categories": "Kategori Belanja",
"Shopping_Category": "Kategori Belanja",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "",
"App": "",
@@ -57,6 +64,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
@@ -142,10 +151,13 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -196,6 +208,8 @@
"Learn_More": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
@@ -215,6 +229,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -327,6 +343,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShoppingBackgroundSyncWarning": "",
"Shopping_Categories": "",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Utilizza IA per importare le immagini delle ricette.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Amministratore",
"Advanced": "Avanzate",
"Advanced Search Settings": "Impostazioni avanzate di ricerca",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Allineamento",
"AllRecipes": "Tutte le ricette",
"Amount": "Quantità",
@@ -88,6 +95,7 @@
"Continue": "Continua",
"Conversion": "Conversione",
"ConversionsHelp": "Con le conversioni è possibile calcolare la quantità di un alimento in diverse unità. Attualmente, questo metodo viene utilizzato solo per il calcolo delle proprietà, ma in futuro potrebbe essere utilizzato anche in altre parti del tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Registro di cucina",
"CookLogHelp": "Le voci nel registro di cucina per le ricette. ",
"Cooked": "Cucinati",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Aggiungi nuova unità",
"Created": "Creata",
"CreatedBy": "Creata da",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Periodo attuale",
"Custom Filter": "Filtro personalizzato",
@@ -203,11 +212,14 @@
"Food_Replace": "Sostituisci alimento",
"Foods": "Alimenti",
"Friday": "Venerdì",
"FromBalance": "",
"Fulltext": "Fulltext",
"FulltextHelp": "Campi per la ricerca full text. Nota: i metodi di ricerca 'web', 'phrase', e 'raw' funzionano solo con i campi fulltext.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Utilizza la ricerca fuzzy per trovare voci anche quando ci sono differenze nel modo in cui la parola è scritta.",
"GettingStarted": "Iniziamo",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Raggruppa per",
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
"Headline": "Intestazione",
@@ -275,6 +287,8 @@
"Link": "Collegamento",
"Load": "Carica",
"Load_More": "Carica altro",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registro ricette cucinate",
"Log_Recipe_Cooking": "Aggiungi a ricette cucinate",
"Logo": "Logo",
@@ -303,6 +317,8 @@
"ModelSelectResultsHelp": "Cerca altri risultati",
"Monday": "Lunedì",
"Month": "Mese",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Altro",
"Move": "Sposta",
"MoveCategory": "Sposta in: ",
@@ -456,6 +472,7 @@
"Servings": "Porzioni",
"ServingsText": "Testo porzioni",
"Settings": "Impostazioni",
"SettingsOnlySuperuser": "",
"Share": "Condividi",
"ShopLater": "Compra dopo",
"ShopNow": "Compra subito",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "Suma",
"App": "",
@@ -56,6 +63,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -72,6 +80,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -128,10 +137,13 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -181,6 +193,8 @@
"Learn_More": "",
"Link": "",
"Load_More": "Įkelti daugiau",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Užregistruoti patiekalo gaminimą",
"Log_Recipe_Cooking": "Užregistruoti recepto pagaminimą",
"Make_Header": "Padaryti antraštę",
@@ -199,6 +213,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "Nuleisti žemyn",
@@ -305,6 +321,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Categories": "",
"Shopping_Category": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "",
"App": "",
@@ -57,6 +64,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
@@ -143,10 +152,13 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -197,6 +209,8 @@
"Learn_More": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
@@ -216,6 +230,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -328,6 +344,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShoppingBackgroundSyncWarning": "",
"Shopping_Categories": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Lagt til av",
"Added_on": "Lagt til",
"Advanced": "Avansert",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"Amount": "Mengde",
"App": "App",
@@ -55,6 +62,7 @@
"Comments_setting": "",
"Completed": "Fullført",
"Conversion": "Omregn enhet",
"ConvertUsingAI": "",
"Copy": "Kopier",
"Copy Link": "Kopier lenke",
"Copy Token": "Kopier Token",
@@ -71,6 +79,7 @@
"Create_New_Shopping Category": "Opprett ny handle kategori",
"Create_New_Shopping_Category": "Opprett new handle kategori",
"Create_New_Unit": "Opprett ny enhet",
"Credits": "",
"Current_Period": "Gjeldende periode",
"Custom Filter": "Egendefinert Filter",
"CustomImageHelp": "Last opp et bilde for å vise \"space\"-oversikten.",
@@ -134,10 +143,13 @@
"FoodOnHand": "Du har {food} på lager.",
"Food_Alias": "Matrett Alias",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupér",
"Hide_Food": "Skjul Matrett",
"Hide_Keyword": "Skjul nøkkelord",
@@ -188,6 +200,8 @@
"Learn_More": "Lær mer",
"Link": "Lenke",
"Load_More": "Last inn flere",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Loggfør tilbereding",
"Log_Recipe_Cooking": "Logg oppskriftsbruk",
"Make_Header": "Bruk som overskrift",
@@ -206,6 +220,8 @@
"Message": "Melding",
"MissingProperties": "",
"Month": "Måned",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flytt",
"MoveCategory": "Flytt til: ",
"Move_Down": "Flytt ned",
@@ -312,6 +328,7 @@
"Selected": "Valgte",
"Servings": "Porsjoner",
"Settings": "Innstillinger",
"SettingsOnlySuperuser": "",
"Share": "Del",
"ShoppingBackgroundSyncWarning": "Dårlig nettverkstilkobling, venter på synkronisering...",
"Shopping_Categories": "Butikk Kategorier",

View File

@@ -1,6 +1,7 @@
{
"AI": "AI",
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -28,6 +29,12 @@
"Admin": "Beheer",
"Advanced": "Geavanceerd",
"Advanced Search Settings": "Geavanceerde zoekinstellingen",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Afstemming",
"AllRecipes": "Alle recepten",
"Amount": "Hoeveelheid",
@@ -89,6 +96,7 @@
"Continue": "Doorgaan",
"Conversion": "Omrekening",
"ConversionsHelp": "Met omrekeningen kun je de hoeveelheid van een ingrediënt in verschillende eenheden berekenen. Momenteel wordt dit alleen gebruikt voor het berekenen van eigenschappen, later kan het ook in andere onderdelen van Tandoor gebruikt worden. ",
"ConvertUsingAI": "",
"CookLog": "Kooklogboek",
"CookLogHelp": "Items in het kooklogboek voor recepten. ",
"Cooked": "Gekookt",
@@ -111,6 +119,7 @@
"Create_New_Unit": "Voeg nieuwe eenheid toe",
"Created": "Gemaakt",
"CreatedBy": "Gemaakt door",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Huidige periode",
"Custom Filter": "Aangepast filter",
@@ -204,11 +213,14 @@
"Food_Replace": "Voedingsmiddelen vervangen",
"Foods": "Voedingsmiddelen",
"Friday": "Vrijdag",
"FromBalance": "",
"Fulltext": "Volledige tekst",
"FulltextHelp": "Velden voor volledige tekstzoekopdrachten. Opmerking: de zoekmethoden web, zin en ruw werken alleen met volledige tekstvelden.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Gebruik fuzzy search om items te vinden, zelfs als het woord anders is gespeld.",
"GettingStarted": "Aan de slag",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Groepeer per",
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
"Headline": "Koptekst",
@@ -276,6 +288,8 @@
"Link": "Link",
"Load": "Laden",
"Load_More": "Laad meer",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registreer bereiding",
"Log_Recipe_Cooking": "Bereiding registreren",
"Logo": "Logo",
@@ -304,6 +318,8 @@
"ModelSelectResultsHelp": "Zoek naar meer resultaten",
"Monday": "Maandag",
"Month": "Maand",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Meer",
"Move": "Verplaats",
"MoveCategory": "Verplaats naar: ",
@@ -457,6 +473,7 @@
"Servings": "Porties",
"ServingsText": "Portie tekst",
"Settings": "Instellingen",
"SettingsOnlySuperuser": "",
"Share": "Deel",
"ShopLater": "Later boodschappen doen",
"ShopNow": "Nu boodschappen doen",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -25,6 +26,12 @@
"Admin": "Administator",
"Advanced": "Zaawansowany",
"Advanced Search Settings": "Ustawienia zaawansowanego wyszukiwania",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Wyrównanie",
"AllRecipes": "Wszystkie przepisy",
"Amount": "Ilość",
@@ -83,6 +90,7 @@
"Confirm": "Potwierdź",
"Continue": "Kontynuuj",
"Conversion": "Konwersja",
"ConvertUsingAI": "",
"Copy": "Kopiuj",
"Copy Link": "Skopiuj link",
"Copy Token": "Kopiuj Token",
@@ -100,6 +108,7 @@
"Create_New_Shopping_Category": "Dodaj nową kategorię zakupów",
"Create_New_Unit": "Dodaj nowa jednostkę",
"Created": "Utworzony",
"Credits": "",
"Current_Period": "Bieżący okres",
"Custom Filter": "Filtr niestandardowy",
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
@@ -169,10 +178,13 @@
"Food_Alias": "Alias żywności",
"Food_Replace": "Zastąp produkt",
"Foods": "Żywność",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupuj według",
"Hide_Food": "Ukryj żywność",
"Hide_Keyword": "Ukryj słowa kluczowe",
@@ -223,6 +235,8 @@
"Learn_More": "Dowiedz się więcej",
"Link": "Link",
"Load_More": "Załaduj więcej",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zanotuj ugotowanie",
"Log_Recipe_Cooking": "Zaloguj gotowanie przepisu",
"Logo": "Logo",
@@ -242,6 +256,8 @@
"Message": "Wiadomość",
"MissingProperties": "",
"Month": "Miesiąc",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Przenieś",
"MoveCategory": "Przenieś do: ",
"Move_Down": "Przesunąć w dół",
@@ -354,6 +370,7 @@
"Selected": "Wybrane",
"Servings": "Porcje",
"Settings": "Ustawienia",
"SettingsOnlySuperuser": "",
"Share": "Udostępnij",
"ShoppingBackgroundSyncWarning": "Słaba sieć, oczekiwanie na synchronizację...",
"Shopping_Categories": "Kategorie zakupów",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "Adicionar",
@@ -14,6 +15,12 @@
"Added_by": "Adicionado por",
"Added_on": "Adicionado a",
"Advanced": "Avançado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"Amount": "Quantidade",
"Apply": "",
@@ -46,6 +53,7 @@
"Coming_Soon": "",
"Completed": "Completo",
"Conversion": "Conversão",
"ConvertUsingAI": "",
"Copy": "Copiar",
"Copy Link": "Copiar Ligação",
"Copy Token": "Copiar Chave",
@@ -60,6 +68,7 @@
"Create_New_Shopping Category": "Criar nova categoria de Compras",
"Create_New_Shopping_Category": "Adicionar nova categoria de compras",
"Create_New_Unit": "Adicionar nova unidade",
"Credits": "",
"Current_Period": "Período atual",
"Custom Filter": "",
"CustomImageHelp": "Fazer upload de uma image para mostrar na visão geral do espaço.",
@@ -114,10 +123,13 @@
"FoodOnHand": "Tem {food} disponível.",
"Food_Alias": "Alcunha da comida",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupar por",
"Hide_Food": "Esconder comida",
"Hide_Keyword": "",
@@ -155,6 +167,8 @@
"Learn_More": "Aprenda mais",
"Link": "Ligação",
"Load_More": "Carregar Mais",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registrar Culinária",
"Log_Recipe_Cooking": "Registrar Receitas de Culinária",
"Make_Header": "Tornar cabeçalho",
@@ -171,6 +185,8 @@
"Merge_Keyword": "Unir palavra-chave",
"MissingProperties": "",
"Month": "Mês",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mover",
"MoveCategory": "Mover para: ",
"Move_Down": "Mover para baixo",
@@ -267,6 +283,7 @@
"Selected": "Selecionado",
"Servings": "Doses",
"Settings": "Definições",
"SettingsOnlySuperuser": "",
"Share": "Partilhar",
"Shopping_Categories": "Categorias de Compras",
"Shopping_Category": "Categoria de Compras",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Use IA para importar imagens das receitas.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "Incluído Em",
"Admin": "Administrador",
"Advanced": "Avançado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"AllRecipes": "Todas Receitas",
"Amount": "Quantidade",
@@ -87,6 +94,7 @@
"Continue": "Continuar",
"Conversion": "Conversão",
"ConversionsHelp": "Com conversões, você pode calcular a quantidade de um alimento em diferentes unidades. Atualmente, isso é usado apenas para cálculo de propriedades, posteriormente poderá ser usado em outras partes do Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Registro de cozimento",
"CookLogHelp": "Entradas no registro de cozimento para receitas. ",
"Cooked": "Cozido",
@@ -109,6 +117,7 @@
"Create_New_Unit": "Incluir Nova Unidade",
"Created": "Criado",
"CreatedBy": "Criado por",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Período Atual",
"Custom Filter": "Filtro Customizado",
@@ -202,11 +211,14 @@
"Food_Replace": "Substituir Alimento",
"Foods": "Alimentos",
"Friday": "Sexta-feira",
"FromBalance": "",
"Fulltext": "Texto completo",
"FulltextHelp": "Campos para pesquisa textual completa. Observação: os métodos de pesquisa 'web', 'phrase' e 'raw' só funcionam com campos de pesquisa textual completa.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Use pesquisa fuzzy para encontrar registros mesmo quando existem diferenças na grafia das palavras utilizadas.",
"GettingStarted": "Começando",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupar Por",
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
"Headline": "Título",
@@ -274,6 +286,8 @@
"Link": "Link",
"Load": "Carregar",
"Load_More": "Carregar mais",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registro de Cozinha",
"Log_Recipe_Cooking": "Registrar receitas feitas",
"Logo": "Logotipo",
@@ -296,6 +310,8 @@
"Message": "Mensagem",
"MissingProperties": "",
"Month": "Mês",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mover",
"MoveCategory": "Mover Para: ",
"Move_Down": "Mover para baixo",
@@ -402,6 +418,7 @@
"Selected": "Selecionado",
"Servings": "Porções",
"Settings": "Configurações",
"SettingsOnlySuperuser": "",
"Share": "Compartilhar",
"ShoppingBackgroundSyncWarning": "Rede ruim, aguardando sincronização...",
"Shopping_Categories": "Categorias de Mercado",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -17,6 +18,12 @@
"Added_on": "Adăugat la",
"Advanced": "Avansat",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Amount": "Cantitate",
"App": "Aplicație",
"Apply": "",
@@ -53,6 +60,7 @@
"Coming_Soon": "În curând",
"Comments_setting": "Afișează comentarii",
"Completed": "Completat",
"ConvertUsingAI": "",
"Copy": "Copie",
"Copy Link": "Copiere link",
"Copy Token": "Copiere token",
@@ -69,6 +77,7 @@
"Create_New_Shopping Category": "Creați o nouă categorie de cumpărături",
"Create_New_Shopping_Category": "Adaugă categorie de cumpărături nouă",
"Create_New_Unit": "Adaugă unitate nouă",
"Credits": "",
"Current_Period": "Perioada curentă",
"Custom Filter": "Filtru personalizat",
"DELETE_ERROR": "",
@@ -121,10 +130,13 @@
"FoodOnHand": "Aveți {food} la îndemână.",
"Food_Alias": "Pseudonim mâncare",
"Foods": "Alimente",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupat de",
"Hide_Food": "Ascunde mâncare",
"Hide_Keyword": "Ascunde cuvintele cheie",
@@ -173,6 +185,8 @@
"Last_name": "Nume de familie",
"Link": "Link",
"Load_More": "Încărcați mai mult",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Jurnal de pregătire",
"Log_Recipe_Cooking": "Jurnalul rețetelor de pregătire",
"Make_Header": "Creare antet",
@@ -191,6 +205,8 @@
"Message": "Mesaj",
"MissingProperties": "",
"Month": "Lună",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mută",
"MoveCategory": "Mută la: ",
"Move_Down": "Deplasați-vă în jos",
@@ -289,6 +305,7 @@
"Selected": "Selectat",
"Servings": "Porții",
"Settings": "Setări",
"SettingsOnlySuperuser": "",
"Share": "Împărtășire",
"Shopping_Categories": "Categorii de cumpărături",
"Shopping_Category": "Categorie de cumpărături",

View File

@@ -1,6 +1,7 @@
{
"AI": "AI",
"AIImportSubtitle": "Используй AI для импорта изображений рецептов.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Админ",
"Advanced": "Расширенный",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Выравнивание",
"AllRecipes": "Все рецепты",
"Amount": "Количество",
@@ -88,6 +95,7 @@
"Continue": "Продолжить",
"Conversion": "Преобразование",
"ConversionsHelp": "С помощью преобразований вы можете рассчитывать количество продукта в разных единицах измерения. В настоящее время это используется только для расчёта свойств, но в будущем может применяться и в других частях Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Журнал приготовления",
"CookLogHelp": "История приготовлений по рецептам. ",
"Cooked": "Приготовлено",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Добавить единицу измерения",
"Created": "Создано",
"CreatedBy": "Создано пользователем",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Текущий период",
"Custom Filter": "Пользовательский фильтр",
@@ -203,11 +212,14 @@
"Food_Replace": "Замена продукта",
"Foods": "Продукты",
"Friday": "Пятница",
"FromBalance": "",
"Fulltext": "Полнотекстовый",
"FulltextHelp": "Поля, используемые в полнотекстовом поиске. Важно: методы поиска web, phrase и raw применимы только к полнотекстовым полям.",
"Fuzzy": "Нечёткий",
"FuzzySearchHelp": "Нечёткий поиск позволяет находить записи, даже если в написании есть ошибки или отличия.",
"GettingStarted": "Начало работы",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Сгруппировать по",
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
"Headline": "Заголовок",
@@ -275,6 +287,8 @@
"Link": "Гиперссылка",
"Load": "Загрузить",
"Load_More": "Загрузить еще",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Журнал приготовления",
"Log_Recipe_Cooking": "Журнал приготовления",
"Logo": "Логотип",
@@ -302,6 +316,8 @@
"ModelSelectResultsHelp": "Показать больше результатов",
"Monday": "Понедельник",
"Month": "Месяц",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Ещё",
"Move": "Переместить",
"MoveCategory": "Переместить в: ",
@@ -454,6 +470,7 @@
"Servings": "Порции",
"ServingsText": "Описание порций",
"Settings": "Настройки",
"SettingsOnlySuperuser": "",
"Share": "Поделиться",
"ShopLater": "Купить позже",
"ShopNow": "Купить сейчас",

View File

@@ -1,6 +1,7 @@
{
"AI": "Umetna inteligenca",
"AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Skrbnik",
"Advanced": "Napredno",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnava",
"AllRecipes": "Vsi recepti",
"Amount": "Količina",
@@ -88,6 +95,7 @@
"Continue": "Nadaljuj",
"Conversion": "Pogovor",
"ConversionsHelp": "S pretvorbami lahko izračunate količino živila v različnih enotah. Trenutno se to uporablja le za izračun lastnosti, kasneje pa se lahko uporabi tudi v drugih delih Tandoorja. ",
"ConvertUsingAI": "",
"CookLog": "Kuharski dnevnik",
"CookLogHelp": "Vnosi v dnevnik kuhanja za recepte. ",
"Cooked": "Kuhano",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Dodaj novo enoto",
"Created": "Ustvarjeno",
"CreatedBy": "Ustvaril/a",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Trenutno obdobje",
"Custom Filter": "Filter po meri",
@@ -203,11 +212,14 @@
"Food_Replace": "Zamenjava živila",
"Foods": "Živila",
"Friday": "Petek",
"FromBalance": "",
"Fulltext": "Celotno besedilo",
"FulltextHelp": "Polja za iskanje po celotnem besedilu. Opomba: metode iskanja »splet«, »fraza« in »surovo« delujejo samo s polji po celotnem besedilu.",
"Fuzzy": "Nejasno",
"FuzzySearchHelp": "Uporabite mehko iskanje za iskanje vnosov, tudi če obstajajo razlike v načinu pisanja besede.",
"GettingStarted": "Začetek",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Združi po",
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
"Headline": "Glavni naslov",
@@ -275,6 +287,8 @@
"Link": "Hiperpovezava",
"Load": "Naloži",
"Load_More": "Naloži več",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zgodovina kuhanja",
"Log_Recipe_Cooking": "Beleži kuharski recept",
"Logo": "Logotip",
@@ -303,6 +317,8 @@
"ModelSelectResultsHelp": "Išči več rezultatov",
"Monday": "Ponedeljek",
"Month": "Mesec",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Več",
"Move": "Premakni",
"MoveCategory": "Premakni v: ",
@@ -456,6 +472,7 @@
"Servings": "Porcije",
"ServingsText": "Besedilo o porcijah",
"Settings": "Nastavitve",
"SettingsOnlySuperuser": "",
"Share": "Deli",
"ShopLater": "Nakupujte pozneje",
"ShopNow": "Nakupujte zdaj",

View File

@@ -1,5 +1,6 @@
{
"AIImportSubtitle": "Använd AI för att importera bilder av recept.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "Tillagd på",
"Admin": "Administratör",
"Advanced": "Avancerat",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Orientering",
"AllRecipes": "Alla recept",
"Amount": "Mängd",
@@ -87,6 +94,7 @@
"Continue": "Fortsätt",
"Conversion": "Omvandling",
"ConversionsHelp": "Med omvandlingar kan du beräkna mängden av ett livsmedel i olika enheter. För närvarande används detta endast för egenskapsberäkning, senare kan det även användas i andra delar av Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Tillagningslogg",
"CookLogHelp": "Poster i tillagningsloggen för recept. ",
"Cooked": "Tillagad",
@@ -109,6 +117,7 @@
"Create_New_Unit": "Lägg till enhet",
"Created": "Skapad",
"CreatedBy": "Skapad av",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Nuvarande period",
"Custom Filter": "Anpassat filter",
@@ -180,10 +189,13 @@
"Food_Alias": "Alias för livsmedel",
"Food_Replace": "Ersätt ingrediens",
"Foods": "Livsmedel",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Gruppera enligt",
"Hide_Food": "Dölj livsmedel",
"Hide_Keyword": "Dölj nyckelord",
@@ -234,6 +246,8 @@
"Learn_More": "Läs mer",
"Link": "Länk",
"Load_More": "Ladda mer",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Logga tillagning",
"Log_Recipe_Cooking": "Logga tillagningen av receptet",
"Logo": "Logga",
@@ -253,6 +267,8 @@
"Message": "Meddelande",
"MissingProperties": "",
"Month": "Månad",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flytta",
"MoveCategory": "Flytta till: ",
"Move_Down": "Flytta ned",
@@ -365,6 +381,7 @@
"Selected": "Vald",
"Servings": "Portioner",
"Settings": "Inställningar",
"SettingsOnlySuperuser": "",
"Share": "Dela",
"ShoppingBackgroundSyncWarning": "Dålig uppkoppling, inväntar synkronisering...",
"Shopping_Categories": "Shopping kategorier",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Ekleyen",
"Added_on": "Eklenme Zamanı",
"Advanced": "Gelişmiş",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Hizalama",
"Amount": "Miktar",
"App": "Uygulama",
@@ -57,6 +64,7 @@
"Comments_setting": "Yorumları Göster",
"Completed": "Tamamlandı",
"Conversion": "Dönüşüm",
"ConvertUsingAI": "",
"Copy": "Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"Copy Token": "Anahtarı Kopyala",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Yeni Alışveriş Kategorisi Ekle",
"Create_New_Unit": "Yeni Birim Ekle",
"Created": "Oluşturuldu",
"Credits": "",
"Current_Period": "Mevcut Dönem",
"Custom Filter": "Özel Filtre",
"CustomImageHelp": "Alan genel bakışında gösterilecek bir resim yükleyin.",
@@ -143,10 +152,13 @@
"Food_Alias": "Yiyecek Takma Adı",
"Food_Replace": "Yiyecek Değiştir",
"Foods": "Yiyecekler",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Gruplandırma Ölçütü",
"Hide_Food": "Yiyeceği Gizle",
"Hide_Keyword": "Anahtar kelimeleri gizle",
@@ -197,6 +209,8 @@
"Learn_More": "Daha Fazla",
"Link": "Bağlantı",
"Load_More": "Daha Fazla Yükle",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Günlük Pişirme",
"Log_Recipe_Cooking": "Günlük Tarif Pişirme",
"Logo": "Logo",
@@ -216,6 +230,8 @@
"Message": "Mesaj",
"MissingProperties": "",
"Month": "Ay",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Taşı",
"MoveCategory": "Taşı: ",
"Move_Down": "Aşağıya Taşı",
@@ -328,6 +344,7 @@
"Selected": "Seçilen",
"Servings": "Servis Sayısı",
"Settings": "Ayarlar",
"SettingsOnlySuperuser": "",
"Share": "Paylaş",
"ShoppingBackgroundSyncWarning": "Kötü bağlantı, senkronizasyon bekleniyor...",
"Shopping_Categories": "Alışveriş Kategorileri",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "Додати",
@@ -14,6 +15,12 @@
"Added_by": "Додано",
"Added_on": "Додано На",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Вирівнювання",
"Amount": "Кількість",
"App": "",
@@ -50,6 +57,7 @@
"Coming_Soon": "",
"Completed": "Виконано",
"Conversion": "Конвертування",
"ConvertUsingAI": "",
"Copy": "Копіювати",
"Copy Link": "Скопіювати Посилання",
"Copy Token": "Скопіювати Токен",
@@ -64,6 +72,7 @@
"Create_New_Shopping Category": "Створити Нову Категорію Покупок",
"Create_New_Shopping_Category": "Додати Нову Категорію Покупок",
"Create_New_Unit": "Додати Нову Одиницю",
"Credits": "",
"Current_Period": "Теперішній Період",
"Custom Filter": "",
"CustomImageHelp": "Завантажте зображення що буде показуватись у огляді простору.",
@@ -124,10 +133,13 @@
"FoodOnHand": "Ви маєте {food} на руках.",
"Food_Alias": "Найменування Їжі",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "По Групі",
"Hide_Food": "Сховати Їжу",
"Hide_Keyword": "",
@@ -172,6 +184,8 @@
"Learn_More": "Дізнатися Більше",
"Link": "Посилання",
"Load_More": "Завантажити більше",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Занотувати приготування",
"Log_Recipe_Cooking": "Журнал приготування",
"Make_Header": "Створити Заголовок",
@@ -188,6 +202,8 @@
"Merge_Keyword": "Об'єднати Ключове слово",
"MissingProperties": "",
"Month": "Місяць",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Перемістити",
"MoveCategory": "Перемістити До: ",
"Move_Down": "Перемістити вниз",
@@ -290,6 +306,7 @@
"Selected": "Вибрано",
"Servings": "Порції",
"Settings": "Налаштування",
"SettingsOnlySuperuser": "",
"Share": "Поділитися",
"Shopping_Categories": "Категорії Покупок",
"Shopping_Category": "Категорія Покупок",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "添加者",
"Added_on": "添加到",
"Advanced": "高级",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "校准",
"Amount": "数量",
"App": "应用",
@@ -57,6 +64,7 @@
"Comments_setting": "显示评论",
"Completed": "完成",
"Conversion": "转换",
"ConvertUsingAI": "",
"Copy": "复制",
"Copy Link": "复制链接",
"Copy Token": "复制令牌",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "添加新的购物类别",
"Create_New_Unit": "添加新的单位",
"Created": "已创建",
"Credits": "",
"Current_Period": "本期",
"Custom Filter": "自定义筛选器",
"CustomImageHelp": "上传图片以在空间概览中显示。",
@@ -143,10 +152,13 @@
"Food_Alias": "食物别名",
"Food_Replace": "食物替换",
"Foods": "食物",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "分组",
"Hide_Food": "隐藏食物",
"Hide_Keyword": "隐藏关键词",
@@ -197,6 +209,8 @@
"Learn_More": "了解更多",
"Link": "链接",
"Load_More": "加载更多",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "烹饪记录",
"Log_Recipe_Cooking": "食谱烹饪记录",
"Logo": "徽标",
@@ -216,6 +230,8 @@
"Message": "信息",
"MissingProperties": "",
"Month": "月份",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "移动",
"MoveCategory": "移动到: ",
"Move_Down": "下移",
@@ -328,6 +344,7 @@
"Selected": "选定",
"Servings": "份量",
"Settings": "设置",
"SettingsOnlySuperuser": "",
"Share": "分享",
"ShoppingBackgroundSyncWarning": "网络状况不佳,正在等待进行同步……",
"Shopping_Categories": "购物类别",

View File

@@ -1,6 +1,7 @@
{
"AI": "人工智慧",
"AIImportSubtitle": "以人工智慧匯入食譜圖片。",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "添加於",
"Admin": "管理者",
"Advanced": "高級",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "對齊",
"AllRecipes": "所有食譜",
"Amount": "數量",
@@ -87,6 +94,7 @@
"Continue": "繼續",
"Conversion": "轉換",
"ConversionsHelp": "透過轉換功能,您可以計算食物在不同單位下的數量。目前這僅用於屬性計算,未來也可能用於 Tandoor 的其他部分。 ",
"ConvertUsingAI": "",
"CookLog": "烹飪記錄",
"CookLogHelp": "食譜的烹飪記錄條目。 ",
"Cooked": "已烹飪",
@@ -109,6 +117,7 @@
"Create_New_Unit": "建立新單位",
"Created": "建立",
"CreatedBy": "建立者",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "當前期間",
"Custom Filter": "自定義篩選器",
@@ -202,11 +211,14 @@
"Food_Replace": "食物替換",
"Foods": "食物",
"Friday": "星期五",
"FromBalance": "",
"Fulltext": "全文",
"FulltextHelp": "全文搜索的字段。注意:'web'、'phrase' 和 'raw' 搜索方法僅對全文欄位有效。",
"Fuzzy": "模糊",
"FuzzySearchHelp": "使用模糊搜索來查找條目,即使單詞的寫法存在差異。",
"GettingStarted": "開始使用",
"Global": "",
"GlobalHelp": "",
"GroupBy": "分組依據",
"HeaderWarning": "警告:變更為標題會刪除數量/單位/食物",
"Headline": "標題",
@@ -274,6 +286,8 @@
"Link": "連結",
"Load": "載入",
"Load_More": "載入更多",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "記錄烹飪",
"Log_Recipe_Cooking": "記錄食譜烹飪",
"Logo": "標誌",
@@ -302,6 +316,8 @@
"ModelSelectResultsHelp": "搜尋更多結果",
"Monday": "星期一",
"Month": "月",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "更多",
"Move": "移動",
"MoveCategory": "移動至: ",
@@ -455,6 +471,7 @@
"Servings": "份量",
"ServingsText": "份量文字",
"Settings": "設定",
"SettingsOnlySuperuser": "",
"Share": "分享",
"ShopLater": "稍後購物",
"ShopNow": "立即購物",

View File

@@ -3,6 +3,8 @@ apis/ApiTokenAuthApi.ts
apis/index.ts
index.ts
models/AccessToken.ts
models/AiLog.ts
models/AiProvider.ts
models/AlignmentEnum.ts
models/AuthToken.ts
models/AutoMealPlan.ts
@@ -27,6 +29,7 @@ models/ExportRequest.ts
models/FdcQuery.ts
models/FdcQueryFoods.ts
models/Food.ts
models/FoodBatchUpdate.ts
models/FoodInheritField.ts
models/FoodShoppingUpdate.ts
models/FoodSimple.ts
@@ -57,6 +60,8 @@ models/OpenDataStoreCategory.ts
models/OpenDataUnit.ts
models/OpenDataUnitTypeEnum.ts
models/OpenDataVersion.ts
models/PaginatedAiLogList.ts
models/PaginatedAiProviderList.ts
models/PaginatedAutomationList.ts
models/PaginatedBookmarkletImportListList.ts
models/PaginatedConnectorConfigList.ts
@@ -103,6 +108,7 @@ models/PaginatedUserSpaceList.ts
models/PaginatedViewLogList.ts
models/ParsedIngredient.ts
models/PatchedAccessToken.ts
models/PatchedAiProvider.ts
models/PatchedAutomation.ts
models/PatchedBookmarkletImport.ts
models/PatchedConnectorConfig.ts

View File

@@ -16,6 +16,8 @@
import * as runtime from '../runtime';
import type {
AccessToken,
AiLog,
AiProvider,
AutoMealPlan,
Automation,
BookmarkletImport,
@@ -28,6 +30,7 @@ import type {
ExportRequest,
FdcQuery,
Food,
FoodBatchUpdate,
FoodInheritField,
FoodShoppingUpdate,
Group,
@@ -49,6 +52,8 @@ import type {
OpenDataStore,
OpenDataUnit,
OpenDataVersion,
PaginatedAiLogList,
PaginatedAiProviderList,
PaginatedAutomationList,
PaginatedBookmarkletImportListList,
PaginatedConnectorConfigList,
@@ -95,6 +100,7 @@ import type {
PaginatedViewLogList,
ParsedIngredient,
PatchedAccessToken,
PatchedAiProvider,
PatchedAutomation,
PatchedBookmarkletImport,
PatchedConnectorConfig,
@@ -179,6 +185,10 @@ import type {
import {
AccessTokenFromJSON,
AccessTokenToJSON,
AiLogFromJSON,
AiLogToJSON,
AiProviderFromJSON,
AiProviderToJSON,
AutoMealPlanFromJSON,
AutoMealPlanToJSON,
AutomationFromJSON,
@@ -203,6 +213,8 @@ import {
FdcQueryToJSON,
FoodFromJSON,
FoodToJSON,
FoodBatchUpdateFromJSON,
FoodBatchUpdateToJSON,
FoodInheritFieldFromJSON,
FoodInheritFieldToJSON,
FoodShoppingUpdateFromJSON,
@@ -245,6 +257,10 @@ import {
OpenDataUnitToJSON,
OpenDataVersionFromJSON,
OpenDataVersionToJSON,
PaginatedAiLogListFromJSON,
PaginatedAiLogListToJSON,
PaginatedAiProviderListFromJSON,
PaginatedAiProviderListToJSON,
PaginatedAutomationListFromJSON,
PaginatedAutomationListToJSON,
PaginatedBookmarkletImportListListFromJSON,
@@ -337,6 +353,8 @@ import {
ParsedIngredientToJSON,
PatchedAccessTokenFromJSON,
PatchedAccessTokenToJSON,
PatchedAiProviderFromJSON,
PatchedAiProviderToJSON,
PatchedAutomationFromJSON,
PatchedAutomationToJSON,
PatchedBookmarkletImportFromJSON,
@@ -522,11 +540,48 @@ export interface ApiAccessTokenUpdateRequest {
}
export interface ApiAiImportCreateRequest {
aiProviderId: number;
file: string | null;
text: string | null;
recipeId: string | null;
}
export interface ApiAiLogListRequest {
page?: number;
pageSize?: number;
}
export interface ApiAiLogRetrieveRequest {
id: number;
}
export interface ApiAiProviderCreateRequest {
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAiProviderDestroyRequest {
id: number;
}
export interface ApiAiProviderListRequest {
page?: number;
pageSize?: number;
}
export interface ApiAiProviderPartialUpdateRequest {
id: number;
patchedAiProvider?: Omit<PatchedAiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAiProviderRetrieveRequest {
id: number;
}
export interface ApiAiProviderUpdateRequest {
id: number;
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAutoPlanCreateRequest {
autoMealPlan: AutoMealPlan;
}
@@ -910,6 +965,10 @@ export interface ApiFdcSearchRetrieveRequest {
query?: string;
}
export interface ApiFoodBatchUpdateUpdateRequest {
foodBatchUpdate: FoodBatchUpdate;
}
export interface ApiFoodCreateRequest {
food: Omit<Food, 'shopping'|'parent'|'numchild'|'fullName'|'substituteOnhand'>;
}
@@ -983,6 +1042,7 @@ export interface ApiGroupRetrieveRequest {
}
export interface ApiImportCreateRequest {
aiProviderId: number;
file: string | null;
text: string | null;
recipeId: string | null;
@@ -1732,7 +1792,7 @@ export interface ApiSpaceListRequest {
export interface ApiSpacePartialUpdateRequest {
id: number;
patchedSpace?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'>;
patchedSpace?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>;
}
export interface ApiSpaceRetrieveRequest {
@@ -2369,6 +2429,13 @@ export class ApiApi extends runtime.BaseAPI {
* given an image or PDF file convert its content to a structured recipe using AI and the scraping system
*/
async apiAiImportCreateRaw(requestParameters: ApiAiImportCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RecipeFromSourceResponse>> {
if (requestParameters['aiProviderId'] == null) {
throw new runtime.RequiredError(
'aiProviderId',
'Required parameter "aiProviderId" was null or undefined when calling apiAiImportCreate().'
);
}
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
@@ -2412,6 +2479,10 @@ export class ApiApi extends runtime.BaseAPI {
formParams = new URLSearchParams();
}
if (requestParameters['aiProviderId'] != null) {
formParams.append('ai_provider_id', requestParameters['aiProviderId'] as any);
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
@@ -2443,6 +2514,319 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogListRaw(requestParameters: ApiAiLogListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedAiLogList>> {
const queryParameters: any = {};
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-log/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedAiLogListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogList(requestParameters: ApiAiLogListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedAiLogList> {
const response = await this.apiAiLogListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogRetrieveRaw(requestParameters: ApiAiLogRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiLog>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiLogRetrieve().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-log/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiLogFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogRetrieve(requestParameters: ApiAiLogRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiLog> {
const response = await this.apiAiLogRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderCreateRaw(requestParameters: ApiAiProviderCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['aiProvider'] == null) {
throw new runtime.RequiredError(
'aiProvider',
'Required parameter "aiProvider" was null or undefined when calling apiAiProviderCreate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-provider/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: AiProviderToJSON(requestParameters['aiProvider']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderCreate(requestParameters: ApiAiProviderCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderDestroyRaw(requestParameters: ApiAiProviderDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderDestroy().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'DELETE',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderDestroy(requestParameters: ApiAiProviderDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.apiAiProviderDestroyRaw(requestParameters, initOverrides);
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderListRaw(requestParameters: ApiAiProviderListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedAiProviderList>> {
const queryParameters: any = {};
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-provider/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedAiProviderListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderList(requestParameters: ApiAiProviderListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedAiProviderList> {
const response = await this.apiAiProviderListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderPartialUpdateRaw(requestParameters: ApiAiProviderPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderPartialUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: PatchedAiProviderToJSON(requestParameters['patchedAiProvider']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderPartialUpdate(requestParameters: ApiAiProviderPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderPartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderRetrieveRaw(requestParameters: ApiAiProviderRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderRetrieve().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderRetrieve(requestParameters: ApiAiProviderRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderUpdateRaw(requestParameters: ApiAiProviderUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderUpdate().'
);
}
if (requestParameters['aiProvider'] == null) {
throw new runtime.RequiredError(
'aiProvider',
'Required parameter "aiProvider" was null or undefined when calling apiAiProviderUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: AiProviderToJSON(requestParameters['aiProvider']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderUpdate(requestParameters: ApiAiProviderUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -5579,6 +5963,46 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiFoodBatchUpdateUpdateRaw(requestParameters: ApiFoodBatchUpdateUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<FoodBatchUpdate>> {
if (requestParameters['foodBatchUpdate'] == null) {
throw new runtime.RequiredError(
'foodBatchUpdate',
'Required parameter "foodBatchUpdate" was null or undefined when calling apiFoodBatchUpdateUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/food/batch_update/`,
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: FoodBatchUpdateToJSON(requestParameters['foodBatchUpdate']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => FoodBatchUpdateFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiFoodBatchUpdateUpdate(requestParameters: ApiFoodBatchUpdateUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<FoodBatchUpdate> {
const response = await this.apiFoodBatchUpdateUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -6252,6 +6676,13 @@ export class ApiApi extends runtime.BaseAPI {
/**
*/
async apiImportCreateRaw(requestParameters: ApiImportCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RecipeFromSourceResponse>> {
if (requestParameters['aiProviderId'] == null) {
throw new runtime.RequiredError(
'aiProviderId',
'Required parameter "aiProviderId" was null or undefined when calling apiImportCreate().'
);
}
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
@@ -6295,6 +6726,10 @@ export class ApiApi extends runtime.BaseAPI {
formParams = new URLSearchParams();
}
if (requestParameters['aiProviderId'] != null) {
formParams.append('ai_provider_id', requestParameters['aiProviderId'] as any);
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}

View File

@@ -0,0 +1,157 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
/**
*
* @export
* @interface AiLog
*/
export interface AiLog {
/**
*
* @type {number}
* @memberof AiLog
*/
id?: number;
/**
*
* @type {AiProvider}
* @memberof AiLog
*/
readonly aiProvider: AiProvider;
/**
*
* @type {string}
* @memberof AiLog
*/
_function: string;
/**
*
* @type {number}
* @memberof AiLog
*/
creditCost: number;
/**
*
* @type {boolean}
* @memberof AiLog
*/
creditsFromBalance?: boolean;
/**
*
* @type {number}
* @memberof AiLog
*/
inputTokens?: number;
/**
*
* @type {number}
* @memberof AiLog
*/
outputTokens?: number;
/**
*
* @type {Date}
* @memberof AiLog
*/
startTime?: Date;
/**
*
* @type {Date}
* @memberof AiLog
*/
endTime?: Date;
/**
*
* @type {number}
* @memberof AiLog
*/
createdBy?: number;
/**
*
* @type {Date}
* @memberof AiLog
*/
readonly createdAt: Date;
/**
*
* @type {Date}
* @memberof AiLog
*/
readonly updatedAt: Date;
}
/**
* Check if a given object implements the AiLog interface.
*/
export function instanceOfAiLog(value: object): value is AiLog {
if (!('aiProvider' in value) || value['aiProvider'] === undefined) return false;
if (!('_function' in value) || value['_function'] === undefined) return false;
if (!('creditCost' in value) || value['creditCost'] === undefined) return false;
if (!('createdAt' in value) || value['createdAt'] === undefined) return false;
if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false;
return true;
}
export function AiLogFromJSON(json: any): AiLog {
return AiLogFromJSONTyped(json, false);
}
export function AiLogFromJSONTyped(json: any, ignoreDiscriminator: boolean): AiLog {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'aiProvider': AiProviderFromJSON(json['ai_provider']),
'_function': json['function'],
'creditCost': json['credit_cost'],
'creditsFromBalance': json['credits_from_balance'] == null ? undefined : json['credits_from_balance'],
'inputTokens': json['input_tokens'] == null ? undefined : json['input_tokens'],
'outputTokens': json['output_tokens'] == null ? undefined : json['output_tokens'],
'startTime': json['start_time'] == null ? undefined : (new Date(json['start_time'])),
'endTime': json['end_time'] == null ? undefined : (new Date(json['end_time'])),
'createdBy': json['created_by'] == null ? undefined : json['created_by'],
'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])),
};
}
export function AiLogToJSON(value?: Omit<AiLog, 'aiProvider'|'createdAt'|'updatedAt'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'function': value['_function'],
'credit_cost': value['creditCost'],
'credits_from_balance': value['creditsFromBalance'],
'input_tokens': value['inputTokens'],
'output_tokens': value['outputTokens'],
'start_time': value['startTime'] == null ? undefined : ((value['startTime'] as any).toISOString()),
'end_time': value['endTime'] == null ? undefined : ((value['endTime'] as any).toISOString()),
'created_by': value['createdBy'],
};
}

View File

@@ -0,0 +1,134 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface AiProvider
*/
export interface AiProvider {
/**
*
* @type {number}
* @memberof AiProvider
*/
id?: number;
/**
*
* @type {string}
* @memberof AiProvider
*/
name: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
description?: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
apiKey?: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
modelName: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
url?: string;
/**
*
* @type {boolean}
* @memberof AiProvider
*/
logCreditCost?: boolean;
/**
*
* @type {number}
* @memberof AiProvider
*/
space?: number;
/**
*
* @type {Date}
* @memberof AiProvider
*/
readonly createdAt: Date;
/**
*
* @type {Date}
* @memberof AiProvider
*/
readonly updatedAt: Date;
}
/**
* Check if a given object implements the AiProvider interface.
*/
export function instanceOfAiProvider(value: object): value is AiProvider {
if (!('name' in value) || value['name'] === undefined) return false;
if (!('modelName' in value) || value['modelName'] === undefined) return false;
if (!('createdAt' in value) || value['createdAt'] === undefined) return false;
if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false;
return true;
}
export function AiProviderFromJSON(json: any): AiProvider {
return AiProviderFromJSONTyped(json, false);
}
export function AiProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean): AiProvider {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'],
'description': json['description'] == null ? undefined : json['description'],
'apiKey': json['api_key'] == null ? undefined : json['api_key'],
'modelName': json['model_name'],
'url': json['url'] == null ? undefined : json['url'],
'logCreditCost': json['log_credit_cost'] == null ? undefined : json['log_credit_cost'],
'space': json['space'] == null ? undefined : json['space'],
'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])),
};
}
export function AiProviderToJSON(value?: Omit<AiProvider, 'createdAt'|'updatedAt'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'description': value['description'],
'api_key': value['apiKey'],
'model_name': value['modelName'],
'url': value['url'],
'log_credit_cost': value['logCreditCost'],
'space': value['space'],
};
}

View File

@@ -0,0 +1,222 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface FoodBatchUpdate
*/
export interface FoodBatchUpdate {
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
foods: Array<number>;
/**
*
* @type {number}
* @memberof FoodBatchUpdate
*/
category?: number;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
substituteAdd: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
substituteRemove: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
substituteSet: Array<number>;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
substituteRemoveAll?: boolean;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
inheritFieldsAdd: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
inheritFieldsRemove: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
inheritFieldsSet: Array<number>;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
inheritFieldsRemoveAll?: boolean;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
childInheritFieldsAdd: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
childInheritFieldsRemove: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
childInheritFieldsSet: Array<number>;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
childInheritFieldsRemoveAll?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
substituteChildren?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
substituteSiblings?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
ignoreShopping?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
onHand?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
parentRemove?: boolean;
/**
*
* @type {number}
* @memberof FoodBatchUpdate
*/
parentSet?: number;
}
/**
* Check if a given object implements the FoodBatchUpdate interface.
*/
export function instanceOfFoodBatchUpdate(value: object): value is FoodBatchUpdate {
if (!('foods' in value) || value['foods'] === undefined) return false;
if (!('substituteAdd' in value) || value['substituteAdd'] === undefined) return false;
if (!('substituteRemove' in value) || value['substituteRemove'] === undefined) return false;
if (!('substituteSet' in value) || value['substituteSet'] === undefined) return false;
if (!('inheritFieldsAdd' in value) || value['inheritFieldsAdd'] === undefined) return false;
if (!('inheritFieldsRemove' in value) || value['inheritFieldsRemove'] === undefined) return false;
if (!('inheritFieldsSet' in value) || value['inheritFieldsSet'] === undefined) return false;
if (!('childInheritFieldsAdd' in value) || value['childInheritFieldsAdd'] === undefined) return false;
if (!('childInheritFieldsRemove' in value) || value['childInheritFieldsRemove'] === undefined) return false;
if (!('childInheritFieldsSet' in value) || value['childInheritFieldsSet'] === undefined) return false;
return true;
}
export function FoodBatchUpdateFromJSON(json: any): FoodBatchUpdate {
return FoodBatchUpdateFromJSONTyped(json, false);
}
export function FoodBatchUpdateFromJSONTyped(json: any, ignoreDiscriminator: boolean): FoodBatchUpdate {
if (json == null) {
return json;
}
return {
'foods': json['foods'],
'category': json['category'] == null ? undefined : json['category'],
'substituteAdd': json['substitute_add'],
'substituteRemove': json['substitute_remove'],
'substituteSet': json['substitute_set'],
'substituteRemoveAll': json['substitute_remove_all'] == null ? undefined : json['substitute_remove_all'],
'inheritFieldsAdd': json['inherit_fields_add'],
'inheritFieldsRemove': json['inherit_fields_remove'],
'inheritFieldsSet': json['inherit_fields_set'],
'inheritFieldsRemoveAll': json['inherit_fields_remove_all'] == null ? undefined : json['inherit_fields_remove_all'],
'childInheritFieldsAdd': json['child_inherit_fields_add'],
'childInheritFieldsRemove': json['child_inherit_fields_remove'],
'childInheritFieldsSet': json['child_inherit_fields_set'],
'childInheritFieldsRemoveAll': json['child_inherit_fields_remove_all'] == null ? undefined : json['child_inherit_fields_remove_all'],
'substituteChildren': json['substitute_children'] == null ? undefined : json['substitute_children'],
'substituteSiblings': json['substitute_siblings'] == null ? undefined : json['substitute_siblings'],
'ignoreShopping': json['ignore_shopping'] == null ? undefined : json['ignore_shopping'],
'onHand': json['on_hand'] == null ? undefined : json['on_hand'],
'parentRemove': json['parent_remove'] == null ? undefined : json['parent_remove'],
'parentSet': json['parent_set'] == null ? undefined : json['parent_set'],
};
}
export function FoodBatchUpdateToJSON(value?: FoodBatchUpdate | null): any {
if (value == null) {
return value;
}
return {
'foods': value['foods'],
'category': value['category'],
'substitute_add': value['substituteAdd'],
'substitute_remove': value['substituteRemove'],
'substitute_set': value['substituteSet'],
'substitute_remove_all': value['substituteRemoveAll'],
'inherit_fields_add': value['inheritFieldsAdd'],
'inherit_fields_remove': value['inheritFieldsRemove'],
'inherit_fields_set': value['inheritFieldsSet'],
'inherit_fields_remove_all': value['inheritFieldsRemoveAll'],
'child_inherit_fields_add': value['childInheritFieldsAdd'],
'child_inherit_fields_remove': value['childInheritFieldsRemove'],
'child_inherit_fields_set': value['childInheritFieldsSet'],
'child_inherit_fields_remove_all': value['childInheritFieldsRemoveAll'],
'substitute_children': value['substituteChildren'],
'substitute_siblings': value['substituteSiblings'],
'ignore_shopping': value['ignoreShopping'],
'on_hand': value['onHand'],
'parent_remove': value['parentRemove'],
'parent_set': value['parentSet'],
};
}

View File

@@ -0,0 +1,101 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { AiLog } from './AiLog';
import {
AiLogFromJSON,
AiLogFromJSONTyped,
AiLogToJSON,
} from './AiLog';
/**
*
* @export
* @interface PaginatedAiLogList
*/
export interface PaginatedAiLogList {
/**
*
* @type {number}
* @memberof PaginatedAiLogList
*/
count: number;
/**
*
* @type {string}
* @memberof PaginatedAiLogList
*/
next?: string;
/**
*
* @type {string}
* @memberof PaginatedAiLogList
*/
previous?: string;
/**
*
* @type {Array<AiLog>}
* @memberof PaginatedAiLogList
*/
results: Array<AiLog>;
/**
*
* @type {Date}
* @memberof PaginatedAiLogList
*/
timestamp?: Date;
}
/**
* Check if a given object implements the PaginatedAiLogList interface.
*/
export function instanceOfPaginatedAiLogList(value: object): value is PaginatedAiLogList {
if (!('count' in value) || value['count'] === undefined) return false;
if (!('results' in value) || value['results'] === undefined) return false;
return true;
}
export function PaginatedAiLogListFromJSON(json: any): PaginatedAiLogList {
return PaginatedAiLogListFromJSONTyped(json, false);
}
export function PaginatedAiLogListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedAiLogList {
if (json == null) {
return json;
}
return {
'count': json['count'],
'next': json['next'] == null ? undefined : json['next'],
'previous': json['previous'] == null ? undefined : json['previous'],
'results': ((json['results'] as Array<any>).map(AiLogFromJSON)),
'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])),
};
}
export function PaginatedAiLogListToJSON(value?: PaginatedAiLogList | null): any {
if (value == null) {
return value;
}
return {
'count': value['count'],
'next': value['next'],
'previous': value['previous'],
'results': ((value['results'] as Array<any>).map(AiLogToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -0,0 +1,101 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
/**
*
* @export
* @interface PaginatedAiProviderList
*/
export interface PaginatedAiProviderList {
/**
*
* @type {number}
* @memberof PaginatedAiProviderList
*/
count: number;
/**
*
* @type {string}
* @memberof PaginatedAiProviderList
*/
next?: string;
/**
*
* @type {string}
* @memberof PaginatedAiProviderList
*/
previous?: string;
/**
*
* @type {Array<AiProvider>}
* @memberof PaginatedAiProviderList
*/
results: Array<AiProvider>;
/**
*
* @type {Date}
* @memberof PaginatedAiProviderList
*/
timestamp?: Date;
}
/**
* Check if a given object implements the PaginatedAiProviderList interface.
*/
export function instanceOfPaginatedAiProviderList(value: object): value is PaginatedAiProviderList {
if (!('count' in value) || value['count'] === undefined) return false;
if (!('results' in value) || value['results'] === undefined) return false;
return true;
}
export function PaginatedAiProviderListFromJSON(json: any): PaginatedAiProviderList {
return PaginatedAiProviderListFromJSONTyped(json, false);
}
export function PaginatedAiProviderListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedAiProviderList {
if (json == null) {
return json;
}
return {
'count': json['count'],
'next': json['next'] == null ? undefined : json['next'],
'previous': json['previous'] == null ? undefined : json['previous'],
'results': ((json['results'] as Array<any>).map(AiProviderFromJSON)),
'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])),
};
}
export function PaginatedAiProviderListToJSON(value?: PaginatedAiProviderList | null): any {
if (value == null) {
return value;
}
return {
'count': value['count'],
'next': value['next'],
'previous': value['previous'],
'results': ((value['results'] as Array<any>).map(AiProviderToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -0,0 +1,130 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface PatchedAiProvider
*/
export interface PatchedAiProvider {
/**
*
* @type {number}
* @memberof PatchedAiProvider
*/
id?: number;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
name?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
description?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
apiKey?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
modelName?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
url?: string;
/**
*
* @type {boolean}
* @memberof PatchedAiProvider
*/
logCreditCost?: boolean;
/**
*
* @type {number}
* @memberof PatchedAiProvider
*/
space?: number;
/**
*
* @type {Date}
* @memberof PatchedAiProvider
*/
readonly createdAt?: Date;
/**
*
* @type {Date}
* @memberof PatchedAiProvider
*/
readonly updatedAt?: Date;
}
/**
* Check if a given object implements the PatchedAiProvider interface.
*/
export function instanceOfPatchedAiProvider(value: object): value is PatchedAiProvider {
return true;
}
export function PatchedAiProviderFromJSON(json: any): PatchedAiProvider {
return PatchedAiProviderFromJSONTyped(json, false);
}
export function PatchedAiProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean): PatchedAiProvider {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'apiKey': json['api_key'] == null ? undefined : json['api_key'],
'modelName': json['model_name'] == null ? undefined : json['model_name'],
'url': json['url'] == null ? undefined : json['url'],
'logCreditCost': json['log_credit_cost'] == null ? undefined : json['log_credit_cost'],
'space': json['space'] == null ? undefined : json['space'],
'createdAt': json['created_at'] == null ? undefined : (new Date(json['created_at'])),
'updatedAt': json['updated_at'] == null ? undefined : (new Date(json['updated_at'])),
};
}
export function PatchedAiProviderToJSON(value?: Omit<PatchedAiProvider, 'createdAt'|'updatedAt'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'description': value['description'],
'api_key': value['apiKey'],
'model_name': value['modelName'],
'url': value['url'],
'log_credit_cost': value['logCreditCost'],
'space': value['space'],
};
}

View File

@@ -31,6 +31,12 @@ import {
SpaceNavTextColorEnumFromJSONTyped,
SpaceNavTextColorEnumToJSON,
} from './SpaceNavTextColorEnum';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
import type { FoodInheritField } from './FoodInheritField';
import {
FoodInheritFieldFromJSON,
@@ -212,6 +218,36 @@ export interface PatchedSpace {
* @memberof PatchedSpace
*/
logoColorSvg?: UserFileView;
/**
*
* @type {number}
* @memberof PatchedSpace
*/
aiCreditsMonthly?: number;
/**
*
* @type {number}
* @memberof PatchedSpace
*/
aiCreditsBalance?: number;
/**
*
* @type {number}
* @memberof PatchedSpace
*/
readonly aiMonthlyCreditsUsed?: number;
/**
*
* @type {boolean}
* @memberof PatchedSpace
*/
aiEnabled?: boolean;
/**
*
* @type {AiProvider}
* @memberof PatchedSpace
*/
aiDefaultProvider?: AiProvider;
}
/**
@@ -258,10 +294,15 @@ export function PatchedSpaceFromJSONTyped(json: any, ignoreDiscriminator: boolea
'logoColor192': json['logo_color_192'] == null ? undefined : UserFileViewFromJSON(json['logo_color_192']),
'logoColor512': json['logo_color_512'] == null ? undefined : UserFileViewFromJSON(json['logo_color_512']),
'logoColorSvg': json['logo_color_svg'] == null ? undefined : UserFileViewFromJSON(json['logo_color_svg']),
'aiCreditsMonthly': json['ai_credits_monthly'] == null ? undefined : json['ai_credits_monthly'],
'aiCreditsBalance': json['ai_credits_balance'] == null ? undefined : json['ai_credits_balance'],
'aiMonthlyCreditsUsed': json['ai_monthly_credits_used'] == null ? undefined : json['ai_monthly_credits_used'],
'aiEnabled': json['ai_enabled'] == null ? undefined : json['ai_enabled'],
'aiDefaultProvider': json['ai_default_provider'] == null ? undefined : AiProviderFromJSON(json['ai_default_provider']),
};
}
export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'> | null): any {
export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'> | null): any {
if (value == null) {
return value;
}
@@ -284,6 +325,10 @@ export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'creat
'logo_color_192': UserFileViewToJSON(value['logoColor192']),
'logo_color_512': UserFileViewToJSON(value['logoColor512']),
'logo_color_svg': UserFileViewToJSON(value['logoColorSvg']),
'ai_credits_monthly': value['aiCreditsMonthly'],
'ai_credits_balance': value['aiCreditsBalance'],
'ai_enabled': value['aiEnabled'],
'ai_default_provider': AiProviderToJSON(value['aiDefaultProvider']),
};
}

View File

@@ -31,12 +31,6 @@ export interface ServerSettings {
* @memberof ServerSettings
*/
enablePdfExport: boolean;
/**
*
* @type {boolean}
* @memberof ServerSettings
*/
enableAiImport: boolean;
/**
*
* @type {boolean}
@@ -159,7 +153,6 @@ export interface ServerSettings {
export function instanceOfServerSettings(value: object): value is ServerSettings {
if (!('shoppingMinAutosyncInterval' in value) || value['shoppingMinAutosyncInterval'] === undefined) return false;
if (!('enablePdfExport' in value) || value['enablePdfExport'] === undefined) return false;
if (!('enableAiImport' in value) || value['enableAiImport'] === undefined) return false;
if (!('disableExternalConnectors' in value) || value['disableExternalConnectors'] === undefined) return false;
if (!('termsUrl' in value) || value['termsUrl'] === undefined) return false;
if (!('privacyUrl' in value) || value['privacyUrl'] === undefined) return false;
@@ -184,7 +177,6 @@ export function ServerSettingsFromJSONTyped(json: any, ignoreDiscriminator: bool
'shoppingMinAutosyncInterval': json['shopping_min_autosync_interval'],
'enablePdfExport': json['enable_pdf_export'],
'enableAiImport': json['enable_ai_import'],
'disableExternalConnectors': json['disable_external_connectors'],
'termsUrl': json['terms_url'],
'privacyUrl': json['privacy_url'],
@@ -215,7 +207,6 @@ export function ServerSettingsToJSON(value?: ServerSettings | null): any {
'shopping_min_autosync_interval': value['shoppingMinAutosyncInterval'],
'enable_pdf_export': value['enablePdfExport'],
'enable_ai_import': value['enableAiImport'],
'disable_external_connectors': value['disableExternalConnectors'],
'terms_url': value['termsUrl'],
'privacy_url': value['privacyUrl'],

View File

@@ -31,6 +31,12 @@ import {
SpaceNavTextColorEnumFromJSONTyped,
SpaceNavTextColorEnumToJSON,
} from './SpaceNavTextColorEnum';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
import type { FoodInheritField } from './FoodInheritField';
import {
FoodInheritFieldFromJSON,
@@ -212,6 +218,36 @@ export interface Space {
* @memberof Space
*/
logoColorSvg?: UserFileView;
/**
*
* @type {number}
* @memberof Space
*/
aiCreditsMonthly?: number;
/**
*
* @type {number}
* @memberof Space
*/
aiCreditsBalance?: number;
/**
*
* @type {number}
* @memberof Space
*/
readonly aiMonthlyCreditsUsed: number;
/**
*
* @type {boolean}
* @memberof Space
*/
aiEnabled?: boolean;
/**
*
* @type {AiProvider}
* @memberof Space
*/
aiDefaultProvider?: AiProvider;
}
/**
@@ -229,6 +265,7 @@ export function instanceOfSpace(value: object): value is Space {
if (!('userCount' in value) || value['userCount'] === undefined) return false;
if (!('recipeCount' in value) || value['recipeCount'] === undefined) return false;
if (!('fileSizeMb' in value) || value['fileSizeMb'] === undefined) return false;
if (!('aiMonthlyCreditsUsed' in value) || value['aiMonthlyCreditsUsed'] === undefined) return false;
return true;
}
@@ -269,10 +306,15 @@ export function SpaceFromJSONTyped(json: any, ignoreDiscriminator: boolean): Spa
'logoColor192': json['logo_color_192'] == null ? undefined : UserFileViewFromJSON(json['logo_color_192']),
'logoColor512': json['logo_color_512'] == null ? undefined : UserFileViewFromJSON(json['logo_color_512']),
'logoColorSvg': json['logo_color_svg'] == null ? undefined : UserFileViewFromJSON(json['logo_color_svg']),
'aiCreditsMonthly': json['ai_credits_monthly'] == null ? undefined : json['ai_credits_monthly'],
'aiCreditsBalance': json['ai_credits_balance'] == null ? undefined : json['ai_credits_balance'],
'aiMonthlyCreditsUsed': json['ai_monthly_credits_used'],
'aiEnabled': json['ai_enabled'] == null ? undefined : json['ai_enabled'],
'aiDefaultProvider': json['ai_default_provider'] == null ? undefined : AiProviderFromJSON(json['ai_default_provider']),
};
}
export function SpaceToJSON(value?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'> | null): any {
export function SpaceToJSON(value?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'> | null): any {
if (value == null) {
return value;
}
@@ -295,6 +337,10 @@ export function SpaceToJSON(value?: Omit<Space, 'createdBy'|'createdAt'|'maxReci
'logo_color_192': UserFileViewToJSON(value['logoColor192']),
'logo_color_512': UserFileViewToJSON(value['logoColor512']),
'logo_color_svg': UserFileViewToJSON(value['logoColorSvg']),
'ai_credits_monthly': value['aiCreditsMonthly'],
'ai_credits_balance': value['aiCreditsBalance'],
'ai_enabled': value['aiEnabled'],
'ai_default_provider': AiProviderToJSON(value['aiDefaultProvider']),
};
}

View File

@@ -1,6 +1,8 @@
/* tslint:disable */
/* eslint-disable */
export * from './AccessToken';
export * from './AiLog';
export * from './AiProvider';
export * from './AlignmentEnum';
export * from './AuthToken';
export * from './AutoMealPlan';
@@ -25,6 +27,7 @@ export * from './ExportRequest';
export * from './FdcQuery';
export * from './FdcQueryFoods';
export * from './Food';
export * from './FoodBatchUpdate';
export * from './FoodInheritField';
export * from './FoodShoppingUpdate';
export * from './FoodSimple';
@@ -55,6 +58,8 @@ export * from './OpenDataStoreCategory';
export * from './OpenDataUnit';
export * from './OpenDataUnitTypeEnum';
export * from './OpenDataVersion';
export * from './PaginatedAiLogList';
export * from './PaginatedAiProviderList';
export * from './PaginatedAutomationList';
export * from './PaginatedBookmarkletImportListList';
export * from './PaginatedConnectorConfigList';
@@ -101,6 +106,7 @@ export * from './PaginatedUserSpaceList';
export * from './PaginatedViewLogList';
export * from './ParsedIngredient';
export * from './PatchedAccessToken';
export * from './PatchedAiProvider';
export * from './PatchedAutomation';
export * from './PatchedBookmarkletImport';
export * from './PatchedConnectorConfig';

View File

@@ -35,6 +35,18 @@
<database-model-col model="MealType"></database-model-col>
</v-row>
<template v-if="useUserPreferenceStore().activeSpace.aiEnabled">
<v-row>
<v-col>
<h2>{{ $t('Ai') }}</h2>
</v-col>
</v-row>
<v-row dense>
<database-model-col model="AiProvider"></database-model-col>
<database-model-col model="AiLog"></database-model-col>
</v-row>
</template>
<template v-for="p in TANDOOR_PLUGINS" :key="p.name">
<component :is="p.databasePageComponent" v-if="p.databasePageComponent"></component>
</template>
@@ -82,6 +94,7 @@
import DatabaseModelCol from "@/components/display/DatabaseModelCol.vue";
import DatabaseLinkCol from "@/components/display/DatabaseLinkCol.vue";
import {TANDOOR_PLUGINS} from "@/types/Plugins.ts";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
</script>

View File

@@ -27,9 +27,17 @@
@delete="loadItems({page: page})"></model-edit-dialog>
</v-btn>
</template>
<!-- TODO build customizable model component system -->
<v-card-actions v-if="genericModel.model.name == 'RecipeImport'">
<v-btn prepend-icon="fa-solid fa-rotate" color="success" @click="importAllRecipes()">{{ $t('ImportAll') }}</v-btn>
</v-card-actions>
<v-card-text v-if="genericModel.model.name == 'AiLog'">
{{ $t('MonthlyCreditsUsed') }} ({{ useUserPreferenceStore().activeSpace.aiMonthlyCreditsUsed }} / {{ useUserPreferenceStore().activeSpace.aiCreditsMonthly }})
{{ $t('AiCreditsBalance') }} : {{ useUserPreferenceStore().activeSpace.aiCreditsBalance }}
<v-progress-linear :model-value="useUserPreferenceStore().activeSpace.aiMonthlyCreditsUsed" :max="useUserPreferenceStore().activeSpace.aiCreditsMonthly"></v-progress-linear>
</v-card-text>
</v-card>
</v-col>
</v-row>
@@ -57,6 +65,9 @@
<v-icon icon="fa-solid fa-ellipsis-v"></v-icon>
<v-menu activator="parent" close-on-content-click>
<v-list density="compact" class="pt-1 pb-1" activatable>
<v-list-item prepend-icon="fa-solid fa-list-check" @click="batchEditDialog = true" v-if="genericModel.model.name == 'Food'">
{{ $t('BatchEdit') }}
</v-list-item>
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" @click="batchMergeDialog = true" v-if="genericModel.model.isMerge">
{{ $t('Merge') }}
</v-list-item>
@@ -67,6 +78,10 @@
</v-menu>
</v-btn>
</template>
<template v-slot:item.space="{ item }" v-if="genericModel.model.name == 'AiProvider'">
<v-chip label v-if="item.space == null" color="success">{{ $t('Global') }}</v-chip>
<v-chip label v-else color="info">{{ $t('Space') }}</v-chip>
</template>
<template v-slot:item.action="{ item }">
<v-btn class="float-right" icon="$menu" variant="plain">
<v-icon icon="$menu"></v-icon>
@@ -105,10 +120,13 @@
</v-row>
<batch-delete-dialog :items="selectedItems" :model="props.model" v-model="batchDeleteDialog" activator="model"
@change="loadItems({page: page, itemsPerPage: pageSize, search: query})"></batch-delete-dialog>
@change="loadItems({page: page, itemsPerPage: pageSize, search: query})"></batch-delete-dialog>
<model-merge-dialog :model="model" :source="selectedItems" v-model="batchMergeDialog" activator="model"
@change="loadItems({page: page, itemsPerPage: pageSize, search: query})"></model-merge-dialog>
<model-merge-dialog :model="model" :source="selectedItems" v-model="batchMergeDialog" activator="model"
@change="loadItems({page: page, itemsPerPage: pageSize, search: query})"></model-merge-dialog>
<batch-edit-food-dialog :items="selectedItems" v-model="batchEditDialog" v-if="model == 'Food'" activator="model"
@change="loadItems({page: page, itemsPerPage: pageSize, search: query})"></batch-edit-food-dialog>
</v-container>
</template>
@@ -132,6 +150,7 @@ import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue";
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
import BatchDeleteDialog from "@/components/dialogs/BatchDeleteDialog.vue";
import {useRouteQuery} from "@vueuse/router";
import BatchEditFoodDialog from "@/components/dialogs/BatchEditFoodDialog.vue";
const {t} = useI18n()
const router = useRouter()
@@ -160,6 +179,7 @@ const selectedItems = ref([] as EditorSupportedTypes[])
const batchDeleteDialog = ref(false)
const batchMergeDialog = ref(false)
const batchEditDialog = ref(false)
// data
const loading = ref(false);
@@ -203,7 +223,7 @@ function loadItems(options: VDataTableUpdateOptions) {
page.value = options.page
pageSize.value = options.itemsPerPage
genericModel.value.list({ query: query.value, page: options.page, pageSize: pageSize.value }).then((r: any) => {
genericModel.value.list({query: query.value, page: options.page, pageSize: pageSize.value}).then((r: any) => {
items.value = r.results
itemCount.value = r.count
}).catch((err: any) => {

View File

@@ -60,7 +60,7 @@
</v-card>
</v-col>
<v-col cols="12" md="6" v-if="useUserPreferenceStore().serverSettings.enableAiImport">
<v-col cols="12" md="6" v-if="useUserPreferenceStore().activeSpace.aiEnabled">
<v-card
:title="$t('AI')"
:subtitle="$t('AIImportSubtitle')"
@@ -69,7 +69,7 @@
:color="(importType == 'ai') ? 'primary' : ''"
elevation="1"
@click="importType = 'ai'"
:disabled="!useUserPreferenceStore().serverSettings.enableAiImport">
:disabled="!useUserPreferenceStore().activeSpace.aiEnabled">
</v-card>
</v-col>
@@ -140,10 +140,22 @@
@keydown.enter="loadRecipeFromUrl({url: importUrl})"></v-text-field>
<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-row>
<v-col md="6">
<ModelSelect model="AiProvider" v-model="selectedAiProvider">
<template #append>
<v-btn icon="$settings" :to="{name:'ModelListPage', params: {model: 'AiProvider'}}" color="success"></v-btn>
</template>
</ModelSelect>
</v-col>
<v-col md="6">
<v-btn-toggle class="mb-2" border divided v-model="aiMode">
<v-btn value="file">{{ $t('File') }}</v-btn>
<v-btn value="text">{{ $t('Text') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-file-upload v-model="image" v-if="aiMode == 'file'" :loading="loading" clearable>
<template #icon>
@@ -540,6 +552,7 @@ import {useI18n} from "vue-i18n";
import {computed, onMounted, ref} from "vue";
import {
AccessToken,
AiProvider,
ApiApi,
ImportLog,
Recipe,
@@ -648,6 +661,7 @@ const appImportDuplicates = ref(false)
const appImportLog = ref<null | ImportLog>(null)
const image = ref<null | File>(null)
const aiMode = ref<'file' | 'text'>('file')
const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
const editAfterImport = ref(false)
const bookmarkletToken = ref("")
@@ -724,12 +738,15 @@ function loadRecipeFromUrl(recipeFromSourceRequest: RecipeFromSource) {
*/
function loadRecipeFromAiImport() {
let request = null
if (selectedAiProvider.value == undefined) {
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, "No AI Provider selected")
}
if (image.value != null && aiMode.value == 'file') {
console.log('file import')
request = doAiImport(image.value)
request = doAiImport(selectedAiProvider.value.id!, image.value)
} else if (sourceImportText.value != '' && aiMode.value == 'text') {
console.log('text import')
request = doAiImport(null, sourceImportText.value)
request = doAiImport(selectedAiProvider.value.id!, null, sourceImportText.value)
}
if (request != null) {
@@ -811,13 +828,13 @@ function deleteStep(step: SourceImportStep) {
}
function handleMergeAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps){
if (importResponse.value.recipe && importResponse.value.recipe.steps) {
mergeAllSteps(importResponse.value.recipe.steps)
}
}
function handleSplitAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps){
if (importResponse.value.recipe && importResponse.value.recipe.steps) {
splitAllSteps(importResponse.value.recipe.steps, '\n')
}
}

View File

@@ -66,36 +66,37 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
currently_updating.value = [from_date, to_date] // certainly no perfect check but better than nothing
loading.value = true
const api = new ApiApi()
return api.apiMealPlanList({
fromDate: DateTime.fromJSDate(from_date).toISODate() as string,
toDate: DateTime.fromJSDate(to_date).toISODate() as string,
pageSize: 100
}).then(r => {
let foundIds: number[] = []
r.results.forEach((p) => {
plans.value.set(p.id!, p)
foundIds.push(p.id!)
})
// delete entries that no longer exist
plans.value.forEach(p => {
if (!foundIds.includes(p.id!)) {
plans.value.delete(p.id!)
}
})
currently_updating.value = [new Date(0), new Date(0)]
}).catch((err) => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
loading.value = false
})
plans.value = new Map<number, MealPlan>()
return recLoadMealPlans(from_date, to_date)
}
return new Promise(() => {
})
}
function recLoadMealPlans(from_date: Date, to_date: Date, page: number = 1): Promise<any> {
const api = new ApiApi()
return api.apiMealPlanList({
fromDate: DateTime.fromJSDate(from_date).toISODate() as string,
toDate: DateTime.fromJSDate(to_date).toISODate() as string,
pageSize: 100,
page: page
}).then(r => {
r.results.forEach((p) => {
plans.value.set(p.id!, p)
})
if (r.next) {
return recLoadMealPlans(from_date, to_date, page + 1)
} else {
loading.value = false
currently_updating.value = [new Date(0), new Date(0)]
}
}).catch((err) => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
function createOrUpdate(object: MealPlan) {
if (object.id == undefined) {
return createObject(object)

View File

@@ -1,5 +1,5 @@
import {
AccessToken,
AccessToken, AiLog, AiProvider,
ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
Food,
Ingredient,
@@ -146,6 +146,8 @@ export type EditorSupportedModels =
| 'ViewLog'
| 'ConnectorConfig'
| 'SearchFields'
| 'AiProvider'
| 'AiLog'
// used to type methods/parameters in conjunction with configuration type
export type EditorSupportedTypes =
@@ -180,6 +182,8 @@ export type EditorSupportedTypes =
| ViewLog
| ConnectorConfig
| SearchFields
| AiProvider
| AiLog
export const TFood = {
name: 'Food',
@@ -788,6 +792,55 @@ export const TConnectorConfig = {
} as Model
registerModel(TConnectorConfig)
export const TAiProvider = {
name: 'AiProvider',
localizationKey: 'AiProvider',
localizationKeyDescription: 'AiProviderHelp',
icon: 'fa-solid fa-wand-magic-sparkles',
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/AiProviderEditor.vue`)),
disableListView: false,
toStringKeys: ['name'],
isPaginated: true,
disableCreate: false,
disableDelete: false,
disableUpdate: false,
tableHeaders: [
{title: 'Name', key: 'name'},
{title: 'Global', key: 'space'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TAiProvider)
export const TAiLog = {
name: 'AiLog',
localizationKey: 'AiLog',
localizationKeyDescription: 'AiLogHelp',
icon: 'fa-solid fa-wand-magic-sparkles',
disableListView: false,
toStringKeys: ['aiProvider.name', 'function', 'created_at'],
isPaginated: true,
disableCreate: true,
disableDelete: true,
disableUpdate: true,
tableHeaders: [
{title: 'Type', key: '_function'},
{title: 'AiProvider', key: 'aiProvider.name',},
{title: 'Credits', key: 'creditCost',},
{title: 'FromBalance', key: 'creditsFromBalance',},
{title: 'CreatedAt', key: 'createdAt'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TAiLog)
export const TFoodInheritField = {
name: 'FoodInheritField',
localizationKey: 'FoodInherit',