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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user