2024-07-16 19:02:02 +02:00
|
|
|
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';
|
2024-08-08 17:52:50 +02:00
|
|
|
import 'package:gemini_app/eeg/eeg_service.dart';
|
|
|
|
import 'package:get_it/get_it.dart';
|
2024-07-16 19:02:02 +02:00
|
|
|
|
|
|
|
class GeminiScreen extends StatelessWidget {
|
|
|
|
const GeminiScreen({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return MultiBlocProvider(
|
|
|
|
providers: [
|
|
|
|
BlocProvider(create: (context) => GeminiCubit()),
|
|
|
|
],
|
|
|
|
child: const GeminiChat(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class GeminiChat extends StatefulWidget {
|
|
|
|
const GeminiChat({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
GeminiChatState createState() => GeminiChatState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class GeminiChatState extends State<GeminiChat> {
|
|
|
|
final _textController = TextEditingController();
|
2024-08-03 22:51:03 +02:00
|
|
|
bool _quizMode = false; // Add this line
|
2024-08-08 17:52:50 +02:00
|
|
|
final EegService _eegService = GetIt.instance<EegService>();
|
2024-07-16 19:02:02 +02:00
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
_startConversation();
|
|
|
|
}
|
2024-07-16 19:02:02 +02:00
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
void _toggleQuizMode() {
|
|
|
|
setState(() {
|
|
|
|
_quizMode = !_quizMode;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void _checkAnswer(int answer) {
|
|
|
|
context.read<GeminiCubit>().checkAnswer(answer);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
2024-08-08 17:52:50 +02:00
|
|
|
_eegService.stopPolling();
|
2024-08-03 22:51:03 +02:00
|
|
|
super.dispose();
|
|
|
|
}
|
2024-07-16 19:02:02 +02:00
|
|
|
|
|
|
|
void _startConversation() async {
|
2024-08-08 17:52:50 +02:00
|
|
|
context.read<GeminiCubit>().startLesson();
|
2024-07-16 19:02:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void _sendMessage() async {
|
2024-08-03 22:51:03 +02:00
|
|
|
context
|
|
|
|
.read<GeminiCubit>()
|
2024-08-08 17:52:50 +02:00
|
|
|
.sendMessage(_textController.text);
|
2024-07-16 19:02:02 +02:00
|
|
|
_textController.clear();
|
|
|
|
}
|
2024-08-03 22:51:03 +02:00
|
|
|
|
2024-07-29 22:33:48 +02:00
|
|
|
void _toggleEegState() {
|
2024-08-08 17:52:50 +02:00
|
|
|
_eegService.toggleState();
|
2024-07-29 22:33:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void _resetConversation() {
|
|
|
|
context.read<GeminiCubit>().resetConversation();
|
|
|
|
_startConversation();
|
|
|
|
}
|
2024-07-16 19:02:02 +02:00
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
void _enterQuizMode() {
|
|
|
|
context.read<GeminiCubit>().enterQuizMode();
|
|
|
|
}
|
|
|
|
|
2024-07-16 19:02:02 +02:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
2024-07-29 22:33:48 +02:00
|
|
|
resizeToAvoidBottomInset: true,
|
2024-07-16 19:02:02 +02:00
|
|
|
appBar: AppBar(
|
2024-08-03 22:51:03 +02:00
|
|
|
title: BlocBuilder<GeminiCubit, GeminiState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
return Text(state.isQuizMode ? 'Quiz Mode' : 'Gemini Pro Chat');
|
|
|
|
},
|
|
|
|
),
|
2024-07-16 19:02:02 +02:00
|
|
|
),
|
|
|
|
body: Padding(
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
child: Column(
|
|
|
|
children: [
|
2024-08-08 17:52:50 +02:00
|
|
|
Card(
|
2024-08-03 22:51:03 +02:00
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(
|
2024-08-08 17:52:50 +02:00
|
|
|
'Mind Wandering: ${_eegService.state.mind_wandering.toStringAsFixed(2)}'),
|
|
|
|
Text('Focus: ${_eegService.state.focus.toStringAsFixed(2)}'),
|
2024-08-03 22:51:03 +02:00
|
|
|
],
|
|
|
|
),
|
2024-07-16 19:02:02 +02:00
|
|
|
),
|
2024-08-08 17:52:50 +02:00
|
|
|
),
|
2024-07-16 19:02:02 +02:00
|
|
|
Expanded(
|
|
|
|
child: BlocBuilder<GeminiCubit, GeminiState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
if (state.status == GeminiStatus.loading) {
|
|
|
|
return buildChatList(state, loading: true);
|
|
|
|
} else if (state.status == GeminiStatus.error) {
|
|
|
|
return Text('Error: ${state.error}');
|
|
|
|
} else {
|
|
|
|
return buildChatList(state);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
2024-08-03 22:51:03 +02:00
|
|
|
BlocBuilder<GeminiCubit, GeminiState>(
|
|
|
|
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(),
|
|
|
|
);
|
|
|
|
},
|
2024-07-16 19:02:02 +02:00
|
|
|
),
|
2024-08-03 22:51:03 +02:00
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
BlocBuilder<GeminiCubit, GeminiState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
return state.isQuizMode
|
|
|
|
? Container()
|
|
|
|
: Expanded(
|
|
|
|
child: ElevatedButton(
|
|
|
|
onPressed: _sendMessage,
|
|
|
|
child: const Text('Send'),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
2024-07-29 22:33:48 +02:00
|
|
|
),
|
2024-08-03 22:51:03 +02:00
|
|
|
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<GeminiCubit, GeminiState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
return state.isQuizMode
|
|
|
|
? Container()
|
|
|
|
: ElevatedButton(
|
|
|
|
onPressed: _enterQuizMode,
|
|
|
|
child: const Text('Start Quiz'),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
2024-07-16 19:02:02 +02:00
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
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(),
|
|
|
|
),
|
2024-07-16 19:02:02 +02:00
|
|
|
),
|
2024-08-03 22:51:03 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state.messages[index].type == MessageType.lessonScript) {
|
|
|
|
// skip
|
|
|
|
return Container();
|
|
|
|
}
|
2024-07-16 19:02:02 +02:00
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
final message = state.messages[index];
|
|
|
|
// String text = message.parts.whereType<TextPart>().map((part) => part.text).join();
|
|
|
|
String text = message.text;
|
2024-07-16 19:02:02 +02:00
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
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)],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
2024-07-16 19:02:02 +02:00
|
|
|
}
|
2024-08-03 22:51:03 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2024-07-16 19:02:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class BouncingDots extends StatefulWidget {
|
|
|
|
const BouncingDots({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
BouncingDotsState createState() => BouncingDotsState();
|
|
|
|
}
|
|
|
|
|
2024-08-03 22:51:03 +02:00
|
|
|
class BouncingDotsState extends State<BouncingDots>
|
|
|
|
with TickerProviderStateMixin {
|
2024-07-16 19:02:02 +02:00
|
|
|
late List<AnimationController> _controllers;
|
|
|
|
late List<Animation<double>> _animations;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
_controllers = List.generate(
|
|
|
|
3,
|
2024-08-03 22:51:03 +02:00
|
|
|
(index) => AnimationController(
|
2024-07-16 19:02:02 +02:00
|
|
|
duration: const Duration(milliseconds: 400),
|
|
|
|
vsync: this,
|
|
|
|
),
|
|
|
|
);
|
2024-08-03 22:51:03 +02:00
|
|
|
_animations = _controllers
|
|
|
|
.map((controller) => Tween<double>(begin: 0, end: -10).animate(
|
|
|
|
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
|
|
|
|
))
|
|
|
|
.toList();
|
2024-07-16 19:02:02 +02:00
|
|
|
|
|
|
|
for (var i = 0; i < 3; i++) {
|
|
|
|
Future.delayed(Duration(milliseconds: i * 180), () {
|
|
|
|
_controllers[i].repeat(reverse: true);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
for (var controller in _controllers) {
|
|
|
|
controller.dispose();
|
|
|
|
}
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
children: List.generate(3, (index) {
|
|
|
|
return AnimatedBuilder(
|
|
|
|
animation: _controllers[index],
|
|
|
|
builder: (context, child) {
|
|
|
|
return Container(
|
|
|
|
padding: const EdgeInsets.all(2.5),
|
|
|
|
child: Transform.translate(
|
|
|
|
offset: Offset(0, _animations[index].value),
|
|
|
|
child: Container(
|
|
|
|
width: 10,
|
|
|
|
height: 10,
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Theme.of(context).primaryColor,
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|