From fe7f8a958241c0523fdefc544eb9bc5007550bc6 Mon Sep 17 00:00:00 2001 From: Nils Reichardt Date: Tue, 2 Jan 2024 20:32:42 +0100 Subject: [PATCH] Introduce GPT-4 support and increase AnkiGPT Plus price. (#136) --- README.md | 4 +- .../infrastructure/session_repository.dart | 2 + lib/src/models/app_user.dart | 13 + lib/src/models/app_user.freezed.dart | 212 ++++++++++++++- lib/src/models/app_user.g.dart | 24 +- lib/src/models/card_generation_size.dart | 2 +- lib/src/models/deck_preview.dart | 4 + lib/src/models/deck_preview.freezed.dart | 176 ++++++++----- lib/src/models/model.dart | 33 +++ lib/src/models/session_dto.dart | 28 +- lib/src/models/session_dto.freezed.dart | 39 ++- lib/src/models/session_dto.g.dart | 7 + lib/src/models/watch_view.dart | 2 + lib/src/models/watch_view.freezed.dart | 34 ++- lib/src/pages/account_page.dart | 8 +- lib/src/pages/deck_page/result_section.dart | 10 +- lib/src/pages/home_page/controls.dart | 166 ++++-------- lib/src/pages/home_page/faq_section.dart | 6 +- lib/src/pages/home_page/my_decks_section.dart | 23 +- lib/src/pages/home_page/new_card.dart | 6 +- lib/src/pages/home_page/options_dialog.dart | 245 ++++++++++++++++++ lib/src/pages/home_page/plus_dialog.dart | 18 +- lib/src/pages/home_page/pricing_section.dart | 71 +++-- .../card_generation_size_provider.dart | 18 -- ...vider.dart => current_usage_provider.dart} | 8 +- ...r.g.dart => current_usage_provider.g.dart} | 17 +- lib/src/providers/deck_list_provider.dart | 3 + lib/src/providers/deck_list_provider.g.dart | 2 +- lib/src/providers/generate_provider.dart | 54 +++- lib/src/providers/generate_provider.g.dart | 2 +- lib/src/providers/has_plus_provider.dart | 2 +- lib/src/providers/has_plus_provider.g.dart | 6 +- lib/src/providers/options_provider.dart | 50 ++++ .../providers/options_provider.freezed.dart | 153 +++++++++++ ...rovider.g.dart => options_provider.g.dart} | 20 +- lib/src/providers/watch_provider.dart | 1 + lib/src/providers/watch_provider.g.dart | 2 +- .../home_page/my_decks_section_test.dart | 4 + .../delete_card_provider_test.mocks.dart | 4 + .../edit_answer_provider_test.mocks.dart | 4 + .../edit_question_provider_test.mocks.dart | 4 + test/providers/generate_provider_test.dart | 12 +- 42 files changed, 1200 insertions(+), 299 deletions(-) create mode 100644 lib/src/models/model.dart create mode 100644 lib/src/pages/home_page/options_dialog.dart delete mode 100644 lib/src/providers/card_generation_size_provider.dart rename lib/src/providers/{current_month_usage_provider.dart => current_usage_provider.dart} (57%) rename lib/src/providers/{current_month_usage_provider.g.dart => current_usage_provider.g.dart} (61%) create mode 100644 lib/src/providers/options_provider.dart create mode 100644 lib/src/providers/options_provider.freezed.dart rename lib/src/providers/{card_generation_size_provider.g.dart => options_provider.g.dart} (53%) diff --git a/README.md b/README.md index 9961794d..18974ddc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ https://github.com/nilsreichardt/ankigpt/assets/24459435/853dee68-d634-48e4-93fe ## Features - 📁 Upload PDF slides or insert text -- 🧠 Generate flashcards up to 150 [Anki](https://apps.ankiweb.net/) flashcards with one click +- 🧠 Generate flashcards up to 150 [Anki](https://apps.ankiweb.net/) flashcards with one click using GPT-3.5 or GPT-4 by OpenAI - 📤 Export generated flashcards by GPT (ChatGPT) to [Anki](https://apps.ankiweb.net/) with CSV file - ✏️ Edit flashcards, in case you want to change something - 🗑️ Delete flashcards, in case GPT (ChatGPT) generated something wrong or useless card @@ -36,7 +36,7 @@ AnkiGPT is designed to be a globally accessible tool, and as such, it supports n ### Which model is used for AnkiGPT? -AnkiGPT uses as underlaying AI model GPT-3.5 by OpenAI. +AnkiGPT primarily utilizes the GPT-3.5 by OpenAI model to generate flashcards, offering a seamless integration of advanced AI technology for effective learning. For users who opt for the AnkiGPT Plus version, they gain the enhanced capability to generate up to 150 flashcards per month using the more advanced [GPT-4 model](https://openai.com/gpt-4), ensuring even more sophisticated and nuanced content creation. ### Is the content from my submitted lecture slides used for AI training? diff --git a/lib/src/infrastructure/session_repository.dart b/lib/src/infrastructure/session_repository.dart index c5ffa19b..b119aa74 100644 --- a/lib/src/infrastructure/session_repository.dart +++ b/lib/src/infrastructure/session_repository.dart @@ -26,6 +26,7 @@ class SessionRepository { Future startSession({ required Input input, required int numberOfCards, + required String model, required SessionId? sessionId, }) async { final result = await functions @@ -36,6 +37,7 @@ class SessionRepository { 'input': input.toJson(), 'sessionId': sessionId, 'numberOfCards': numberOfCards, + 'model': model, } }); return result.data['id']; diff --git a/lib/src/models/app_user.dart b/lib/src/models/app_user.dart index 966edd71..705cd7e9 100644 --- a/lib/src/models/app_user.dart +++ b/lib/src/models/app_user.dart @@ -13,6 +13,7 @@ class AppUser with _$AppUser { @Default(Usage( generatedCardsCurrentMonth: 0, generatedMnemonicsCurrentMonth: 0, + generatedCardsCurrentMonthByModel: UsagePerMonthPerModel(), )) Usage usage, }) = _AppUser; @@ -37,7 +38,19 @@ class Usage with _$Usage { const factory Usage({ @Default(0) int generatedCardsCurrentMonth, @Default(0) int generatedMnemonicsCurrentMonth, + @Default(UsagePerMonthPerModel()) + UsagePerMonthPerModel generatedCardsCurrentMonthByModel, }) = _Usage; factory Usage.fromJson(Map json) => _$UsageFromJson(json); } + +@freezed +class UsagePerMonthPerModel with _$UsagePerMonthPerModel { + const factory UsagePerMonthPerModel({ + @JsonKey(name: 'gpt-4') @Default(0) int gpt4, + }) = _UsagePerMonthPerModel; + + factory UsagePerMonthPerModel.fromJson(Map json) => + _$UsagePerMonthPerModelFromJson(json); +} diff --git a/lib/src/models/app_user.freezed.dart b/lib/src/models/app_user.freezed.dart index 331a095a..20a12b2c 100644 --- a/lib/src/models/app_user.freezed.dart +++ b/lib/src/models/app_user.freezed.dart @@ -121,7 +121,9 @@ class _$AppUserImpl extends _AppUser { const _$AppUserImpl( {this.hasPlus = false, this.usage = const Usage( - generatedCardsCurrentMonth: 0, generatedMnemonicsCurrentMonth: 0)}) + generatedCardsCurrentMonth: 0, + generatedMnemonicsCurrentMonth: 0, + generatedCardsCurrentMonthByModel: UsagePerMonthPerModel())}) : super._(); factory _$AppUserImpl.fromJson(Map json) => @@ -191,6 +193,8 @@ Usage _$UsageFromJson(Map json) { mixin _$Usage { int get generatedCardsCurrentMonth => throw _privateConstructorUsedError; int get generatedMnemonicsCurrentMonth => throw _privateConstructorUsedError; + UsagePerMonthPerModel get generatedCardsCurrentMonthByModel => + throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -203,7 +207,11 @@ abstract class $UsageCopyWith<$Res> { _$UsageCopyWithImpl<$Res, Usage>; @useResult $Res call( - {int generatedCardsCurrentMonth, int generatedMnemonicsCurrentMonth}); + {int generatedCardsCurrentMonth, + int generatedMnemonicsCurrentMonth, + UsagePerMonthPerModel generatedCardsCurrentMonthByModel}); + + $UsagePerMonthPerModelCopyWith<$Res> get generatedCardsCurrentMonthByModel; } /// @nodoc @@ -221,6 +229,7 @@ class _$UsageCopyWithImpl<$Res, $Val extends Usage> $Res call({ Object? generatedCardsCurrentMonth = null, Object? generatedMnemonicsCurrentMonth = null, + Object? generatedCardsCurrentMonthByModel = null, }) { return _then(_value.copyWith( generatedCardsCurrentMonth: null == generatedCardsCurrentMonth @@ -231,8 +240,23 @@ class _$UsageCopyWithImpl<$Res, $Val extends Usage> ? _value.generatedMnemonicsCurrentMonth : generatedMnemonicsCurrentMonth // ignore: cast_nullable_to_non_nullable as int, + generatedCardsCurrentMonthByModel: null == + generatedCardsCurrentMonthByModel + ? _value.generatedCardsCurrentMonthByModel + : generatedCardsCurrentMonthByModel // ignore: cast_nullable_to_non_nullable + as UsagePerMonthPerModel, ) as $Val); } + + @override + @pragma('vm:prefer-inline') + $UsagePerMonthPerModelCopyWith<$Res> get generatedCardsCurrentMonthByModel { + return $UsagePerMonthPerModelCopyWith<$Res>( + _value.generatedCardsCurrentMonthByModel, (value) { + return _then( + _value.copyWith(generatedCardsCurrentMonthByModel: value) as $Val); + }); + } } /// @nodoc @@ -243,7 +267,12 @@ abstract class _$$UsageImplCopyWith<$Res> implements $UsageCopyWith<$Res> { @override @useResult $Res call( - {int generatedCardsCurrentMonth, int generatedMnemonicsCurrentMonth}); + {int generatedCardsCurrentMonth, + int generatedMnemonicsCurrentMonth, + UsagePerMonthPerModel generatedCardsCurrentMonthByModel}); + + @override + $UsagePerMonthPerModelCopyWith<$Res> get generatedCardsCurrentMonthByModel; } /// @nodoc @@ -259,6 +288,7 @@ class __$$UsageImplCopyWithImpl<$Res> $Res call({ Object? generatedCardsCurrentMonth = null, Object? generatedMnemonicsCurrentMonth = null, + Object? generatedCardsCurrentMonthByModel = null, }) { return _then(_$UsageImpl( generatedCardsCurrentMonth: null == generatedCardsCurrentMonth @@ -269,6 +299,11 @@ class __$$UsageImplCopyWithImpl<$Res> ? _value.generatedMnemonicsCurrentMonth : generatedMnemonicsCurrentMonth // ignore: cast_nullable_to_non_nullable as int, + generatedCardsCurrentMonthByModel: null == + generatedCardsCurrentMonthByModel + ? _value.generatedCardsCurrentMonthByModel + : generatedCardsCurrentMonthByModel // ignore: cast_nullable_to_non_nullable + as UsagePerMonthPerModel, )); } } @@ -278,7 +313,8 @@ class __$$UsageImplCopyWithImpl<$Res> class _$UsageImpl implements _Usage { const _$UsageImpl( {this.generatedCardsCurrentMonth = 0, - this.generatedMnemonicsCurrentMonth = 0}); + this.generatedMnemonicsCurrentMonth = 0, + this.generatedCardsCurrentMonthByModel = const UsagePerMonthPerModel()}); factory _$UsageImpl.fromJson(Map json) => _$$UsageImplFromJson(json); @@ -289,10 +325,13 @@ class _$UsageImpl implements _Usage { @override @JsonKey() final int generatedMnemonicsCurrentMonth; + @override + @JsonKey() + final UsagePerMonthPerModel generatedCardsCurrentMonthByModel; @override String toString() { - return 'Usage(generatedCardsCurrentMonth: $generatedCardsCurrentMonth, generatedMnemonicsCurrentMonth: $generatedMnemonicsCurrentMonth)'; + return 'Usage(generatedCardsCurrentMonth: $generatedCardsCurrentMonth, generatedMnemonicsCurrentMonth: $generatedMnemonicsCurrentMonth, generatedCardsCurrentMonthByModel: $generatedCardsCurrentMonthByModel)'; } @override @@ -307,13 +346,17 @@ class _$UsageImpl implements _Usage { (identical(other.generatedMnemonicsCurrentMonth, generatedMnemonicsCurrentMonth) || other.generatedMnemonicsCurrentMonth == - generatedMnemonicsCurrentMonth)); + generatedMnemonicsCurrentMonth) && + (identical(other.generatedCardsCurrentMonthByModel, + generatedCardsCurrentMonthByModel) || + other.generatedCardsCurrentMonthByModel == + generatedCardsCurrentMonthByModel)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, generatedCardsCurrentMonth, generatedMnemonicsCurrentMonth); + int get hashCode => Object.hash(runtimeType, generatedCardsCurrentMonth, + generatedMnemonicsCurrentMonth, generatedCardsCurrentMonthByModel); @JsonKey(ignore: true) @override @@ -331,8 +374,10 @@ class _$UsageImpl implements _Usage { abstract class _Usage implements Usage { const factory _Usage( - {final int generatedCardsCurrentMonth, - final int generatedMnemonicsCurrentMonth}) = _$UsageImpl; + {final int generatedCardsCurrentMonth, + final int generatedMnemonicsCurrentMonth, + final UsagePerMonthPerModel generatedCardsCurrentMonthByModel}) = + _$UsageImpl; factory _Usage.fromJson(Map json) = _$UsageImpl.fromJson; @@ -341,7 +386,154 @@ abstract class _Usage implements Usage { @override int get generatedMnemonicsCurrentMonth; @override + UsagePerMonthPerModel get generatedCardsCurrentMonthByModel; + @override @JsonKey(ignore: true) _$$UsageImplCopyWith<_$UsageImpl> get copyWith => throw _privateConstructorUsedError; } + +UsagePerMonthPerModel _$UsagePerMonthPerModelFromJson( + Map json) { + return _UsagePerMonthPerModel.fromJson(json); +} + +/// @nodoc +mixin _$UsagePerMonthPerModel { + @JsonKey(name: 'gpt-4') + int get gpt4 => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UsagePerMonthPerModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UsagePerMonthPerModelCopyWith<$Res> { + factory $UsagePerMonthPerModelCopyWith(UsagePerMonthPerModel value, + $Res Function(UsagePerMonthPerModel) then) = + _$UsagePerMonthPerModelCopyWithImpl<$Res, UsagePerMonthPerModel>; + @useResult + $Res call({@JsonKey(name: 'gpt-4') int gpt4}); +} + +/// @nodoc +class _$UsagePerMonthPerModelCopyWithImpl<$Res, + $Val extends UsagePerMonthPerModel> + implements $UsagePerMonthPerModelCopyWith<$Res> { + _$UsagePerMonthPerModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? gpt4 = null, + }) { + return _then(_value.copyWith( + gpt4: null == gpt4 + ? _value.gpt4 + : gpt4 // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UsagePerMonthPerModelImplCopyWith<$Res> + implements $UsagePerMonthPerModelCopyWith<$Res> { + factory _$$UsagePerMonthPerModelImplCopyWith( + _$UsagePerMonthPerModelImpl value, + $Res Function(_$UsagePerMonthPerModelImpl) then) = + __$$UsagePerMonthPerModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({@JsonKey(name: 'gpt-4') int gpt4}); +} + +/// @nodoc +class __$$UsagePerMonthPerModelImplCopyWithImpl<$Res> + extends _$UsagePerMonthPerModelCopyWithImpl<$Res, + _$UsagePerMonthPerModelImpl> + implements _$$UsagePerMonthPerModelImplCopyWith<$Res> { + __$$UsagePerMonthPerModelImplCopyWithImpl(_$UsagePerMonthPerModelImpl _value, + $Res Function(_$UsagePerMonthPerModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? gpt4 = null, + }) { + return _then(_$UsagePerMonthPerModelImpl( + gpt4: null == gpt4 + ? _value.gpt4 + : gpt4 // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UsagePerMonthPerModelImpl implements _UsagePerMonthPerModel { + const _$UsagePerMonthPerModelImpl({@JsonKey(name: 'gpt-4') this.gpt4 = 0}); + + factory _$UsagePerMonthPerModelImpl.fromJson(Map json) => + _$$UsagePerMonthPerModelImplFromJson(json); + + @override + @JsonKey(name: 'gpt-4') + final int gpt4; + + @override + String toString() { + return 'UsagePerMonthPerModel(gpt4: $gpt4)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UsagePerMonthPerModelImpl && + (identical(other.gpt4, gpt4) || other.gpt4 == gpt4)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, gpt4); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UsagePerMonthPerModelImplCopyWith<_$UsagePerMonthPerModelImpl> + get copyWith => __$$UsagePerMonthPerModelImplCopyWithImpl< + _$UsagePerMonthPerModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UsagePerMonthPerModelImplToJson( + this, + ); + } +} + +abstract class _UsagePerMonthPerModel implements UsagePerMonthPerModel { + const factory _UsagePerMonthPerModel( + {@JsonKey(name: 'gpt-4') final int gpt4}) = _$UsagePerMonthPerModelImpl; + + factory _UsagePerMonthPerModel.fromJson(Map json) = + _$UsagePerMonthPerModelImpl.fromJson; + + @override + @JsonKey(name: 'gpt-4') + int get gpt4; + @override + @JsonKey(ignore: true) + _$$UsagePerMonthPerModelImplCopyWith<_$UsagePerMonthPerModelImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/src/models/app_user.g.dart b/lib/src/models/app_user.g.dart index ad0fc8e3..8ab4f1d3 100644 --- a/lib/src/models/app_user.g.dart +++ b/lib/src/models/app_user.g.dart @@ -11,7 +11,9 @@ _$AppUserImpl _$$AppUserImplFromJson(Map json) => hasPlus: json['hasPlus'] as bool? ?? false, usage: json['usage'] == null ? const Usage( - generatedCardsCurrentMonth: 0, generatedMnemonicsCurrentMonth: 0) + generatedCardsCurrentMonth: 0, + generatedMnemonicsCurrentMonth: 0, + generatedCardsCurrentMonthByModel: UsagePerMonthPerModel()) : Usage.fromJson(json['usage'] as Map), ); @@ -26,10 +28,30 @@ _$UsageImpl _$$UsageImplFromJson(Map json) => _$UsageImpl( json['generatedCardsCurrentMonth'] as int? ?? 0, generatedMnemonicsCurrentMonth: json['generatedMnemonicsCurrentMonth'] as int? ?? 0, + generatedCardsCurrentMonthByModel: + json['generatedCardsCurrentMonthByModel'] == null + ? const UsagePerMonthPerModel() + : UsagePerMonthPerModel.fromJson( + json['generatedCardsCurrentMonthByModel'] + as Map), ); Map _$$UsageImplToJson(_$UsageImpl instance) => { 'generatedCardsCurrentMonth': instance.generatedCardsCurrentMonth, 'generatedMnemonicsCurrentMonth': instance.generatedMnemonicsCurrentMonth, + 'generatedCardsCurrentMonthByModel': + instance.generatedCardsCurrentMonthByModel.toJson(), + }; + +_$UsagePerMonthPerModelImpl _$$UsagePerMonthPerModelImplFromJson( + Map json) => + _$UsagePerMonthPerModelImpl( + gpt4: json['gpt-4'] as int? ?? 0, + ); + +Map _$$UsagePerMonthPerModelImplToJson( + _$UsagePerMonthPerModelImpl instance) => + { + 'gpt-4': instance.gpt4, }; diff --git a/lib/src/models/card_generation_size.dart b/lib/src/models/card_generation_size.dart index a294b110..5ace2da2 100644 --- a/lib/src/models/card_generation_size.dart +++ b/lib/src/models/card_generation_size.dart @@ -30,7 +30,7 @@ enum CardGenrationSize { } String getUiText() { - return '${toInt()} cards'; + return '${toInt()}'; } String getDurationText() { diff --git a/lib/src/models/deck_preview.dart b/lib/src/models/deck_preview.dart index 8c30893b..0278a389 100644 --- a/lib/src/models/deck_preview.dart +++ b/lib/src/models/deck_preview.dart @@ -1,3 +1,4 @@ +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/models/session_id.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -10,6 +11,7 @@ class DeckPreview with _$DeckPreview { required DateTime createdAt, required String name, required SessionId sessionId, + required Model model, required int numberOfCards, }) = DeckPreviewCreated; const factory DeckPreview.loading({ @@ -17,6 +19,7 @@ class DeckPreview with _$DeckPreview { required String name, required int numberOfCards, required SessionId sessionId, + required Model model, }) = DeckPreviewLoading; const factory DeckPreview.error({ required String message, @@ -24,5 +27,6 @@ class DeckPreview with _$DeckPreview { required String name, required int numberOfCards, required SessionId sessionId, + required Model model, }) = DeckPreviewError; } diff --git a/lib/src/models/deck_preview.freezed.dart b/lib/src/models/deck_preview.freezed.dart index fd8ae9b7..5067158d 100644 --- a/lib/src/models/deck_preview.freezed.dart +++ b/lib/src/models/deck_preview.freezed.dart @@ -19,43 +19,44 @@ mixin _$DeckPreview { DateTime get createdAt => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; String get sessionId => throw _privateConstructorUsedError; + Model get model => throw _privateConstructorUsedError; int get numberOfCards => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ required TResult Function(List questions, DateTime createdAt, - String name, String sessionId, int numberOfCards) + String name, String sessionId, Model model, int numberOfCards) created, required TResult Function(DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) loading, required TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) error, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult? Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult? Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, required TResult orElse(), }) => @@ -95,7 +96,11 @@ abstract class $DeckPreviewCopyWith<$Res> { _$DeckPreviewCopyWithImpl<$Res, DeckPreview>; @useResult $Res call( - {DateTime createdAt, String name, String sessionId, int numberOfCards}); + {DateTime createdAt, + String name, + String sessionId, + Model model, + int numberOfCards}); } /// @nodoc @@ -114,6 +119,7 @@ class _$DeckPreviewCopyWithImpl<$Res, $Val extends DeckPreview> Object? createdAt = null, Object? name = null, Object? sessionId = null, + Object? model = null, Object? numberOfCards = null, }) { return _then(_value.copyWith( @@ -129,6 +135,10 @@ class _$DeckPreviewCopyWithImpl<$Res, $Val extends DeckPreview> ? _value.sessionId : sessionId // ignore: cast_nullable_to_non_nullable as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, numberOfCards: null == numberOfCards ? _value.numberOfCards : numberOfCards // ignore: cast_nullable_to_non_nullable @@ -150,6 +160,7 @@ abstract class _$$DeckPreviewCreatedImplCopyWith<$Res> DateTime createdAt, String name, String sessionId, + Model model, int numberOfCards}); } @@ -168,6 +179,7 @@ class __$$DeckPreviewCreatedImplCopyWithImpl<$Res> Object? createdAt = null, Object? name = null, Object? sessionId = null, + Object? model = null, Object? numberOfCards = null, }) { return _then(_$DeckPreviewCreatedImpl( @@ -187,6 +199,10 @@ class __$$DeckPreviewCreatedImplCopyWithImpl<$Res> ? _value.sessionId : sessionId // ignore: cast_nullable_to_non_nullable as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, numberOfCards: null == numberOfCards ? _value.numberOfCards : numberOfCards // ignore: cast_nullable_to_non_nullable @@ -203,6 +219,7 @@ class _$DeckPreviewCreatedImpl implements DeckPreviewCreated { required this.createdAt, required this.name, required this.sessionId, + required this.model, required this.numberOfCards}) : _questions = questions; @@ -221,11 +238,13 @@ class _$DeckPreviewCreatedImpl implements DeckPreviewCreated { @override final String sessionId; @override + final Model model; + @override final int numberOfCards; @override String toString() { - return 'DeckPreview.created(questions: $questions, createdAt: $createdAt, name: $name, sessionId: $sessionId, numberOfCards: $numberOfCards)'; + return 'DeckPreview.created(questions: $questions, createdAt: $createdAt, name: $name, sessionId: $sessionId, model: $model, numberOfCards: $numberOfCards)'; } @override @@ -240,6 +259,7 @@ class _$DeckPreviewCreatedImpl implements DeckPreviewCreated { (identical(other.name, name) || other.name == name) && (identical(other.sessionId, sessionId) || other.sessionId == sessionId) && + (identical(other.model, model) || other.model == model) && (identical(other.numberOfCards, numberOfCards) || other.numberOfCards == numberOfCards)); } @@ -251,6 +271,7 @@ class _$DeckPreviewCreatedImpl implements DeckPreviewCreated { createdAt, name, sessionId, + model, numberOfCards); @JsonKey(ignore: true) @@ -264,50 +285,52 @@ class _$DeckPreviewCreatedImpl implements DeckPreviewCreated { @optionalTypeArgs TResult when({ required TResult Function(List questions, DateTime createdAt, - String name, String sessionId, int numberOfCards) + String name, String sessionId, Model model, int numberOfCards) created, required TResult Function(DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) loading, required TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) error, }) { - return created(questions, createdAt, name, sessionId, numberOfCards); + return created(questions, createdAt, name, sessionId, model, numberOfCards); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult? Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult? Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, }) { - return created?.call(questions, createdAt, name, sessionId, numberOfCards); + return created?.call( + questions, createdAt, name, sessionId, model, numberOfCards); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, required TResult orElse(), }) { if (created != null) { - return created(questions, createdAt, name, sessionId, numberOfCards); + return created( + questions, createdAt, name, sessionId, model, numberOfCards); } return orElse(); } @@ -353,6 +376,7 @@ abstract class DeckPreviewCreated implements DeckPreview { required final DateTime createdAt, required final String name, required final String sessionId, + required final Model model, required final int numberOfCards}) = _$DeckPreviewCreatedImpl; List get questions; @@ -363,6 +387,8 @@ abstract class DeckPreviewCreated implements DeckPreview { @override String get sessionId; @override + Model get model; + @override int get numberOfCards; @override @JsonKey(ignore: true) @@ -379,7 +405,11 @@ abstract class _$$DeckPreviewLoadingImplCopyWith<$Res> @override @useResult $Res call( - {DateTime createdAt, String name, int numberOfCards, String sessionId}); + {DateTime createdAt, + String name, + int numberOfCards, + String sessionId, + Model model}); } /// @nodoc @@ -397,6 +427,7 @@ class __$$DeckPreviewLoadingImplCopyWithImpl<$Res> Object? name = null, Object? numberOfCards = null, Object? sessionId = null, + Object? model = null, }) { return _then(_$DeckPreviewLoadingImpl( createdAt: null == createdAt @@ -415,6 +446,10 @@ class __$$DeckPreviewLoadingImplCopyWithImpl<$Res> ? _value.sessionId : sessionId // ignore: cast_nullable_to_non_nullable as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, )); } } @@ -426,7 +461,8 @@ class _$DeckPreviewLoadingImpl implements DeckPreviewLoading { {required this.createdAt, required this.name, required this.numberOfCards, - required this.sessionId}); + required this.sessionId, + required this.model}); @override final DateTime createdAt; @@ -436,10 +472,12 @@ class _$DeckPreviewLoadingImpl implements DeckPreviewLoading { final int numberOfCards; @override final String sessionId; + @override + final Model model; @override String toString() { - return 'DeckPreview.loading(createdAt: $createdAt, name: $name, numberOfCards: $numberOfCards, sessionId: $sessionId)'; + return 'DeckPreview.loading(createdAt: $createdAt, name: $name, numberOfCards: $numberOfCards, sessionId: $sessionId, model: $model)'; } @override @@ -453,12 +491,13 @@ class _$DeckPreviewLoadingImpl implements DeckPreviewLoading { (identical(other.numberOfCards, numberOfCards) || other.numberOfCards == numberOfCards) && (identical(other.sessionId, sessionId) || - other.sessionId == sessionId)); + other.sessionId == sessionId) && + (identical(other.model, model) || other.model == model)); } @override - int get hashCode => - Object.hash(runtimeType, createdAt, name, numberOfCards, sessionId); + int get hashCode => Object.hash( + runtimeType, createdAt, name, numberOfCards, sessionId, model); @JsonKey(ignore: true) @override @@ -471,50 +510,50 @@ class _$DeckPreviewLoadingImpl implements DeckPreviewLoading { @optionalTypeArgs TResult when({ required TResult Function(List questions, DateTime createdAt, - String name, String sessionId, int numberOfCards) + String name, String sessionId, Model model, int numberOfCards) created, required TResult Function(DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) loading, required TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) error, }) { - return loading(createdAt, name, numberOfCards, sessionId); + return loading(createdAt, name, numberOfCards, sessionId, model); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult? Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult? Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, }) { - return loading?.call(createdAt, name, numberOfCards, sessionId); + return loading?.call(createdAt, name, numberOfCards, sessionId, model); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, required TResult orElse(), }) { if (loading != null) { - return loading(createdAt, name, numberOfCards, sessionId); + return loading(createdAt, name, numberOfCards, sessionId, model); } return orElse(); } @@ -559,7 +598,8 @@ abstract class DeckPreviewLoading implements DeckPreview { {required final DateTime createdAt, required final String name, required final int numberOfCards, - required final String sessionId}) = _$DeckPreviewLoadingImpl; + required final String sessionId, + required final Model model}) = _$DeckPreviewLoadingImpl; @override DateTime get createdAt; @@ -570,6 +610,8 @@ abstract class DeckPreviewLoading implements DeckPreview { @override String get sessionId; @override + Model get model; + @override @JsonKey(ignore: true) _$$DeckPreviewLoadingImplCopyWith<_$DeckPreviewLoadingImpl> get copyWith => throw _privateConstructorUsedError; @@ -588,7 +630,8 @@ abstract class _$$DeckPreviewErrorImplCopyWith<$Res> DateTime createdAt, String name, int numberOfCards, - String sessionId}); + String sessionId, + Model model}); } /// @nodoc @@ -607,6 +650,7 @@ class __$$DeckPreviewErrorImplCopyWithImpl<$Res> Object? name = null, Object? numberOfCards = null, Object? sessionId = null, + Object? model = null, }) { return _then(_$DeckPreviewErrorImpl( message: null == message @@ -629,6 +673,10 @@ class __$$DeckPreviewErrorImplCopyWithImpl<$Res> ? _value.sessionId : sessionId // ignore: cast_nullable_to_non_nullable as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, )); } } @@ -641,7 +689,8 @@ class _$DeckPreviewErrorImpl implements DeckPreviewError { required this.createdAt, required this.name, required this.numberOfCards, - required this.sessionId}); + required this.sessionId, + required this.model}); @override final String message; @@ -653,10 +702,12 @@ class _$DeckPreviewErrorImpl implements DeckPreviewError { final int numberOfCards; @override final String sessionId; + @override + final Model model; @override String toString() { - return 'DeckPreview.error(message: $message, createdAt: $createdAt, name: $name, numberOfCards: $numberOfCards, sessionId: $sessionId)'; + return 'DeckPreview.error(message: $message, createdAt: $createdAt, name: $name, numberOfCards: $numberOfCards, sessionId: $sessionId, model: $model)'; } @override @@ -671,12 +722,13 @@ class _$DeckPreviewErrorImpl implements DeckPreviewError { (identical(other.numberOfCards, numberOfCards) || other.numberOfCards == numberOfCards) && (identical(other.sessionId, sessionId) || - other.sessionId == sessionId)); + other.sessionId == sessionId) && + (identical(other.model, model) || other.model == model)); } @override int get hashCode => Object.hash( - runtimeType, message, createdAt, name, numberOfCards, sessionId); + runtimeType, message, createdAt, name, numberOfCards, sessionId, model); @JsonKey(ignore: true) @override @@ -689,50 +741,51 @@ class _$DeckPreviewErrorImpl implements DeckPreviewError { @optionalTypeArgs TResult when({ required TResult Function(List questions, DateTime createdAt, - String name, String sessionId, int numberOfCards) + String name, String sessionId, Model model, int numberOfCards) created, required TResult Function(DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) loading, required TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId) + int numberOfCards, String sessionId, Model model) error, }) { - return error(message, createdAt, name, numberOfCards, sessionId); + return error(message, createdAt, name, numberOfCards, sessionId, model); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult? Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult? Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, }) { - return error?.call(message, createdAt, name, numberOfCards, sessionId); + return error?.call( + message, createdAt, name, numberOfCards, sessionId, model); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function(List questions, DateTime createdAt, String name, - String sessionId, int numberOfCards)? + String sessionId, Model model, int numberOfCards)? created, TResult Function(DateTime createdAt, String name, int numberOfCards, - String sessionId)? + String sessionId, Model model)? loading, TResult Function(String message, DateTime createdAt, String name, - int numberOfCards, String sessionId)? + int numberOfCards, String sessionId, Model model)? error, required TResult orElse(), }) { if (error != null) { - return error(message, createdAt, name, numberOfCards, sessionId); + return error(message, createdAt, name, numberOfCards, sessionId, model); } return orElse(); } @@ -778,7 +831,8 @@ abstract class DeckPreviewError implements DeckPreview { required final DateTime createdAt, required final String name, required final int numberOfCards, - required final String sessionId}) = _$DeckPreviewErrorImpl; + required final String sessionId, + required final Model model}) = _$DeckPreviewErrorImpl; String get message; @override @@ -790,6 +844,8 @@ abstract class DeckPreviewError implements DeckPreview { @override String get sessionId; @override + Model get model; + @override @JsonKey(ignore: true) _$$DeckPreviewErrorImplCopyWith<_$DeckPreviewErrorImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/src/models/model.dart b/lib/src/models/model.dart new file mode 100644 index 00000000..73511e9d --- /dev/null +++ b/lib/src/models/model.dart @@ -0,0 +1,33 @@ +/// The model that is used for generating the cards. +enum Model { + /// The default model. + gpt3_5, + + /// The model that is only available for plus users. + gpt4; + + /// Returns the name of the model. + /// + /// Uses the model naming convention of our backend. + String get snakeCaseName { + return switch (this) { + Model.gpt3_5 => 'gpt-3.5', + Model.gpt4 => 'gpt-4', + }; + } + + /// Returns if the model is only available for plus users. + bool isPlus() { + return switch (this) { + Model.gpt3_5 => false, + Model.gpt4 => true, + }; + } + + String getUiText() { + return switch (this) { + Model.gpt3_5 => 'GPT-3.5', + Model.gpt4 => 'GPT-4', + }; + } +} diff --git a/lib/src/models/session_dto.dart b/lib/src/models/session_dto.dart index 882ca292..1b6faee1 100644 --- a/lib/src/models/session_dto.dart +++ b/lib/src/models/session_dto.dart @@ -5,6 +5,7 @@ import 'package:ankigpt/src/models/anki_card.dart'; import 'package:ankigpt/src/models/csv_metadata.dart'; import 'package:ankigpt/src/models/input_type.dart'; import 'package:ankigpt/src/models/language.dart'; +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/models/session_id.dart'; import 'package:ankigpt/src/models/user_id.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; @@ -27,6 +28,7 @@ class SessionDto with _$SessionDto { required int numberOfCards, @JsonKey(fromJson: parseVisibility) required Visibility visibility, required UserId userId, + @JsonKey(fromJson: parseModel) required Model model, }) = _SessionDto; factory SessionDto.fromJson(Map json) => @@ -68,7 +70,31 @@ Visibility parseVisibility(Map? json) { return defaultValue; } - return Visibility.values.tryByName(json['value']); + return Visibility.values.tryByName( + value, + defaultValue: defaultValue, + ); +} + +Model parseModel(Map? json) { + const defaultValue = Model.gpt3_5; + + if (json == null) { + return defaultValue; + } + + final value = json['name']; + if (value is! String?) { + return defaultValue; + } + + for (final model in Model.values) { + if (model.snakeCaseName == value) { + return model; + } + } + + return defaultValue; } extension EnumByNameWithDefault on Iterable { diff --git a/lib/src/models/session_dto.freezed.dart b/lib/src/models/session_dto.freezed.dart index 2d116c74..2deb5113 100644 --- a/lib/src/models/session_dto.freezed.dart +++ b/lib/src/models/session_dto.freezed.dart @@ -35,6 +35,8 @@ mixin _$SessionDto { @JsonKey(fromJson: parseVisibility) Visibility get visibility => throw _privateConstructorUsedError; String get userId => throw _privateConstructorUsedError; + @JsonKey(fromJson: parseModel) + Model get model => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -59,7 +61,8 @@ abstract class $SessionDtoCopyWith<$Res> { @JsonKey(fromJson: parseError) String? error, int numberOfCards, @JsonKey(fromJson: parseVisibility) Visibility visibility, - String userId}); + String userId, + @JsonKey(fromJson: parseModel) Model model}); $InputCopyWith<$Res> get input; $CsvMetadataCopyWith<$Res>? get csv; @@ -89,6 +92,7 @@ class _$SessionDtoCopyWithImpl<$Res, $Val extends SessionDto> Object? numberOfCards = null, Object? visibility = null, Object? userId = null, + Object? model = null, }) { return _then(_value.copyWith( id: null == id @@ -135,6 +139,10 @@ class _$SessionDtoCopyWithImpl<$Res, $Val extends SessionDto> ? _value.userId : userId // ignore: cast_nullable_to_non_nullable as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, ) as $Val); } @@ -178,7 +186,8 @@ abstract class _$$SessionDtoImplCopyWith<$Res> @JsonKey(fromJson: parseError) String? error, int numberOfCards, @JsonKey(fromJson: parseVisibility) Visibility visibility, - String userId}); + String userId, + @JsonKey(fromJson: parseModel) Model model}); @override $InputCopyWith<$Res> get input; @@ -208,6 +217,7 @@ class __$$SessionDtoImplCopyWithImpl<$Res> Object? numberOfCards = null, Object? visibility = null, Object? userId = null, + Object? model = null, }) { return _then(_$SessionDtoImpl( id: null == id @@ -254,6 +264,10 @@ class __$$SessionDtoImplCopyWithImpl<$Res> ? _value.userId : userId // ignore: cast_nullable_to_non_nullable as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, )); } } @@ -273,7 +287,8 @@ class _$SessionDtoImpl implements _SessionDto { @JsonKey(fromJson: parseError) this.error, required this.numberOfCards, @JsonKey(fromJson: parseVisibility) required this.visibility, - required this.userId}) + required this.userId, + @JsonKey(fromJson: parseModel) required this.model}) : _cards = cards; factory _$SessionDtoImpl.fromJson(Map json) => @@ -313,10 +328,13 @@ class _$SessionDtoImpl implements _SessionDto { final Visibility visibility; @override final String userId; + @override + @JsonKey(fromJson: parseModel) + final Model model; @override String toString() { - return 'SessionDto(id: $id, language: $language, input: $input, createdAt: $createdAt, csv: $csv, cards: $cards, status: $status, error: $error, numberOfCards: $numberOfCards, visibility: $visibility, userId: $userId)'; + return 'SessionDto(id: $id, language: $language, input: $input, createdAt: $createdAt, csv: $csv, cards: $cards, status: $status, error: $error, numberOfCards: $numberOfCards, visibility: $visibility, userId: $userId, model: $model)'; } @override @@ -338,7 +356,8 @@ class _$SessionDtoImpl implements _SessionDto { other.numberOfCards == numberOfCards) && (identical(other.visibility, visibility) || other.visibility == visibility) && - (identical(other.userId, userId) || other.userId == userId)); + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.model, model) || other.model == model)); } @JsonKey(ignore: true) @@ -355,7 +374,8 @@ class _$SessionDtoImpl implements _SessionDto { error, numberOfCards, visibility, - userId); + userId, + model); @JsonKey(ignore: true) @override @@ -384,7 +404,9 @@ abstract class _SessionDto implements SessionDto { @JsonKey(fromJson: parseError) final String? error, required final int numberOfCards, @JsonKey(fromJson: parseVisibility) required final Visibility visibility, - required final String userId}) = _$SessionDtoImpl; + required final String userId, + @JsonKey(fromJson: parseModel) + required final Model model}) = _$SessionDtoImpl; factory _SessionDto.fromJson(Map json) = _$SessionDtoImpl.fromJson; @@ -416,6 +438,9 @@ abstract class _SessionDto implements SessionDto { @override String get userId; @override + @JsonKey(fromJson: parseModel) + Model get model; + @override @JsonKey(ignore: true) _$$SessionDtoImplCopyWith<_$SessionDtoImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/src/models/session_dto.g.dart b/lib/src/models/session_dto.g.dart index bd4d7428..85c3edb4 100644 --- a/lib/src/models/session_dto.g.dart +++ b/lib/src/models/session_dto.g.dart @@ -21,6 +21,7 @@ _$SessionDtoImpl _$$SessionDtoImplFromJson(Map json) => numberOfCards: json['numberOfCards'] as int, visibility: parseVisibility(json['visibility'] as Map?), userId: json['userId'] as String, + model: parseModel(json['model'] as Map?), ); Map _$$SessionDtoImplToJson(_$SessionDtoImpl instance) => @@ -36,6 +37,7 @@ Map _$$SessionDtoImplToJson(_$SessionDtoImpl instance) => 'numberOfCards': instance.numberOfCards, 'visibility': _$VisibilityEnumMap[instance.visibility]!, 'userId': instance.userId, + 'model': _$ModelEnumMap[instance.model]!, }; const _$LanguageEnumMap = { @@ -190,6 +192,11 @@ const _$VisibilityEnumMap = { Visibility.anyoneWithLink: 'anyoneWithLink', }; +const _$ModelEnumMap = { + Model.gpt3_5: 'gpt3_5', + Model.gpt4: 'gpt4', +}; + _$InputImpl _$$InputImplFromJson(Map json) => _$InputImpl( text: json['text'] as String?, type: $enumDecode(_$InputTypeEnumMap, json['type']), diff --git a/lib/src/models/watch_view.dart b/lib/src/models/watch_view.dart index de7c13a2..10bf572f 100644 --- a/lib/src/models/watch_view.dart +++ b/lib/src/models/watch_view.dart @@ -1,4 +1,5 @@ import 'package:ankigpt/src/models/language.dart'; +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/models/session_id.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -18,6 +19,7 @@ class WatchView with _$WatchView { String? inputText, // Defines if the user is the owner of the session. bool? isOwner, + Model? model, }) = _WatchView; bool get hasFile => fileName != null; diff --git a/lib/src/models/watch_view.freezed.dart b/lib/src/models/watch_view.freezed.dart index 87cf4533..f1c8896d 100644 --- a/lib/src/models/watch_view.freezed.dart +++ b/lib/src/models/watch_view.freezed.dart @@ -25,6 +25,7 @@ mixin _$WatchView { String? get inputText => throw _privateConstructorUsedError; // Defines if the user is the owner of the session. bool? get isOwner => throw _privateConstructorUsedError; + Model? get model => throw _privateConstructorUsedError; @JsonKey(ignore: true) $WatchViewCopyWith get copyWith => @@ -44,7 +45,8 @@ abstract class $WatchViewCopyWith<$Res> { String? fileName, String? sessionId, String? inputText, - bool? isOwner}); + bool? isOwner, + Model? model}); } /// @nodoc @@ -68,6 +70,7 @@ class _$WatchViewCopyWithImpl<$Res, $Val extends WatchView> Object? sessionId = freezed, Object? inputText = freezed, Object? isOwner = freezed, + Object? model = freezed, }) { return _then(_value.copyWith( isLoading: null == isLoading @@ -102,6 +105,10 @@ class _$WatchViewCopyWithImpl<$Res, $Val extends WatchView> ? _value.isOwner : isOwner // ignore: cast_nullable_to_non_nullable as bool?, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model?, ) as $Val); } } @@ -122,7 +129,8 @@ abstract class _$$WatchViewImplCopyWith<$Res> String? fileName, String? sessionId, String? inputText, - bool? isOwner}); + bool? isOwner, + Model? model}); } /// @nodoc @@ -144,6 +152,7 @@ class __$$WatchViewImplCopyWithImpl<$Res> Object? sessionId = freezed, Object? inputText = freezed, Object? isOwner = freezed, + Object? model = freezed, }) { return _then(_$WatchViewImpl( isLoading: null == isLoading @@ -178,6 +187,10 @@ class __$$WatchViewImplCopyWithImpl<$Res> ? _value.isOwner : isOwner // ignore: cast_nullable_to_non_nullable as bool?, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model?, )); } } @@ -193,7 +206,8 @@ class _$WatchViewImpl extends _WatchView { this.fileName, this.sessionId, this.inputText, - this.isOwner}) + this.isOwner, + this.model}) : super._(); @override @@ -214,10 +228,12 @@ class _$WatchViewImpl extends _WatchView { // Defines if the user is the owner of the session. @override final bool? isOwner; + @override + final Model? model; @override String toString() { - return 'WatchView(isLoading: $isLoading, downloadUrl: $downloadUrl, language: $language, error: $error, fileName: $fileName, sessionId: $sessionId, inputText: $inputText, isOwner: $isOwner)'; + return 'WatchView(isLoading: $isLoading, downloadUrl: $downloadUrl, language: $language, error: $error, fileName: $fileName, sessionId: $sessionId, inputText: $inputText, isOwner: $isOwner, model: $model)'; } @override @@ -238,12 +254,13 @@ class _$WatchViewImpl extends _WatchView { other.sessionId == sessionId) && (identical(other.inputText, inputText) || other.inputText == inputText) && - (identical(other.isOwner, isOwner) || other.isOwner == isOwner)); + (identical(other.isOwner, isOwner) || other.isOwner == isOwner) && + (identical(other.model, model) || other.model == model)); } @override int get hashCode => Object.hash(runtimeType, isLoading, downloadUrl, language, - error, fileName, sessionId, inputText, isOwner); + error, fileName, sessionId, inputText, isOwner, model); @JsonKey(ignore: true) @override @@ -261,7 +278,8 @@ abstract class _WatchView extends WatchView { final String? fileName, final String? sessionId, final String? inputText, - final bool? isOwner}) = _$WatchViewImpl; + final bool? isOwner, + final Model? model}) = _$WatchViewImpl; const _WatchView._() : super._(); @override @@ -281,6 +299,8 @@ abstract class _WatchView extends WatchView { @override // Defines if the user is the owner of the session. bool? get isOwner; @override + Model? get model; + @override @JsonKey(ignore: true) _$$WatchViewImplCopyWith<_$WatchViewImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/src/pages/account_page.dart b/lib/src/pages/account_page.dart index e059f93a..52f68c77 100644 --- a/lib/src/pages/account_page.dart +++ b/lib/src/pages/account_page.dart @@ -520,7 +520,7 @@ class _AvatarCard extends ConsumerWidget { ), if (!view.hasPlus) ...[ const SizedBox(height: 32), - _Usage( + _FreeUsage( generatedCardsCurrentMonth: view.generatedCardsCurrentMonth, generatedMnemonicsCurrentMonth: @@ -568,8 +568,8 @@ class _AvatarCard extends ConsumerWidget { } } -class _Usage extends StatelessWidget { - const _Usage({ +class _FreeUsage extends StatelessWidget { + const _FreeUsage({ required this.generatedCardsCurrentMonth, required this.generatedMnemonicsCurrentMonth, }); @@ -597,7 +597,7 @@ class _Usage extends StatelessWidget { ), const SizedBox(height: 12), Text( - 'Limit: $freeUsageLimitPerMonth cards per month, ${(percentage * 100).toStringAsFixed(0)}% used'), + 'Limit: $freeUsageLimitPerMonth cards with per month, ${(percentage * 100).toStringAsFixed(0)}% used'), const SizedBox(height: 6), LinearProgressIndicator( value: percentage, diff --git a/lib/src/pages/deck_page/result_section.dart b/lib/src/pages/deck_page/result_section.dart index 4b3ee173..b26aa153 100644 --- a/lib/src/pages/deck_page/result_section.dart +++ b/lib/src/pages/deck_page/result_section.dart @@ -3,6 +3,7 @@ import 'package:ankigpt/src/models/anki_card.dart'; import 'package:ankigpt/src/models/card_feedback.dart'; import 'package:ankigpt/src/models/card_id.dart'; import 'package:ankigpt/src/models/language.dart'; +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/pages/deck_page/error_card.dart'; import 'package:ankigpt/src/pages/deck_page/search_bar.dart'; import 'package:ankigpt/src/pages/deck_page/warning_card.dart'; @@ -58,7 +59,10 @@ class ResultSection extends ConsumerWidget { ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _Subtitle(language: view.language), + _Subtitle( + language: view.language, + model: view.model, + ), if (view.hasError) ErrorCard( text: view.error, @@ -821,9 +825,11 @@ class _CardIconButton extends StatelessWidget { class _Subtitle extends ConsumerWidget { const _Subtitle({ required this.language, + required this.model, }); final Language? language; + final Model? model; @override Widget build(BuildContext context, WidgetRef ref) { @@ -841,7 +847,7 @@ class _Subtitle extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(bottom: 12, top: 12), child: Text( - 'Detected language: ${language == null ? '...' : language!.getDisplayName()}, $cardsCount cards', + 'Detected language: ${language == null ? '...' : language!.getDisplayName()}, $cardsCount cards (${model?.getUiText()})', style: TextStyle(color: Colors.grey[500]), ), ); diff --git a/lib/src/pages/home_page/controls.dart b/lib/src/pages/home_page/controls.dart index 3e802f9c..b769b83d 100644 --- a/lib/src/pages/home_page/controls.dart +++ b/lib/src/pages/home_page/controls.dart @@ -1,20 +1,17 @@ import 'dart:math'; import 'package:animations/animations.dart'; -import 'package:ankigpt/src/models/card_generation_size.dart'; import 'package:ankigpt/src/models/generate_state.dart'; -import 'package:ankigpt/src/pages/home_page/plus_dialog.dart'; import 'package:ankigpt/src/pages/deck_page/error_card.dart'; +import 'package:ankigpt/src/pages/home_page/options_dialog.dart'; +import 'package:ankigpt/src/pages/home_page/plus_dialog.dart'; import 'package:ankigpt/src/pages/widgets/ankigpt_card.dart'; -import 'package:ankigpt/src/pages/widgets/cancel_text_button.dart'; import 'package:ankigpt/src/pages/widgets/elevated_button.dart'; import 'package:ankigpt/src/pages/widgets/extensions.dart'; import 'package:ankigpt/src/pages/widgets/max_width_constrained_box.dart'; -import 'package:ankigpt/src/pages/widgets/plus_badge.dart'; import 'package:ankigpt/src/pages/widgets/video_player.dart'; -import 'package:ankigpt/src/providers/card_generation_size_provider.dart'; import 'package:ankigpt/src/providers/generate_provider.dart'; -import 'package:ankigpt/src/providers/has_plus_provider.dart'; +import 'package:ankigpt/src/providers/options_provider.dart'; import 'package:ankigpt/src/providers/search_provider.dart'; import 'package:ankigpt/src/providers/session_id_provider.dart'; import 'package:ankigpt/src/providers/total_cards_counter_provider.dart'; @@ -117,10 +114,10 @@ class _OptionsButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // Keep the provider alive - ref.watch(generationSizeProvider); + ref.watch(optionsControllerProvider); return AnkiGptElevatedButton.icon( - tooltip: 'Edit options (e.g. number of cards)', + tooltip: 'Edit options (e.g. number of cards, model)', icon: const Icon(Icons.tune), label: const Text('Options'), border: Border.all( @@ -132,7 +129,7 @@ class _OptionsButton extends ConsumerWidget { onPressed: () { showModal( context: context, - builder: (context) => const _OptionsDialog(), + builder: (context) => const OptionsDialog(), routeSettings: const RouteSettings(name: '/options'), ); }, @@ -178,6 +175,17 @@ class _GenerateButton extends ConsumerWidget { return; } + if (e is Gpt4LimitExceededException) { + showPlusDialog( + context, + top: _Gpt4LimitExceededCard( + currentDeckSize: e.currentDeckSize, + remainingCardsForCurrentMonth: e.remainingGpt4Limit, + ), + ); + return; + } + context.showTextSnackBar('$e'); } } @@ -187,14 +195,14 @@ class _GenerateButton extends ConsumerWidget { final isGenerating = ref.watch(generateNotifierProvider) is GenerationStateLoading; - final size = ref.watch(generationSizeProvider); + final options = ref.watch(optionsControllerProvider); return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: AnkiGptElevatedButton.icon( key: ValueKey(isGenerating), tooltip: isGenerating ? 'Generating...' - : 'Generate ${size.getUiText()} flashcards', + : 'Generate ${options.size.getUiText()} flashcards (${options.model.getUiText()})', icon: isGenerating ? null : const Icon(Icons.play_arrow), label: isGenerating ? const _GenerateButtonLoadingIndicator() @@ -234,6 +242,38 @@ class _FreeLimitExceededCard extends StatelessWidget { final int currentDeckSize; final int remainingCardsForCurrentMonth; + @override + Widget build(BuildContext context) { + return _LimitExceededCard(text: '''**Limit reached!** + +As a free user, you can create a maximum of $freeUsageLimitPerMonth cards per month. You have $remainingCardsForCurrentMonth remaining, but you attempted to generate $currentDeckSize cards. To produce more cards, consider upgrading to Plus.'''); + } +} + +class _Gpt4LimitExceededCard extends StatelessWidget { + const _Gpt4LimitExceededCard({ + required this.currentDeckSize, + required this.remainingCardsForCurrentMonth, + }); + + final int currentDeckSize; + final int remainingCardsForCurrentMonth; + + @override + Widget build(BuildContext context) { + return _LimitExceededCard(text: '''**Limit reached!** + +You can create a maximum of $plusGpt4UsageLimitPerMonth cards with GPT-4 per month. You have $remainingCardsForCurrentMonth remaining, but you attempted to generate $currentDeckSize cards.'''); + } +} + +class _LimitExceededCard extends StatelessWidget { + const _LimitExceededCard({ + required this.text, + }); + + final String text; + @override Widget build(BuildContext context) { return Padding( @@ -247,9 +287,7 @@ class _FreeLimitExceededCard extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), child: MarkdownBody( - data: '''**Limit reached!** - -As a free user, you can create a maximum of $freeUsageLimitPerMonth cards per month. You have $remainingCardsForCurrentMonth remaining, but you attempted to generate $currentDeckSize cards. To produce more cards, consider upgrading to Plus.''', + data: text, styleSheet: MarkdownStyleSheet( p: const TextStyle( color: Colors.deepOrange, @@ -388,106 +426,6 @@ class _WarningAfterDownload extends ConsumerWidget { } } -class _OptionsDialog extends StatelessWidget { - const _OptionsDialog(); - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Options'), - content: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 300, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Number of cards', - style: TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 2), - Text( - 'Specify the number of cards to generate.', - style: TextStyle( - color: Colors.grey[600]!, - fontSize: 12, - ), - ), - const SizedBox(height: 8), - const NumberOfCardsDropdown(), - ], - ), - ), - actions: [ - const CancelTextButton(), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('OK'), - ), - ], - ); - } -} - -class NumberOfCardsDropdown extends ConsumerWidget { - const NumberOfCardsDropdown({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasPlus = ref.watch(hasPlusProvider); - final hasPickedFile = ref.watch(pickedFileProvider) != null; - - final availableSizes = CardGenrationSize.values - .where((c) => hasPickedFile ? c.isAvailableForFiles() : true) - .toList(); - - return SizedBox( - width: double.infinity, - child: DropdownButtonFormField( - value: ref.watch(generationSizeProvider), - items: [ - ...availableSizes.map( - (c) => DropdownMenuItem( - value: c, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(c.getUiText()), - if (!hasPlus && c.isPlus()) ...[ - const SizedBox(width: 12), - const SizedBox( - width: 38, - child: PlusBadge( - withText: false, - ), - ) - ] - ], - ), - ), - ) - ], - onChanged: (v) { - if (v != null) { - if (!hasPlus && v.isPlus()) { - showPlusDialog(context); - } - - ref.read(generationSizeProvider.notifier).set(v); - } - }, - ), - ); - } -} - class _LoadingButton extends ConsumerWidget { const _LoadingButton(); diff --git a/lib/src/pages/home_page/faq_section.dart b/lib/src/pages/home_page/faq_section.dart index 18298ef2..307a8cca 100644 --- a/lib/src/pages/home_page/faq_section.dart +++ b/lib/src/pages/home_page/faq_section.dart @@ -2,6 +2,7 @@ import 'package:ankigpt/src/pages/widgets/animated_swap.dart'; import 'package:ankigpt/src/pages/widgets/max_width_constrained_box.dart'; import 'package:ankigpt/src/pages/widgets/section_title.dart'; import 'package:ankigpt/src/pages/widgets/theme.dart'; +import 'package:ankigpt/src/providers/generate_provider.dart'; import 'package:ankigpt/src/providers/home_page_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -75,8 +76,9 @@ class _WhichModelIsUsed extends StatelessWidget { Widget build(BuildContext context) { return const _FaqCard( question: Text('Which model is used for AnkiGPT?'), - answer: Text( - "AnkiGPT uses as underlaying AI model GPT-3.5 by OpenAI.", + answer: _MarkdownAnswer( + text: + "AnkiGPT primarily utilizes the GPT-3.5 by OpenAI model to generate flashcards, offering a seamless integration of advanced AI technology for effective learning. For users who opt for the AnkiGPT Plus version, they gain the enhanced capability to generate up to $plusGpt4UsageLimitPerMonth flashcards per month using the more advanced [GPT-4 model](https://openai.com/gpt-4), ensuring even more sophisticated and nuanced content creation.", ), ); } diff --git a/lib/src/pages/home_page/my_decks_section.dart b/lib/src/pages/home_page/my_decks_section.dart index 7dfc5883..0afafcec 100644 --- a/lib/src/pages/home_page/my_decks_section.dart +++ b/lib/src/pages/home_page/my_decks_section.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/models/session_id.dart'; import 'package:ankigpt/src/pages/widgets/ankigpt_card.dart'; import 'package:ankigpt/src/pages/widgets/max_width_constrained_box.dart'; @@ -52,31 +53,34 @@ class MyDecksSection extends ConsumerWidget { for (final session in sessions) session.maybeWhen( created: (questions, createdAt, name, sessionId, - numberOfCards) => + model, numberOfCards) => _CreatedHistoryDeck( questions: questions, createdAt: session.createdAt, name: name, sessionId: sessionId, + model: model, numberOfCards: numberOfCards, ), loading: (createdAt, name, numberOfCards, - sessionId) => + sessionId, model) => _LoadingHistoryDeck( createdAt: session.createdAt, name: name, numberOfCards: numberOfCards, isTesting: isTesting, sessionId: sessionId, + model: model, ), error: (error, createdAt, name, numberOfCards, - sessionId) => + sessionId, model) => _ErrorHistoryDeck( createdAt: session.createdAt, error: error, name: name, numberOfCards: numberOfCards, sessionId: sessionId, + model: model, ), orElse: () => const SizedBox(), ) @@ -118,6 +122,7 @@ class _CreatedHistoryDeck extends ConsumerWidget { required this.questions, required this.numberOfCards, required this.sessionId, + required this.model, }); final String name; @@ -125,12 +130,14 @@ class _CreatedHistoryDeck extends ConsumerWidget { final List questions; final int numberOfCards; final SessionId sessionId; + final Model model; @override Widget build(BuildContext context, WidgetRef ref) { final hasMoreThanFiveQuestions = questions.length > 5; return _HistoryDeckBase( numberOfCards: numberOfCards, + model: model, name: name, createdAt: createdAt, sessionId: sessionId, @@ -169,6 +176,7 @@ class _LoadingHistoryDeck extends StatelessWidget { required this.numberOfCards, required this.isTesting, required this.sessionId, + required this.model, }); final String name; @@ -176,10 +184,12 @@ class _LoadingHistoryDeck extends StatelessWidget { final int numberOfCards; final bool isTesting; final SessionId sessionId; + final Model model; @override Widget build(BuildContext context) { return _HistoryDeckBase( + model: model, numberOfCards: numberOfCards, name: name, createdAt: createdAt, @@ -208,6 +218,7 @@ class _ErrorHistoryDeck extends StatelessWidget { required this.error, required this.numberOfCards, required this.sessionId, + required this.model, }); final String name; @@ -215,11 +226,13 @@ class _ErrorHistoryDeck extends StatelessWidget { final String? error; final int numberOfCards; final SessionId sessionId; + final Model model; @override Widget build(BuildContext context) { return _HistoryDeckBase( numberOfCards: numberOfCards, + model: model, name: name, sessionId: sessionId, createdAt: createdAt, @@ -245,6 +258,7 @@ class _HistoryDeckBase extends StatelessWidget { required this.body, required this.numberOfCards, required this.sessionId, + required this.model, this.color = Colors.blue, }); @@ -254,6 +268,7 @@ class _HistoryDeckBase extends StatelessWidget { final Widget body; final int numberOfCards; final SessionId sessionId; + final Model model; @override Widget build(BuildContext context) { @@ -277,7 +292,7 @@ class _HistoryDeckBase extends StatelessWidget { ), ), Text( - '${DateFormat.yMEd().add_jms().format(createdAt!)}, $numberOfCards cards', + '${DateFormat.yMEd().add_jms().format(createdAt!)}, $numberOfCards cards (${model.getUiText()})', style: Theme.of(context).textTheme.bodySmall!.copyWith( color: Theme.of(context) .colorScheme diff --git a/lib/src/pages/home_page/new_card.dart b/lib/src/pages/home_page/new_card.dart index 21c63e9d..3ba6ea4b 100644 --- a/lib/src/pages/home_page/new_card.dart +++ b/lib/src/pages/home_page/new_card.dart @@ -1,4 +1,3 @@ -import 'package:ankigpt/src/pages/home_page/pricing_section.dart'; import 'package:ankigpt/src/pages/widgets/ankigpt_card.dart'; import 'package:ankigpt/src/providers/shared_preferences_provider.dart'; import 'package:flutter/material.dart'; @@ -13,7 +12,7 @@ class NewCard extends ConsumerStatefulWidget { } class _NewCardState extends ConsumerState { - static const _key = 'shown-mnemonic-card'; + static const _key = 'shown-gpt4-card'; bool? shouldShow; @override @@ -43,7 +42,6 @@ class _NewCardState extends ConsumerState { ? Padding( padding: const EdgeInsets.only(top: 24, left: 12, right: 12), child: AnkiGptCard( - onPressed: () => showMnemonicHelpDialog(context), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), color: Colors.orange.withOpacity(0.2), @@ -52,7 +50,7 @@ class _NewCardState extends ConsumerState { children: [ const Flexible( child: Text( - 'New: Generate mnemonics for your flashcards!', + 'New: GPT-4 support for AnkiGPT Plus users!', style: TextStyle( color: Colors.deepOrange, ), diff --git a/lib/src/pages/home_page/options_dialog.dart b/lib/src/pages/home_page/options_dialog.dart new file mode 100644 index 00000000..f49befb8 --- /dev/null +++ b/lib/src/pages/home_page/options_dialog.dart @@ -0,0 +1,245 @@ +import 'package:ankigpt/src/models/card_generation_size.dart'; +import 'package:ankigpt/src/models/model.dart'; +import 'package:ankigpt/src/pages/home_page/plus_dialog.dart'; +import 'package:ankigpt/src/pages/widgets/cancel_text_button.dart'; +import 'package:ankigpt/src/pages/widgets/plus_badge.dart'; +import 'package:ankigpt/src/providers/current_usage_provider.dart'; +import 'package:ankigpt/src/providers/generate_provider.dart'; +import 'package:ankigpt/src/providers/has_plus_provider.dart'; +import 'package:ankigpt/src/providers/options_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class OptionsDialog extends StatelessWidget { + const OptionsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Options'), + content: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 350, + maxWidth: 350, + ), + child: const SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _NumberOfCardsOption(), + SizedBox(height: 24), + _ModelOption(), + ], + ), + ), + ), + actions: [ + const CancelTextButton(), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ); + } +} + +class _Option extends StatelessWidget { + const _Option({ + required this.title, + required this.subtitle, + required this.child, + }); + + final Widget title; + final Widget subtitle; + final Widget child; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultTextStyle.merge( + style: const TextStyle(fontSize: 18), + child: title, + ), + const SizedBox(height: 2), + DefaultTextStyle( + style: TextStyle( + color: Colors.grey[600]!, + fontSize: 12, + ), + child: subtitle, + ), + const SizedBox(height: 8), + child, + ], + ); + } +} + +class _NumberOfCardsOption extends StatelessWidget { + const _NumberOfCardsOption(); + + @override + Widget build(BuildContext context) { + return const _Option( + title: Text('Number of cards'), + subtitle: Text('Specify the number of cards to generate.'), + child: NumberOfCardsDropdown(), + ); + } +} + +class NumberOfCardsDropdown extends ConsumerWidget { + const NumberOfCardsDropdown({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasPlus = ref.watch(hasPlusProvider); + final hasPickedFile = ref.watch(pickedFileProvider) != null; + + final availableSizes = CardGenrationSize.values + .where((c) => hasPickedFile ? c.isAvailableForFiles() : true) + .toList(); + + return SizedBox( + width: double.infinity, + child: DropdownButtonFormField( + value: ref.watch(optionsControllerProvider.select((v) => v.size)), + items: [ + ...availableSizes.map( + (c) => DropdownMenuItem( + value: c, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${c.getUiText()} cards'), + if (!hasPlus && c.isPlus()) ...[ + const SizedBox(width: 12), + const SizedBox( + width: 38, + child: PlusBadge( + withText: false, + ), + ) + ] + ], + ), + ), + ) + ], + onChanged: (v) { + if (v != null) { + if (!hasPlus && v.isPlus()) { + showPlusDialog(context); + } + + ref.read(optionsControllerProvider.notifier).setSize(v); + } + }, + ), + ); + } +} + +// Use can choose between GPT-3.5 and GPT-4 (only plus) in a DropdownMenu. +class _ModelOption extends StatelessWidget { + const _ModelOption(); + + @override + Widget build(BuildContext context) { + return const _Option( + title: Text('Model'), + subtitle: Text( + 'Specify the LLM model that will be used to generate the flashcards. A better model will generate better flashcards, but will take longer to generate.'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ModelDropdown(), + _Gpt4Usage(), + ], + ), + ); + } +} + +class ModelDropdown extends ConsumerWidget { + const ModelDropdown({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasPlus = ref.watch(hasPlusProvider); + return SizedBox( + width: double.infinity, + child: DropdownButtonFormField( + value: ref.watch(optionsControllerProvider.select((v) => v.model)), + items: [ + ...Model.values.map( + (c) => DropdownMenuItem( + value: c, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(c.getUiText()), + if (!hasPlus && c.isPlus()) ...[ + const SizedBox(width: 12), + const SizedBox( + width: 38, + child: PlusBadge(withText: false), + ) + ] + ], + ), + ), + ), + ], + onChanged: (v) { + if (v != null) { + if (!hasPlus && v.isPlus()) { + showPlusDialog(context); + } + + ref.read(optionsControllerProvider.notifier).setModel(v); + } + }, + ), + ); + } +} + +class _Gpt4Usage extends ConsumerWidget { + const _Gpt4Usage(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasPlus = ref.watch(hasPlusProvider); + if (!hasPlus) return const SizedBox(); + + final gpt4Usage = ref.watch(currentGpt4UsageProvider); + final selectedModel = + ref.watch(optionsControllerProvider.select((v) => v.model)); + if (selectedModel != Model.gpt4) return const SizedBox(); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: selectedModel == Model.gpt4 + ? Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'You have used $gpt4Usage of your $plusGpt4UsageLimitPerMonth monthly GPT-4 limit.', + style: TextStyle( + color: Colors.grey[600]!, + fontSize: 12, + ), + ), + ) + : const SizedBox(), + ); + } +} diff --git a/lib/src/pages/home_page/plus_dialog.dart b/lib/src/pages/home_page/plus_dialog.dart index e1b8ebbc..e63dc86d 100644 --- a/lib/src/pages/home_page/plus_dialog.dart +++ b/lib/src/pages/home_page/plus_dialog.dart @@ -6,6 +6,7 @@ import 'package:ankigpt/src/pages/widgets/ankigpt_card.dart'; import 'package:ankigpt/src/pages/widgets/extensions.dart'; import 'package:ankigpt/src/pages/widgets/max_width_constrained_box.dart'; import 'package:ankigpt/src/providers/buy_button_analytics.dart'; +import 'package:ankigpt/src/providers/generate_provider.dart'; import 'package:ankigpt/src/providers/has_account_provider.dart'; import 'package:ankigpt/src/providers/is_signed_in_provider.dart'; import 'package:ankigpt/src/providers/stripe_checkout_provider.dart'; @@ -85,8 +86,10 @@ class PlusAdvantages extends StatelessWidget { return const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SellingPoint(text: 'Unlimited cards per month'), - SellingPoint(text: 'Up to 150 cards per request'), + SellingPoint(text: 'Unlimited cards with GPT-3.5 per month'), + SellingPoint( + text: '$plusGpt4UsageLimitPerMonth cards with GPT-4 per month'), + SellingPoint(text: 'Up to 150 cards per deck'), SellingPoint( text: 'Up to 500,000 input characters (~ 200 pages) per request'), SellingPoint(text: 'Unlimited mnemonics per month'), @@ -106,17 +109,14 @@ class _PlusPrice extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '€9.99', - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ), + const Price( + priceEurPart: '€14', + priceCentPart: '.99', ), Text( 'Lifetime (one-time payment)', style: TextStyle( - color: Colors.grey[600]!, + color: Colors.grey[700]!, ), ), ], diff --git a/lib/src/pages/home_page/pricing_section.dart b/lib/src/pages/home_page/pricing_section.dart index f7d7fc04..9fe9b18f 100644 --- a/lib/src/pages/home_page/pricing_section.dart +++ b/lib/src/pages/home_page/pricing_section.dart @@ -79,16 +79,16 @@ class _FreeTier extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return _TierBase( name: 'Free', - price: '€0', + priceEurPart: '€0', points: const [ - PointData('$freeUsageLimitPerMonth cards per month'), - PointData('Up to 20 cards per request'), + PointData('$freeUsageLimitPerMonth cards with GPT-3.5 per month'), + PointData('Up to 20 cards per deck'), PointData('Up to 4,000 input characters per request'), + PointData('Delete, edit & search cards'), PointData( 'Generate $freeMnemonicsUsagePerMonth mnemonics per month', trailing: _HelpMnemonicsIconButton(), ), - PointData('Delete, edit & search cards'), PointData('Share decks with a link'), ], onPressedCallToAction: () { @@ -199,11 +199,13 @@ class _PlusTierState extends ConsumerState<_PlusTier> { final hasPlus = ref.watch(hasPlusProvider); return _TierBase( name: 'Plus', - price: '€9.99', + priceEurPart: '€14', + priceCentPart: '.99', priceDescription: 'Lifetime (one-time payment)', points: const [ - PointData('Unlimited cards per month'), - PointData('Up to 150 cards per request'), + PointData('Unlimited cards with GPT-3.5 per month'), + PointData('$plusGpt4UsageLimitPerMonth cards with GPT-4 per month'), + PointData('Up to 150 cards per deck'), PointData('Up to 500,000 input characters (~ 200 pages) per request'), PointData( 'Generate unlimited mnemonics', @@ -222,7 +224,8 @@ class _PlusTierState extends ConsumerState<_PlusTier> { class _TierBase extends StatelessWidget { const _TierBase({ required this.name, - required this.price, + required this.priceEurPart, + this.priceCentPart, required this.points, required this.onPressedCallToAction, required this.callToActionText, @@ -231,7 +234,8 @@ class _TierBase extends StatelessWidget { }); final String name; - final String price; + final String priceEurPart; + final String? priceCentPart; final String? priceDescription; final List points; final VoidCallback onPressedCallToAction; @@ -245,7 +249,7 @@ class _TierBase extends StatelessWidget { width: _isMobileView(context) ? MediaQuery.of(context).size.width * 0.85 : 365, - height: _isMobileView(context) ? 550 : 565, + height: 620, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -258,12 +262,9 @@ class _TierBase extends StatelessWidget { name, style: const TextStyle(fontSize: 24), ), - Text( - price, - style: const TextStyle( - fontSize: 48, - fontWeight: FontWeight.bold, - ), + Price( + priceEurPart: priceEurPart, + priceCentPart: priceCentPart, ), Text(priceDescription ?? ''), ], @@ -288,6 +289,44 @@ class _TierBase extends StatelessWidget { } } +class Price extends StatelessWidget { + const Price({ + super.key, + required this.priceEurPart, + required this.priceCentPart, + }); + + final String priceEurPart; + final String? priceCentPart; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + priceEurPart, + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + ), + ), + if (priceCentPart != null) + Padding( + padding: const EdgeInsets.only(left: 2, bottom: 11), + child: Text( + priceCentPart!, + style: TextStyle( + fontSize: 16, + color: Colors.grey[600]!, + ), + ), + ) + ], + ); + } +} + class _CallToActionButton extends StatelessWidget { const _CallToActionButton({ required this.text, diff --git a/lib/src/providers/card_generation_size_provider.dart b/lib/src/providers/card_generation_size_provider.dart deleted file mode 100644 index 452984f2..00000000 --- a/lib/src/providers/card_generation_size_provider.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:ankigpt/src/models/card_generation_size.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'card_generation_size_provider.g.dart'; - -@riverpod -class GenerationSize extends _$GenerationSize { - static const defaultSize = CardGenrationSize.ten; - - @override - CardGenrationSize build() { - return defaultSize; - } - - void set(CardGenrationSize size) { - state = size; - } -} diff --git a/lib/src/providers/current_month_usage_provider.dart b/lib/src/providers/current_usage_provider.dart similarity index 57% rename from lib/src/providers/current_month_usage_provider.dart rename to lib/src/providers/current_usage_provider.dart index ee20e08b..3af1ece3 100644 --- a/lib/src/providers/current_month_usage_provider.dart +++ b/lib/src/providers/current_usage_provider.dart @@ -1,10 +1,16 @@ import 'package:ankigpt/src/providers/app_user_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'current_month_usage_provider.g.dart'; +part 'current_usage_provider.g.dart'; @Riverpod() int currentMonthUsage(CurrentMonthUsageRef ref) { final appUser = ref.watch(appUserProvider); return appUser.value?.usage.generatedCardsCurrentMonth ?? 0; } + +@Riverpod() +int currentGpt4Usage(CurrentGpt4UsageRef ref) { + final appUser = ref.watch(appUserProvider); + return appUser.value?.usage.generatedCardsCurrentMonthByModel.gpt4 ?? 0; +} diff --git a/lib/src/providers/current_month_usage_provider.g.dart b/lib/src/providers/current_usage_provider.g.dart similarity index 61% rename from lib/src/providers/current_month_usage_provider.g.dart rename to lib/src/providers/current_usage_provider.g.dart index ef70d108..8787680b 100644 --- a/lib/src/providers/current_month_usage_provider.g.dart +++ b/lib/src/providers/current_usage_provider.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'current_month_usage_provider.dart'; +part of 'current_usage_provider.dart'; // ************************************************************************** // RiverpodGenerator @@ -21,5 +21,20 @@ final currentMonthUsageProvider = AutoDisposeProvider.internal( ); typedef CurrentMonthUsageRef = AutoDisposeProviderRef; +String _$currentGpt4UsageHash() => r'cb47dde1553cadb781bfb9c2ac31166486574750'; + +/// See also [currentGpt4Usage]. +@ProviderFor(currentGpt4Usage) +final currentGpt4UsageProvider = AutoDisposeProvider.internal( + currentGpt4Usage, + name: r'currentGpt4UsageProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentGpt4UsageHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef CurrentGpt4UsageRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/providers/deck_list_provider.dart b/lib/src/providers/deck_list_provider.dart index 65843638..afcf66ee 100644 --- a/lib/src/providers/deck_list_provider.dart +++ b/lib/src/providers/deck_list_provider.dart @@ -26,6 +26,7 @@ Stream> deckList(DeckListRef ref) { name: name, numberOfCards: dto.numberOfCards, sessionId: dto.id, + model: dto.model, ); } @@ -35,6 +36,7 @@ Stream> deckList(DeckListRef ref) { name: name, numberOfCards: dto.numberOfCards, sessionId: dto.id, + model: dto.model, ); } @@ -47,6 +49,7 @@ Stream> deckList(DeckListRef ref) { questions: questions.length < 5 ? questions : questions.sublist(0, 5), sessionId: dto.id, + model: dto.model, ); }).toList() ?? [], diff --git a/lib/src/providers/deck_list_provider.g.dart b/lib/src/providers/deck_list_provider.g.dart index 0b8879a8..dfa1d810 100644 --- a/lib/src/providers/deck_list_provider.g.dart +++ b/lib/src/providers/deck_list_provider.g.dart @@ -6,7 +6,7 @@ part of 'deck_list_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$deckListHash() => r'1bcf98fc23e6e042922a4916dd85015865635857'; +String _$deckListHash() => r'4345f455f0dccd3a986aeede176653d4d6f7a476'; /// See also [deckList]. @ProviderFor(deckList) diff --git a/lib/src/providers/generate_provider.dart b/lib/src/providers/generate_provider.dart index fcd6239c..a0453b3f 100644 --- a/lib/src/providers/generate_provider.dart +++ b/lib/src/providers/generate_provider.dart @@ -5,17 +5,18 @@ import 'package:ankigpt/src/infrastructure/user_repository.dart'; import 'package:ankigpt/src/models/card_generation_size.dart'; import 'package:ankigpt/src/models/generate_state.dart'; import 'package:ankigpt/src/models/input_type.dart'; +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/models/session_dto.dart'; import 'package:ankigpt/src/models/session_id.dart'; import 'package:ankigpt/src/models/user_id.dart'; import 'package:ankigpt/src/providers/analytics_provider.dart'; -import 'package:ankigpt/src/providers/card_generation_size_provider.dart'; import 'package:ankigpt/src/providers/clear_session_state_provider.dart'; -import 'package:ankigpt/src/providers/current_month_usage_provider.dart'; +import 'package:ankigpt/src/providers/current_usage_provider.dart'; import 'package:ankigpt/src/providers/has_account_provider.dart'; import 'package:ankigpt/src/providers/has_plus_provider.dart'; import 'package:ankigpt/src/providers/input_text_field_controller.dart'; import 'package:ankigpt/src/providers/logger/logger_provider.dart'; +import 'package:ankigpt/src/providers/options_provider.dart'; import 'package:ankigpt/src/providers/router_provider.dart'; import 'package:ankigpt/src/providers/session_repository_provider.dart'; import 'package:ankigpt/src/providers/user_repository_provider.dart'; @@ -35,6 +36,8 @@ const freeUsageLimitPerMonth = 100; const freeMnemonicsUsagePerMonth = 5; +const plusGpt4UsageLimitPerMonth = 150; + @Riverpod(keepAlive: true, dependencies: [hasPlus]) class GenerateNotifier extends _$GenerateNotifier { Logger get _logger => ref.read(loggerProvider); @@ -45,6 +48,7 @@ class GenerateNotifier extends _$GenerateNotifier { ref.read(sessionRepositoryProvider); bool get _hasPlus => ref.read(hasPlusProvider); int get _currentMonthUsage => ref.read(currentMonthUsageProvider); + int get _currentGpt4Usage => ref.read(currentGpt4UsageProvider); Analytics get _analytics => ref.read(analyticsProvider); static const _analyticsPage = 'generate'; @@ -58,11 +62,11 @@ class GenerateNotifier extends _$GenerateNotifier { } Future submit() async { - final size = ref.read(generationSizeProvider); + final options = ref.read(optionsControllerProvider); _logger.d("Generating cards..."); - if (!_hasPlus && size.isPlus()) { + if (!_hasPlus && options.hasPlusOption()) { _logPlusRequiredToGenerate(); throw PlusMembershipRequiredException(); } @@ -72,7 +76,8 @@ class GenerateNotifier extends _$GenerateNotifier { _throwIfTextInputIsInvalid(text); } - _throwIfFreeLimitReached(size); + _throwIfFreeLimitReached(options.size); + _throwIfGpt4LimitReached(options.size, options.model); if (!ref.read(hasAccount2Provider)) { ref.read(wantsToGenerateProvider.notifier).set(true); @@ -101,8 +106,9 @@ class GenerateNotifier extends _$GenerateNotifier { } await _sessionRepository.startSession( - numberOfCards: size.toInt(), + numberOfCards: options.size.toInt(), sessionId: sessionId, + model: options.model.snakeCaseName, input: Input( text: text.isEmpty ? null : text, type: _hasPickedFile ? InputType.file : InputType.text, @@ -114,7 +120,7 @@ class GenerateNotifier extends _$GenerateNotifier { : null, ), ); - _logStartSession(size); + _logStartSession(options.size); _logger.d("Started session with id: $sessionId"); @@ -124,7 +130,7 @@ class GenerateNotifier extends _$GenerateNotifier { _pickedFile = null; _textEditingController.clear(); _pdfPassword = null; - ref.read(generationSizeProvider.notifier).set(GenerationSize.defaultSize); + ref.read(optionsControllerProvider.notifier).reset(); final router = ref.read(routerProvider); router.go('/deck/$sessionId'); @@ -182,6 +188,18 @@ class GenerateNotifier extends _$GenerateNotifier { } } + void _throwIfGpt4LimitReached(CardGenrationSize size, Model model) { + if (_hasPlus && model == Model.gpt4) { + final remainingGpt4Limit = plusGpt4UsageLimitPerMonth - _currentGpt4Usage; + if (remainingGpt4Limit < size.toInt()) { + throw Gpt4LimitExceededException( + currentDeckSize: size.toInt(), + remainingGpt4Limit: remainingGpt4Limit, + ); + } + } + } + void _logPlusRequiredToGenerate() { unawaited( _analytics.logEvent( @@ -293,11 +311,10 @@ class GenerateNotifier extends _$GenerateNotifier { // Maybe increase the size, when the current size is the default one. Slides are usually longer so we can assume // that the user wants to generate more cards. - final size = ref.read(generationSizeProvider); + final size = ref.read(optionsControllerProvider).size; if (!size.isAvailableForFiles()) { - ref - .read(generationSizeProvider.notifier) - .set(_hasPlus ? CardGenrationSize.fifty : CardGenrationSize.twenty); + ref.read(optionsControllerProvider.notifier).setSize( + _hasPlus ? CardGenrationSize.fifty : CardGenrationSize.twenty); } } @@ -326,6 +343,19 @@ class FreeLimitExceededException implements Exception { final int remainingFreeLimit; } +class Gpt4LimitExceededException implements Exception { + const Gpt4LimitExceededException({ + required this.currentDeckSize, + required this.remainingGpt4Limit, + }); + + /// The deck size that the user tried to generate. + final int currentDeckSize; + + /// The number of cards the user has left for GPT-4. + final int remainingGpt4Limit; +} + @riverpod class PickedFile extends _$PickedFile { @override diff --git a/lib/src/providers/generate_provider.g.dart b/lib/src/providers/generate_provider.g.dart index 67a264c5..13cd0704 100644 --- a/lib/src/providers/generate_provider.g.dart +++ b/lib/src/providers/generate_provider.g.dart @@ -21,7 +21,7 @@ final hasPickedFileProvider = AutoDisposeProvider.internal( ); typedef HasPickedFileRef = AutoDisposeProviderRef; -String _$generateNotifierHash() => r'cdea7e3551bbab66938aa375a69264d445ed904d'; +String _$generateNotifierHash() => r'f4cf4316d9e6e462ef59d3eff3e32132f3d17eb9'; /// See also [GenerateNotifier]. @ProviderFor(GenerateNotifier) diff --git a/lib/src/providers/has_plus_provider.dart b/lib/src/providers/has_plus_provider.dart index b3ffdde2..7684bd67 100644 --- a/lib/src/providers/has_plus_provider.dart +++ b/lib/src/providers/has_plus_provider.dart @@ -3,7 +3,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'has_plus_provider.g.dart'; -@Riverpod(dependencies: []) +@Riverpod(dependencies: [], keepAlive: true) bool hasPlus(HasPlusRef ref) { final appUser = ref.watch(appUserProvider); return appUser.value?.hasPlus ?? false; diff --git a/lib/src/providers/has_plus_provider.g.dart b/lib/src/providers/has_plus_provider.g.dart index f860161a..8e5b4428 100644 --- a/lib/src/providers/has_plus_provider.g.dart +++ b/lib/src/providers/has_plus_provider.g.dart @@ -6,11 +6,11 @@ part of 'has_plus_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$hasPlusHash() => r'b57e0e9b5a524af58131dcbd8bec59f1ef69d52d'; +String _$hasPlusHash() => r'22232d4d910924d97bf1fcd11f626ca6048c2b77'; /// See also [hasPlus]. @ProviderFor(hasPlus) -final hasPlusProvider = AutoDisposeProvider.internal( +final hasPlusProvider = Provider.internal( hasPlus, name: r'hasPlusProvider', debugGetCreateSourceHash: @@ -19,6 +19,6 @@ final hasPlusProvider = AutoDisposeProvider.internal( allTransitiveDependencies: const {}, ); -typedef HasPlusRef = AutoDisposeProviderRef; +typedef HasPlusRef = ProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/providers/options_provider.dart b/lib/src/providers/options_provider.dart new file mode 100644 index 00000000..d960ae1d --- /dev/null +++ b/lib/src/providers/options_provider.dart @@ -0,0 +1,50 @@ +import 'package:ankigpt/src/models/card_generation_size.dart'; +import 'package:ankigpt/src/models/model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'options_provider.freezed.dart'; +part 'options_provider.g.dart'; + +@riverpod +class OptionsController extends _$OptionsController { + static const defaultSize = CardGenrationSize.ten; + static const defaultModel = Model.gpt3_5; + + static const defaultOptions = GenerationOptions( + size: defaultSize, + model: defaultModel, + ); + + @override + GenerationOptions build() { + return defaultOptions; + } + + void setSize(CardGenrationSize size) { + state = state.copyWith(size: size); + } + + void setModel(Model model) { + state = state.copyWith(model: model); + } + + void reset() { + state = defaultOptions; + } +} + +@freezed +class GenerationOptions with _$GenerationOptions { + const GenerationOptions._(); + + const factory GenerationOptions({ + required CardGenrationSize size, + required Model model, + }) = _GenerationOptions; + + /// Returns `true` if an option is selected, which requires AnkiGPT Plus. + bool hasPlusOption() { + return size.isPlus() || model.isPlus(); + } +} diff --git a/lib/src/providers/options_provider.freezed.dart b/lib/src/providers/options_provider.freezed.dart new file mode 100644 index 00000000..77bc5aad --- /dev/null +++ b/lib/src/providers/options_provider.freezed.dart @@ -0,0 +1,153 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'options_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$GenerationOptions { + CardGenrationSize get size => throw _privateConstructorUsedError; + Model get model => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $GenerationOptionsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GenerationOptionsCopyWith<$Res> { + factory $GenerationOptionsCopyWith( + GenerationOptions value, $Res Function(GenerationOptions) then) = + _$GenerationOptionsCopyWithImpl<$Res, GenerationOptions>; + @useResult + $Res call({CardGenrationSize size, Model model}); +} + +/// @nodoc +class _$GenerationOptionsCopyWithImpl<$Res, $Val extends GenerationOptions> + implements $GenerationOptionsCopyWith<$Res> { + _$GenerationOptionsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? size = null, + Object? model = null, + }) { + return _then(_value.copyWith( + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as CardGenrationSize, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$GenerationOptionsImplCopyWith<$Res> + implements $GenerationOptionsCopyWith<$Res> { + factory _$$GenerationOptionsImplCopyWith(_$GenerationOptionsImpl value, + $Res Function(_$GenerationOptionsImpl) then) = + __$$GenerationOptionsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({CardGenrationSize size, Model model}); +} + +/// @nodoc +class __$$GenerationOptionsImplCopyWithImpl<$Res> + extends _$GenerationOptionsCopyWithImpl<$Res, _$GenerationOptionsImpl> + implements _$$GenerationOptionsImplCopyWith<$Res> { + __$$GenerationOptionsImplCopyWithImpl(_$GenerationOptionsImpl _value, + $Res Function(_$GenerationOptionsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? size = null, + Object? model = null, + }) { + return _then(_$GenerationOptionsImpl( + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as CardGenrationSize, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as Model, + )); + } +} + +/// @nodoc + +class _$GenerationOptionsImpl extends _GenerationOptions { + const _$GenerationOptionsImpl({required this.size, required this.model}) + : super._(); + + @override + final CardGenrationSize size; + @override + final Model model; + + @override + String toString() { + return 'GenerationOptions(size: $size, model: $model)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GenerationOptionsImpl && + (identical(other.size, size) || other.size == size) && + (identical(other.model, model) || other.model == model)); + } + + @override + int get hashCode => Object.hash(runtimeType, size, model); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$GenerationOptionsImplCopyWith<_$GenerationOptionsImpl> get copyWith => + __$$GenerationOptionsImplCopyWithImpl<_$GenerationOptionsImpl>( + this, _$identity); +} + +abstract class _GenerationOptions extends GenerationOptions { + const factory _GenerationOptions( + {required final CardGenrationSize size, + required final Model model}) = _$GenerationOptionsImpl; + const _GenerationOptions._() : super._(); + + @override + CardGenrationSize get size; + @override + Model get model; + @override + @JsonKey(ignore: true) + _$$GenerationOptionsImplCopyWith<_$GenerationOptionsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/providers/card_generation_size_provider.g.dart b/lib/src/providers/options_provider.g.dart similarity index 53% rename from lib/src/providers/card_generation_size_provider.g.dart rename to lib/src/providers/options_provider.g.dart index 056a2a43..7150b22d 100644 --- a/lib/src/providers/card_generation_size_provider.g.dart +++ b/lib/src/providers/options_provider.g.dart @@ -1,26 +1,26 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'card_generation_size_provider.dart'; +part of 'options_provider.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$generationSizeHash() => r'4052d0761579c36540ea35eb9f471579909550da'; +String _$optionsControllerHash() => r'fa313e2b380519c471b4fef67dca09f03bcb5a91'; -/// See also [GenerationSize]. -@ProviderFor(GenerationSize) -final generationSizeProvider = - AutoDisposeNotifierProvider.internal( - GenerationSize.new, - name: r'generationSizeProvider', +/// See also [OptionsController]. +@ProviderFor(OptionsController) +final optionsControllerProvider = + AutoDisposeNotifierProvider.internal( + OptionsController.new, + name: r'optionsControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$generationSizeHash, + : _$optionsControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$GenerationSize = AutoDisposeNotifier; +typedef _$OptionsController = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/providers/watch_provider.dart b/lib/src/providers/watch_provider.dart index 997d298a..91cbc5df 100644 --- a/lib/src/providers/watch_provider.dart +++ b/lib/src/providers/watch_provider.dart @@ -57,6 +57,7 @@ class Watch extends _$Watch { fileName: dto.input.file?.name, inputText: dto.input.text, isOwner: _isOwner(userId, dto), + model: dto.model, ); }, onError: (error, stackTrace) { diff --git a/lib/src/providers/watch_provider.g.dart b/lib/src/providers/watch_provider.g.dart index f4a617fa..735d2a48 100644 --- a/lib/src/providers/watch_provider.g.dart +++ b/lib/src/providers/watch_provider.g.dart @@ -6,7 +6,7 @@ part of 'watch_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$watchHash() => r'11ab676f96d613b0d68f8bc4555fcf18e3bd3bde'; +String _$watchHash() => r'165a61bc01459c1924f3437a6d99fb1cc487e2e0'; /// Copied from Dart SDK class _SystemHash { diff --git a/test/pages/home_page/my_decks_section_test.dart b/test/pages/home_page/my_decks_section_test.dart index fffc4c99..de17f1ed 100644 --- a/test/pages/home_page/my_decks_section_test.dart +++ b/test/pages/home_page/my_decks_section_test.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:adaptive_test/adaptive_test.dart'; import 'package:ankigpt/src/models/deck_preview.dart'; +import 'package:ankigpt/src/models/model.dart'; import 'package:ankigpt/src/pages/home_page/my_decks_section.dart'; import 'package:ankigpt/src/providers/deck_list_provider.dart'; import 'package:flutter/material.dart'; @@ -44,6 +45,7 @@ void main() { createdAt: DateTime(2023, 1, 1), name: _generateString(random), sessionId: _generateString(random), + model: Model.gpt3_5, numberOfCards: random.nextInt(100), ), DeckPreview.loading( @@ -51,6 +53,7 @@ void main() { name: _generateString(random), numberOfCards: random.nextInt(100), sessionId: _generateString(random), + model: Model.gpt3_5, ), DeckPreview.error( message: _generateString(random), @@ -58,6 +61,7 @@ void main() { name: _generateString(random), numberOfCards: random.nextInt(100), sessionId: _generateString(random), + model: Model.gpt3_5, ), ]; await pumpMyDecksSection( diff --git a/test/providers/delete_card_provider_test.mocks.dart b/test/providers/delete_card_provider_test.mocks.dart index 3b66e8f7..2a64a43e 100644 --- a/test/providers/delete_card_provider_test.mocks.dart +++ b/test/providers/delete_card_provider_test.mocks.dart @@ -122,6 +122,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { _i7.Future startSession({ required _i8.Input? input, required int? numberOfCards, + required String? model, required String? sessionId, }) => (super.noSuchMethod( @@ -131,6 +132,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), @@ -142,6 +144,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), @@ -155,6 +158,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), diff --git a/test/providers/edit_answer_provider_test.mocks.dart b/test/providers/edit_answer_provider_test.mocks.dart index 9cea7b67..720957ee 100644 --- a/test/providers/edit_answer_provider_test.mocks.dart +++ b/test/providers/edit_answer_provider_test.mocks.dart @@ -122,6 +122,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { _i7.Future startSession({ required _i8.Input? input, required int? numberOfCards, + required String? model, required String? sessionId, }) => (super.noSuchMethod( @@ -131,6 +132,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), @@ -142,6 +144,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), @@ -155,6 +158,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), diff --git a/test/providers/edit_question_provider_test.mocks.dart b/test/providers/edit_question_provider_test.mocks.dart index db81ce2d..ffb5645b 100644 --- a/test/providers/edit_question_provider_test.mocks.dart +++ b/test/providers/edit_question_provider_test.mocks.dart @@ -122,6 +122,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { _i7.Future startSession({ required _i8.Input? input, required int? numberOfCards, + required String? model, required String? sessionId, }) => (super.noSuchMethod( @@ -131,6 +132,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), @@ -142,6 +144,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), @@ -155,6 +158,7 @@ class MockSessionRepository extends _i1.Mock implements _i5.SessionRepository { { #input: input, #numberOfCards: numberOfCards, + #model: model, #sessionId: sessionId, }, ), diff --git a/test/providers/generate_provider_test.dart b/test/providers/generate_provider_test.dart index 2e18a450..7d548bd1 100644 --- a/test/providers/generate_provider_test.dart +++ b/test/providers/generate_provider_test.dart @@ -1,11 +1,11 @@ import 'package:ankigpt/src/models/card_generation_size.dart'; import 'package:ankigpt/src/models/generate_state.dart'; import 'package:ankigpt/src/providers/analytics_provider.dart'; -import 'package:ankigpt/src/providers/card_generation_size_provider.dart'; import 'package:ankigpt/src/providers/generate_provider.dart'; import 'package:ankigpt/src/providers/has_plus_provider.dart'; import 'package:ankigpt/src/providers/input_text_field_controller.dart'; import 'package:ankigpt/src/providers/logger/logger_provider.dart'; +import 'package:ankigpt/src/providers/options_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:logger/logger.dart'; @@ -107,9 +107,9 @@ void main() { test('throws exception when user has selected a plus size', () { container - .read(generationSizeProvider.notifier) - // Fifty cards are only inlcuded in plus version - .set(CardGenrationSize.fifty); + .read(optionsControllerProvider.notifier) + // Fifty cards are only included in plus version + .setSize(CardGenrationSize.fifty); // Except that .submit throws PlusVersionException expect( @@ -120,9 +120,9 @@ void main() { test('logs event when user has selected a plus size', () async { container - .read(generationSizeProvider.notifier) + .read(optionsControllerProvider.notifier) // Fifty cards are only inlcuded in plus version - .set(CardGenrationSize.fifty); + .setSize(CardGenrationSize.fifty); try { await container.read(generateNotifierProvider.notifier).submit();