gemini-app/lib/bloc/gemini_state.dart

386 lines
12 KiB
Dart
Raw Normal View History

2024-08-03 22:51:03 +02:00
import 'dart:convert';
2024-07-16 19:02:02 +02:00
import 'package:bloc/bloc.dart';
import 'package:flutter/services.dart';
2024-08-08 18:18:01 +02:00
import 'package:gemini_app/api_key.dart';
import 'package:gemini_app/eeg/eeg_service.dart';
import 'package:get_it/get_it.dart';
2024-07-16 19:02:02 +02:00
import 'package:google_generative_ai/google_generative_ai.dart';
2024-08-03 22:51:03 +02:00
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 short but the lesson can be as long as needed.
2024-08-09 01:34:20 +02:00
Student is 20 years old. You can only interact using text, no videos, images, or audio.
2024-08-09 01:03:08 +02:00
Make the lesson more in the style of a lecture, with you explaining the topic and the student asking questions.
2024-08-03 22:51:03 +02:00
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):
2024-08-08 19:25:46 +02:00
optional: <QUIZ_START_TOKEN> (makes the app transition to quiz mode, do not write the question yourself, use it ONLY when you want to start the quiz)
2024-08-03 22:51:03 +02:00
<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
""";
2024-08-08 18:18:01 +02:00
const String LESSON_START_TOKEN = "<LESSON_START_TOKEN>";
const String ANALYSIS_START_TOKEN = "<ANALYSIS_START_TOKEN>";
const String QUIZ_START_TOKEN = "<QUIZ_START_TOKEN>";
2024-07-16 19:02:02 +02:00
enum GeminiStatus { initial, loading, success, error }
2024-08-03 22:51:03 +02:00
// enum MessageType { text, image, audio, video }
enum MessageSource { user, agent, app }
2024-07-16 19:02:02 +02:00
2024-08-03 22:51:03 +02:00
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,
});
}
enum MessageType { text, lessonScript, quizQuestion, quizAnswer }
2024-07-16 19:02:02 +02:00
class Message {
final String text;
final MessageType type;
final MessageSource source;
2024-08-03 22:51:03 +02:00
final List<String>? quizOptions; // Add this for ABCD options
final int? correctAnswer; // Add this for the correct answer index
2024-07-16 19:02:02 +02:00
Message({
required this.text,
required this.type,
required this.source,
2024-08-03 22:51:03 +02:00
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() {
2024-08-08 19:25:46 +02:00
switch (type) {
case MessageType.text:
if (source == MessageSource.user) {
return Content.text(text);
} else {
return Content.model([TextPart(text)]);
}
case MessageType.lessonScript:
return Content.text(text);
case MessageType.quizQuestion:
String question = text;
List<String> options =
quizOptions!.map((option) => option.trim()).toList();
2024-08-08 19:25:46 +02:00
String answer = options[correctAnswer!];
String formattedQuestion =
"$question\n\nOptions:\n${options.map((option) => "- $option").join('\n')}\n\nCorrect Answer: $answer";
2024-08-08 19:25:46 +02:00
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";
2024-08-08 19:25:46 +02:00
return Content.text(result);
default:
throw UnsupportedError('Unsupported message type');
2024-08-03 22:51:03 +02:00
}
}
}
class QuizQuestion {
String question;
List<String> options;
int correctAnswer;
QuizQuestion({
required this.question,
required this.options,
required this.correctAnswer,
2024-07-16 19:02:02 +02:00
});
}
class GeminiState {
GeminiStatus status;
String error;
2024-08-03 22:51:03 +02:00
List<Message> messages;
List<QuizQuestion>? quizQuestions;
bool isQuizMode;
int currentQuizIndex;
GenerativeModel? model;
String? lessonId;
2024-07-16 19:02:02 +02:00
2024-08-03 22:51:03 +02:00
GeminiState(
{required this.status,
this.error = '',
this.messages = const [],
this.quizQuestions,
this.isQuizMode = false,
this.currentQuizIndex = -1,
this.model,
this.lessonId});
2024-08-03 22:51:03 +02:00
GeminiState copyWith({
GeminiStatus? status,
String? error,
List<Message>? messages,
List<QuizQuestion>? quizQuestions,
bool? isQuizMode,
int? currentQuizIndex,
GenerativeModel? model,
String? lessonId,
2024-08-03 22:51:03 +02:00
}) {
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,
lessonId: lessonId ?? this.lessonId,
2024-08-03 22:51:03 +02:00
);
}
2024-07-16 19:02:02 +02:00
static GeminiState get initialState => GeminiState(
2024-08-03 22:51:03 +02:00
status: GeminiStatus.initial,
messages: [],
2024-08-03 22:51:03 +02:00
error: '',
);
2024-07-16 19:02:02 +02:00
}
class GeminiCubit extends Cubit<GeminiState> {
GeminiCubit() : super(GeminiState.initialState);
void startLesson(String lessonId) async {
final quizQuestions = await loadQuizQuestions(lessonId);
2024-08-08 18:18:01 +02:00
final String lessonScript =
await rootBundle.loadString('assets/lessons/$lessonId.md');
// final String prompt =
2024-08-08 18:18:01 +02:00
// "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 =
2024-08-09 01:34:20 +02:00
"You are a lecturer teaching a class with one student. The student has the ability to ask questions during the lesson, while you are responsible for lecturing and presenting the topic. Start conducting the lecture for one student based on the script below:\n$lessonScript";
2024-08-03 22:51:03 +02:00
final safetySettings = [
SafetySetting(HarmCategory.harassment, HarmBlockThreshold.none),
SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.none),
SafetySetting(HarmCategory.sexuallyExplicit, HarmBlockThreshold.none),
SafetySetting(HarmCategory.dangerousContent, HarmBlockThreshold.none),
];
final model = GenerativeModel(
model: 'gemini-1.5-pro-latest',
apiKey: geminiApiKey,
safetySettings: safetySettings,
systemInstruction: Content.system(systemPrmpt));
Message lessonScriptMessage = Message(
text: prompt,
type: MessageType.lessonScript,
source: MessageSource.agent,
2024-07-29 22:33:48 +02:00
);
2024-07-16 19:02:02 +02:00
2024-08-03 22:51:03 +02:00
GeminiState initialState = GeminiState(
status: GeminiStatus.loading,
error: '',
messages: [lessonScriptMessage],
quizQuestions: quizQuestions,
isQuizMode: false,
model: model,
lessonId: lessonId);
2024-08-03 22:51:03 +02:00
emit(initialState);
2024-08-08 18:18:01 +02:00
sendMessage("");
2024-08-03 22:51:03 +02:00
}
void sendMessage(String prompt) async {
2024-08-03 22:51:03 +02:00
List<Message> messagesWithoutPrompt = state.messages;
2024-08-08 18:18:01 +02:00
List<Message> messagesWithPrompt;
if (prompt == "") {
messagesWithPrompt = state.messages;
} else {
messagesWithPrompt = state.messages +
[
Message(
text: prompt,
type: MessageType.text,
source: MessageSource.user)
];
}
2024-08-03 22:51:03 +02:00
emit(state.copyWith(
status: GeminiStatus.loading,
2024-07-16 19:02:02 +02:00
messages: messagesWithPrompt,
));
2024-08-03 22:51:03 +02:00
try {
final chatHistory =
messagesWithoutPrompt.map((mess) => mess.toGeminiContent()).toList();
final chat = state.model!.startChat(history: chatHistory);
2024-08-03 22:51:03 +02:00
final stream = chat.sendMessageStream(Content.text(
"EEG DATA:\n${GetIt.instance<EegService>().state.getJsonString()}\nUser message:\n$prompt"));
2024-08-03 22:51:03 +02:00
String responseText = '';
2024-08-08 18:18:01 +02:00
bool isAnalysisDone = false;
2024-08-08 19:25:46 +02:00
String analysisData = "";
2024-08-08 18:18:01 +02:00
2024-08-03 22:51:03 +02:00
await for (final chunk in stream) {
responseText += chunk.text ?? '';
2024-08-08 18:18:01 +02:00
if (responseText.contains(LESSON_START_TOKEN)) {
isAnalysisDone = true;
var startIndex = responseText.indexOf(LESSON_START_TOKEN) +
LESSON_START_TOKEN.length;
2024-08-08 19:25:46 +02:00
analysisData = responseText.substring(0, startIndex);
2024-08-08 18:18:01 +02:00
print("ANALYSIS DATA: $analysisData");
responseText =
responseText.substring(startIndex, responseText.length);
}
if (isAnalysisDone) {
emit(state.copyWith(
status: GeminiStatus.success,
messages: messagesWithPrompt +
[
Message(
source: MessageSource.agent,
text: responseText,
type: MessageType.text)
]));
}
2024-08-03 22:51:03 +02:00
}
if (responseText.contains(QUIZ_START_TOKEN) ||
analysisData.contains(QUIZ_START_TOKEN)) {
emit(state.copyWith(
status: GeminiStatus.success, messages: messagesWithPrompt));
2024-08-03 22:51:03 +02:00
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(String lessonId) async {
2024-08-03 22:51:03 +02:00
final String quizJson =
await rootBundle.loadString('assets/lessons/$lessonId.json');
2024-08-03 22:51:03 +02:00
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;
2024-08-08 19:25:46 +02:00
if (currentQuizIndex >= state.quizQuestions!.length ||
currentQuizIndex >= 2) {
// if (currentQuizIndex >= 2) {
2024-08-08 19:25:46 +02:00
List<Message> messagesWithPrompt = state.messages +
[
Message(
text: "Quiz is over. Write a summary of user's performance.",
type: MessageType.text,
source: MessageSource.app)
];
2024-08-08 19:25:46 +02:00
// Quiz is over
emit(state.copyWith(
isQuizMode: false,
currentQuizIndex: 0,
messages: messagesWithPrompt));
2024-08-08 19:25:46 +02:00
// Send a message to Gemini to end the quiz
sendMessage("");
return;
}
2024-08-03 22:51:03 +02:00
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);
2024-07-16 19:02:02 +02:00
}
2024-07-29 22:33:48 +02:00
void resetConversation() {
emit(GeminiState.initialState);
}
2024-08-03 22:51:03 +02:00
}