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
@@ -0,0 +1,34 @@
import 'package:app/components/conversation_thread/conversation.dart';
import '/components/conversation_thread/conversation_thread_widget.dart';
import '/components/model_item/model_item_widget.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'conversation_widget.dart' show ConversationWidget;
import 'package:flutter/material.dart';
class ConversationModel extends FlutterFlowModel<ConversationWidget> {
/// State fields for stateful widgets in this page.
final unfocusNode = FocusNode();
// Model for ConversationThread component.
late ConversationThreadModel conversationThreadModel;
// Model for ModelItem component.
late ModelItemModel modelItemModel;
late Conversation conversation;
ConversationModel(this.conversation);
@override
void initState(BuildContext context) {
conversationThreadModel = createModel(context, () => ConversationThreadModel(conversation)); // TODO
modelItemModel = createModel(context, () => ModelItemModel());
}
@override
void dispose() {
unfocusNode.dispose();
conversationThreadModel.dispose();
modelItemModel.dispose();
}
}
@@ -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,
@@ -0,0 +1,125 @@
import 'package:app/components/conversation_thread/conversation.dart';
import '/components/conversation_details_overlay/conversation_details_overlay_widget.dart';
import '/components/conversation_thread/conversation_thread_widget.dart';
import '/components/model_item/model_item_widget.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:app/theme.dart';
import '../components/conversation_model.dart';
export '../components/conversation_model.dart';
class ConversationWidget extends StatefulWidget {
final Conversation conversation;
const ConversationWidget({super.key, required this.conversation});
@override
State<ConversationWidget> createState() => _ConversationWidgetState(conversation);
}
class _ConversationWidgetState extends State<ConversationWidget> {
late ConversationModel _model;
Conversation conversation;
_ConversationWidgetState(this.conversation);
final scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
_model = createModel(context, () => ConversationModel(conversation));
}
@override
void dispose() {
_model.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _model.unfocusNode.canRequestFocus
? FocusScope.of(context).requestFocus(_model.unfocusNode)
: FocusScope.of(context).unfocus(),
child: Scaffold(
key: scaffoldKey,
backgroundColor: AppTheme.secondaryBackground,
appBar: AppBar(
backgroundColor: AppTheme.primaryBackground,
automaticallyImplyLeading: false,
// ---------- Back Button
leading: FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 60,
icon: const Icon(
Icons.arrow_back_rounded,
color: AppTheme.lightest,
size: 30,
),
onPressed: () async {
// 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);
},
),
title: wrapWithModel(
model: _model.modelItemModel,
updateCallback: () => setState(() {}),
child: ModelItemWidget(conversation: conversation,),
),
actions: [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 8, 16, 8),
// ---------- Options Button
child: FlutterFlowIconButton(
icon: const Icon(
Icons.more_vert,
color: AppTheme.lightest,
size: 24,
),
onPressed: () async {
await showModalBottomSheet(
isScrollControlled: true,
backgroundColor: Colors.transparent,
enableDrag: false,
context: context,
builder: (context) {
return GestureDetector(
onTap: () => _model.unfocusNode.canRequestFocus
? FocusScope.of(context)
.requestFocus(_model.unfocusNode)
: FocusScope.of(context).unfocus(),
child: Padding(
padding: MediaQuery.viewInsetsOf(context),
child: const ConversationDetailsOverlayWidget(),
),
);
},
).then((value) => safeSetState(() {}));
},
),
),
],
centerTitle: false,
elevation: 0,
),
body: SafeArea(
top: true,
child: wrapWithModel(
model: _model.conversationThreadModel,
updateCallback: () => setState(() {}),
child: ConversationThreadWidget(conversation: conversation),
),
),
),
);
}
}
@@ -0,0 +1,16 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'conversations_list_widget.dart' show ConversationsListWidget;
import 'package:flutter/material.dart';
class ConversationsListModel extends FlutterFlowModel<ConversationsListWidget> {
final unfocusNode = FocusNode();
@override
void initState(BuildContext context) {}
@override
void dispose() {
unfocusNode.dispose();
}
}
@@ -0,0 +1,251 @@
import 'package:app/components/conversation_thread/conversation.dart';
import 'package:app/theme.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 '../components/conversations_list_model.dart';
export '../components/conversations_list_model.dart';
class ConversationsListWidget extends StatefulWidget {
const ConversationsListWidget({super.key});
@override
State<ConversationsListWidget> createState() =>
_ConversationsListWidgetState();
}
class _ConversationsListWidgetState extends State<ConversationsListWidget> {
late ConversationsListModel _model;
static List<ConversationWidget> conversations = [];
final scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
Backend().init();
if(conversations.isEmpty) {
List<Conversation> convoys = Backend.conversations;
for(Conversation c in convoys) {
conversations.add(ConversationWidget(conversation: c,));
}
}
_model = createModel(context, () => ConversationsListModel());
}
@override
void dispose() {
_model.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Clean conversation list prior to building widgets, to prevent bloating UI with empty conversations.
// Additionally, delete conversations that have history disabled.
for(int i = 0; i < conversations.length; i++) {
if(
conversations[i].conversation.getChats().isEmpty ||
!conversations[i].conversation.history
) {
Backend.conversations.removeAt(i);
conversations.removeAt(i);
Backend.maxConversationID--; // maxConversationID will eventually be deprecated with better ID system.
i--;
}
}
return GestureDetector(
onTap: () => _model.unfocusNode.canRequestFocus
? FocusScope.of(context).requestFocus(_model.unfocusNode)
: FocusScope.of(context).unfocus(),
child: SafeArea(
top: true,
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Align(
alignment: const AlignmentDirectional(0, 0),
child: ListView.builder(
itemCount: conversations.length,
padding: EdgeInsets.zero,
reverse: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return Align(
alignment: const AlignmentDirectional(0, 0),
child: InkWell(
splashColor: AppTheme.accent,
focusColor: AppTheme.lightest,
hoverColor: AppTheme.dark,
highlightColor: AppTheme.darkest,
// TODO
// onTap: () async {
// context.pushNamed('Conversation');
// },
child: Container(
decoration: const BoxDecoration(
//color: AppTheme.darkest,
border: Border(
top: BorderSide(width: 2.0, color: AppTheme.darkest),
),
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(color: AppTheme.lightest,),
child: const Align(
alignment: AlignmentDirectional(0, 0),
child: Icon(
Icons.settings_outlined, // TODO: Dynamically generate or allow users to color code/tag conversations.
color: AppTheme.secondaryText,
size: 48,
),
),
),
Expanded(
child: Align(
alignment: const AlignmentDirectional(-1, 0),
child: Container(
width: 100,
height: 100,
constraints: BoxConstraints(
minWidth: MediaQuery.sizeOf(context).width,
),
decoration: const BoxDecoration(
color: AppTheme.lightest,
shape: BoxShape.rectangle,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Align(
alignment: const AlignmentDirectional(-1, 0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
conversations[index].conversation.conversationLabel,
style: AppTheme.labelLarge,
),
],
),
),
Align(
alignment:
const AlignmentDirectional(-1, 0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
conversations[index].conversation.conversationSubLabel,
style: AppTheme.labelSmall,
),
),
],
),
),
],
),
),
),
),
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: AppTheme.lightest,
),
child: const Icon(
Icons.chevron_right_rounded,
color: AppTheme.secondaryText,
size: 48,
),
),
],
),
),
onTap: () => {
Backend.loadConversation(index),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationWidget(conversation: Backend.getLoadedConversation(),),
),
)
},
onLongPress: () => {
showModalBottomSheet(
isScrollControlled: true,
backgroundColor:
Colors.transparent,
enableDrag: false,
useSafeArea: true,
context: context,
builder: (context) {
return Padding(
padding:
MediaQuery.viewInsetsOf(
context),
child: const PopupWidget(),
);
},
).then((value) =>
safeSetState(() {})),
},
),
);
}
),
),
),
],
),
Positioned(
bottom: 20.0, // Adjust positioning as needed
right: 20.0, // Adjust positioning as needed
child: FloatingActionButton(
onPressed: () => {
Backend.loadConversation(Backend.createConversation()), // TODO: Dynamically get the next ID
conversations.add(ConversationWidget(conversation: Backend.getLoadedConversation())),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationWidget(conversation: Backend.loadedConversation,),
),
), // Handle the button press
},
backgroundColor: AppTheme.darkest,
child: const Icon(
Icons.bubble_chart,
color: AppTheme.lightest,
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'image_details_widget.dart' show ImageDetailsWidget;
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
class ImageDetailsModel extends FlutterFlowModel<ImageDetailsWidget> {
/// State fields for stateful widgets in this page.
final unfocusNode = FocusNode();
@override
void initState(BuildContext context) {}
@override
void dispose() {
unfocusNode.dispose();
}
}
@@ -0,0 +1,243 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:app/theme.dart';
import 'image_details_model.dart';
export 'image_details_model.dart';
class ImageDetailsWidget extends StatefulWidget {
const ImageDetailsWidget({super.key});
@override
State<ImageDetailsWidget> createState() => _ImageDetailsWidgetState();
}
class _ImageDetailsWidgetState extends State<ImageDetailsWidget>
with TickerProviderStateMixin {
late ImageDetailsModel _model;
final scaffoldKey = GlobalKey<ScaffoldState>();
final animationsMap = <String, AnimationInfo>{};
@override
void initState() {
super.initState();
_model = createModel(context, () => ImageDetailsModel());
animationsMap.addAll({
'imageOnPageLoadAnimation': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 1.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 300.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 300.0.ms,
begin: const Offset(0.0, 30.0),
end: const Offset(0.0, 0.0),
),
ScaleEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 300.0.ms,
begin: const Offset(0.7, 0.7),
end: const Offset(1.0, 1.0),
),
],
),
'rowOnPageLoadAnimation': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 1.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 300.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 300.0.ms,
begin: const Offset(0.0, 30.0),
end: const Offset(0.0, 0.0),
),
TiltEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 300.0.ms,
begin: const Offset(-0.698, 0),
end: const Offset(0, 0),
),
],
),
});
}
@override
void dispose() {
_model.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _model.unfocusNode.canRequestFocus
? FocusScope.of(context).requestFocus(_model.unfocusNode)
: FocusScope.of(context).unfocus(),
child: Scaffold(
key: scaffoldKey,
backgroundColor: AppTheme.secondaryBackground,
appBar: AppBar(
backgroundColor: AppTheme.secondaryBackground,
automaticallyImplyLeading: false,
leading: const FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 60,
icon: Icon(
Icons.arrow_back_rounded,
color: AppTheme.primaryTextColor,
size: 30,
),
// TODO
// onPressed: () async {
// context.pop();
// },
),
actions: const [],
centerTitle: false,
elevation: 0,
),
body: SafeArea(
top: true,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: Hero(
tag: 'hero',
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 500),
fadeOutDuration: const Duration(milliseconds: 500),
imageUrl: 'https://picsum.photos/seed/150/600',
width: double.infinity,
fit: BoxFit.fitWidth,
memCacheWidth: 1200,
memCacheHeight: 1200,
),
),
).animateOnPageLoad(
animationsMap['imageOnPageLoadAnimation']!),
),
),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(12, 12, 12, 32),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 4, 8, 16),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppTheme.accent1,
borderRadius: BorderRadius.circular(12),
shape: BoxShape.rectangle,
border: Border.all(
color: AppTheme.primary,
width: 2,
),
),
child: Padding(
padding: const EdgeInsets.all(2),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
imageUrl:
'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w0NTYyMDF8MHwxfHNlYXJjaHwyMHx8cHJvZmlsZXxlbnwwfHx8fDE2OTU4MjQ5OTN8MA&ixlib=rb-4.0.3&q=80&w=1080',
width: 44,
height: 44,
fit: BoxFit.cover,
),
),
),
),
),
Flexible(
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 8),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SelectionArea(
child: AutoSizeText(
'Thanks so much! We really do appreciate it!',
textAlign: TextAlign.start,
style: AppTheme.bodyMedium,
)),
const Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 4, 0, 0),
child: Text(
'Just Now',
style: AppTheme.labelSmall,
),
),
].divide(const SizedBox(width: 4)),
),
const Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0, 4, 0, 0),
child: SelectionArea(
child: AutoSizeText(
'Thanks so much! We really do appreciate it!',
textAlign: TextAlign.start,
style: AppTheme.labelLarge,
)),
),
],
),
),
),
],
).animateOnPageLoad(animationsMap['rowOnPageLoadAnimation']!),
),
],
),
),
),
);
}
}
@@ -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();
}
}
@@ -0,0 +1,16 @@
import '/components/popup/popup_widget.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'settings_widget.dart' show SettingsWidget;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
class SettingsModel extends FlutterFlowModel<SettingsWidget> {
@override
void initState(BuildContext context) {}
@override
void dispose() {}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'setup_widget.dart' show SetupWidget;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
class SetupModel extends FlutterFlowModel<SetupWidget> {
/// State fields for stateful widgets in this page.
final unfocusNode = FocusNode();
@override
void initState(BuildContext context) {}
@override
void dispose() {
unfocusNode.dispose();
}
}
+303
View File
@@ -0,0 +1,303 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:app/theme.dart';
import 'setup_model.dart';
export 'setup_model.dart';
class SetupWidget extends StatefulWidget {
const SetupWidget({super.key});
@override
State<SetupWidget> createState() => _SetupWidgetState();
}
class _SetupWidgetState extends State<SetupWidget>
with TickerProviderStateMixin {
late SetupModel _model;
final scaffoldKey = GlobalKey<ScaffoldState>();
final animationsMap = <String, AnimationInfo>{};
@override
void initState() {
super.initState();
_model = createModel(context, () => SetupModel());
animationsMap.addAll({
'containerOnPageLoadAnimation1': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 1.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 400.0.ms,
begin: 0.0,
end: 1.0,
),
ScaleEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 400.0.ms,
begin: const Offset(3.0, 3.0),
end: const Offset(1.0, 1.0),
),
],
),
'containerOnPageLoadAnimation2': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 300.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 300.0.ms,
duration: 300.0.ms,
begin: 0.0,
end: 1.0,
),
ScaleEffect(
curve: Curves.bounceOut,
delay: 300.0.ms,
duration: 300.0.ms,
begin: const Offset(0.6, 0.6),
end: const Offset(1.0, 1.0),
),
],
),
'textOnPageLoadAnimation1': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 350.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 350.0.ms,
duration: 400.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 350.0.ms,
duration: 400.0.ms,
begin: const Offset(0.0, 30.0),
end: const Offset(0.0, 0.0),
),
],
),
'textOnPageLoadAnimation2': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 400.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 400.0.ms,
duration: 400.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 400.0.ms,
duration: 400.0.ms,
begin: const Offset(0.0, 30.0),
end: const Offset(0.0, 0.0),
),
],
),
'rowOnPageLoadAnimation': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
VisibilityEffect(duration: 300.ms),
FadeEffect(
curve: Curves.easeInOut,
delay: 300.0.ms,
duration: 600.0.ms,
begin: 0.0,
end: 1.0,
),
ScaleEffect(
curve: Curves.bounceOut,
delay: 300.0.ms,
duration: 600.0.ms,
begin: const Offset(0.6, 0.6),
end: const Offset(1.0, 1.0),
),
],
),
});
}
@override
void dispose() {
_model.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _model.unfocusNode.canRequestFocus
? FocusScope.of(context).requestFocus(_model.unfocusNode)
: FocusScope.of(context).unfocus(),
child: Scaffold(
key: scaffoldKey,
backgroundColor: AppTheme.secondaryBackground,
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Container(
width: double.infinity,
height: 500,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.primary,
AppTheme.error,
AppTheme.tertiary
],
stops: [0, 0.5, 1],
begin: AlignmentDirectional(-1, -1),
end: AlignmentDirectional(1, 1),
),
),
child: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0x00FFFFFF),
AppTheme.secondaryBackground
],
stops: [0, 1],
begin: AlignmentDirectional(0, -1),
end: AlignmentDirectional(0, 1),
),
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: const BoxDecoration(
color: AppTheme.accent4,
shape: BoxShape.circle,
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Image.network(
'https://storage.googleapis.com/flutterflow-io-6f20.appspot.com/projects/f-f-templates-q1-23-fbcr63/assets/ax4fvwjz7awx/@4xff_badgeDesign_dark_small.png',
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
).animateOnPageLoad(
animationsMap['containerOnPageLoadAnimation2']!),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 44, 0, 0),
child: const Text(
'Welcome!',
style: AppTheme.displaySmall,
).animateOnPageLoad(
animationsMap['textOnPageLoadAnimation1']!),
),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(44, 8, 44, 0),
child: const Text(
'Thanks for joining! Access or create your account below, and get started on your journey!',
textAlign: TextAlign.center,
style: AppTheme.labelMedium,
).animateOnPageLoad(
animationsMap['textOnPageLoadAnimation2']!),
),
],
),
),
).animateOnPageLoad(
animationsMap['containerOnPageLoadAnimation1']!),
),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 24, 16, 44),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Align(
alignment: const AlignmentDirectional(0, 0),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 0, 8, 16),
child: FFButtonWidget(
onPressed: () {
print('Button pressed ...');
},
text: 'Get Started',
options: FFButtonOptions(
width: 230,
height: 52,
padding: const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
iconPadding:
const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
color: AppTheme
.secondaryBackground,
textStyle: AppTheme.bodyLarge,
elevation: 0,
borderSide: const BorderSide(
color: AppTheme.alternate,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
Expanded(
child: Align(
alignment: const AlignmentDirectional(0, 0),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 16),
child: FFButtonWidget(
onPressed: () {
print('Button pressed ...');
},
text: 'My Account',
options: FFButtonOptions(
width: 230,
height: 52,
padding: const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
iconPadding:
const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
color: AppTheme.primary,
textStyle: AppTheme.titleSmall,
elevation: 3,
borderSide: const BorderSide(
color: Colors.transparent,
width: 1,
),
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
],
).animateOnPageLoad(animationsMap['rowOnPageLoadAnimation']!),
),
],
),
),
);
}
}
@@ -0,0 +1,40 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'support_form_widget.dart' show SupportFormWidget;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
class SupportFormModel extends FlutterFlowModel<SupportFormWidget> {
/// State fields for stateful widgets in this page.
final unfocusNode = FocusNode();
// State field(s) for TextField widget.
FocusNode? textFieldFocusNode1;
TextEditingController? textController1;
String? Function(BuildContext, String?)? textController1Validator;
// State field(s) for TextField widget.
FocusNode? textFieldFocusNode2;
TextEditingController? textController2;
String? Function(BuildContext, String?)? textController2Validator;
// State field(s) for TextField widget.
FocusNode? textFieldFocusNode3;
TextEditingController? textController3;
String? Function(BuildContext, String?)? textController3Validator;
@override
void initState(BuildContext context) {}
@override
void dispose() {
unfocusNode.dispose();
textFieldFocusNode1?.dispose();
textController1?.dispose();
textFieldFocusNode2?.dispose();
textController2?.dispose();
textFieldFocusNode3?.dispose();
textController3?.dispose();
}
}
@@ -0,0 +1,496 @@
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../components/support_form_model.dart';
export '../components/support_form_model.dart';
import 'package:app/theme.dart';
class SupportFormWidget extends StatefulWidget {
const SupportFormWidget({super.key});
@override
State<SupportFormWidget> createState() => _SupportFormWidgetState();
}
class _SupportFormWidgetState extends State<SupportFormWidget>
with TickerProviderStateMixin {
late SupportFormModel _model;
final scaffoldKey = GlobalKey<ScaffoldState>();
final animationsMap = <String, AnimationInfo>{};
@override
void initState() {
super.initState();
_model = createModel(context, () => SupportFormModel());
_model.textController1 ??= TextEditingController();
_model.textFieldFocusNode1 ??= FocusNode();
_model.textController2 ??= TextEditingController();
_model.textFieldFocusNode2 ??= FocusNode();
_model.textController3 ??= TextEditingController();
_model.textFieldFocusNode3 ??= FocusNode();
animationsMap.addAll({
'containerOnPageLoadAnimation1': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
FadeEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 600.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 600.0.ms,
begin: const Offset(0.0, 110.0),
end: const Offset(0.0, 0.0),
),
],
),
'containerOnPageLoadAnimation2': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
FadeEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 600.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 600.0.ms,
begin: const Offset(0.0, 110.0),
end: const Offset(0.0, 0.0),
),
],
),
'containerOnPageLoadAnimation3': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
effectsBuilder: () => [
FadeEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 600.0.ms,
begin: 0.0,
end: 1.0,
),
MoveEffect(
curve: Curves.easeInOut,
delay: 0.0.ms,
duration: 600.0.ms,
begin: const Offset(0.0, 110.0),
end: const Offset(0.0, 0.0),
),
],
),
});
setupAnimations(
animationsMap.values.where((anim) =>
anim.trigger == AnimationTrigger.onActionTrigger ||
!anim.applyInitialState),
this,
);
}
@override
void dispose() {
_model.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _model.unfocusNode.canRequestFocus
? FocusScope.of(context).requestFocus(_model.unfocusNode)
: FocusScope.of(context).unfocus(),
child: Scaffold(
key: scaffoldKey,
backgroundColor: AppTheme.secondaryBackground,
appBar: AppBar(
backgroundColor: AppTheme.secondaryBackground,
automaticallyImplyLeading: false,
leading: FlutterFlowIconButton(
borderColor: Colors.transparent,
borderRadius: 30,
borderWidth: 1,
buttonSize: 60,
icon: const Icon(
Icons.arrow_back_rounded,
color: AppTheme.primaryTextColor,
size: 30,
),
onPressed: () async {
//TODO: context.pop();
},
),
title: const Text(
'Submit Ticket',
style: AppTheme.titleLarge,
),
actions: const [],
centerTitle: false,
elevation: 0,
),
body: SafeArea(
top: true,
child: Align(
alignment: const AlignmentDirectional(0, 1),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 12, 16, 0),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Welcome to support',
style: AppTheme.labelLarge,
),
const Padding(
padding: EdgeInsetsDirectional.fromSTEB(0, 4, 0, 0),
child: Text(
'Submit a bug',
style: AppTheme.headlineMedium,
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(0, 16, 0, 0),
child: Container(
width: 120,
constraints: const BoxConstraints(
maxWidth: 500,
),
decoration: BoxDecoration(
color: AppTheme
.secondaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.alternate,
width: 2,
),
),
child: const Padding(
padding: EdgeInsetsDirectional.fromSTEB(
8, 16, 8, 16),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.email_outlined,
color:
AppTheme.primary,
size: 36,
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 12, 0, 0),
child: Text(
'Email Us',
textAlign: TextAlign.center,
style: AppTheme.bodyMedium,
),
),
],
),
),
).animateOnPageLoad(animationsMap[
'containerOnPageLoadAnimation1']!),
),
),
Expanded(
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(0, 16, 0, 0),
child: Container(
width: 120,
constraints: const BoxConstraints(
maxWidth: 500,
),
decoration: BoxDecoration(
color: AppTheme
.secondaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.alternate,
width: 2,
),
),
child: const Padding(
padding: EdgeInsetsDirectional.fromSTEB(
8, 16, 8, 16),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.search_rounded,
color:
AppTheme.primary,
size: 36,
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 12, 0, 0),
child: Text(
'Search FAQs',
textAlign: TextAlign.center,
style: AppTheme.bodyMedium,
),
),
],
),
),
).animateOnPageLoad(animationsMap[
'containerOnPageLoadAnimation2']!),
),
),
].divide(const SizedBox(width: 12)),
),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 16, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
TextFormField(
controller: _model.textController1,
focusNode: _model.textFieldFocusNode1,
autofocus: true,
obscureText: false,
decoration: InputDecoration(
labelText: 'Channel Name',
labelStyle: AppTheme.labelMedium,
hintStyle: AppTheme.labelMedium,
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.alternate,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.primary,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.error,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.error,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsetsDirectional.fromSTEB(
16, 12, 16, 12),
),
style: AppTheme.bodyMedium,
cursorColor: AppTheme.primary,
validator: _model.textController1Validator
.asValidator(context),
),
TextFormField(
controller: _model.textController2,
focusNode: _model.textFieldFocusNode2,
autofocus: true,
obscureText: false,
decoration: InputDecoration(
labelText: 'Channel ID',
labelStyle: AppTheme.labelMedium,
hintStyle: AppTheme.labelMedium,
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.alternate,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.primary,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.error,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.error,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsetsDirectional.fromSTEB(
16, 12, 16, 12),
),
style: AppTheme.bodyMedium,
cursorColor: AppTheme.primary,
validator: _model.textController2Validator
.asValidator(context),
),
TextFormField(
controller: _model.textController3,
focusNode: _model.textFieldFocusNode3,
autofocus: true,
obscureText: false,
decoration: InputDecoration(
labelStyle: AppTheme.labelMedium,
hintText:
'Short Description of what is going on...',
hintStyle: AppTheme.labelMedium,
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.alternate,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.primary,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.error,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppTheme.error,
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsetsDirectional.fromSTEB(
16, 24, 16, 12),
),
style: AppTheme.bodyMedium,
maxLines: 16,
minLines: 6,
cursorColor: AppTheme.primary,
validator: _model.textController3Validator
.asValidator(context),
),
].divide(const SizedBox(height: 12)),
),
),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 16, 0, 0),
child: Container(
width: double.infinity,
constraints: const BoxConstraints(
maxWidth: 500,
),
decoration: BoxDecoration(
color:
AppTheme.secondaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.alternate,
width: 2,
),
),
child: const Padding(
padding: EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(
Icons.add_a_photo_rounded,
color: AppTheme.primary,
size: 32,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(16, 0, 0, 0),
child: Text(
'Upload Screenshot',
textAlign: TextAlign.center,
style: AppTheme.bodyMedium,
),
),
],
),
),
).animateOnPageLoad(
animationsMap['containerOnPageLoadAnimation3']!),
),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 24, 0, 12),
child: FFButtonWidget(
onPressed: () {
print('Button pressed ...');
},
text: 'Submit Ticket',
icon: const Icon(
Icons.receipt_long,
size: 15,
),
options: FFButtonOptions(
width: double.infinity,
height: 48,
padding: const EdgeInsets.all(0),
iconPadding:
const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
color: AppTheme.primary,
textStyle: AppTheme.titleSmall,
elevation: 4,
borderSide: const BorderSide(
color: Colors.transparent,
width: 1,
),
borderRadius: BorderRadius.circular(60),
),
),
),
],
),
),
),
),
),
),
);
}
}
+231
View File
@@ -0,0 +1,231 @@
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/components/util/speech_to_text.dart';
import 'model.dart';
// Uses Singleton design pattern to ensure that any server references across the app are using the same instance.
class Backend {
// SERVER VARIABLES
static final Backend singleBackend = Backend._internal();
Backend._internal();
// TODO: One-time process to establish server connection is required for easy setup/maintenance.
static const url = "http://10.0.2.2:11434/api/"; // For emulated device in testing
//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;
// AI VARIABLES
static final SpeechToText stt = SpeechToText();
static late bool _speechEnabled;
// 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
static late int conversationID;
static late int maxConversationID;
static late Model model;
static String prompt = "";
static String response = "";
static bool initiated = false;
// TODO: Verify all different components of STT, TTS, LLM, etc have been initialized.
// TODO: Handshake w/ server to validate identity
factory Backend() {
return singleBackend;
}
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
// TODO: Dynamically obtain conversations.
// TODO: Obtain all user permissions at once.
}
// 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.
if (!_speechEnabled) {
return "Unable to process Speech-To-Text on-device.";
}
prompt = stt.getTextWhenReady() as String;
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, 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
@Deprecated("")
static List<Conversation> getConversationsList() { return conversations; }
static Conversation getLoadedConversation() { return loadedConversation; }
// Load conversation should be called prior to any other server requests.
static void loadConversation(id) {
conversationID = id;
loadedConversation = conversations[id];
loadedChats = loadedConversation.chats;
}
// TODO: Better conversation ID management is needed. Preferably randomized IDs to not indicate the number of conversations.
static int createConversation() {
int id = conversations.length;
conversations.add(Conversation(id, "Conversation $id", "Another AI conversation"));
return id;
}
static void deleteConversation(id) {
loadedChats = conversations[conversationID].chats; // TODO: Temporarily saving the chats for "undo" popup.
conversations.removeAt(id);
}
@Deprecated('Just directly get loadedChats list. Unless multiple chats can be opened simultaneously?')
static List<Chat> getChatsByConversationID(id) { return conversations[id].chats; }
// SERVER UTILS
static List<Map<String, String>> convertMessages() {
// TODO: Integrate with getConversationByID()
List<Map<String, String>> messageList = [];
for (Chat c in loadedChats) {
messageList.add({
"role": c.role ? "user" : "assistant",
"content": c.content,
});
}
return messageList;
}
// Formats the prompt in JSON.
static String constructPrompt() {
// TODO: Allow for additional flags, ie. continuous conversation, images, etc.
// TODO: Allow for toggleable states between stream
// TODO: Allow for different models to be dynamically selected.
Map<String, dynamic> data;
if(conversations[conversationID].conversationContext) {
loadedChats = [...loadedChats, Chat(prompt, true, 0)];
endpoint = "chat";
List<Map<String, String>> messageList = convertMessages();
data = {
"model": model.name,
"messages": messageList,
"stream": loadedConversation.stream,
};
}
else {
endpoint = "generate";
data = {
"model": model.name,
"prompt": prompt,
"stream": loadedConversation.stream,
};
}
// Encode the JSON payload
return json.encode(data);
}
// Send the HTTP request to the server.
static void httpSendRequest() async {
// TODO: Add AES256 encryption to the prompt/messages thread.
// TODO: Add SHA256 checksum to be sent and verified to prevent/notify in case of data loss.
String json = constructPrompt();
// Send the HTTP POST request
final serverResponse = await http.post(
Uri.parse('$url$endpoint'),
headers: headers,
body: json,
);
if (serverResponse.statusCode == 200) {
Map<String, dynamic> re = jsonDecode(serverResponse.body);
if(conversations[conversationID].conversationContext) {
response = re["message"]["content"];
// TODO: Modify in case of different response types.
loadedChats = [...loadedChats, Chat(response, false, 0)];
} else {
response = re["response"];
}
}
conversations[conversationID].add(Chat(response, false, 0));
prompt = "";
response = "";
}
}
@@ -0,0 +1,5 @@
class Model {
late String name;
Model(this.name);
}
@@ -0,0 +1,59 @@
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 = "";
// Check if speech recognition is enabled.
bool isSpeechEnabled() => speechEnabled;
Future<String> getTextWhenReady() async {
await speechToText.stop();
return text;
}
// 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,
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
/// and the SpeechToText plugin supports setting timeouts on the
/// listen method.
void stopListening() async => await speechToText.stop();
SpeechToText() {
initSpeech();
}
/// This has to happen only once per app
void initSpeech() async => speechEnabled = await speechToText.initialize();
/// This is the callback that the SpeechToText plugin calls when
/// the platform returns recognized words.
void onSpeechResult(SpeechRecognitionResult result) => (text = result.recognizedWords);
}
@@ -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();
}
}