diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index afa5d0a..8a18d2a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -50,6 +50,16 @@ "description": "Fifth tip displayed in the sidebar", "context": "Visible in the sidebar" }, + "deleteChat": "Delete", + "@deleteChat": { + "description": "Text displayed for delete chat option", + "context": "Visible in the chat view, desktop only" + }, + "renameChat": "Rename", + "@renameChat": { + "description": "Text displayed for rename chat option", + "context": "Visible in the chat view, desktop only" + }, "takeImage": "Take Image", "@takeImage": { "description": "Text displayed for take image button", @@ -265,10 +275,10 @@ "description": "Text displayed when the host is invalid", "context": "Visible in the settings view", "placeholders": { - "type": { - "type": "String", - "description": "Type of the issue, either 'url' or 'other' (preferably 'host')" - } + "type": { + "type": "String", + "description": "Type of the issue, either 'url' or 'other' (preferably 'host')" + } } }, "settingsHostHeaderTitle": "Set host header", @@ -286,10 +296,10 @@ "description": "Text displayed when the host is invalid", "context": "Visible in the settings view", "placeholders": { - "type": { - "type": "String", - "description": "Type of the issue, either 'url' or 'other' (preferably 'host')" - } + "type": { + "type": "String", + "description": "Type of the issue, either 'url' or 'other' (preferably 'host')" + } } }, "settingsSystemMessage": "System message", @@ -382,10 +392,10 @@ "description": "Text displayed as description for keep model loaded for set time toggle", "context": "Visible in the settings view", "placeholders": { - "minutes": { - "type": "String", - "description": "Minutes the model should be kept loaded" - } + "minutes": { + "type": "String", + "description": "Minutes the model should be kept loaded" + } } }, "settingsEnableHapticFeedback": "Enable haptic feedback", @@ -574,10 +584,10 @@ "description": "Text displayed when an update is available", "context": "Visible in the settings view", "placeholders": { - "version": { - "type": "String", - "description": "Version number of the available update" - } + "version": { + "type": "String", + "description": "Version number of the available update" + } } }, "settingsUpdateRateLimit": "Can't check, API rate limit exceeded", @@ -640,10 +650,10 @@ "description": "Text displayed as description for version", "context": "Visible in the settings view", "placeholders": { - "version": { - "type": "String", - "description": "Version number of the app" - } + "version": { + "type": "String", + "description": "Version number of the app" + } } } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7fc3d4e..0f7f71f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,7 +39,8 @@ import 'package:url_launcher/url_launcher.dart'; // use host or not, if false dialog is shown const useHost = false; -// host of ollama, must be accessible from the client, without trailing slash, will always be accepted as valid +// host of ollama, must be accessible from the client, without trailing slash +// ! will always be accepted as valid, even if [useHost] is false const fixedHost = "http://example.com:11434"; // use model or not, if false selector is shown const useModel = false; @@ -66,6 +67,7 @@ bool multimodal = false; List messages = []; String? chatUuid; bool chatAllowed = true; +String hoveredChat = ""; final user = types.User(id: const Uuid().v4()); final assistant = types.User(id: const Uuid().v4()); @@ -283,14 +285,13 @@ class _MainAppState extends State { ), const SizedBox(width: 16), ]))))), - desktopLayoutNotRequired(context) + (desktopLayoutNotRequired(context) || + (!allowMultipleChats && !allowSettings)) ? const SizedBox.shrink() - : (!allowMultipleChats && !allowSettings) - ? const SizedBox.shrink() - : Divider( - color: desktopLayout(context) - ? Theme.of(context).colorScheme.onSurface.withAlpha(20) - : null), + : Divider( + color: desktopLayout(context) + ? Theme.of(context).colorScheme.onSurface.withAlpha(20) + : null), (allowMultipleChats) ? (Padding( padding: padding, @@ -360,10 +361,13 @@ class _MainAppState extends State { const SizedBox(width: 16), ]))))) : const SizedBox.shrink(), - Divider( - color: desktopLayout(context) - ? Theme.of(context).colorScheme.onSurface.withAlpha(20) - : null), + (desktopLayoutNotRequired(context) && + (!allowMultipleChats && !allowSettings)) + ? const SizedBox.shrink() + : Divider( + color: desktopLayout(context) + ? Theme.of(context).colorScheme.onSurface.withAlpha(20) + : null), ((prefs?.getStringList("chats") ?? []).isNotEmpty) ? const SizedBox.shrink() : (Padding( @@ -444,135 +448,466 @@ class _MainAppState extends State { }), ]) ..addAll((prefs?.getStringList("chats") ?? []).map((item) { - return Dismissible( - key: Key(jsonDecode(item)["uuid"]), - direction: (chatAllowed) - ? DismissDirection.startToEnd - : DismissDirection.none, - confirmDismiss: (direction) async { - bool returnValue = false; - if (!chatAllowed) return false; - - if (prefs!.getBool("askBeforeDeletion") ?? false) { - await showDialog( - context: context, - builder: (context) { - return StatefulBuilder(builder: (context, setLocalState) { - return AlertDialog( - title: Text(AppLocalizations.of(context)! - .deleteDialogTitle), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context)! - .deleteDialogDescription), - ]), - actions: [ - TextButton( - onPressed: () { - selectionHaptic(); - Navigator.of(context).pop(); - returnValue = false; - }, - child: Text(AppLocalizations.of(context)! - .deleteDialogCancel)), - TextButton( - onPressed: () { - selectionHaptic(); - Navigator.of(context).pop(); - returnValue = true; - }, - child: Text(AppLocalizations.of(context)! - .deleteDialogDelete)) - ]); - }); - }); - } else { - returnValue = true; - } - return returnValue; - }, - onDismissed: (direction) { - selectionHaptic(); - for (var i = 0; - i < (prefs!.getStringList("chats") ?? []).length; - i++) { - if (jsonDecode( - (prefs!.getStringList("chats") ?? [])[i])["uuid"] == - jsonDecode(item)["uuid"]) { - List tmp = prefs!.getStringList("chats")!; - tmp.removeAt(i); - prefs!.setStringList("chats", tmp); - break; - } - } - if (chatUuid == jsonDecode(item)["uuid"]) { - messages = []; - chatUuid = null; - if (!desktopLayoutRequired(context)) { - Navigator.of(context).pop(); - } - } - setState(() {}); - }, - child: Padding( - padding: padding, - child: InkWell( - customBorder: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(50))), - onTap: () { - selectionHaptic(); - if (!desktopLayoutRequired(context)) { - Navigator.of(context).pop(); - } - if (!chatAllowed) return; - if (chatUuid == jsonDecode(item)["uuid"]) return; - loadChat(jsonDecode(item)["uuid"], setState); - chatUuid = jsonDecode(item)["uuid"]; - }, - onLongPress: () async { - selectionHaptic(); - if (!chatAllowed) return; - if (!allowSettings) return; - String oldTitle = jsonDecode(item)["title"]; - var newTitle = await prompt(context, - title: - AppLocalizations.of(context)!.dialogEnterNewTitle, - value: oldTitle, - uuid: jsonDecode(item)["uuid"]); - var tmp = (prefs!.getStringList("chats") ?? []); - for (var i = 0; i < tmp.length; i++) { - if (jsonDecode((prefs!.getStringList("chats") ?? - [])[i])["uuid"] == - jsonDecode(item)["uuid"]) { - var tmp2 = jsonDecode(tmp[i]); - tmp2["title"] = newTitle; - tmp[i] = jsonEncode(tmp2); - break; + var child = Padding( + padding: padding, + child: InkWell( + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(50))), + onTap: () { + selectionHaptic(); + if (!desktopLayoutRequired(context)) { + Navigator.of(context).pop(); + } + if (!chatAllowed) return; + if (chatUuid == jsonDecode(item)["uuid"]) return; + loadChat(jsonDecode(item)["uuid"], setState); + chatUuid = jsonDecode(item)["uuid"]; + }, + onHover: (value) { + setState(() { + if (value) { + hoveredChat = jsonDecode(item)["uuid"]; + } else { + hoveredChat = ""; + } + }); + }, + onLongPress: desktopFeature() + ? null + : () async { + selectionHaptic(); + if (!chatAllowed) return; + if (!allowSettings) return; + String oldTitle = jsonDecode(item)["title"]; + var newTitle = await prompt(context, + title: AppLocalizations.of(context)! + .dialogEnterNewTitle, + value: oldTitle, + uuid: jsonDecode(item)["uuid"]); + var tmp = (prefs!.getStringList("chats") ?? []); + for (var i = 0; i < tmp.length; i++) { + if (jsonDecode((prefs!.getStringList("chats") ?? + [])[i])["uuid"] == + jsonDecode(item)["uuid"]) { + var tmp2 = jsonDecode(tmp[i]); + tmp2["title"] = newTitle; + tmp[i] = jsonEncode(tmp2); + break; + } } - } - prefs!.setStringList("chats", tmp); - setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.only(top: 16, bottom: 16), - child: Row(children: [ - Padding( + prefs!.setStringList("chats", tmp); + setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.only(top: 16, bottom: 16), + child: Row(children: [ + allowMultipleChats + ? Padding( padding: const EdgeInsets.only(left: 16, right: 16), child: Icon((chatUuid == jsonDecode(item)["uuid"]) ? Icons.location_on_rounded - : Icons.restore_rounded)), - Expanded( - child: Text(jsonDecode(item)["title"], - softWrap: false, - maxLines: 1, - overflow: TextOverflow.fade, - style: const TextStyle( - fontWeight: FontWeight.w500)), - ), - const SizedBox(width: 16), - ]))))); + : Icons.restore_rounded)) + : const SizedBox(width: 16), + Expanded( + child: Text(jsonDecode(item)["title"], + softWrap: false, + maxLines: 1, + overflow: TextOverflow.fade, + style: + const TextStyle(fontWeight: FontWeight.w500)), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + child: + ((desktopFeature() && + (hoveredChat == + jsonDecode(item)["uuid"])) || + !allowMultipleChats) + ? Padding( + padding: const EdgeInsets.only( + left: 16, right: 16), + child: SizedBox( + height: 24, + width: 24, + child: IconButton( + onPressed: () { + if (!allowMultipleChats) { + for (var i = 0; + i < + (prefs!.getStringList( + "chats") ?? + []) + .length; + i++) { + if (jsonDecode((prefs! + .getStringList( + "chats") ?? + [])[i])["uuid"] == + jsonDecode(item)["uuid"]) { + List tmp = prefs! + .getStringList("chats")!; + tmp.removeAt(i); + prefs!.setStringList( + "chats", tmp); + break; + } + } + messages = []; + chatUuid = null; + if (!desktopLayoutRequired( + context)) { + Navigator.of(context).pop(); + } + setState(() {}); + return; + } + if (!allowSettings) { + if (prefs!.getBool( + "askBeforeDeletion") ?? + false) { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, + setLocalState) { + return AlertDialog( + title: Text( + AppLocalizations.of( + context)! + .deleteDialogTitle), + content: Column( + mainAxisSize: + MainAxisSize + .min, + children: [ + Text(AppLocalizations.of( + context)! + .deleteDialogDescription), + ]), + actions: [ + TextButton( + onPressed: + () { + Navigator.of( + context) + .pop(); + }, + child: Text(AppLocalizations.of( + context)! + .deleteDialogCancel)), + TextButton( + onPressed: + () { + Navigator.of( + context) + .pop(); + for (var i = + 0; + i < (prefs!.getStringList("chats") ?? []).length; + i++) { + if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])[ + "uuid"] == + jsonDecode( + item)["uuid"]) { + List + tmp = + prefs!.getStringList("chats")!; + tmp.removeAt( + i); + prefs!.setStringList( + "chats", + tmp); + break; + } + } + if (chatUuid == + jsonDecode( + item)["uuid"]) { + messages = + []; + chatUuid = + null; + if (!desktopLayoutRequired( + context)) { + Navigator.of(context) + .pop(); + } + } + setState( + () {}); + }, + child: Text(AppLocalizations.of( + context)! + .deleteDialogDelete)) + ]); + }); + }); + } else { + for (var i = 0; + i < + (prefs!.getStringList( + "chats") ?? + []) + .length; + i++) { + if (jsonDecode((prefs! + .getStringList( + "chats") ?? + [])[i])["uuid"] == + jsonDecode( + item)["uuid"]) { + List tmp = prefs! + .getStringList( + "chats")!; + tmp.removeAt(i); + prefs!.setStringList( + "chats", tmp); + break; + } + } + if (chatUuid == + jsonDecode(item)["uuid"]) { + messages = []; + chatUuid = null; + if (!desktopLayoutRequired( + context)) { + Navigator.of(context).pop(); + } + } + setState(() {}); + } + return; + } + if (!chatAllowed) return; + showDialog( + context: context, + builder: (context) { + return Dialog( + alignment: + Alignment.bottomLeft, + insetPadding: + const EdgeInsets.only( + left: 12, + bottom: 12), + child: Container( + width: 100, + padding: + const EdgeInsets + .only( + left: 16, + right: 16, + top: 16), + child: Column( + mainAxisSize: + MainAxisSize + .min, + children: [ + SizedBox( + width: double + .infinity, + child: OutlinedButton + .icon( + onPressed: + () { + Navigator.of(context).pop(); + if (prefs!.getBool("askBeforeDeletion") ?? + false) { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder(builder: (context, setLocalState) { + return AlertDialog( + title: Text(AppLocalizations.of(context)!.deleteDialogTitle), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(AppLocalizations.of(context)!.deleteDialogDescription), + ]), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context)!.deleteDialogCancel)), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) { + if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) { + List tmp = prefs!.getStringList("chats")!; + tmp.removeAt(i); + prefs!.setStringList("chats", tmp); + break; + } + } + if (chatUuid == jsonDecode(item)["uuid"]) { + messages = []; + chatUuid = null; + if (!desktopLayoutRequired(context)) { + Navigator.of(context).pop(); + } + } + setState(() {}); + }, + child: Text(AppLocalizations.of(context)!.deleteDialogDelete)) + ]); + }); + }); + } else { + for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) { + if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) { + List tmp = prefs!.getStringList("chats")!; + tmp.removeAt(i); + prefs!.setStringList("chats", tmp); + break; + } + } + if (chatUuid == jsonDecode(item)["uuid"]) { + messages = []; + chatUuid = null; + if (!desktopLayoutRequired(context)) { + Navigator.of(context).pop(); + } + } + setState(() {}); + } + }, + icon: const Icon(Icons + .delete_forever_rounded), + label: + Text(AppLocalizations.of(context)!.deleteChat))), + const SizedBox( + height: 8), + SizedBox( + width: double + .infinity, + child: OutlinedButton + .icon( + onPressed: + () async { + Navigator.of(context).pop(); + String + oldTitle = + jsonDecode(item)["title"]; + var newTitle = await prompt(context, + title: AppLocalizations.of(context)!.dialogEnterNewTitle, + value: oldTitle, + uuid: jsonDecode(item)["uuid"]); + var tmp = + (prefs!.getStringList("chats") ?? []); + for (var i = 0; + i < tmp.length; + i++) { + if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) { + var tmp2 = jsonDecode(tmp[i]); + tmp2["title"] = newTitle; + tmp[i] = jsonEncode(tmp2); + break; + } + } + prefs!.setStringList("chats", + tmp); + setState(() {}); + }, + icon: const Icon(Icons + .edit_rounded), + label: + Text(AppLocalizations.of(context)!.renameChat))), + const SizedBox( + height: 16) + ])), + ); + }); + }, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + icon: Transform.translate( + offset: const Offset(-8, -8), + child: const Icon(allowMultipleChats + ? allowSettings + ? Icons.more_horiz_rounded + : Icons.close_rounded + : Icons.restart_alt_rounded), + ), + ), + )) + : const SizedBox(width: 16)), + ])))); + return desktopFeature() || !allowMultipleChats + ? child + : Dismissible( + key: Key(jsonDecode(item)["uuid"]), + direction: (chatAllowed) + ? DismissDirection.startToEnd + : DismissDirection.none, + confirmDismiss: (direction) async { + bool returnValue = false; + if (!chatAllowed) return false; + + if (prefs!.getBool("askBeforeDeletion") ?? false) { + await showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setLocalState) { + return AlertDialog( + title: Text(AppLocalizations.of(context)! + .deleteDialogTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context)! + .deleteDialogDescription), + ]), + actions: [ + TextButton( + onPressed: () { + selectionHaptic(); + Navigator.of(context).pop(); + returnValue = false; + }, + child: Text(AppLocalizations.of(context)! + .deleteDialogCancel)), + TextButton( + onPressed: () { + selectionHaptic(); + Navigator.of(context).pop(); + returnValue = true; + }, + child: Text(AppLocalizations.of(context)! + .deleteDialogDelete)) + ]); + }); + }); + } else { + returnValue = true; + } + return returnValue; + }, + onDismissed: (direction) { + selectionHaptic(); + for (var i = 0; + i < (prefs!.getStringList("chats") ?? []).length; + i++) { + if (jsonDecode( + (prefs!.getStringList("chats") ?? [])[i])["uuid"] == + jsonDecode(item)["uuid"]) { + List tmp = prefs!.getStringList("chats")!; + tmp.removeAt(i); + prefs!.setStringList("chats", tmp); + break; + } + } + if (chatUuid == jsonDecode(item)["uuid"]) { + messages = []; + chatUuid = null; + if (!desktopLayoutRequired(context)) { + Navigator.of(context).pop(); + } + } + setState(() {}); + }, + child: child); }).toList()); } @@ -610,7 +945,9 @@ class _MainAppState extends State { padding: EdgeInsets.all(16), child: Text( "*Build Error:*\n\nuseHost: $useHost\nallowSettings: $allowSettings\n\nYou created this build? One of them must be set to true or the app is not functional!\n\nYou received this build by someone else? Please contact them and report the issue.", - style: TextStyle(color: Colors.red))))); + style: TextStyle( + color: Colors.red, + fontFamily: "monospace"))))); }); } @@ -726,91 +1063,98 @@ class _MainAppState extends State { : [Expanded(child: selector)]), actions: desktopControlsActions(context, [ const SizedBox(width: 4), - IconButton( - onPressed: () { - selectionHaptic(); - if (!chatAllowed) return; + allowMultipleChats + ? IconButton( + onPressed: () { + selectionHaptic(); + if (!chatAllowed) return; - if (prefs!.getBool("askBeforeDeletion") ?? - // ignore: dead_code - false && messages.isNotEmpty) { - showDialog( - context: context, - builder: (context) { - return StatefulBuilder( - builder: (context, setLocalState) { - return AlertDialog( - title: Text(AppLocalizations.of(context)! - .deleteDialogTitle), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context)! - .deleteDialogDescription), - ]), - actions: [ - TextButton( - onPressed: () { - selectionHaptic(); - Navigator.of(context).pop(); - }, - child: Text( - AppLocalizations.of(context)! - .deleteDialogCancel)), - TextButton( - onPressed: () { - selectionHaptic(); - Navigator.of(context).pop(); + if (prefs!.getBool("askBeforeDeletion") ?? + // ignore: dead_code + false && messages.isNotEmpty) { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setLocalState) { + return AlertDialog( + title: Text( + AppLocalizations.of(context)! + .deleteDialogTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context)! + .deleteDialogDescription), + ]), + actions: [ + TextButton( + onPressed: () { + selectionHaptic(); + Navigator.of(context).pop(); + }, + child: Text( + AppLocalizations.of(context)! + .deleteDialogCancel)), + TextButton( + onPressed: () { + selectionHaptic(); + Navigator.of(context).pop(); - for (var i = 0; - i < - (prefs!.getStringList( - "chats") ?? - []) - .length; - i++) { - if (jsonDecode((prefs! - .getStringList( - "chats") ?? - [])[i])["uuid"] == - chatUuid) { - List tmp = prefs! - .getStringList("chats")!; - tmp.removeAt(i); - prefs!.setStringList( - "chats", tmp); - break; - } - } - messages = []; - chatUuid = null; - setState(() {}); - }, - child: Text( - AppLocalizations.of(context)! - .deleteDialogDelete)) - ]); - }); - }); - } else { - for (var i = 0; - i < (prefs!.getStringList("chats") ?? []).length; - i++) { - if (jsonDecode((prefs!.getStringList("chats") ?? - [])[i])["uuid"] == - chatUuid) { - List tmp = prefs!.getStringList("chats")!; - tmp.removeAt(i); - prefs!.setStringList("chats", tmp); - break; + for (var i = 0; + i < + (prefs!.getStringList( + "chats") ?? + []) + .length; + i++) { + if (jsonDecode((prefs! + .getStringList( + "chats") ?? + [])[i])["uuid"] == + chatUuid) { + List tmp = prefs! + .getStringList( + "chats")!; + tmp.removeAt(i); + prefs!.setStringList( + "chats", tmp); + break; + } + } + messages = []; + chatUuid = null; + setState(() {}); + }, + child: Text( + AppLocalizations.of(context)! + .deleteDialogDelete)) + ]); + }); + }); + } else { + for (var i = 0; + i < + (prefs!.getStringList("chats") ?? []) + .length; + i++) { + if (jsonDecode((prefs!.getStringList("chats") ?? + [])[i])["uuid"] == + chatUuid) { + List tmp = + prefs!.getStringList("chats")!; + tmp.removeAt(i); + prefs!.setStringList("chats", tmp); + break; + } + } + messages = []; + chatUuid = null; } - } - messages = []; - chatUuid = null; - } - setState(() {}); - }, - icon: const Icon(Icons.restart_alt_rounded)) + setState(() {}); + }, + icon: const Icon(Icons.restart_alt_rounded)) + : const SizedBox.shrink() ]), bottom: PreferredSize( preferredSize: const Size.fromHeight(1), @@ -875,6 +1219,13 @@ class _MainAppState extends State { {required messageWidth, required showName}) { var white = const TextStyle(color: Colors.white); + bool greyed = false; + String text = p0.text; + if (text.trim() == "") { + text = + "_Empty AI response, try restarting conversation_"; // Warning icon: U+26A0 + greyed = true; + } return Padding( padding: const EdgeInsets.only( left: 20, @@ -889,7 +1240,7 @@ class _MainAppState extends State { WidgetStatePropertyAll( Colors.grey))), child: MarkdownBody( - data: p0.text, + data: text, onTapLink: (text, href, title) async { selectionHaptic(); try { @@ -1020,9 +1371,8 @@ class _MainAppState extends State { Colors.white), codeblockDecoration: BoxDecoration( color: Colors.white, - borderRadius: - BorderRadius.circular( - 8)), + borderRadius: BorderRadius.circular( + 8)), h1: white, h2: white, h3: white, @@ -1042,8 +1392,10 @@ class _MainAppState extends State { : (Theme.of(context).brightness == Brightness.light) ? MarkdownStyleSheet( - p: const TextStyle( - color: Colors.black, + p: TextStyle( + color: greyed + ? Colors.grey + : Colors.black, fontSize: 16, fontWeight: FontWeight.w500), @@ -1056,8 +1408,7 @@ class _MainAppState extends State { ), code: const TextStyle( color: Colors.white, - backgroundColor: - Colors.black), + backgroundColor: Colors.black), codeblockDecoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(8)), horizontalRuleDecoration: BoxDecoration(border: Border(top: BorderSide(color: Colors.grey[200]!, width: 1)))) : MarkdownStyleSheet( @@ -1476,7 +1827,7 @@ class _MainAppState extends State { inputTextColor: (theme ?? ThemeData()).colorScheme.onSurface, inputBorderRadius: const BorderRadius.all(Radius.circular(64)), inputPadding: const EdgeInsets.all(16), - inputMargin: EdgeInsets.only(left: 8, right: 8, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8), + inputMargin: EdgeInsets.only(left: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, right: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8), messageMaxWidth: (MediaQuery.of(context).size.width >= 1000) ? (MediaQuery.of(context).size.width >= 1600) ? (MediaQuery.of(context).size.width >= 2200) @@ -1532,7 +1883,7 @@ class _MainAppState extends State { ), ], ), - drawerEdgeDragWidth: (prefs!.getBool("fixCodeblockScroll") ?? false) + drawerEdgeDragWidth: (prefs?.getBool("fixCodeblockScroll") ?? false) ? null : (desktopLayout(context) ? null diff --git a/lib/screen_settings.dart b/lib/screen_settings.dart index 3c7f1c8..75b0a69 100644 --- a/lib/screen_settings.dart +++ b/lib/screen_settings.dart @@ -255,17 +255,21 @@ class _ScreenSettingsState extends State { return; } - http.Response request; + http.Response? request; try { - request = await http - .get( - Uri.parse(tmpHost), - headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map) - .cast(), - ) + var client = http.Client(); + final requestBase = http.Request("get", Uri.parse(tmpHost)) + ..headers.addAll( + (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map) + .cast(), + ) + ..followRedirects = false; + request = await http.Response.fromStream(await requestBase + .send() .timeout(const Duration(seconds: 5), onTimeout: () { - return http.Response("Error", 408); - }); + return http.StreamedResponse(const Stream.empty(), 408); + })); + client.close(); } catch (e) { setState(() { hostInvalidHost = true; @@ -278,7 +282,8 @@ class _ScreenSettingsState extends State { setState(() { hostLoading = false; host = tmpHost; - if (hostInputController.text != host!) { + if (hostInputController.text != host! && + (Uri.parse(tmpHost).toString() != fixedHost)) { hostInputController.text = host!; } }); diff --git a/lib/settings/export.dart b/lib/settings/export.dart index 8239aff..c7c1b9d 100644 --- a/lib/settings/export.dart +++ b/lib/settings/export.dart @@ -28,13 +28,11 @@ class _ScreenSettingsExportState extends State { color: Theme.of(context).colorScheme.surface, child: Scaffold( appBar: AppBar( - title: Row(children: [ - Text(AppLocalizations.of(context)!.settingsTitleExport), - Expanded(child: SizedBox(height: 200, child: MoveWindow())) - ]), - actions: - desktopControlsActions(context) - ), + title: Row(children: [ + Text(AppLocalizations.of(context)!.settingsTitleExport), + Expanded(child: SizedBox(height: 200, child: MoveWindow())) + ]), + actions: desktopControlsActions(context)), body: Padding( padding: const EdgeInsets.only(left: 16, right: 16), child: Column(children: [ @@ -58,78 +56,89 @@ class _ScreenSettingsExportState extends State { jsonEncode(prefs!.getStringList("chats") ?? [])); } }), - button(AppLocalizations.of(context)!.settingsImportChats, - Icons.download_rounded, () { - selectionHaptic(); - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(AppLocalizations.of(context)! - .settingsImportChatsTitle), - content: Text(AppLocalizations.of(context)! - .settingsImportChatsDescription), - actions: [ - TextButton( - onPressed: () { - selectionHaptic(); - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context)! - .settingsImportChatsCancel)), - TextButton( - onPressed: () async { - selectionHaptic(); - FilePickerResult? result = - await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ["json"]); - if (result == null) { - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - return; - } + allowMultipleChats + ? button( + AppLocalizations.of(context)!.settingsImportChats, + Icons.download_rounded, () { + selectionHaptic(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(AppLocalizations.of(context)! + .settingsImportChatsTitle), + content: Text( + AppLocalizations.of(context)! + .settingsImportChatsDescription), + actions: [ + TextButton( + onPressed: () { + selectionHaptic(); + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of( + context)! + .settingsImportChatsCancel)), + TextButton( + onPressed: () async { + selectionHaptic(); + FilePickerResult? result = + await FilePicker.platform + .pickFiles( + type: FileType.custom, + allowedExtensions: [ + "json" + ]); + if (result == null) { + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + return; + } - File file = - File(result.files.single.path!); - var content = await file.readAsString(); - List tmpHistory = - jsonDecode(content); - List history = []; + File file = File( + result.files.single.path!); + var content = + await file.readAsString(); + List tmpHistory = + jsonDecode(content); + List history = []; - for (var i = 0; - i < tmpHistory.length; - i++) { - history.add(tmpHistory[i]); - } + for (var i = 0; + i < tmpHistory.length; + i++) { + history.add(tmpHistory[i]); + } - prefs!.setStringList("chats", history); + prefs!.setStringList( + "chats", history); - messages = []; - chatUuid = null; + messages = []; + chatUuid = null; - setState(() {}); + setState(() {}); - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text(AppLocalizations - // ignore: use_build_context_synchronously - .of(context)! - .settingsImportChatsSuccess), - showCloseIcon: true)); - }, - child: Text(AppLocalizations.of(context)! - .settingsImportChatsImport)) - ]); - }); - }) + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + content: Text(AppLocalizations + // ignore: use_build_context_synchronously + .of(context)! + .settingsImportChatsSuccess), + showCloseIcon: true)); + }, + child: Text( + AppLocalizations.of(context)! + .settingsImportChatsImport)) + ]); + }); + }) + : const SizedBox.shrink() ]), ), const SizedBox(height: 8), diff --git a/lib/worker/sender.dart b/lib/worker/sender.dart index 2dcefbf..8502605 100644 --- a/lib/worker/sender.dart +++ b/lib/worker/sender.dart @@ -162,90 +162,90 @@ Future send(String value, BuildContext context, Function setState, .cast(), baseUrl: "$host/api"); - try { - if ((prefs!.getString("requestType") ?? "stream") == "stream") { - final stream = client - .generateChatCompletionStream( - request: llama.GenerateChatCompletionRequest( - model: model!, - messages: history, - keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")), - ) - .timeout(const Duration(seconds: 30)); + // try { + if ((prefs!.getString("requestType") ?? "stream") == "stream") { + final stream = client + .generateChatCompletionStream( + request: llama.GenerateChatCompletionRequest( + model: model!, + messages: history, + keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")), + ) + .timeout(const Duration(seconds: 30)); - await for (final res in stream) { - text += (res.message?.content ?? ""); - for (var i = 0; i < messages.length; i++) { - if (messages[i].id == newId) { - messages.removeAt(i); - break; - } + await for (final res in stream) { + text += (res.message?.content ?? ""); + for (var i = 0; i < messages.length; i++) { + if (messages[i].id == newId) { + messages.removeAt(i); + break; } - if (chatAllowed) return ""; - if (text.trim() == "") { - throw Exception(); - } - messages.insert( - 0, types.TextMessage(author: assistant, id: newId, text: text)); - if (onStream != null) { - onStream(text, false); - } - setState(() {}); - heavyHaptic(); } - } else { - llama.GenerateChatCompletionResponse request; - request = await client - .generateChatCompletion( - request: llama.GenerateChatCompletionRequest( - model: model!, - messages: history, - keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")), - ) - .timeout(const Duration(seconds: 30)); if (chatAllowed) return ""; - if (request.message!.content.trim() == "") { - throw Exception(); - } + // if (text.trim() == "") { + // throw Exception(); + // } messages.insert( - 0, - types.TextMessage( - author: assistant, id: newId, text: request.message!.content)); - text = request.message!.content; + 0, types.TextMessage(author: assistant, id: newId, text: text)); + if (onStream != null) { + onStream(text, false); + } setState(() {}); heavyHaptic(); } - } catch (e) { - for (var i = 0; i < messages.length; i++) { - if (messages[i].id == newId) { - messages.removeAt(i); - break; - } - } - setState(() { - chatAllowed = true; - messages.removeAt(0); - if (messages.isEmpty) { - var tmp = (prefs!.getStringList("chats") ?? []); - chatUuid = null; - for (var i = 0; i < tmp.length; i++) { - if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == - chatUuid) { - tmp.removeAt(i); - prefs!.setStringList("chats", tmp); - break; - } - } - } - }); - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: - // ignore: use_build_context_synchronously - Text(AppLocalizations.of(context)!.settingsHostInvalid("timeout")), - showCloseIcon: true)); - return ""; + } else { + llama.GenerateChatCompletionResponse request; + request = await client + .generateChatCompletion( + request: llama.GenerateChatCompletionRequest( + model: model!, + messages: history, + keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")), + ) + .timeout(const Duration(seconds: 30)); + if (chatAllowed) return ""; + // if (request.message!.content.trim() == "") { + // throw Exception(); + // } + messages.insert( + 0, + types.TextMessage( + author: assistant, id: newId, text: request.message!.content)); + text = request.message!.content; + setState(() {}); + heavyHaptic(); } + // } catch (e) { + // for (var i = 0; i < messages.length; i++) { + // if (messages[i].id == newId) { + // messages.removeAt(i); + // break; + // } + // } + // setState(() { + // chatAllowed = true; + // messages.removeAt(0); + // if (messages.isEmpty) { + // var tmp = (prefs!.getStringList("chats") ?? []); + // for (var i = 0; i < tmp.length; i++) { + // if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == + // chatUuid) { + // tmp.removeAt(i); + // prefs!.setStringList("chats", tmp); + // break; + // } + // } + // chatUuid = null; + // } + // }); + // // ignore: use_build_context_synchronously + // ScaffoldMessenger.of(context).showSnackBar(SnackBar( + // content: + // // ignore: use_build_context_synchronously + // Text(AppLocalizations.of(context)!.settingsHostInvalid("timeout")), + // showCloseIcon: true)); + // return ""; + // } if ((prefs!.getString("requestType") ?? "stream") == "stream") { if (onStream != null) { diff --git a/lib/worker/setter.dart b/lib/worker/setter.dart index c7ba0ea..eea8ba9 100644 --- a/lib/worker/setter.dart +++ b/lib/worker/setter.dart @@ -82,7 +82,8 @@ void setModel(BuildContext context, Function setState) { if (!loaded) return; loaded = false; if (usedIndex >= 0 && modelsReal[usedIndex] != model) { - if (prefs!.getBool("resetOnModelSelect") ?? true) { + if (prefs!.getBool("resetOnModelSelect") ?? + true && allowMultipleChats) { messages = []; chatUuid = null; } @@ -467,6 +468,24 @@ Future prompt(BuildContext context, autofocus: true, keyboardType: keyboard, maxLines: maxLines, + onSubmitted: (value) async { + if (validator != null) { + selectionHaptic(); + setLocalState(() { + error = null; + }); + bool valid = await validator(controller.text); + if (!valid) { + setLocalState(() { + error = validatorError; + }); + return; + } + } + returnText = controller.text; + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }, decoration: InputDecoration( border: const OutlineInputBorder(), hintText: placeholder, diff --git a/untranslated_messages.json b/untranslated_messages.json index 3b4a0d9..9bf6d6d 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,5 +1,7 @@ { "de": [ + "deleteChat", + "renameChat", "settingsDescriptionBehavior", "settingsDescriptionInterface", "settingsDescriptionVoice", @@ -14,6 +16,8 @@ ], "zh": [ + "deleteChat", + "renameChat", "settingsDescriptionBehavior", "settingsDescriptionInterface", "settingsDescriptionVoice",