added multimedia support and STT. STT and photos are have bugs but are functional. file upload is conceptualized but will require additional backend work. video is on hold for now.

This commit is contained in:
Joshua
2024-07-10 19:15:03 -06:00
parent e5b69aa2b6
commit cbd040f57b
36 changed files with 742 additions and 137 deletions
@@ -1,9 +1,32 @@
class Chat {
final String content;
final bool role;
int chatType = 0;
// Most basic chat, text from server or from user.
Chat(this.content, this.role);
Chat(this.content, this.role, this.chatType);
// TODO: Overload constructors to allow for multimedia.
// A chat with an included image.
Chat.image(this.content, this.role, this.chatType);
// A chat with an included file.
Chat.file(this.content, this.role, this.chatType);
}
// TODO: Add proper filetype support. File should either be parsed on-device, or uploaded to the server.
// TODO: Implement dynamic pathing based upon filetypes.
enum Filetype {
pdf,
doc,
docx,
pages,
note,
todo,
cal,
calendar,
event,
contact,
mp3,
mp4,
wav,
}
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:app/components/util/backend.dart';
import 'package:app/theme.dart';
import 'package:flutter/material.dart';
import 'chat.dart';
@@ -29,10 +32,20 @@ class ChatBubbleWidget extends StatelessWidget{
),
color: c.role ? AppTheme.darkest : AppTheme.dark,
),
child: Text(
c.content,
style: AppTheme.bodyMedium,
),
child:
// TODO: Properly display filetypes in the thread.
c.chatType == 0 ? Text(
c.content,
style: AppTheme.bodyMedium,
)
: c.chatType == 1 ? Image(image: Backend.convertFromBase64(c.content))
: c.chatType == 2 ? const Icon(
Icons.file_copy,
) : const Text("null"),
// Text(
// c.content,
// style: AppTheme.bodyMedium,
// ),
),
],
);
@@ -1,4 +1,4 @@
import 'package:app/util/backend.dart';
import 'package:app/components/util/backend.dart';
import 'package:flutter/material.dart';
import 'chat.dart';
@@ -6,10 +6,12 @@ class Conversation with ChangeNotifier {
int conversationID;
String conversationLabel;
String conversationSubLabel;
bool history = true;
// History is disabled by default. This means chats are auto-deleted once the user exits that conversation.
// TODO: Fix history, very, verry strange behavior when disabled.
bool history = true; // History is disabled by default. This means chats are auto-deleted once the user exits that conversation.\
bool conversationContext = true; // The prior prompts and responses are used as additional context for the next prompt and response.
late bool stream = false; // Update the UI with a stream of characters as received from the server, or as one text.
var lastAccessTime = DateTime.timestamp(); // Used to sort conversations based on most recent usage.
List<Chat> chats = [];
Conversation(this.conversationID, this.conversationLabel, this.conversationSubLabel);
@@ -23,11 +25,13 @@ class Conversation with ChangeNotifier {
void toggleStream() => stream = !stream;
void add(Chat message) {
lastAccessTime = DateTime.timestamp();
chats.add(message);
notifyListeners();
}
void loadChats() {
lastAccessTime = DateTime.timestamp();
Backend.loadConversation;
chats = Backend.loadedChats;
}
@@ -41,10 +45,12 @@ class Conversation with ChangeNotifier {
}
List<Chat> getChats() {
lastAccessTime = DateTime.timestamp();
return chats;
}
void setLabels(String cLabel, String cSubLabel) {
lastAccessTime = DateTime.timestamp();
conversationLabel = cLabel;
conversationSubLabel = cSubLabel;
}
@@ -1,7 +1,7 @@
import 'package:app/components/conversation_thread/chat_bubble_model.dart';
import 'package:app/components/conversation_thread/chat.dart';
import 'package:app/components/conversation_thread/conversation.dart';
import 'package:app/util/backend.dart';
import 'package:app/components/util/backend.dart';
import 'package:app/components/prompt_box/prompt_box_widget.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
@@ -1,5 +1,5 @@
import 'package:app/components/conversation_thread/conversation.dart';
import 'package:app/util/backend.dart';
import 'package:app/components/util/backend.dart';
import 'package:provider/provider.dart';
import '/components/conversation_thread/chat_bubble_widget.dart';
@@ -50,6 +50,7 @@ class _ConversationThreadWidgetState extends State<ConversationThreadWidget> {
@override
Widget build(BuildContext context) {
// TODO: Add a scrollbar
return Container(
width: double.infinity,
height: double.infinity,
@@ -9,8 +9,8 @@ import 'package:flutter/services.dart';
import 'package:app/theme.dart';
import 'conversation_model.dart';
export 'conversation_model.dart';
import '../components/conversation_model.dart';
export '../components/conversation_model.dart';
class ConversationWidget extends StatefulWidget {
final Conversation conversation;
@@ -64,8 +64,8 @@ class _ConversationWidgetState extends State<ConversationWidget> {
size: 30,
),
onPressed: () async {
print("something pressed in conversation_widget.dart");
// TODO: Smooth out the animation to prevent the conversations list from loading half-way up the screen.
// Note: that only applies if the input was already selected.
SystemChannels.textInput.invokeMethod('TextInput.hide');
Navigator.pop(context);
},
@@ -1,15 +1,15 @@
import 'package:app/components/conversation_thread/conversation.dart';
import 'package:app/theme.dart';
import 'package:app/pages/conversation_widget.dart';
import 'package:app/util/backend.dart';
import 'package:app/components/conversation_widget.dart';
import 'package:app/components/util/backend.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:flutter/material.dart';
import 'package:app/components/options_dialog/options_dialog_widget.dart';
import 'conversations_list_model.dart';
export 'conversations_list_model.dart';
import '../components/conversations_list_model.dart';
export '../components/conversations_list_model.dart';
class ConversationsListWidget extends StatefulWidget {
const ConversationsListWidget({super.key});
@@ -213,9 +213,6 @@ class _ConversationsListWidgetState extends State<ConversationsListWidget> {
},
).then((value) =>
safeSetState(() {})),
// TODO: Need to implement.
print("opening options")
},
),
);
@@ -235,7 +232,7 @@ class _ConversationsListWidgetState extends State<ConversationsListWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationWidget(conversation: Backend.getLoadedConversation(),),
builder: (context) => ConversationWidget(conversation: Backend.loadedConversation,),
),
), // Handle the button press
},
@@ -1,13 +0,0 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'no_chats_widget.dart' show NoChatsWidget;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
class NoChatsModel extends FlutterFlowModel<NoChatsWidget> {
@override
void initState(BuildContext context) {}
@override
void dispose() {}
}
@@ -5,9 +5,6 @@ import 'package:provider/provider.dart';
import 'package:app/theme.dart';
import 'no_chats_model.dart';
export 'no_chats_model.dart';
class NoChatsWidget extends StatefulWidget {
const NoChatsWidget({
super.key,
@@ -26,24 +23,19 @@ class NoChatsWidget extends StatefulWidget {
}
class _NoChatsWidgetState extends State<NoChatsWidget> {
late NoChatsModel _model;
@override
void setState(VoidCallback callback) {
super.setState(callback);
_model.onUpdate();
}
@override
void initState() {
super.initState();
_model = createModel(context, () => NoChatsModel());
}
@override
void dispose() {
_model.maybeDispose();
super.dispose();
}
@@ -1,10 +1,17 @@
import 'package:app/util/backend.dart';
import 'dart:io';
import 'package:app/components/util/backend.dart';
import 'package:camera/camera.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/material.dart';
import 'package:app/theme.dart';
import 'package:image_picker/image_picker.dart';
import '../conversation_thread/chat.dart';
import 'prompt_box_model.dart';
export 'prompt_box_model.dart';
@@ -17,7 +24,7 @@ class PromptBoxWidget extends StatefulWidget {
class _PromptBoxWidgetState extends State<PromptBoxWidget> {
late PromptBoxModel _model;
late String entry_text;
String entryText = "";
@override
void setState(VoidCallback callback) {
@@ -43,6 +50,7 @@ class _PromptBoxWidgetState extends State<PromptBoxWidget> {
@override
Widget build(BuildContext context) {
const double buttonPadding = 4;
return SafeArea(
child: Container(
width: double.infinity,
@@ -62,50 +70,59 @@ class _PromptBoxWidgetState extends State<PromptBoxWidget> {
children: [
Expanded( // TODO: File context upload
child: Padding(
padding: const EdgeInsets.all(14),
child: FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 40,
padding: const EdgeInsets.all(buttonPadding),
child: IconButton(
icon: const Icon(
Icons.file_upload_outlined,
color: AppTheme.darkest,
size: 30,
),
onPressed: () {
print('IconButton pressed ...');
onPressed: () async {
final picker = FilePicker.platform;
final result = await picker.pickFiles(allowMultiple: true); // Allow multiple files
if (result != null) {
for (final platformFile in result.files) {
// TODO: Upload file to server
final file = File(platformFile.path!);
}
} else {
print('User canceled file selection');
}
},
),
),
),
Expanded( // TODO: Photo context upload
Expanded( // TODO: Gallery context upload
child: Padding(
padding: const EdgeInsets.all(14),
child: FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 40,
padding: const EdgeInsets.all(buttonPadding),
child: IconButton(
icon: const Icon(
Icons.photo_outlined,
color: AppTheme.darkest,
size: 30,
),
onPressed: () {
print('IconButton pressed ...');
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
// TODO: Allow for multiple pictures/photos to be selected.
// final images = await picker.pickMultiImage();
if (image != null) {
// TODO: Upload to server and add to conversation.chats
String i = Backend.convertToBase64(File(image.path)).toString();
Backend.loadedConversation.add(Chat.image(i, true, 1));
} else {
print('User canceled image selection');
}
},
),
),
),
Expanded( // TODO: Video context upload
child: Padding(
padding: const EdgeInsets.all(14),
child: FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 40,
padding: const EdgeInsets.all(buttonPadding),
child: IconButton(
icon: const Icon(
Icons.videocam_outlined,
color: AppTheme.darkest,
@@ -119,19 +136,48 @@ class _PromptBoxWidgetState extends State<PromptBoxWidget> {
),
Expanded( // TODO: Camera context upload
child: Padding(
padding: const EdgeInsets.all(14),
child: FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 40,
padding: const EdgeInsets.all(buttonPadding),
child: IconButton(
icon: const Icon(
Icons.camera_alt_outlined,
color: AppTheme.darkest,
size: 30,
),
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
// TODO: Upload to server and add to conversation.chats
final i = Backend.convertToBase64(File(image.path));
Backend.loadedConversation.add(Chat.image(i.toString(), true, 1));
} else {
print('User canceled photo capture');
}
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(buttonPadding),
child: IconButton(
icon: Icon(
Backend.stt.speechToText.isNotListening ? Icons.mic_off_outlined : Icons.mic_none,
color: AppTheme.darkest,
size: 30,
),
onPressed: () {
print('IconButton pressed ...');
// TODO: Audio input is still buggy, it appears to only enter the text field once the button is pressed again to stop.
if(Backend.stt.speechToText.isNotListening) {
print('STT engine starting ...');
Backend.stt.startListening();
updateText();
} else {
print('STT engine stopping');
Backend.stt.stopListening();
updateText();
}
},
),
),
@@ -190,10 +236,13 @@ class _PromptBoxWidgetState extends State<PromptBoxWidget> {
fillColor: AppTheme.alternate,
suffixIcon: GestureDetector(
onTap: () {
// TODO: Connect with multimedia
entry_text = _model.textController.text;
Backend.respondWhenReady(entry_text);
_model.textController?.clear(); // Clear the text field
if(_model.textController.text != "") {
// TODO: Connect with multimedia
entryText = _model.textController.text;
Backend.respondWhenReady(entryText);
_model.textController?.clear(); // Clear the text field
SystemChannels.textInput.invokeMethod('TextInput.hide');
}
},
child: const Icon(
Icons.send,
@@ -206,12 +255,14 @@ class _PromptBoxWidgetState extends State<PromptBoxWidget> {
textAlign: TextAlign.start,
maxLines: 5,
minLines: 1,
validator: _model.textControllerValidator.asValidator(context),
textInputAction: TextInputAction.send, // "Send" on keyboard
onFieldSubmitted: (text) {
entry_text = _model.textController.text;
Backend.respondWhenReady(entry_text); // TODO: Connect with multimedia
_model.textController?.clear(); // Clear the text field
// Don't take any action unless there's an actual input.
if(_model.textController.text != "") {
entryText = _model.textController.text;
Backend.respondWhenReady(entryText); // TODO: Connect with multimedia
_model.textController?.clear(); // Clear the text field
}
},
),
),
@@ -223,4 +274,12 @@ class _PromptBoxWidgetState extends State<PromptBoxWidget> {
),
);
}
}
void updateText() {
_model.textController.text = Backend.stt.text;
entryText = _model.textController.text;
print(entryText);
print(_model.textController.text);
Backend.stt.reset();
}
}
@@ -4,8 +4,8 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'support_form_model.dart';
export 'support_form_model.dart';
import '../components/support_form_model.dart';
export '../components/support_form_model.dart';
import 'package:app/theme.dart';
@@ -1,10 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
import 'package:app/components/conversation_thread/conversation.dart';
import 'package:camera/camera.dart';
import 'package:http/http.dart' as http;
import 'package:app/components/conversation_thread/chat.dart';
import 'package:app/util/speech_to_text.dart';
import 'package:app/components/util/speech_to_text.dart';
import 'model.dart';
@@ -18,6 +22,7 @@ class Backend {
//static final url = "http://192.168.1.68:11434/api"; // For local access to home server
static String endpoint = "";
static final Map<String, String> headers = {'Content-Type': 'application/json'};
static final bool UI_TESTING = true;
@@ -27,11 +32,13 @@ class Backend {
// PRIVACY-FOCUSED VARIABLES
// PRIVACY-NECESSARY VARIABLES
// TODO: Reorder based on most recent usage.
static List<Conversation> conversations = [];
static List<Chat> loadedChats = [];
static late Conversation loadedConversation;
static late List<CameraDescription> cameras;
static late CameraController controller;
// RUNTIME & STATUS VARIABLES
@@ -45,40 +52,35 @@ class Backend {
// TODO: Verify all different components of STT, TTS, LLM, etc have been initialized.
// TODO: Handshake w/ server to validate identity
factory Backend() {
// Speech-To-Text engine activation.
_speechEnabled = stt.isSpeechEnabled();
// loadedChats = conversations[conversationID].chats;
return singleBackend;
}
void init() {
initiated = true;
void init() async {
// Prevent multiple initiations
if(!initiated) {
initiated = true;
// Speech-To-Text engine activation.
_speechEnabled = stt.isSpeechEnabled();
model = Model("llama3"); // Primarily used llama3, testing gemma2
maxConversationID = conversations.length;
cameras = await availableCameras();
controller = CameraController(cameras[0], ResolutionPreset.max);
controller.initialize(); // TODO: Add error catching on camera
}
// TODO: Dynamically get available models and models that can be pulled.
// TODO: Dynamically get the user's default model
model = Model("llama3"); // Primarily used llama3, testing gemma2
// TODO: Dynamically obtain conversations.
// conversations.add(
// Conversation.completeConversation(
// 0,
// "Test Conversation",
// "Testing Functionality",
// [
// (Chat("Hello there! You are part of a prototype and are running locally on my Macbook Air. Please keep your responses brief as every prompt freezes my computer.", true)),
// (Chat("Understood! How can I help?", false)),
// ]
// )
// );
maxConversationID = conversations.length;
// TODO: Obtain all user permissions at once.
}
// PRIMARY INTERFACE
// PRIMARY INTERFACES
@Deprecated("Use STT interface directly and use respondWhenReady().")
static Future<String> sttGetResponseWhenReady() async {
// TODO: In case on-device STT is unavailable, use server-side STT service.
@@ -87,19 +89,38 @@ class Backend {
}
prompt = stt.getTextWhenReady() as String;
conversations[conversationID].add(Chat(prompt, true));
httpSendRequest();
conversations[conversationID].add(Chat(response, false));
conversations[conversationID].add(Chat(prompt, true, 0));
if(!UI_TESTING) {
httpSendRequest();
}
conversations[conversationID].add(Chat(response, false, 0));
return response;
}
static void respondWhenReady(String p) async {
prompt = p;
conversations[conversationID].add(Chat(prompt, true));
httpSendRequest();
conversations[conversationID].add(Chat(prompt, true, 0));
if(!UI_TESTING) {
httpSendRequest();
}
}
static ImageProvider convertFromBase64(img) {
Uint8List imageBytes = base64Decode(img);
return Image.memory(imageBytes).image;
}
// Converts image to base64 encoded string for data transfer.
static String convertToBase64(f) {
List<int> imageBytes = f.readAsBytesSync();
String img = base64Encode(imageBytes);
// TODO: Add special handling for images included in prompts.
conversations[conversationID].add(Chat.image(img, true, 1));
return img;
}
// CONVERSATION MANAGEMENT
@@ -155,7 +176,7 @@ class Backend {
Map<String, dynamic> data;
if(conversations[conversationID].conversationContext) {
loadedChats = [...loadedChats, Chat(prompt, true)];
loadedChats = [...loadedChats, Chat(prompt, true, 0)];
endpoint = "chat";
List<Map<String, String>> messageList = convertMessages();
@@ -196,12 +217,13 @@ class Backend {
Map<String, dynamic> re = jsonDecode(serverResponse.body);
if(conversations[conversationID].conversationContext) {
response = re["message"]["content"];
loadedChats = [...loadedChats, Chat(response, false)];
// TODO: Modify in case of different response types.
loadedChats = [...loadedChats, Chat(response, false, 0)];
} else {
response = re["response"];
}
}
conversations[conversationID].add(Chat(response, false));
conversations[conversationID].add(Chat(response, false, 0));
prompt = "";
response = "";
@@ -1,9 +1,16 @@
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
import 'package:speech_to_text/speech_to_text.dart';
/// Handles standard Speech-To-Text using on-device processing.
class SpeechToText {
final stt.SpeechToText speechToText = stt.SpeechToText();
// TODO: User customizable STT??
final SpeechListenOptions options = stt.SpeechListenOptions(
partialResults: true,
onDevice: true,
);
bool speechEnabled = false;
String text = "";
@@ -16,13 +23,22 @@ class SpeechToText {
}
// Retrieve the text.
@Deprecated("TODO: Deprecated message for speechToText.getText")
String getText() => text;
// Reset either after saving the text or prior to using STT again.
void reset() => text = "";
/// Start a speech recognition session
void startListening() async => await speechToText.listen(onResult: onSpeechResult);
void startListening() async {
await speechToText.listen(
onResult: onSpeechResult,
pauseFor: const Duration(seconds: 3),
// TODO: I think this is a bug with the speech_to_text library? The docs correctly identify the named parameter of SpeechListenOptions, but it's not recognized.
// SpeechListenOptions: options,
);
}
/// Manually stop the active speech recognition session
/// Note that there are also timeouts that each platform enforces
@@ -0,0 +1,317 @@
import 'dart:math';
class UsageQueue<T> implements List {
@override
var first;
@override
var last;
@override
late int length;
@override
List operator +(List other) {
// TODO: implement +
throw UnimplementedError();
}
@override
operator [](int index) {
// TODO: implement []
throw UnimplementedError();
}
@override
void operator []=(int index, value) {
// TODO: implement []=
}
@override
void add(value) {
// TODO: implement add
}
@override
void addAll(Iterable iterable) {
// TODO: implement addAll
}
@override
bool any(bool Function(dynamic element) test) {
// TODO: implement any
throw UnimplementedError();
}
@override
Map<int, dynamic> asMap() {
// TODO: implement asMap
throw UnimplementedError();
}
@override
List<R> cast<R>() {
// TODO: implement cast
throw UnimplementedError();
}
@override
void clear() {
// TODO: implement clear
}
@override
bool contains(Object? element) {
// TODO: implement contains
throw UnimplementedError();
}
@override
elementAt(int index) {
// TODO: implement elementAt
throw UnimplementedError();
}
@override
bool every(bool Function(dynamic element) test) {
// TODO: implement every
throw UnimplementedError();
}
@override
Iterable<T> expand<T>(Iterable<T> Function(dynamic element) toElements) {
// TODO: implement expand
throw UnimplementedError();
}
@override
void fillRange(int start, int end, [fillValue]) {
// TODO: implement fillRange
}
@override
firstWhere(bool Function(dynamic element) test, {Function()? orElse}) {
// TODO: implement firstWhere
throw UnimplementedError();
}
@override
T fold<T>(T initialValue, T Function(T previousValue, dynamic element) combine) {
// TODO: implement fold
throw UnimplementedError();
}
@override
Iterable followedBy(Iterable other) {
// TODO: implement followedBy
throw UnimplementedError();
}
@override
void forEach(void Function(dynamic element) action) {
// TODO: implement forEach
}
@override
Iterable getRange(int start, int end) {
// TODO: implement getRange
throw UnimplementedError();
}
@override
int indexOf(element, [int start = 0]) {
// TODO: implement indexOf
throw UnimplementedError();
}
@override
int indexWhere(bool Function(dynamic element) test, [int start = 0]) {
// TODO: implement indexWhere
throw UnimplementedError();
}
@override
void insert(int index, element) {
// TODO: implement insert
}
@override
void insertAll(int index, Iterable iterable) {
// TODO: implement insertAll
}
@override
// TODO: implement isEmpty
bool get isEmpty => throw UnimplementedError();
@override
// TODO: implement isNotEmpty
bool get isNotEmpty => throw UnimplementedError();
@override
// TODO: implement iterator
Iterator get iterator => throw UnimplementedError();
@override
String join([String separator = ""]) {
// TODO: implement join
throw UnimplementedError();
}
@override
int lastIndexOf(element, [int? start]) {
// TODO: implement lastIndexOf
throw UnimplementedError();
}
@override
int lastIndexWhere(bool Function(dynamic element) test, [int? start]) {
// TODO: implement lastIndexWhere
throw UnimplementedError();
}
@override
lastWhere(bool Function(dynamic element) test, {Function()? orElse}) {
// TODO: implement lastWhere
throw UnimplementedError();
}
@override
Iterable<T> map<T>(T Function(dynamic e) toElement) {
// TODO: implement map
throw UnimplementedError();
}
@override
reduce(Function(dynamic value, dynamic element) combine) {
// TODO: implement reduce
throw UnimplementedError();
}
@override
bool remove(Object? value) {
// TODO: implement remove
throw UnimplementedError();
}
@override
removeAt(int index) {
// TODO: implement removeAt
throw UnimplementedError();
}
@override
removeLast() {
// TODO: implement removeLast
throw UnimplementedError();
}
@override
void removeRange(int start, int end) {
// TODO: implement removeRange
}
@override
void removeWhere(bool Function(dynamic element) test) {
// TODO: implement removeWhere
}
@override
void replaceRange(int start, int end, Iterable replacements) {
// TODO: implement replaceRange
}
@override
void retainWhere(bool Function(dynamic element) test) {
// TODO: implement retainWhere
}
@override
// TODO: implement reversed
Iterable get reversed => throw UnimplementedError();
@override
void setAll(int index, Iterable iterable) {
// TODO: implement setAll
}
@override
void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) {
// TODO: implement setRange
}
@override
void shuffle([Random? random]) {
// TODO: implement shuffle
}
@override
// TODO: implement single
get single => throw UnimplementedError();
@override
singleWhere(bool Function(dynamic element) test, {Function()? orElse}) {
// TODO: implement singleWhere
throw UnimplementedError();
}
@override
Iterable skip(int count) {
// TODO: implement skip
throw UnimplementedError();
}
@override
Iterable skipWhile(bool Function(dynamic value) test) {
// TODO: implement skipWhile
throw UnimplementedError();
}
@override
void sort([int Function(dynamic a, dynamic b)? compare]) {
// TODO: implement sort
}
@override
List sublist(int start, [int? end]) {
// TODO: implement sublist
throw UnimplementedError();
}
@override
Iterable take(int count) {
// TODO: implement take
throw UnimplementedError();
}
@override
Iterable takeWhile(bool Function(dynamic value) test) {
// TODO: implement takeWhile
throw UnimplementedError();
}
@override
List toList({bool growable = true}) {
// TODO: implement toList
throw UnimplementedError();
}
@override
Set toSet() {
// TODO: implement toSet
throw UnimplementedError();
}
@override
Iterable where(bool Function(dynamic element) test) {
// TODO: implement where
throw UnimplementedError();
}
@override
Iterable<T> whereType<T>() {
// TODO: implement whereType
throw UnimplementedError();
}
}
+8 -14
View File
@@ -1,6 +1,6 @@
import 'package:app/pages/conversation_widget.dart';
import 'package:app/pages/settings_widget.dart';
import 'package:app/pages/conversations_list_widget.dart';
import 'package:app/components/conversation_widget.dart';
import 'package:app/components/settings_widget.dart';
import 'package:app/components/conversations_list_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
@@ -36,6 +36,7 @@ BottomNavigationBarItem _buildNavigationItem(int index) {
icon: Icon(iconData),
label: label,
backgroundColor: AppTheme.lightest,
);
}
@@ -67,21 +68,14 @@ class MyApp extends ConsumerWidget {
title: 'One AI',
home: Scaffold(
backgroundColor: AppTheme.medium,
appBar: AppBar(
backgroundColor: AppTheme.secondaryBackground,
automaticallyImplyLeading: false,
title: const Text(
'One AI',
style: AppTheme.displayLarge,
),
actions: const [],
centerTitle: false,
elevation: 0,
),
body: _pages[selectedIndex],
bottomNavigationBar: BottomNavigationBar(
backgroundColor: AppTheme.darkest,
selectedItemColor: AppTheme.lightest,
showUnselectedLabels: false,
showSelectedLabels: false,
iconSize: 30,
// Set the number of items to the length of the pages list
items: List.generate(_pages.length, (index) => _buildNavigationItem(index)),
currentIndex: selectedIndex,