diff --git a/assets/lessons/lessons.json b/assets/lessons/lessons.json new file mode 100644 index 0000000..be9ac19 --- /dev/null +++ b/assets/lessons/lessons.json @@ -0,0 +1,63 @@ +[ + { + "id": "rjp", + "title": "Uniformly accelerated motion", + "content": "This lesson covers the basics of uniformly accelerated motion, including the equations for displacement, velocity, and acceleration." + }, + { + "id": "cells", + "title": "Cells", + "content": "This lesson covers the structure and function of cells, including the cell membrane, cytoplasm, and organelles." + }, + { + "id": "pw", + "title": "Photosynthesis and Respiration", + "content": "This lesson explores the processes of photosynthesis and respiration, detailing how plants convert light energy into chemical energy and how both plants and animals use respiration for energy." + }, + { + "id": "thermodynamics", + "title": "Thermodynamics", + "content": "This lesson introduces the laws of thermodynamics and their applications in energy transfer, heat engines, and entropy." + }, + { + "id": "newton", + "title": "Newton's Laws of Motion", + "content": "This lesson discusses Newton's three laws of motion and their significance in understanding the behavior of objects in motion." + }, + { + "id": "genetics", + "title": "Genetics", + "content": "This lesson covers the basics of genetics, including DNA structure, gene expression, and inheritance patterns." + }, + { + "id": "waves", + "title": "Waves and Sound", + "content": "This lesson explores the properties of waves, including frequency, wavelength, and amplitude, as well as the principles of sound and hearing." + }, + { + "id": "electricity", + "title": "Electricity and Magnetism", + "content": "This lesson covers the fundamentals of electricity and magnetism, including electric circuits, Ohm's law, and electromagnetic waves." + }, + { + "id": "chemistry", + "title": "Introduction to Chemistry", + "content": "This lesson provides an overview of basic chemistry concepts, such as atoms, molecules, chemical bonds, and reactions." + }, + { + "id": "ecology", + "title": "Ecology and Ecosystems", + "content": "This lesson discusses the interactions between living organisms and their environments, including food webs, energy flow, and ecosystem dynamics." + }, + { + "id": "astronomy", + "title": "Introduction to Astronomy", + "content": "This lesson introduces the study of astronomy, including the solar system, stars, galaxies, and the universe." + }, + { + "id": "geology", + "title": "Geology and Earth Science", + "content": "This lesson provides an overview of geology, including the structure of the Earth, rock formation, plate tectonics, and natural hazards." + } +] + \ No newline at end of file diff --git a/lib/bloc/gemini_state.dart b/lib/bloc/gemini_state.dart index 10fa258..63eacdd 100644 --- a/lib/bloc/gemini_state.dart +++ b/lib/bloc/gemini_state.dart @@ -10,8 +10,8 @@ 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. -Student is 15 years old. +Keep the analysis short but the lesson can be as long as needed. +Student is 15 years old. You can only interact using text, no videos, images, or audio. 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 @@ -31,7 +31,7 @@ enum GeminiStatus { initial, loading, success, error } // enum MessageType { text, image, audio, video } -enum MessageSource { user, agent, app} +enum MessageSource { user, agent, app } class QuizMessage { final String content; @@ -97,14 +97,18 @@ class Message { return Content.text(text); case MessageType.quizQuestion: String question = text; - List options = quizOptions!.map((option) => option.trim()).toList(); + List options = + quizOptions!.map((option) => option.trim()).toList(); String answer = options[correctAnswer!]; - String formattedQuestion = "$question\n\nOptions:\n${options.map((option) => "- $option").join('\n')}\n\nCorrect Answer: $answer"; + String formattedQuestion = + "$question\n\nOptions:\n${options.map((option) => "- $option").join('\n')}\n\nCorrect Answer: $answer"; return Content.model([TextPart(formattedQuestion)]); case MessageType.quizAnswer: String expectedAnswer = quizOptions![correctAnswer!]; bool userCorrect = expectedAnswer == text; - String result = userCorrect ? "User answered correctly with: $text" : "User answered incorrectly with: $text instead of $expectedAnswer"; + String result = userCorrect + ? "User answered correctly with: $text" + : "User answered incorrectly with: $text instead of $expectedAnswer"; return Content.text(result); default: throw UnsupportedError('Unsupported message type'); @@ -132,6 +136,7 @@ class GeminiState { bool isQuizMode; int currentQuizIndex; GenerativeModel? model; + String? lessonId; GeminiState( {required this.status, @@ -140,7 +145,8 @@ class GeminiState { this.quizQuestions, this.isQuizMode = false, this.currentQuizIndex = -1, - this.model}); + this.model, + this.lessonId}); GeminiState copyWith({ GeminiStatus? status, @@ -150,6 +156,7 @@ class GeminiState { bool? isQuizMode, int? currentQuizIndex, GenerativeModel? model, + String? lessonId, }) { return GeminiState( status: status ?? this.status, @@ -159,6 +166,7 @@ class GeminiState { isQuizMode: isQuizMode ?? this.isQuizMode, currentQuizIndex: currentQuizIndex ?? this.currentQuizIndex, model: model ?? this.model, + lessonId: lessonId ?? this.lessonId, ); } @@ -172,10 +180,10 @@ class GeminiState { class GeminiCubit extends Cubit { GeminiCubit() : super(GeminiState.initialState); - void startLesson() async { - final quizQuestions = await loadQuizQuestions(); + void startLesson(String lessonId) async { + final quizQuestions = await loadQuizQuestions(lessonId); final String lessonScript = - await rootBundle.loadString('assets/lessons/cells.md'); + await rootBundle.loadString('assets/lessons/$lessonId.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"; final String prompt = @@ -206,38 +214,11 @@ class GeminiCubit extends Cubit { messages: [lessonScriptMessage], quizQuestions: quizQuestions, isQuizMode: false, - model: model); + model: model, + lessonId: lessonId); emit(initialState); sendMessage(""); - - // try { - // final chat = state.model!.startChat(history: [Content.text(prompt)]); - // final stream = chat.sendMessageStream(Content.text( - // "EEG DATA:\n${GetIt.instance().state.getJsonString()}\nMessage:\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(), - // )); - // } } void sendMessage(String prompt) async { @@ -261,11 +242,9 @@ class GeminiCubit extends Cubit { )); try { - final chatHistory = messagesWithoutPrompt - .map((mess) => mess.toGeminiContent()) - .toList(); - final chat = state.model!.startChat( - history: chatHistory); + final chatHistory = + messagesWithoutPrompt.map((mess) => mess.toGeminiContent()).toList(); + final chat = state.model!.startChat(history: chatHistory); final stream = chat.sendMessageStream(Content.text( "EEG DATA:\n${GetIt.instance().state.getJsonString()}\nUser message:\n$prompt")); @@ -298,10 +277,10 @@ class GeminiCubit extends Cubit { } } - if (responseText.contains(QUIZ_START_TOKEN) || analysisData.contains(QUIZ_START_TOKEN)) { - emit(state.copyWith( - status: GeminiStatus.success, - messages: messagesWithPrompt)); + if (responseText.contains(QUIZ_START_TOKEN) || + analysisData.contains(QUIZ_START_TOKEN)) { + emit(state.copyWith( + status: GeminiStatus.success, messages: messagesWithPrompt)); enterQuizMode(); } } catch (e) { @@ -334,9 +313,9 @@ class GeminiCubit extends Cubit { askNextQuizQuestion(); } - Future> loadQuizQuestions() async { + Future> loadQuizQuestions(String lessonId) async { final String quizJson = - await rootBundle.loadString('assets/lessons/cells.json'); + await rootBundle.loadString('assets/lessons/$lessonId.json'); final List quizData = json.decode(quizJson); return quizData @@ -356,19 +335,22 @@ class GeminiCubit extends Cubit { void askNextQuizQuestion() { var currentQuizIndex = state.currentQuizIndex + 1; - if (currentQuizIndex >= state.quizQuestions!.length) { - // if (currentQuizIndex >= 2) { + if (currentQuizIndex >= state.quizQuestions!.length || + currentQuizIndex >= 2) { + // if (currentQuizIndex >= 2) { List messagesWithPrompt = state.messages + - [ - Message( - text: "Quiz is over. Write a summary of user's performance.", - type: MessageType.text, - source: MessageSource.app) - ]; + [ + Message( + text: "Quiz is over. Write a summary of user's performance.", + type: MessageType.text, + source: MessageSource.app) + ]; // Quiz is over - emit(state.copyWith(isQuizMode: false, currentQuizIndex: 0, - messages: messagesWithPrompt)); + emit(state.copyWith( + isQuizMode: false, + currentQuizIndex: 0, + messages: messagesWithPrompt)); // Send a message to Gemini to end the quiz sendMessage(""); @@ -398,7 +380,6 @@ class GeminiCubit extends Cubit { } void checkAnswer(int answerIndex) { - print("checkAnswer $answerIndex"); passAnswerToGemini(answerIndex); } diff --git a/lib/lesson.dart b/lib/lesson.dart new file mode 100644 index 0000000..14a7c6a --- /dev/null +++ b/lib/lesson.dart @@ -0,0 +1,15 @@ +class Lesson { + final String id; + final String title; + final String content; + + Lesson({required this.id, required this.title, required this.content}); + + factory Lesson.fromJson(Map json) { + return Lesson( + id: json['id'], + title: json['title'], + content: json['content'], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 5ad8dee..bf42256 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:gemini_app/eeg/eeg_service.dart'; -import 'package:gemini_app/screens/gemini_chat_screen.dart'; +import 'package:gemini_app/screens/eeg_calibration_screen.dart'; import 'package:get_it/get_it.dart'; void main() { @@ -11,119 +11,36 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'MindEasy', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const GeminiScreen(), + home: const MindWanderScreen(), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); +Route createSmoothRoute(Widget page) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + final tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: Curves.easeInOut)); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + transitionDuration: + Duration(milliseconds: 300), // Adjust the duration to your preference + reverseTransitionDuration: + Duration(milliseconds: 300), // Adjust for reverse transition too + ); } diff --git a/lib/screens/eeg_calibration_screen.dart b/lib/screens/eeg_calibration_screen.dart new file mode 100644 index 0000000..e3f1de1 --- /dev/null +++ b/lib/screens/eeg_calibration_screen.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:gemini_app/main.dart'; +import 'package:gemini_app/screens/lesson_list_screen.dart'; + +class MindWanderScreen extends StatefulWidget { + const MindWanderScreen({super.key}); + + @override + MindWanderScreenState createState() => MindWanderScreenState(); +} + +class MindWanderScreenState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + int _secondsRemaining = 60; + Timer? _timer; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); + + // _startTimer(); + } + + void _startTimer() { + setState(() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_secondsRemaining > 0) { + setState(() { + _secondsRemaining--; + }); + } else { + _timer!.cancel(); + // Perform any action when the timer reaches zero + _skipCalibration(); + } + }); + }); + } + + @override + void dispose() { + _controller.dispose(); + _timer?.cancel(); + super.dispose(); + } + + void _skipCalibration() { + Navigator.push(context, createSmoothRoute(LessonListScreen())); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('EEG Calibration'), + ), + body: Stack( + children: [ + // Background Animation + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Opacity( + opacity: _animation.value, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.withOpacity(0.5), + Colors.green.withOpacity(0.5), + ], + transform: + GradientRotation(_animation.value * 2 * 3.1415926535), + ), + ), + ), + ); + }, + ), + // Main Content + Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + """When you're ready, press the start button and start mind wandering by letting your thoughts drift to your favorite places or stories while you sit quietly""", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + _timer == null + ? ElevatedButton( + onPressed: _startTimer, + child: const Text('Start'), + ) + : Text( + 'Time remaining: $_secondsRemaining seconds', + style: const TextStyle(fontSize: 18), + ), + ], + ), + ), + ), + Positioned( + bottom: 16.0, + right: 16.0, + child: ElevatedButton( + onPressed: _skipCalibration, + child: const Text('Skip'), + ), + ), + ], + ), + ); + } +} + +void main() { + runApp(MaterialApp( + home: MindWanderScreen(), + )); +} diff --git a/lib/screens/gemini_chat_screen.dart b/lib/screens/gemini_chat_screen.dart index d07d85e..596414e 100644 --- a/lib/screens/gemini_chat_screen.dart +++ b/lib/screens/gemini_chat_screen.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:gemini_app/bloc/gemini_state.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gemini_app/config.dart'; import 'package:gemini_app/eeg/eeg_service.dart'; import 'package:get_it/get_it.dart'; class GeminiScreen extends StatelessWidget { - const GeminiScreen({super.key}); + const GeminiScreen({super.key, required this.lessonId}); + + final String lessonId; @override Widget build(BuildContext context) { @@ -14,13 +17,17 @@ class GeminiScreen extends StatelessWidget { providers: [ BlocProvider(create: (context) => GeminiCubit()), ], - child: const GeminiChat(), + child: GeminiChat( + lessonId: lessonId, + ), ); } } class GeminiChat extends StatefulWidget { - const GeminiChat({super.key}); + const GeminiChat({super.key, required this.lessonId}); + + final String lessonId; @override GeminiChatState createState() => GeminiChatState(); @@ -54,7 +61,7 @@ class GeminiChatState extends State { } void _startConversation() async { - context.read().startLesson(); + context.read().startLesson(widget.lessonId); } void _sendMessage() async { @@ -93,20 +100,22 @@ class GeminiChatState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Mind Wandering: ${_eegService.state.mindWandering.toStringAsFixed(2)}'), - Text( - 'Focus: ${_eegService.state.focus.toStringAsFixed(2)}'), - ], - ), - ), - ), + isDebug + ? Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mind Wandering: ${_eegService.state.mindWandering.toStringAsFixed(2)}'), + Text( + 'Focus: ${_eegService.state.focus.toStringAsFixed(2)}'), + ], + ), + ), + ) + : Container(), const SizedBox(height: 16), Expanded( child: BlocBuilder( @@ -145,30 +154,6 @@ class GeminiChatState extends State { const SizedBox(height: 16), Row( children: [ - // BlocBuilder( - // builder: (context, state) { - // return state.isQuizMode - // ? Container() - // : Expanded( - // child: ElevatedButton( - // onPressed: _sendMessage, - // child: const Text('Send'), - // ), - // ); - // }, - // ), - ElevatedButton( - onPressed: _sendMessage, - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.send), - SizedBox(width: 3), - Text('Send'), - ], - ), - ), - ElevatedButton( onPressed: _resetConversation, child: const Row( @@ -181,27 +166,29 @@ class GeminiChatState extends State { ), ), ElevatedButton( - onPressed: _toggleEegState, + onPressed: _sendMessage, child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.toggle_on), + Icon(Icons.send), SizedBox(width: 3), - Text('Toggle State'), + Text('Send'), ], ), ), - // ElevatedButton( - // onPressed: _enterQuizMode, - // child: const Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Icon(Icons.quiz), - // SizedBox(width: 8), - // Text('Start Quiz'), - // ], - // ), - // ), + isDebug + ? ElevatedButton( + onPressed: _toggleEegState, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.toggle_on), + SizedBox(width: 3), + Text('Toggle State'), + ], + ), + ) + : Container(), ], ), ], @@ -226,8 +213,8 @@ class GeminiChatState extends State { ); } - if (state.messages[index].type == MessageType.lessonScript || - state.messages[index].source == MessageSource.app) { + if (state.messages[index].type == MessageType.lessonScript || + state.messages[index].source == MessageSource.app) { // skip return Container(); } @@ -255,7 +242,8 @@ class GeminiChatState extends State { ), ); } else if (message.type == MessageType.quizAnswer) { - bool correct = message.text == message.quizOptions![message.correctAnswer!]; + bool correct = + message.text == message.quizOptions![message.correctAnswer!]; var text = Text( correct ? "Correct!" diff --git a/lib/screens/lesson_list_screen.dart b/lib/screens/lesson_list_screen.dart new file mode 100644 index 0000000..ad882b2 --- /dev/null +++ b/lib/screens/lesson_list_screen.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gemini_app/lesson.dart'; +import 'package:gemini_app/main.dart'; +import 'package:gemini_app/screens/gemini_chat_screen.dart'; + +class LessonListScreen extends StatefulWidget { + @override + _LessonListScreenState createState() => _LessonListScreenState(); +} + +class _LessonListScreenState extends State { + List lessons = []; + + @override + void initState() { + super.initState(); + loadLessons(); + } + + Future loadLessons() async { + final String response = + await rootBundle.loadString('assets/lessons/lessons.json'); + final List data = json.decode(response); + setState(() { + lessons = data.map((json) => Lesson.fromJson(json)).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Available Lessons'), + ), + body: ListView.separated( + itemCount: lessons.length, + itemBuilder: (context, index) { + final lesson = lessons[index]; + return ListTile( + title: Text(lesson.title), + subtitle: Text(lesson.content), + onTap: () { + Navigator.push( + context, + createSmoothRoute( + GeminiScreen(lessonId: lesson.id.toString()), + ), + ); + }, + ); + }, + separatorBuilder: (context, index) { + return Divider(); // or any other widget you want to use as a separator + }, + ), + ); + } +}