Quiz fixes

This commit is contained in:
Dawid Pietrykowski 2024-08-03 22:51:03 +02:00
parent bc088720ee
commit 7158824ffa
8 changed files with 557 additions and 201 deletions

33
assets/lessons/rjp.json Normal file
View File

@ -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
}
]

View File

@ -2,9 +2,6 @@
**Wstęp:** **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:** **Definicja ruchu jednostajnie przyspieszanego:**
* Ruch jednostajnie przyspieszony to ruch, w którym przyspieszenie obiektu jest stałe. * Ruch jednostajnie przyspieszony to ruch, w którym przyspieszenie obiektu jest stałe.

3
devtools_options.yaml Normal file
View File

@ -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:

View File

@ -18,11 +18,10 @@ class EegState {
} }
} }
class EegCubit extends Cubit<EegState> { class EegCubit extends Cubit<EegState> {
EegCubit() : super(EegState(mind_wandering: 0.9, focus: 0.1)) { EegCubit() : super(EegState(mind_wandering: 0.9, focus: 0.1)) {
// Start the timer when the cubit is created // Start the timer when the cubit is created
if (isDebug) { if (!isDebug) {
startPolling(); startPolling();
} }
} }
@ -38,46 +37,44 @@ class EegCubit extends Cubit<EegState> {
// double newFocus = 1 - newMindWandering; // double newFocus = 1 - newMindWandering;
fetchEegData().then((data) { fetchEegData().then((data) {
double newMindWandering = data[0]; double newMindWandering = data[0];
double newFocus = data[1]; double newFocus = data[1];
// Update the state with the new EEG data // Update the state with the new EEG data
updateEegData(newMindWandering, newFocus); updateEegData(newMindWandering, newFocus);
}); });
// updateEegData(newMindWandering, newFocus); // updateEegData(newMindWandering, newFocus);
}); });
} }
Future<List<double>> fetchEegData() async {
Future<List<double>> fetchEegData() async { if (isDebug) {
if (isDebug) { return [0.9, 0.1]; // Placeholder ret
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<String> 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');
}
}
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<String> 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() { void stopPolling() {
_timer?.cancel(); _timer?.cancel();

View File

@ -1,102 +1,346 @@
import 'dart:convert';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gemini_app/config.dart'; import 'package:gemini_app/config.dart';
import 'package:gemini_app/bloc/eeg_state.dart'; import 'package:gemini_app/bloc/eeg_state.dart';
import 'package:google_generative_ai/google_generative_ai.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 <QUIZ_START_TOKEN> at the start of your response
Write the response in markdown and split it into two parts (include the tokens):
optional: <QUIZ_START_TOKEN>
<ANALYSIS_START_TOKEN>
here describe what is the state of the student and how to best approach them
<LESSON_START_TOKEN>
here continue with the lesson, respond to answers, etc
""";
enum GeminiStatus { initial, loading, success, error } enum GeminiStatus { initial, loading, success, error }
enum MessageType { text, image, audio, video }
// enum MessageType { text, image, audio, video }
enum MessageSource { user, agent } enum MessageSource { user, agent }
class QuizMessage {
final String content;
final List<String> 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 { class Message {
final String text; final String text;
final MessageType type; final MessageType type;
final MessageSource source; final MessageSource source;
final List<String>? quizOptions; // Add this for ABCD options
final int? correctAnswer; // Add this for the correct answer index
Message({ Message({
required this.text, required this.text,
required this.type, required this.type,
required this.source, 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<String> options;
int correctAnswer;
QuizQuestion({
required this.question,
required this.options,
required this.correctAnswer,
}); });
} }
class GeminiState { class GeminiState {
GeminiStatus status; GeminiStatus status;
String error; String error;
List<Content> messages; List<Message> messages;
List<QuizQuestion>? quizQuestions;
bool isQuizMode;
int currentQuizIndex;
GenerativeModel? model;
GeminiState({ GeminiState(
required this.status, {required this.status,
required this.error, this.error = '',
this.messages = const [], this.messages = const [],
}); this.quizQuestions,
this.isQuizMode = false,
this.currentQuizIndex = -1,
this.model});
GeminiState copyWith({
GeminiStatus? status,
String? error,
List<Message>? messages,
List<QuizQuestion>? 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( static GeminiState get initialState => GeminiState(
status: GeminiStatus.initial, status: GeminiStatus.initial,
// messages: [Message(text: "Hello, I'm Gemini Pro. How can I help you?", type: MessageType.text, source: MessageSource.agent)], // 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?")])], messages: [
error: '', // Message.fromGeminiContent(Content.model(
); // [TextPart("Hello, I'm Gemini Pro. How can I help you?")]))
],
error: '',
);
} }
class GeminiCubit extends Cubit<GeminiState> { class GeminiCubit extends Cubit<GeminiState> {
GeminiCubit() : super(GeminiState.initialState); GeminiCubit() : super(GeminiState.initialState);
void sendMessage(String prompt, EegState eegState) async { void startLesson(EegState eegState) async {
var messagesWithoutPrompt = state.messages; final quizQuestions = await loadQuizQuestions();
var messagesWithPrompt = state.messages + [ final String rjp = await rootBundle.loadString('assets/lessons/rjp.md');
Content.text(prompt) 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 = [
final safetySettings = [ SafetySetting(HarmCategory.harassment, HarmBlockThreshold.none),
SafetySetting(HarmCategory.harassment, HarmBlockThreshold.none), SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.none),
SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.none), SafetySetting(HarmCategory.sexuallyExplicit, HarmBlockThreshold.none),
SafetySetting(HarmCategory.sexuallyExplicit, HarmBlockThreshold.none), SafetySetting(HarmCategory.dangerousContent, HarmBlockThreshold.none),
SafetySetting(HarmCategory.dangerousContent, HarmBlockThreshold.none), ];
// SafetySetting(HarmCategory.unspecified, 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));
Message lessonScriptMessage = Message(
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. text: prompt,
Use language: POLISH type: MessageType.lessonScript,
Write the response in markdown and split it into two parts: source: MessageSource.agent,
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")
); );
String responseText = ''; GeminiState initialState = GeminiState(
status: GeminiStatus.loading,
await for (final chunk in stream) {
responseText += chunk.text ?? '';
emit(GeminiState(
status: GeminiStatus.success,
messages: messagesWithPrompt + [Content.model([TextPart(responseText)])],
error: '', 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( // enterQuizMode();
status: GeminiStatus.error,
messages: messagesWithPrompt, // sendMessage(prompt, eegState);
error: e.toString(), }
));
void sendMessage(String prompt, EegState eegState) async {
List<Message> 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("<QUIZ_START_TOKEN>")) {
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<Message> updatedMessages = [
...state.messages,
answerMessage,
];
emit(state.copyWith(messages: updatedMessages));
askNextQuizQuestion();
}
Future<List<QuizQuestion>> loadQuizQuestions() async {
final String quizJson =
await rootBundle.loadString('assets/lessons/rjp.json');
final List<dynamic> quizData = json.decode(quizJson);
return quizData
.map((question) => QuizQuestion(
question: question['question'],
options: List<String>.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<Message> updatedMessages = [
...state.messages,
quizQuestionMessage,
];
emit(state.copyWith(
messages: updatedMessages,
isQuizMode: true,
currentQuizIndex: currentQuizIndex));
}
void checkAnswer(int answerIndex) {
passAnswerToGemini(answerIndex);
} }
}
void resetConversation() { void resetConversation() {
emit(GeminiState.initialState); emit(GeminiState.initialState);

View File

@ -30,29 +30,41 @@ class GeminiChat extends StatefulWidget {
class GeminiChatState extends State<GeminiChat> { class GeminiChatState extends State<GeminiChat> {
final _textController = TextEditingController(); final _textController = TextEditingController();
bool _quizMode = false; // Add this line
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_startConversation(); _startConversation();
} }
@override void _toggleQuizMode() {
void dispose() { setState(() {
context.read<EegCubit>().stopPolling(); _quizMode = !_quizMode;
super.dispose(); });
} }
void _checkAnswer(int answer) {
context.read<GeminiCubit>().checkAnswer(answer);
}
@override
void dispose() {
context.read<EegCubit>().stopPolling();
super.dispose();
}
void _startConversation() async { void _startConversation() async {
final String rjp = await rootBundle.loadString('assets/lessons/rjp.md'); context.read<GeminiCubit>().startLesson(context.read<EegCubit>().state);
print(rjp);
context.read<GeminiCubit>().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<EegCubit>().state);
} }
void _sendMessage() async { void _sendMessage() async {
context.read<GeminiCubit>().sendMessage(_textController.text, context.read<EegCubit>().state); context
.read<GeminiCubit>()
.sendMessage(_textController.text, context.read<EegCubit>().state);
_textController.clear(); _textController.clear();
} }
void _toggleEegState() { void _toggleEegState() {
context.read<EegCubit>().toggleState(); context.read<EegCubit>().toggleState();
} }
@ -62,35 +74,42 @@ void dispose() {
_startConversation(); _startConversation();
} }
void _enterQuizMode() {
context.read<GeminiCubit>().enterQuizMode();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
title: const Text('Gemini Pro Chat'), title: BlocBuilder<GeminiCubit, GeminiState>(
builder: (context, state) {
return Text(state.isQuizMode ? 'Quiz Mode' : 'Gemini Pro Chat');
},
),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
BlocBuilder<EegCubit, EegState>(
BlocBuilder<EegCubit, EegState>( builder: (context, eegState) {
builder: (context, eegState) { return Card(
return Card( child: Padding(
child: Padding( padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text('Mind Wandering: ${eegState.mind_wandering.toStringAsFixed(2)}'), 'Mind Wandering: ${eegState.mind_wandering.toStringAsFixed(2)}'),
Text('Focus: ${eegState.focus.toStringAsFixed(2)}'), Text('Focus: ${eegState.focus.toStringAsFixed(2)}'),
], ],
),
), ),
), );
); },
}, ),
),
Expanded( Expanded(
child: BlocBuilder<GeminiCubit, GeminiState>( child: BlocBuilder<GeminiCubit, GeminiState>(
builder: (context, state) { builder: (context, state) {
@ -104,82 +123,143 @@ void dispose() {
}, },
), ),
), ),
TextField( BlocBuilder<GeminiCubit, GeminiState>(
controller: _textController, builder: (context, state) {
decoration: const InputDecoration( return state.isQuizMode
hintText: 'Enter your message', ? Container() // Hide text input in quiz mode
), : TextField(
onSubmitted: (_) => _sendMessage(), controller: _textController,
decoration: const InputDecoration(
hintText: 'Enter your message',
),
onSubmitted: (_) => _sendMessage(),
);
},
), ),
Row( Row(
children: [ children: [
Expanded( BlocBuilder<GeminiCubit, GeminiState>(
child: ElevatedButton( builder: (context, state) {
onPressed: _sendMessage, return state.isQuizMode
child: const Text('Send'), ? Container()
: Expanded(
child: ElevatedButton(
onPressed: _sendMessage,
child: const Text('Send'),
),
);
},
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), ElevatedButton(
ElevatedButton( onPressed: _resetConversation,
onPressed: _resetConversation, child: const Text('Reset'),
child: const Text('Reset'), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), ElevatedButton(
ElevatedButton( onPressed: _toggleEegState,
onPressed: _toggleEegState, child: const Text('Toggle State'),
child: const Text('Toggle State'), ),
), const SizedBox(width: 8),
], BlocBuilder<GeminiCubit, GeminiState>(
), builder: (context, state) {
return state.isQuizMode
? Container()
: ElevatedButton(
onPressed: _enterQuizMode,
child: const Text('Start Quiz'),
);
},
),
],
),
], ],
), ),
), ),
); );
} }
ListView buildChatList(GeminiState state, {bool loading = false}) { ListView buildChatList(GeminiState state, {bool loading = false}) {
return ListView.builder( return ListView.builder(
itemCount: state.messages.length + (loading ? 1 : 0), itemCount: state.messages.length + (loading ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == state.messages.length && loading) { if (index == state.messages.length && loading) {
return Card( return const Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: BouncingDots(), child: BouncingDots(),
),
), ),
), );
);
}
final message = state.messages[index];
String text = "";
for (var part in message.parts) {
if (part is TextPart) {
text += part.text;
} }
}
return Card( if (state.messages[index].type == MessageType.lessonScript) {
child: Padding( // skip
padding: const EdgeInsets.all(8.0), return Container();
child: Column( }
crossAxisAlignment: message.role != 'user'
? CrossAxisAlignment.start
: CrossAxisAlignment.end,
children: [
MarkdownBody(data: text),
],
),
),
);
},
);
}
}
final message = state.messages[index];
// String text = message.parts.whereType<TextPart>().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 { class BouncingDots extends StatefulWidget {
const BouncingDots({super.key}); const BouncingDots({super.key});
@ -188,7 +268,8 @@ class BouncingDots extends StatefulWidget {
BouncingDotsState createState() => BouncingDotsState(); BouncingDotsState createState() => BouncingDotsState();
} }
class BouncingDotsState extends State<BouncingDots> with TickerProviderStateMixin { class BouncingDotsState extends State<BouncingDots>
with TickerProviderStateMixin {
late List<AnimationController> _controllers; late List<AnimationController> _controllers;
late List<Animation<double>> _animations; late List<Animation<double>> _animations;
@ -197,16 +278,16 @@ class BouncingDotsState extends State<BouncingDots> with TickerProviderStateMixi
super.initState(); super.initState();
_controllers = List.generate( _controllers = List.generate(
3, 3,
(index) => AnimationController( (index) => AnimationController(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
vsync: this, vsync: this,
), ),
); );
_animations = _controllers.map((controller) => _animations = _controllers
Tween<double>(begin: 0, end: -10).animate( .map((controller) => Tween<double>(begin: 0, end: -10).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut), CurvedAnimation(parent: controller, curve: Curves.easeInOut),
) ))
).toList(); .toList();
for (var i = 0; i < 3; i++) { for (var i = 0; i < 3; i++) {
Future.delayed(Duration(milliseconds: i * 180), () { Future.delayed(Duration(milliseconds: i * 180), () {

View File

@ -70,6 +70,7 @@ flutter:
assets: assets:
- assets/lessons/rjp.md - assets/lessons/rjp.md
- assets/lessons/rjp.json
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware