frontend/frontend-flutter/lib/utils/custom_input_field.dart (459 lines of code) (raw):
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/src/models/input_clear_mode.dart';
import 'package:flutter_chat_ui/src/models/send_button_visibility_mode.dart';
import 'package:flutter_chat_ui/src/util.dart';
import 'package:flutter_chat_ui/src/widgets/state/inherited_chat_theme.dart';
import 'package:flutter_chat_ui/src/widgets/state/inherited_l10n.dart';
import 'package:flutter_chat_ui/src/widgets/input/attachment_button.dart';
import 'package:flutter_chat_ui/src/widgets/input/input_text_field_controller.dart';
import 'package:flutter_chat_ui/src/widgets/input/send_button.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'dart:html' as html;
import '../services/new_suggestions/new_suggestion_cubit.dart';
import '../services/new_suggestions/new_suggestion_state.dart';
import 'TextToDocParameter.dart';
/// A class that represents bottom bar widget with a text field, attachment and
/// send buttons inside. By default hides send button when text field is empty.
class CustomInputField extends StatefulWidget {
/// Creates [Input] widget.
const CustomInputField({
super.key,
this.isAttachmentUploading,
this.onAttachmentPressed,
required this.onSendPressed,
this.db,
this.options = const InputOptions(),
});
/// Whether attachment is uploading. Will replace attachment button with a
/// [CircularProgressIndicator]. Since we don't have libraries for
/// managing media in dependencies we have no way of knowing if
/// something is uploading so you need to set this manually.
final bool? isAttachmentUploading;
final FirebaseFirestore? db;
/// See [AttachmentButton.onPressed].
final VoidCallback? onAttachmentPressed;
/// Will be called on [SendButton] tap. Has [types.PartialText] which can
/// be transformed to [types.TextMessage] and added to the messages list.
final void Function(types.PartialText) onSendPressed;
/// Customisation options for the [Input].
final InputOptions options;
@override
State<CustomInputField> createState() => _InputState();
}
/// [Input] widget state.
class _InputState extends State<CustomInputField> {
List<Suggestion> suggestionsList = [];
late final _inputFocusNode = FocusNode(
onKeyEvent: (node, event) {
if (event.physicalKey == PhysicalKeyboardKey.enter &&
!HardwareKeyboard.instance.physicalKeysPressed.any(
(el) => <PhysicalKeyboardKey>{
PhysicalKeyboardKey.shiftLeft,
PhysicalKeyboardKey.shiftRight,
}.contains(el),
)) {
if (kIsWeb && _textController.value.isComposingRangeValid) {
return KeyEventResult.ignored;
}
if (event is KeyDownEvent) {
_handleSendPressed();
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
bool _sendButtonVisible = false;
late TextEditingController _textController;
@override
void initState() {
super.initState();
_textController =
widget.options.textEditingController ?? InputTextFieldController();
_handleSendButtonVisibilityModeChange();
}
void _handleSendButtonVisibilityModeChange() {
_textController.removeListener(_handleTextControllerChange);
if (widget.options.sendButtonVisibilityMode ==
SendButtonVisibilityMode.hidden) {
_sendButtonVisible = false;
} else if (widget.options.sendButtonVisibilityMode ==
SendButtonVisibilityMode.editing) {
_sendButtonVisible = _textController.text.trim() != '';
_textController.addListener(_handleTextControllerChange);
} else {
_sendButtonVisible = true;
}
}
void _handleSendPressed() {
print(
"CustomInputField: build() : _inputBuilder() : TypeAheadField : _handleSendPressed()");
final trimmedText = _textController.text.trim();
if (trimmedText != '') {
final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
if (widget.options.inputClearMode == InputClearMode.always) {
_textController.clear();
}
}
}
void _handleTextControllerChange() {
if (_textController.value.isComposingRangeValid) {
return;
}
setState(() {
_sendButtonVisible = _textController.text.trim() != '';
});
}
Widget _inputBuilder() {
final query = MediaQuery.of(context);
final buttonPadding = InheritedChatTheme.of(context)
.theme
.inputPadding
.copyWith(left: 16, right: 16);
final safeAreaInsets = isMobile
? EdgeInsets.fromLTRB(
query.padding.left,
0,
query.padding.right,
query.viewInsets.bottom + query.padding.bottom,
)
: EdgeInsets.zero;
final textPadding = InheritedChatTheme.of(context)
.theme
.inputPadding
.copyWith(left: 0, right: 0)
.add(
EdgeInsets.fromLTRB(
widget.onAttachmentPressed != null ? 0 : 24,
0,
_sendButtonVisible ? 0 : 24,
0,
),
);
return Focus(
autofocus: !widget.options.autofocus,
child: Padding(
padding: InheritedChatTheme.of(context).theme.inputMargin,
child: Material(
borderRadius: InheritedChatTheme.of(context).theme.inputBorderRadius,
color: InheritedChatTheme.of(context).theme.inputBackgroundColor,
surfaceTintColor:
InheritedChatTheme.of(context).theme.inputSurfaceTintColor,
elevation: InheritedChatTheme.of(context).theme.inputElevation,
child: Container(
decoration: BoxDecoration(
color: Color(0xFFF0F2F6), // Background color
),
//InheritedChatTheme.of(context).theme.inputContainerDecoration,
padding: safeAreaInsets,
child: Row(
textDirection: TextDirection.ltr,
children: [
/*if (widget.onAttachmentPressed != null)
AttachmentButton(
isLoading: widget.isAttachmentUploading ?? false,
onPressed: widget.onAttachmentPressed,
padding: buttonPadding,
),*/
Container(width: 30),
Expanded(
child: Padding(
padding: textPadding,
child: FutureBuilder(
future: getAllquestions(),
builder: (context, snapshot) {
if(snapshot.hasData) {
suggestionsList = snapshot.data!;
print(
"CustomInputField: build() : _inputBuilder() : suggestionList.length = ${suggestionsList.length}");
}
return TypeAheadField<Suggestion>(
hideOnEmpty: true,
controller: _textController,
direction: VerticalDirection.up,
loadingBuilder: (context) =>
const Text('Loading...'),
onSelected: (entry) {
_textController.text = entry.suggestion!;
entry.scenarioNumber;
BlocProvider.of<NewSuggestionCubit>(context)
.generateNewSuggestions(
entry.scenarioNumber!, entry.suggestion!,
isACannedQuestion: false,
userGrouping: entry.userGrouping
);
},
itemBuilder: (context, entry) {
print(
"CustomInputField: build() : TypeAheadField : _inputBuilder(): itemBuilder: entry = $entry");
return ListTile(
title: Text(entry.suggestion!),
);
},
suggestionsCallback:
_textController.text.length <= 1
? suggestionsEmptyCallback
: suggestionsCallback,
builder:
(context, _textController, inputFocusNode) {
print(
"CustomInputField: build() : TypeAheadField : _inputBuilder(): builder: focusNode = $inputFocusNode");
return TextField(
enabled: widget.options.enabled,
autocorrect: widget.options.autocorrect,
autofocus: widget.options.autofocus,
enableSuggestions:
widget.options.enableSuggestions,
controller: _textController,
cursorColor: InheritedChatTheme.of(context)
.theme
.inputTextCursorColor,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
50.0), // Adjust the radius as needed
),
//labelText: 'Password',
filled: true,
fillColor: Colors.white,
suffixIcon: Visibility(
visible: _sendButtonVisible,
child: IconButton(
onPressed: () {
print(
"CustomInputField: build() : _inputBuilder() : TypeAheadField : IconButton : onPressed: ()");
_handleSendPressed();
},
icon: Icon(Icons.send),
),
),
),
focusNode: inputFocusNode,
keyboardType: widget.options.keyboardType,
maxLines: 5,
minLines: 1,
onChanged: widget.options.onTextChanged,
onTap: widget.options.onTextFieldTap,
style: TextStyle(color: Colors.black),
textCapitalization:
TextCapitalization.sentences,
);
},
);
},
)),
),
Container(width: 30),
],
),
),
),
),
);
}
Future<void> loadCfgFromFirestore() async {
/*db = await FirebaseFirestore.instanceFor(
app: app, databaseId: 'opendataqna-session-logs');*/
print("CustomInputField: loadCfgFromFirestore() : db = $widget.db");
if (TextToDocParameter.userID.isEmpty) {
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.userID is empty = ${TextToDocParameter.userID}");
return;
}
try {
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.userID = ${TextToDocParameter.userID}");
DocumentSnapshot doc = await widget.db!
.collection("front_end_flutter_cfg")
.doc('${TextToDocParameter.userID}')
.get();
if (doc != null) {
final data = doc.data() as Map<String, dynamic>;
TextToDocParameter.anonymized_data = data["anonymized_data"];
TextToDocParameter.expert_mode = data["expert_mode"];
TextToDocParameter.endpoint_opendataqnq = data["endpoint_opendataqnq"];
TextToDocParameter.firestore_database_id =
data["firestore_database_id"];
TextToDocParameter.firebase_app_name = data["firebase_app_name"];
TextToDocParameter.firestore_history_collection =
data["firestore_history_collection"];
TextToDocParameter.firestore_cfg_collection =
data["firestore_cfg_collection"];
TextToDocParameter.imported_questions = data["imported_questions"];
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.anonymized_data = ${TextToDocParameter.anonymized_data}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.expert_mode = ${TextToDocParameter.expert_mode}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firestore_database_id = ${TextToDocParameter.firestore_database_id}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.endpoint_opendataqnq = ${TextToDocParameter.endpoint_opendataqnq}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firebase_app_name = ${TextToDocParameter.firebase_app_name}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firestore_history_collection = ${TextToDocParameter.firestore_history_collection}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firestore_cfg_collection = ${TextToDocParameter.firestore_cfg_collection}");
print(
"CustomInputField: loadCfgFromFirestore() : TextToDocParameter.imported_questions = ${TextToDocParameter.imported_questions}");
} else {
print("CustomInputField: loadCfgFromFirestore() : doc == null");
}
} catch (e) {
print("CustomInputField: loadCfgFromFirestore() : EXCEPTION ON FIRESTORE : e = $e");
//https://www.acodeblog.com/post/2022/5/29/flutter-showdialog-without-context-using-the-navigatorkey
}
}
Future<List<Suggestion>> getAllquestions() async {
List<Suggestion> resp = [];
List <String> userGroupingList = [];
print('CustomInputField: getAllquestions() : START');
await loadCfgFromFirestore();
userGroupingList = await _getUserGrouping();
print('CustomInputField: getAllquestions() : userGroupingList = ${userGroupingList}');
for (String userGrouping in userGroupingList) {
var list = await getAllquestionsFromUserGroup(userGrouping);
resp.addAll((list as List<String>)
.map((question) =>
Suggestion(
suggestion: question, userGrouping: userGrouping!))
.toList());
print('CustomInputField: getAllquestions() : userGrouping = $userGrouping : resp.length = ${resp.length}');
}
print('CustomInputField: getAllquestions() : END : resp.length = ${resp.length}');
return resp;
}
Future<List<String>> getAllquestionsFromUserGroup(String userGrouping) async {
List<String> resp = [];
String body = "";
print('CustomInputField : getAllquestionsFromUserGroup() : START');
//Create the header
Map<String, String>? _headers = {
"Content-Type": "application/json",
//"Authorization": " Bearer ${client!.credentials.accessToken.toString()}",
};
//Create the body
body = '''{
"user_grouping": "$userGrouping"
}''';
print('CustomInputField : getAllquestionsFromUserGroup() : body = ' + body);
try {
var response = await html.HttpRequest.requestCrossOrigin(
'${TextToDocParameter.endpoint_opendataqnq}/get_known_sql',
method: "POST",
sendData: body);
print('CustomInputField : getAllquestionsFromUserGroup() : response = ' +
response.toString());
final jsonData = jsonDecode(response);
if (jsonData != null) {
print('CustomInputField: getAllquestionsFromUserGroup() : jsonData = $jsonData');
//KnownSQL = [{"example_user_question": "question1", "example_generated_sql": "sql1"},
// {"example_user_question": "question2", "example_generated_sql": "sql2"},
// ...]
var knownSql =
jsonData["KnownSQL"].replaceAll(RegExp(r'((\\n)|(\\r))'), '');
print('CustomInputField: getAllquestionsFromUserGroup() : knownSql = $knownSql');
var knownSqlMap = jsonDecode(knownSql);
for (int i = 0; i < knownSqlMap.length; i++) {
for (var entry in knownSqlMap[i].entries) {
print('${entry.key} : ${entry.value}');
if (entry.key == "example_user_question") resp.add(entry.value);
}
}
}
} catch (e) {
print('CustomInputField: getAllquestionsFromUserGroup() : EXCEPTION = $e');
throw Exception('Failed to get questions: $e');
} finally {
print('CustomInputField: getAllquestionsFromUserGroup() : END : resp = ${resp}');
return resp;
}
}
Future<List<String>> _getUserGrouping() async {
print('CustomInputField : _getUserGrouping() : START');
List<String> resp = [];
Map<String, String>? _headers = {
"Content-Type": "application/json",
//"Authorization": " Bearer ${client!.credentials.accessToken.toString()}",
};
try {
print(
'CustomInputField : _getUserGrouping() : url = ${TextToDocParameter.endpoint_opendataqnq}/available_databases');
var response = await html.HttpRequest.requestCrossOrigin(
'${TextToDocParameter.endpoint_opendataqnq}/available_databases',
method: "GET");
print('CustomInputField : _getUserGrouping() : response = ' + response.toString());
final jsonData = jsonDecode(response);
if (jsonData != null) {
print('CustomInputField : _getUserGrouping() : jsonData = $jsonData');
/* Expected response :
{
"Error": "",
"KnownDB": "[{\"table_schema\":\"imdb-postgres\"},{\"table_schema\":\"retail-postgres\"}]",
"ResponseCode": 200
}*/
var knownSqlMap = jsonDecode(jsonData['KnownDB']);
print('CustomInputField : _getUserGrouping() : knownSqlMap = ${knownSqlMap}');
print(
'CustomInputField : _getUserGrouping() : knownSqlMap[0] = ${knownSqlMap[0].toString()}');
for (int i = 0; i < knownSqlMap.length; i++) {
for (var entry in knownSqlMap[i].entries) {
print('${entry.key} : ${entry.value}');
if (entry.key == "table_schema") resp.add(entry.value);
}
}
} else {
resp.add("");
}
} catch (e) {
print('CustomInputField : _getUserGrouping() : EXCEPTION = $e');
throw Exception('Failed to get earning calls question suggestions: $e');
} finally {
print('CustomInputField : _getUserGrouping() : resp = $resp');
return resp;
}
}
@override
void didUpdateWidget(covariant CustomInputField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.options.sendButtonVisibilityMode !=
oldWidget.options.sendButtonVisibilityMode) {
_handleSendButtonVisibilityModeChange();
}
}
@override
void dispose() {
_inputFocusNode.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
print("CustomInputField: build(): START");
return GestureDetector(
onTap: () => _inputFocusNode.requestFocus(),
child: _inputBuilder(),
);
}
Future<List<Suggestion>> suggestionsCallback(String pattern) async =>
Future<List<Suggestion>>.delayed(
Duration(milliseconds: 300),
() => suggestionsList.where((entry) {
final nameLower = entry.suggestion!.toLowerCase();
final patternLower = pattern.toLowerCase();//pattern.toLowerCase().split(' ').join('');
return nameLower.contains(patternLower);
}).toList(),
);
Future<List<Suggestion>> suggestionsEmptyCallback(String pattern) async =>
Future<List<Suggestion>>.delayed(Duration(milliseconds: 300), () {
return [];
/*return [
Suggestion(
suggestion: "looking for suggestions ...", scenarioNumber: 0)
].where((entry) {
final nameLower = entry.suggestion!.toLowerCase();
final patternLower = pattern.toLowerCase().split(' ').join('');
return nameLower.contains(patternLower);
}).toList()*/
;
});
}
@immutable
class InputOptions {
const InputOptions({
this.inputClearMode = InputClearMode.always,
this.keyboardType = TextInputType.multiline,
this.onTextChanged,
this.onTextFieldTap,
this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing,
this.textEditingController,
this.autocorrect = true,
this.autofocus = false,
this.enableSuggestions = true,
this.enabled = true,
});
/// Controls the [Input] clear behavior. Defaults to [InputClearMode.always].
final InputClearMode inputClearMode;
/// Controls the [Input] keyboard type. Defaults to [TextInputType.multiline].
final TextInputType keyboardType;
/// Will be called whenever the text inside [TextField] changes.
final void Function(String)? onTextChanged;
/// Will be called on [TextField] tap.
final VoidCallback? onTextFieldTap;
/// Controls the visibility behavior of the [SendButton] based on the
/// [TextField] state inside the [Input] widget.
/// Defaults to [SendButtonVisibilityMode.editing].
final SendButtonVisibilityMode sendButtonVisibilityMode;
/// Custom [TextEditingController]. If not provided, defaults to the
/// [InputTextFieldController], which extends [TextEditingController] and has
/// additional fatures like markdown support. If you want to keep additional
/// features but still need some methods from the default [TextEditingController],
/// you can create your own [InputTextFieldController] (imported from this lib)
/// and pass it here.
final TextEditingController? textEditingController;
/// Controls the [TextInput] autocorrect behavior. Defaults to [true].
final bool autocorrect;
/// Whether [TextInput] should have focus. Defaults to [false].
final bool autofocus;
/// Controls the [TextInput] enableSuggestions behavior. Defaults to [true].
final bool enableSuggestions;
/// Controls the [TextInput] enabled behavior. Defaults to [true].
final bool enabled;
}
class Suggestion {
final String suggestion;
final String userGrouping;
final int? scenarioNumber;
Suggestion({
required this.suggestion,
required this.userGrouping,
this.scenarioNumber = 0,
});
}