From 7158824ffa2d0b6470d31b960e20549c38ff44a0 Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sat, 3 Aug 2024 22:51:03 +0200 Subject: [PATCH] Quiz fixes --- assets/lessons/rjp.json | 33 +++ assets/lessons/rjp.md | 3 - devtools_options.yaml | 3 + lib/bloc/eeg_state.dart | 67 +++-- lib/bloc/gemini_state.dart | 362 +++++++++++++++++++++++----- lib/config.dart | 2 +- lib/screens/gemini_chat_screen.dart | 287 ++++++++++++++-------- pubspec.yaml | 1 + 8 files changed, 557 insertions(+), 201 deletions(-) create mode 100644 assets/lessons/rjp.json create mode 100644 devtools_options.yaml diff --git a/assets/lessons/rjp.json b/assets/lessons/rjp.json new file mode 100644 index 0000000..e4f61ed --- /dev/null +++ b/assets/lessons/rjp.json @@ -0,0 +1,33 @@ +[ + { + "question": "Które z poniższych stwierdzeń opisuje ruch jednostajnie przyspieszony?", + "options": [ + "Prędkość obiektu pozostaje stała.", + "Przyspieszenie obiektu jest stałe.", + "Pozycja obiektu zmienia się w regularnych odstępach czasu.", + "Prędkość obiektu zmienia się w sposób losowy." + ], + "correctAnswer": 1 + }, + { + "question": "Które z poniższych równań opisuje ruch jednostajnie przyspieszony?", + "options": [ + "v = u", + "a = 0", + "s = ut + ½at²", + "v² = u² - 2as" + ], + "correctAnswer": 2 + }, + { + "question": "Samochód przyspiesza z 0 km/h do 100 km/h w ciągu 10 sekund. Jaka jest wartość przyspieszenia samochodu?", + "options": [ + "5 m/s²", + "10 m/s²", + "15 m/s²", + "20 m/s²" + ], + "correctAnswer": 1 + } + ] + \ No newline at end of file diff --git a/assets/lessons/rjp.md b/assets/lessons/rjp.md index 93f5540..a7fdbdf 100644 --- a/assets/lessons/rjp.md +++ b/assets/lessons/rjp.md @@ -2,9 +2,6 @@ **Wstęp:** -* Witam wszystkich. Dzisiaj będziemy rozmawiać o ruchu jednostajnie przyspieszonym. -* Jest to rodzaj ruchu, w którym obiekt porusza się z coraz większą prędkością w regularnych odstępach czasu. - **Definicja ruchu jednostajnie przyspieszanego:** * Ruch jednostajnie przyspieszony to ruch, w którym przyspieszenie obiektu jest stałe. diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/bloc/eeg_state.dart b/lib/bloc/eeg_state.dart index 751979a..54c3cc2 100644 --- a/lib/bloc/eeg_state.dart +++ b/lib/bloc/eeg_state.dart @@ -18,17 +18,16 @@ class EegState { } } - class EegCubit extends Cubit { EegCubit() : super(EegState(mind_wandering: 0.9, focus: 0.1)) { // Start the timer when the cubit is created - if (isDebug) { + if (!isDebug) { startPolling(); } } Timer? _timer; - + void startPolling() { // Poll every 1 second (adjust the duration as needed) _timer = Timer.periodic(Duration(seconds: 1), (timer) { @@ -38,46 +37,44 @@ class EegCubit extends Cubit { // double newFocus = 1 - newMindWandering; fetchEegData().then((data) { - double newMindWandering = data[0]; - double newFocus = data[1]; - // Update the state with the new EEG data - updateEegData(newMindWandering, newFocus); + double newMindWandering = data[0]; + double newFocus = data[1]; + // Update the state with the new EEG data + updateEegData(newMindWandering, newFocus); }); // updateEegData(newMindWandering, newFocus); }); } - -Future> fetchEegData() async { - if (isDebug) { - return [0.9, 0.1]; // Placeholder ret - } - - final url = Uri.parse('http://192.168.83.153:1234'); - - try { - final response = await http.get(url); - - if (response.statusCode == 200) { - // Split the response body by newline and parse as floats - List values = response.body.trim().split('\n'); - if (values.length == 2) { - return [ - double.parse(values[0]), - double.parse(values[1]), - ]; - } else { - throw Exception('Unexpected response format'); - } - } else { - throw Exception('Failed to load EEG data: ${response.statusCode}'); + Future> fetchEegData() async { + if (isDebug) { + return [0.9, 0.1]; // Placeholder ret } - } catch (e) { - throw Exception('Error fetching EEG data: $e'); - } -} + final url = Uri.parse('http://192.168.83.153:1234'); + + try { + final response = await http.get(url); + + if (response.statusCode == 200) { + // Split the response body by newline and parse as floats + List values = response.body.trim().split('\n'); + if (values.length == 2) { + return [ + double.parse(values[0]), + double.parse(values[1]), + ]; + } else { + throw Exception('Unexpected response format'); + } + } else { + throw Exception('Failed to load EEG data: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Error fetching EEG data: $e'); + } + } void stopPolling() { _timer?.cancel(); diff --git a/lib/bloc/gemini_state.dart b/lib/bloc/gemini_state.dart index 36fa7cd..e3042f8 100644 --- a/lib/bloc/gemini_state.dart +++ b/lib/bloc/gemini_state.dart @@ -1,104 +1,348 @@ +import 'dart:convert'; + import 'package:bloc/bloc.dart'; import 'package:flutter/services.dart'; import 'package:gemini_app/config.dart'; import 'package:gemini_app/bloc/eeg_state.dart'; import 'package:google_generative_ai/google_generative_ai.dart'; +const String systemPrmpt = + """You are an AI tutor helping students understand topics with help of biometric data. You will be supplied with a json containing data extracted from an EEG device, use that data to modify your approach and help the student learn more effectively. +At the start you will be provided a script with a lesson to cover. +Keep the analysis and responses short. +Use language: POLISH + +After completing the theoretical part there's a quiz, you can start it yourself at the appropriate time or react to users' request by including at the start of your response + +Write the response in markdown and split it into two parts (include the tokens): +optional: + +here describe what is the state of the student and how to best approach them + +here continue with the lesson, respond to answers, etc +"""; + enum GeminiStatus { initial, loading, success, error } -enum MessageType { text, image, audio, video } + +// enum MessageType { text, image, audio, video } + enum MessageSource { user, agent } +class QuizMessage { + final String content; + final List options; + final int correctAnswer; + int? userAnswer; + + QuizMessage({ + required this.content, + required this.options, + required this.correctAnswer, + this.userAnswer, + }); +} + +// class Message { +// final String text; +// final MessageType type; +// final MessageSource source; + +// Message({ +// required this.text, +// required this.type, +// required this.source, +// }); +// } + +enum MessageType { text, lessonScript, quizQuestion, quizAnswer } + class Message { final String text; final MessageType type; final MessageSource source; + final List? quizOptions; // Add this for ABCD options + final int? correctAnswer; // Add this for the correct answer index Message({ required this.text, required this.type, required this.source, + this.quizOptions, + this.correctAnswer, + }); + + static Message fromGeminiContent(Content content) { + if (content.parts.isNotEmpty) { + final part = content.parts.first; + if (part is TextPart) { + return Message( + text: part.text, + type: MessageType.text, + source: content.role == 'model' + ? MessageSource.agent + : MessageSource.user, + ); + } + } + throw UnsupportedError('Unsupported content type'); + } + + Content toGeminiContent() { + if (source == MessageSource.user || type == MessageType.lessonScript) { + return Content.text(text); + } else { + return Content.model([TextPart(text)]); + } + } +} + +class QuizQuestion { + String question; + List options; + int correctAnswer; + + QuizQuestion({ + required this.question, + required this.options, + required this.correctAnswer, }); } class GeminiState { GeminiStatus status; String error; - List messages; + List messages; + List? quizQuestions; + bool isQuizMode; + int currentQuizIndex; + GenerativeModel? model; - GeminiState({ - required this.status, - required this.error, - this.messages = const [], - }); + GeminiState( + {required this.status, + this.error = '', + this.messages = const [], + this.quizQuestions, + this.isQuizMode = false, + this.currentQuizIndex = -1, + this.model}); + + GeminiState copyWith({ + GeminiStatus? status, + String? error, + List? messages, + List? quizQuestions, + bool? isQuizMode, + int? currentQuizIndex, + GenerativeModel? model, + }) { + return GeminiState( + status: status ?? this.status, + error: error ?? this.error, + messages: messages ?? this.messages, + quizQuestions: quizQuestions ?? this.quizQuestions, + isQuizMode: isQuizMode ?? this.isQuizMode, + currentQuizIndex: currentQuizIndex ?? this.currentQuizIndex, + model: model ?? this.model, + ); + } static GeminiState get initialState => GeminiState( - status: GeminiStatus.initial, - // messages: [Message(text: "Hello, I'm Gemini Pro. How can I help you?", type: MessageType.text, source: MessageSource.agent)], - messages: [Content.model([TextPart("Hello, I'm Gemini Pro. How can I help you?")])], - error: '', - ); + status: GeminiStatus.initial, + // messages: [Message(text: "Hello, I'm Gemini Pro. How can I help you?", type: MessageType.text, source: MessageSource.agent)], + messages: [ + // Message.fromGeminiContent(Content.model( + // [TextPart("Hello, I'm Gemini Pro. How can I help you?")])) + ], + error: '', + ); } class GeminiCubit extends Cubit { GeminiCubit() : super(GeminiState.initialState); -void sendMessage(String prompt, EegState eegState) async { - var messagesWithoutPrompt = state.messages; - var messagesWithPrompt = state.messages + [ - Content.text(prompt) - ]; + void startLesson(EegState eegState) async { + final quizQuestions = await loadQuizQuestions(); + final String rjp = await rootBundle.loadString('assets/lessons/rjp.md'); + final String prompt = + "Jesteś nauczycielem/chatbotem prowadzącym zajęcia z jednym uczniem. Uczeń ma możliwość zadawania pytań w trakcie, natomiast jesteś odpowiedzialny za prowadzenie lekcji i przedstawienie tematu. Zacznij prowadzić lekcje dla jednego ucznia na podstawie poniszego skryptu:\n$rjp"; - emit(GeminiState(status: GeminiStatus.loading, messages: messagesWithPrompt, error: '')); - final safetySettings = [ - SafetySetting(HarmCategory.harassment, HarmBlockThreshold.none), - SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.none), - SafetySetting(HarmCategory.sexuallyExplicit, HarmBlockThreshold.none), - SafetySetting(HarmCategory.dangerousContent, HarmBlockThreshold.none), - // SafetySetting(HarmCategory.unspecified, HarmBlockThreshold.none), - ]; + final safetySettings = [ + SafetySetting(HarmCategory.harassment, HarmBlockThreshold.none), + SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.none), + SafetySetting(HarmCategory.sexuallyExplicit, HarmBlockThreshold.none), + SafetySetting(HarmCategory.dangerousContent, HarmBlockThreshold.none), + ]; - final String rjp = await rootBundle.loadString('assets/lessons/rjp.md'); + final model = GenerativeModel( + model: 'gemini-1.5-pro-latest', + apiKey: geminiApiKey, + safetySettings: safetySettings, + systemInstruction: Content.system(systemPrmpt)); - - const String systemPrmpt = """You are an AI tutor helping students understand topics with help of biometric data. You will be supplied with a json containing data extracted from an EEG device, use that data to modify your approach and help the student learn more effectively. - Use language: POLISH -Write the response in markdown and split it into two parts: -State analysis: describe what is the state of the student and how to best approach them -Tutor response: continue with the lesson, respond to answers, etc"""; - - final model = GenerativeModel( - model: 'gemini-1.5-pro-latest', - apiKey: geminiApiKey, - safetySettings: safetySettings, - systemInstruction: Content.system(systemPrmpt) - ); - - try { - final chat = model.startChat(history: messagesWithoutPrompt); - final stream = chat.sendMessageStream( - Content.text("EEG DATA:\n${eegState.getJsonString()}\nPytanie:\n$prompt") + Message lessonScriptMessage = Message( + text: prompt, + type: MessageType.lessonScript, + source: MessageSource.agent, ); - String responseText = ''; - - await for (final chunk in stream) { - responseText += chunk.text ?? ''; - emit(GeminiState( - status: GeminiStatus.success, - messages: messagesWithPrompt + [Content.model([TextPart(responseText)])], + GeminiState initialState = GeminiState( + status: GeminiStatus.loading, error: '', + messages: [lessonScriptMessage], + quizQuestions: quizQuestions, + isQuizMode: false, + model: model); + emit(initialState); + + try { + final chat = state.model!.startChat(history: [Content.text(prompt)]); + final stream = chat.sendMessageStream(Content.text( + "EEG DATA:\n${eegState.getJsonString()}\nPytanie:\n$prompt")); + + String responseText = ''; + + await for (final chunk in stream) { + responseText += chunk.text ?? ''; + emit(initialState.copyWith( + status: GeminiStatus.success, + messages: [ + lessonScriptMessage, + Message( + source: MessageSource.agent, + text: responseText, + type: MessageType.text) + ], + model: model)); + } + } catch (e) { + emit(GeminiState( + status: GeminiStatus.error, + messages: state.messages, + error: e.toString(), )); } - } catch (e) { - emit(GeminiState( - status: GeminiStatus.error, - messages: messagesWithPrompt, - error: e.toString(), - )); + + // enterQuizMode(); + + // sendMessage(prompt, eegState); + } + + void sendMessage(String prompt, EegState eegState) async { + List messagesWithoutPrompt = state.messages; + var messagesWithPrompt = state.messages + + [ + Message( + text: prompt, type: MessageType.text, source: MessageSource.user) + ]; + + emit(state.copyWith( + status: GeminiStatus.loading, + messages: messagesWithPrompt, + )); + + try { + final chat = state.model!.startChat( + history: messagesWithoutPrompt + .map((mess) => mess.toGeminiContent()) + .toList()); + final stream = chat.sendMessageStream(Content.text( + "EEG DATA:\n${eegState.getJsonString()}\nWiadomość od ucznia:\n$prompt")); + + String responseText = ''; + + await for (final chunk in stream) { + responseText += chunk.text ?? ''; + emit(state.copyWith( + status: GeminiStatus.success, + messages: messagesWithPrompt + + [ + Message( + source: MessageSource.agent, + text: responseText, + type: MessageType.text) + ])); + } + + if (responseText.contains("")) { + enterQuizMode(); + } + } catch (e) { + emit(GeminiState( + status: GeminiStatus.error, + messages: messagesWithPrompt, + error: e.toString(), + )); + } + } + + void passAnswerToGemini(int answer) async { + final quizQuestion = state.quizQuestions![state.currentQuizIndex]; + + final answerMessage = Message( + text: quizQuestion.options[answer], + type: MessageType.quizAnswer, + source: MessageSource.user, + quizOptions: quizQuestion.options, + correctAnswer: quizQuestion.correctAnswer, + ); + + final List updatedMessages = [ + ...state.messages, + answerMessage, + ]; + + emit(state.copyWith(messages: updatedMessages)); + + askNextQuizQuestion(); + } + + Future> loadQuizQuestions() async { + final String quizJson = + await rootBundle.loadString('assets/lessons/rjp.json'); + final List quizData = json.decode(quizJson); + + return quizData + .map((question) => QuizQuestion( + question: question['question'], + options: List.from(question['options']), + correctAnswer: question['correctAnswer'], + )) + .toList(); + } + + void enterQuizMode() async { + if (state.isQuizMode) return; // Prevent re-entering quiz mode + askNextQuizQuestion(); + } + + void askNextQuizQuestion() { + var currentQuizIndex = state.currentQuizIndex + 1; + final quizQuestion = state.quizQuestions![currentQuizIndex]; + + final quizQuestionMessage = Message( + text: quizQuestion.question, + type: MessageType.quizQuestion, + source: MessageSource.agent, + quizOptions: quizQuestion.options, + correctAnswer: quizQuestion.correctAnswer, + ); + + final List updatedMessages = [ + ...state.messages, + quizQuestionMessage, + ]; + + emit(state.copyWith( + messages: updatedMessages, + isQuizMode: true, + currentQuizIndex: currentQuizIndex)); + } + + void checkAnswer(int answerIndex) { + passAnswerToGemini(answerIndex); } -} void resetConversation() { emit(GeminiState.initialState); } -} \ No newline at end of file +} diff --git a/lib/config.dart b/lib/config.dart index f1b065f..d208279 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -1,2 +1,2 @@ const String geminiApiKey = ''; -const bool isDebug = true; \ No newline at end of file +const bool isDebug = true; diff --git a/lib/screens/gemini_chat_screen.dart b/lib/screens/gemini_chat_screen.dart index 8f3bbed..3449ef4 100644 --- a/lib/screens/gemini_chat_screen.dart +++ b/lib/screens/gemini_chat_screen.dart @@ -30,29 +30,41 @@ class GeminiChat extends StatefulWidget { class GeminiChatState extends State { final _textController = TextEditingController(); + bool _quizMode = false; // Add this line -@override -void initState() { - super.initState(); - _startConversation(); -} + @override + void initState() { + super.initState(); + _startConversation(); + } -@override -void dispose() { - context.read().stopPolling(); - super.dispose(); -} + void _toggleQuizMode() { + setState(() { + _quizMode = !_quizMode; + }); + } + + void _checkAnswer(int answer) { + context.read().checkAnswer(answer); + } + + @override + void dispose() { + context.read().stopPolling(); + super.dispose(); + } void _startConversation() async { - final String rjp = await rootBundle.loadString('assets/lessons/rjp.md'); - print(rjp); - context.read().sendMessage("Jesteś nauczycielem/chatbotem prowadzącym zajęcia z jednym uczniem. Uczeń ma możliwość zadawania pytań w trakcie, natomiast jesteś odpowiedzialny za prowadzenie lekcji i przedstawienie tematu. Zacznij prowadzić lekcje dla jednego ucznia na podstawie poniszego skryptu:\n" + rjp, context.read().state); + context.read().startLesson(context.read().state); } void _sendMessage() async { - context.read().sendMessage(_textController.text, context.read().state); + context + .read() + .sendMessage(_textController.text, context.read().state); _textController.clear(); } + void _toggleEegState() { context.read().toggleState(); } @@ -62,35 +74,42 @@ void dispose() { _startConversation(); } + void _enterQuizMode() { + context.read().enterQuizMode(); + } + @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( - title: const Text('Gemini Pro Chat'), + title: BlocBuilder( + builder: (context, state) { + return Text(state.isQuizMode ? 'Quiz Mode' : 'Gemini Pro Chat'); + }, + ), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ - - BlocBuilder( - builder: (context, eegState) { - return Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Mind Wandering: ${eegState.mind_wandering.toStringAsFixed(2)}'), - Text('Focus: ${eegState.focus.toStringAsFixed(2)}'), - ], + BlocBuilder( + builder: (context, eegState) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mind Wandering: ${eegState.mind_wandering.toStringAsFixed(2)}'), + Text('Focus: ${eegState.focus.toStringAsFixed(2)}'), + ], + ), ), - ), - ); - }, - ), - + ); + }, + ), Expanded( child: BlocBuilder( builder: (context, state) { @@ -104,82 +123,143 @@ void dispose() { }, ), ), - TextField( - controller: _textController, - decoration: const InputDecoration( - hintText: 'Enter your message', - ), - onSubmitted: (_) => _sendMessage(), + BlocBuilder( + builder: (context, state) { + return state.isQuizMode + ? Container() // Hide text input in quiz mode + : TextField( + controller: _textController, + decoration: const InputDecoration( + hintText: 'Enter your message', + ), + onSubmitted: (_) => _sendMessage(), + ); + }, ), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: _sendMessage, - child: const Text('Send'), + Row( + children: [ + BlocBuilder( + builder: (context, state) { + return state.isQuizMode + ? Container() + : Expanded( + child: ElevatedButton( + onPressed: _sendMessage, + child: const Text('Send'), + ), + ); + }, ), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _resetConversation, - child: const Text('Reset'), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _toggleEegState, - child: const Text('Toggle State'), - ), - ], - ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _resetConversation, + child: const Text('Reset'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _toggleEegState, + child: const Text('Toggle State'), + ), + const SizedBox(width: 8), + BlocBuilder( + builder: (context, state) { + return state.isQuizMode + ? Container() + : ElevatedButton( + onPressed: _enterQuizMode, + child: const Text('Start Quiz'), + ); + }, + ), + ], + ), ], ), ), ); } -ListView buildChatList(GeminiState state, {bool loading = false}) { - return ListView.builder( - itemCount: state.messages.length + (loading ? 1 : 0), - itemBuilder: (context, index) { - if (index == state.messages.length && loading) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - alignment: Alignment.centerLeft, - child: BouncingDots(), + ListView buildChatList(GeminiState state, {bool loading = false}) { + return ListView.builder( + itemCount: state.messages.length + (loading ? 1 : 0), + itemBuilder: (context, index) { + if (index == state.messages.length && loading) { + return const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Align( + alignment: Alignment.centerLeft, + child: BouncingDots(), + ), ), - ), - ); - } - - final message = state.messages[index]; - - String text = ""; - for (var part in message.parts) { - if (part is TextPart) { - text += part.text; + ); } - } - return Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: message.role != 'user' - ? CrossAxisAlignment.start - : CrossAxisAlignment.end, - children: [ - MarkdownBody(data: text), - ], - ), - ), - ); - }, - ); -} -} + if (state.messages[index].type == MessageType.lessonScript) { + // skip + return Container(); + } + final message = state.messages[index]; + // String text = message.parts.whereType().map((part) => part.text).join(); + String text = message.text; + + if (message.type == MessageType.quizQuestion) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownBody(data: text), + ...message.quizOptions!.asMap().entries.map((entry) { + return ElevatedButton( + onPressed: () => {_checkAnswer(entry.key)}, + child: Text(entry.value), + ); + }), + ], + ), + ), + ); + } else if (message.type == MessageType.quizAnswer) { + bool correct = message.text == message.correctAnswer; + var text = Text( + correct + ? "Correct!" + : "Incorrect. The correct answer was: ${message.quizOptions![message.correctAnswer!]}", + style: TextStyle( + color: correct ? Colors.green : Colors.red, + fontWeight: FontWeight.bold, + ), + ); + + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [text], + ), + ), + ); + } else { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: message.source == MessageSource.agent + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + children: [MarkdownBody(data: text)], + ), + ), + ); + } + }, + ); + } +} class BouncingDots extends StatefulWidget { const BouncingDots({super.key}); @@ -188,7 +268,8 @@ class BouncingDots extends StatefulWidget { BouncingDotsState createState() => BouncingDotsState(); } -class BouncingDotsState extends State with TickerProviderStateMixin { +class BouncingDotsState extends State + with TickerProviderStateMixin { late List _controllers; late List> _animations; @@ -197,16 +278,16 @@ class BouncingDotsState extends State with TickerProviderStateMixi super.initState(); _controllers = List.generate( 3, - (index) => AnimationController( + (index) => AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ), ); - _animations = _controllers.map((controller) => - Tween(begin: 0, end: -10).animate( - CurvedAnimation(parent: controller, curve: Curves.easeInOut), - ) - ).toList(); + _animations = _controllers + .map((controller) => Tween(begin: 0, end: -10).animate( + CurvedAnimation(parent: controller, curve: Curves.easeInOut), + )) + .toList(); for (var i = 0; i < 3; i++) { Future.delayed(Duration(milliseconds: i * 180), () { diff --git a/pubspec.yaml b/pubspec.yaml index 71af876..801eacb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ flutter: assets: - assets/lessons/rjp.md + - assets/lessons/rjp.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware