Fixed transitions, added baseline screen, added list screen

This commit is contained in:
Dawid Pietrykowski 2024-08-09 00:18:15 +02:00
parent b04585e2e2
commit 55325b260d
7 changed files with 384 additions and 224 deletions

View File

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

View File

@ -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 <QUIZ_START_TOKEN> 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<String> options = quizOptions!.map((option) => option.trim()).toList();
List<String> 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<GeminiState> {
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<GeminiState> {
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<EegService>().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<GeminiState> {
));
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<EegService>().state.getJsonString()}\nUser message:\n$prompt"));
@ -298,10 +277,10 @@ class GeminiCubit extends Cubit<GeminiState> {
}
}
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<GeminiState> {
askNextQuizQuestion();
}
Future<List<QuizQuestion>> loadQuizQuestions() async {
Future<List<QuizQuestion>> loadQuizQuestions(String lessonId) async {
final String quizJson =
await rootBundle.loadString('assets/lessons/cells.json');
await rootBundle.loadString('assets/lessons/$lessonId.json');
final List<dynamic> quizData = json.decode(quizJson);
return quizData
@ -356,19 +335,22 @@ class GeminiCubit extends Cubit<GeminiState> {
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<Message> 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<GeminiState> {
}
void checkAnswer(int answerIndex) {
print("checkAnswer $answerIndex");
passAnswerToGemini(answerIndex);
}

15
lib/lesson.dart Normal file
View File

@ -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<String, dynamic> json) {
return Lesson(
id: json['id'],
title: json['title'],
content: json['content'],
);
}
}

View File

@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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: <Widget>[
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
);
}

View File

@ -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<MindWanderScreen>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _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<double>(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(),
));
}

View File

@ -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<GeminiChat> {
}
void _startConversation() async {
context.read<GeminiCubit>().startLesson();
context.read<GeminiCubit>().startLesson(widget.lessonId);
}
void _sendMessage() async {
@ -93,20 +100,22 @@ class GeminiChatState extends State<GeminiChat> {
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<GeminiCubit, GeminiState>(
@ -145,30 +154,6 @@ class GeminiChatState extends State<GeminiChat> {
const SizedBox(height: 16),
Row(
children: [
// BlocBuilder<GeminiCubit, GeminiState>(
// 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<GeminiChat> {
),
),
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<GeminiChat> {
);
}
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<GeminiChat> {
),
);
} 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!"

View File

@ -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<LessonListScreen> {
List<Lesson> lessons = [];
@override
void initState() {
super.initState();
loadLessons();
}
Future<void> loadLessons() async {
final String response =
await rootBundle.loadString('assets/lessons/lessons.json');
final List<dynamic> 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
},
),
);
}
}